{"id":9633,"date":"2024-12-30T23:40:57","date_gmt":"2024-12-30T14:40:57","guid":{"rendered":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=9633"},"modified":"2024-12-30T17:51:05","modified_gmt":"2024-12-30T08:51:05","slug":"%ec%95%88%eb%93%9c%eb%a1%9c%ec%9d%b4%eb%93%9c-%ec%9d%b8%ec%95%b1-%ea%b5%ac%eb%8f%85-%ec%84%9c%eb%b9%84%ec%8a%a4-%ea%b5%ac%ed%98%84%ed%95%98%ea%b8%b0-java","status":"publish","type":"post","link":"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=9633","title":{"rendered":"\uc548\ub4dc\ub85c\uc774\ub4dc &#8211; \uc778\uc571 \uad6c\ub3c5 \uc11c\ube44\uc2a4 \uad6c\ud604\ud558\uae30 (Java)"},"content":{"rendered":"<h1>\uc548\ub4dc\ub85c\uc774\ub4dc &#8211; \uc778\uc571 \uad6c\ub3c5 \uc11c\ube44\uc2a4 \uad6c\ud604\ud558\uae30 (Java)<\/h1>\n<p>\uad6c\uae00 \ud50c\ub808\uc774\uc5d0\uc11c\uc758 \uad6c\ub3c5\ucde8\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\ub85c \uc804\uc1a1\ud558\ub294 \ubd80\ubd84\uc740 \ub204\ub77d\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.<\/p>\n<p>. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .<\/p>\n<h2>SubscriptionViewModel \uad6c\ud604<\/h2>\n<h3>\ucf54\ub9e8\ud2b8 \ubc84\uc804<\/h3>\n<pre><code class=\"language-java\">public class SubscriptionViewModel extends AndroidViewModel {\n\n    \/**\n     * ViewModel \uc0dd\uc131\uc790\n     * BillingClient\ub97c \ucd08\uae30\ud654\ud558\uace0 Google Play \uacb0\uc81c \uc11c\ube44\uc2a4\uc5d0 \uc5f0\uacb0\uc744 \uc2dc\uc791\ud569\ub2c8\ub2e4.\n     * \n     * @param application Application \ucee8\ud14d\uc2a4\ud2b8\n     *\/\n    public SubscriptionViewModel(@NonNull Application application) {\n        super(application);\n        billingClient = BillingClient.newBuilder(application)\n                .setListener(purchasesUpdatedListener)\n                .enablePendingPurchases()\n                .build();\n\n        connectToBillingClient();\n    }\n\n    \/**\n     * Google Play Billing \uc11c\ube44\uc2a4\uc5d0 \uc5f0\uacb0\uc744 \uc2dc\ub3c4\ud569\ub2c8\ub2e4.\n     * \uc5f0\uacb0\uc774 \uc131\uacf5\ud558\uba74 \uc0c1\ud488 \uc870\ud68c\uc640 \uad6c\ub3c5 \uc0c1\ud0dc \ud655\uc778\uc744 \uc218\ud589\ud569\ub2c8\ub2e4.\n     * \uc5f0\uacb0\uc774 \uc2e4\ud328\ud558\uba74 \uc7ac\uc5f0\uacb0\uc744 \uc2dc\ub3c4\ud569\ub2c8\ub2e4.\n     *\/\n    private void connectToBillingClient() {\n        billingClient.startConnection(new BillingClientStateListener() {\n            @Override\n            public void onBillingSetupFinished(BillingResult billingResult) {\n                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {\n                    queryAvailableProducts();\n                    checkSubscriptionStatus();\n                }\n            }\n\n            @Override\n            public void onBillingServiceDisconnected() {\n                connectToBillingClient();\n            }\n        });\n    }\n\n    \/**\n     * Google Play Store\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 \uad6c\ub3c5 \uc0c1\ud488 \uc815\ubcf4\ub97c \uc870\ud68c\ud569\ub2c8\ub2e4.\n     * \uc6d4\uac04 \uad6c\ub3c5\uacfc \uc5f0\uac04 \uad6c\ub3c5 \uc0c1\ud488\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc635\ub2c8\ub2e4.\n     * \uc870\ud68c\ub41c \uc0c1\ud488 \uc815\ubcf4\ub294 monthlySubscription\uacfc yearlySubscription \ubcc0\uc218\uc5d0 \uc800\uc7a5\ub429\ub2c8\ub2e4.\n     *\/\n    private void queryAvailableProducts() {\n        List&lt;QueryProductDetailsParams.Product&gt; productList = new ArrayList&lt;&gt;();\n        productList.add(\n                QueryProductDetailsParams.Product.newBuilder()\n                        .setProductId(&quot;monthly_sub&quot;)\n                        .setProductType(BillingClient.ProductType.SUBS)\n                        .build()\n        );\n        \/\/ ... \uad6c\ud604 \ub0b4\uc6a9 ...\n    }\n\n    \/**\n     * \ud604\uc7ac \uc0ac\uc6a9\uc790\uc758 \uad6c\ub3c5 \uc0c1\ud0dc\ub97c \ud655\uc778\ud569\ub2c8\ub2e4.\n     * \uad6c\ub9e4 \ub0b4\uc5ed\uc744 \uc870\ud68c\ud558\uc5ec \uc720\ud6a8\ud55c \uad6c\ub3c5\uc774 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uace0,\n     * \uad6c\ub3c5 \uc0c1\ud0dc\ub97c LiveData\ub97c \ud1b5\ud574 UI\uc5d0 \ubc18\uc601\ud569\ub2c8\ub2e4.\n     *\/\n    private void checkSubscriptionStatus() {\n        billingClient.queryPurchasesAsync(\n                QueryPurchasesParams.newBuilder()\n                        .setProductType(BillingClient.ProductType.SUBS)\n                        .build(),\n                (billingResult, purchases) -&gt; {\n                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {\n                        verifySubscriptionStatus(purchases);\n                    }\n                }\n        );\n    }\n\n    \/**\n     * \uad6c\ub9e4 \ub0b4\uc5ed\uc744 \uac80\uc99d\ud558\uace0 \uad6c\ub3c5 \uc0c1\ud0dc\ub97c \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.\n     * PURCHASED \uc0c1\ud0dc\uc758 \uad6c\ub9e4 \uac74\uc744 \ucc98\ub9ac\ud558\uace0 \uad6c\ub3c5 \uc0c1\ud0dc\ub97c LiveData\uc5d0 \ubc18\uc601\ud569\ub2c8\ub2e4.\n     * \n     * @param purchases \uad6c\ub9e4 \ub0b4\uc5ed \ub9ac\uc2a4\ud2b8\n     *\/\n    private void verifySubscriptionStatus(List&lt;Purchase&gt; purchases) {\n        boolean hasValidSubscription = false;\n        for (Purchase purchase : purchases) {\n            if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {\n                hasValidSubscription = true;\n                handlePurchase(purchase);\n            }\n        }\n        isSubscribed.postValue(hasValidSubscription);\n    }\n\n    \/**\n     * \uad6c\ub3c5 \uad6c\ub9e4 \ud50c\ub85c\uc6b0\ub97c \uc2dc\uc791\ud569\ub2c8\ub2e4.\n     * \uc120\ud0dd\ub41c \uc0c1\ud488(\uc6d4\uac04\/\uc5f0\uac04)\uc758 \uad6c\ub9e4 \ud654\uba74\uc744 \uc2e4\ud589\ud569\ub2c8\ub2e4.\n     * \n     * @param activity \ud604\uc7ac Activity \ucee8\ud14d\uc2a4\ud2b8\n     * @param productId \uad6c\ub9e4\ud560 \uc0c1\ud488 ID (&quot;monthly_sub&quot; \ub610\ub294 &quot;yearly_sub&quot;)\n     *\/\n    public void purchaseSubscription(Activity activity, String productId) {\n        ProductDetails productDetails = &quot;monthly_sub&quot;.equals(productId) \n                ? monthlySubscription : yearlySubscription;\n        \/\/ ... \uad6c\ud604 \ub0b4\uc6a9 ...\n    }\n\n    \/**\n     * \uad6c\ub9e4 \uc644\ub8cc\ub41c \uc0c1\ud488\uc744 \ucc98\ub9ac\ud569\ub2c8\ub2e4.\n     * \uad6c\ub9e4 \ud655\uc778(Acknowledge)\uc744 \uc218\ud589\ud558\uace0, \uc11c\ubc84\uc5d0 \uad6c\ub3c5 \uc815\ubcf4\ub97c \uc804\uc1a1\ud569\ub2c8\ub2e4.\n     * \n     * @param purchase \uad6c\ub9e4 \uc644\ub8cc\ub41c \uc0c1\ud488 \uc815\ubcf4\n     *\/\n    private void handlePurchase(Purchase purchase) {\n        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {\n            if (!purchase.isAcknowledged()) {\n                AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()\n                        .setPurchaseToken(purchase.getPurchaseToken())\n                        .build();\n                \/\/ ... \uad6c\ud604 \ub0b4\uc6a9 ...\n            }\n        }\n    }\n\n    \/**\n     * Google Play Billing\uc758 \uad6c\ub9e4 \uc5c5\ub370\uc774\ud2b8 \ub9ac\uc2a4\ub108\n     * \uad6c\ub9e4 \uc0c1\ud0dc\uac00 \ubcc0\uacbd\ub420 \ub54c\ub9c8\ub2e4 \ud638\ucd9c\ub418\uc5b4 \uad6c\ub9e4 \ucc98\ub9ac\ub97c \uc218\ud589\ud569\ub2c8\ub2e4.\n     *\/\n    private final PurchasesUpdatedListener purchasesUpdatedListener = \n            (billingResult, purchases) -&gt; {\n        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK \n                &amp;&amp; purchases != null) {\n            for (Purchase purchase : purchases) {\n                handlePurchase(purchase);\n            }\n            checkSubscriptionStatus();\n        }\n    };\n\n    \/**\n     * \ud604\uc7ac \uad6c\ub3c5 \uc0c1\ud0dc\ub97c \ubc18\ud658\ud558\ub294 LiveData\ub97c \uc81c\uacf5\ud569\ub2c8\ub2e4.\n     * UI\uc5d0\uc11c \uc774 LiveData\ub97c \uad00\ucc30\ud558\uc5ec \uad6c\ub3c5 \uc0c1\ud0dc \ubcc0\uacbd\uc744 \uac10\uc9c0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n     * \n     * @return \uad6c\ub3c5 \uc0c1\ud0dc\ub97c \ub098\ud0c0\ub0b4\ub294 LiveData\n     *\/\n    public LiveData&lt;Boolean&gt; getIsSubscribed() {\n        return isSubscribed;\n    }\n\n    \/**\n     * ViewModel\uc774 \uc81c\uac70\ub420 \ub54c \ud638\ucd9c\ub429\ub2c8\ub2e4.\n     * BillingClient \uc5f0\uacb0\uc744 \uc885\ub8cc\ud558\uace0 \ub9ac\uc18c\uc2a4\ub97c \uc815\ub9ac\ud569\ub2c8\ub2e4.\n     *\/\n    @Override\n    protected void onCleared() {\n        super.onCleared();\n        billingClient.endConnection();\n    }\n}<\/code><\/pre>\n<h3>\uc804\uccb4\uc18c\uc2a4\ucf54\ub4dc<\/h3>\n<pre><code class=\"language-java\">public class SubscriptionViewModel extends AndroidViewModel {\n    private static final String TAG = &quot;SubscriptionViewModel&quot;;\n    private final BillingClient billingClient;\n    private final MutableLiveData&lt;Boolean&gt; isSubscribed = new MutableLiveData&lt;&gt;(false);\n    private ProductDetails monthlySubscription;\n    private ProductDetails yearlySubscription;\n\n    public SubscriptionViewModel(@NonNull Application application) {\n        super(application);\n\n        billingClient = BillingClient.newBuilder(application)\n                .setListener(purchasesUpdatedListener)\n                .enablePendingPurchases()\n                .build();\n\n        connectToBillingClient();\n    }\n\n    private void connectToBillingClient() {\n        billingClient.startConnection(new BillingClientStateListener() {\n            @Override\n            public void onBillingSetupFinished(BillingResult billingResult) {\n                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {\n                    queryAvailableProducts();\n                    checkSubscriptionStatus();\n                }\n            }\n\n            @Override\n            public void onBillingServiceDisconnected() {\n                \/\/ \uc7ac\uc5f0\uacb0 \ub85c\uc9c1\n                connectToBillingClient();\n            }\n        });\n    }\n\n    private void queryAvailableProducts() {\n        List&lt;QueryProductDetailsParams.Product&gt; productList = new ArrayList&lt;&gt;();\n\n        \/\/ \uc6d4\uac04 \uad6c\ub3c5\n        productList.add(\n                QueryProductDetailsParams.Product.newBuilder()\n                        .setProductId(&quot;monthly_sub&quot;)\n                        .setProductType(BillingClient.ProductType.SUBS)\n                        .build()\n        );\n\n        \/\/ \uc5f0\uac04 \uad6c\ub3c5\n        productList.add(\n                QueryProductDetailsParams.Product.newBuilder()\n                        .setProductId(&quot;yearly_sub&quot;)\n                        .setProductType(BillingClient.ProductType.SUBS)\n                        .build()\n        );\n\n        QueryProductDetailsParams params = QueryProductDetailsParams.newBuilder()\n                .setProductList(productList)\n                .build();\n\n        billingClient.queryProductDetailsAsync(\n                params,\n                (billingResult, productDetailsList) -&gt; {\n                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {\n                        for (ProductDetails productDetails : productDetailsList) {\n                            if (&quot;monthly_sub&quot;.equals(productDetails.getProductId())) {\n                                monthlySubscription = productDetails;\n                            } else if (&quot;yearly_sub&quot;.equals(productDetails.getProductId())) {\n                                yearlySubscription = productDetails;\n                            }\n                        }\n                    }\n                }\n        );\n    }\n\n    private void checkSubscriptionStatus() {\n        billingClient.queryPurchasesAsync(\n                QueryPurchasesParams.newBuilder()\n                        .setProductType(BillingClient.ProductType.SUBS)\n                        .build(),\n                (billingResult, purchases) -&gt; {\n                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {\n                        verifySubscriptionStatus(purchases);\n                    }\n                }\n        );\n    }\n\n    private void verifySubscriptionStatus(List&lt;Purchase&gt; purchases) {\n        boolean hasValidSubscription = false;\n\n        for (Purchase purchase : purchases) {\n            if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {\n                hasValidSubscription = true;\n                handlePurchase(purchase);\n            }\n        }\n\n        isSubscribed.postValue(hasValidSubscription);\n    }\n\n    public void purchaseSubscription(Activity activity, String productId) {\n        ProductDetails productDetails = &quot;monthly_sub&quot;.equals(productId) \n                ? monthlySubscription : yearlySubscription;\n\n        if (productDetails != null) {\n            List&lt;BillingFlowParams.ProductDetailsParams&gt; productDetailsParamsList = \n                    List.of(\n                            BillingFlowParams.ProductDetailsParams.newBuilder()\n                                    .setProductDetails(productDetails)\n                                    .setOfferToken(productDetails.getSubscriptionOfferDetails()\n                                            .get(0).getOfferToken())\n                                    .build()\n                    );\n\n            BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()\n                    .setProductDetailsParamsList(productDetailsParamsList)\n                    .build();\n\n            billingClient.launchBillingFlow(activity, billingFlowParams);\n        }\n    }\n\n    private void handlePurchase(Purchase purchase) {\n        if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {\n            if (!purchase.isAcknowledged()) {\n                AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder()\n                        .setPurchaseToken(purchase.getPurchaseToken())\n                        .build();\n\n                billingClient.acknowledgePurchase(params, billingResult -&gt; {\n                    if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {\n                        \/\/ \uc11c\ubc84\uc5d0 \uad6c\ub3c5 \uc815\ubcf4 \uc804\uc1a1\n                        sendSubscriptionToServer(purchase);\n                    }\n                });\n            }\n        }\n    }\n\n    private final PurchasesUpdatedListener purchasesUpdatedListener = \n            (billingResult, purchases) -&gt; {\n        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK \n                &amp;&amp; purchases != null) {\n            for (Purchase purchase : purchases) {\n                handlePurchase(purchase);\n            }\n            checkSubscriptionStatus();\n        }\n    };\n\n    public LiveData&lt;Boolean&gt; getIsSubscribed() {\n        return isSubscribed;\n    }\n\n    @Override\n    protected void onCleared() {\n        super.onCleared();\n        billingClient.endConnection();\n    }\n}<\/code><\/pre>\n<h2>SubscriptionActivity<\/h2>\n<pre><code class=\"language-java\">public class SubscriptionActivity extends AppCompatActivity {\n    private SubscriptionViewModel viewModel;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_subscription);\n\n        viewModel = new ViewModelProvider(this).get(SubscriptionViewModel.class);\n\n        \/\/ \uad6c\ub3c5 \uc0c1\ud0dc \uad00\ucc30\n        viewModel.getIsSubscribed().observe(this, isSubscribed -&gt; {\n            if (isSubscribed) {\n                \/\/ \uad6c\ub3c5 \uc0c1\ud0dc\uc5d0 \ub530\ub978 UI \uc5c5\ub370\uc774\ud2b8\n                showSubscribedUI();\n            } else {\n                showUnsubscribedUI();\n            }\n        });\n\n        \/\/ \uad6c\ub3c5 \ubc84\ud2bc \ud074\ub9ad \ub9ac\uc2a4\ub108\n        findViewById(R.id.btnSubscribeMonthly).setOnClickListener(v -&gt; \n                viewModel.purchaseSubscription(this, &quot;monthly_sub&quot;));\n\n        findViewById(R.id.btnSubscribeYearly).setOnClickListener(v -&gt; \n                viewModel.purchaseSubscription(this, &quot;yearly_sub&quot;));\n    }\n}<\/code><\/pre>\n<h2>\uc11c\ubc84 \ud1b5\uc2e0<\/h2>\n<pre><code class=\"language-java\">public class SubscriptionApiService {\n    private final String baseUrl = &quot;your_server_url&quot;;\n    private final OkHttpClient client = new OkHttpClient();\n\n    public void sendSubscriptionToServer(Purchase purchase) {\n        JSONObject json = new JSONObject();\n        try {\n            json.put(&quot;purchaseToken&quot;, purchase.getPurchaseToken());\n            json.put(&quot;productId&quot;, purchase.getProducts().get(0));\n            json.put(&quot;purchaseTime&quot;, purchase.getPurchaseTime());\n\n            RequestBody body = RequestBody.create(\n                    MediaType.parse(&quot;application\/json&quot;), \n                    json.toString()\n            );\n\n            Request request = new Request.Builder()\n                    .url(baseUrl + &quot;\/subscription\/verify&quot;)\n                    .post(body)\n                    .build();\n\n            client.newCall(request).enqueue(new Callback() {\n                @Override\n                public void onFailure(@NonNull Call call, @NonNull IOException e) {\n                    Log.e(&quot;SubscriptionApi&quot;, &quot;Error sending subscription data&quot;, e);\n                }\n\n                @Override\n                public void onResponse(@NonNull Call call, @NonNull Response response) {\n                    \/\/ \uc751\ub2f5 \ucc98\ub9ac\n                }\n            });\n        } catch (JSONException e) {\n            Log.e(&quot;SubscriptionApi&quot;, &quot;Error creating JSON&quot;, e);\n        }\n    }\n}<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>\uc548\ub4dc\ub85c\uc774\ub4dc &#8211; \uc778\uc571 \uad6c\ub3c5 \uc11c\ube44\uc2a4 \uad6c\ud604\ud558\uae30 (Java) \uad6c\uae00 \ud50c\ub808\uc774\uc5d0\uc11c\uc758 \uad6c\ub3c5\ucde8\uc18c \uc815\ubcf4\ub97c \uc11c\ubc84\ub85c \uc804\uc1a1\ud558\ub294 \ubd80\ubd84\uc740 \ub204\ub77d\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .\u2026 <span class=\"read-more\"><a href=\"https:\/\/www.skyer9.pe.kr\/wordpress\/?p=9633\">Read More &raquo;<\/a><\/span><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[32],"tags":[],"class_list":["post-9633","post","type-post","status-publish","format-standard","hentry","category-android"],"_links":{"self":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/9633","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=9633"}],"version-history":[{"count":3,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/9633\/revisions"}],"predecessor-version":[{"id":9636,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/9633\/revisions\/9636"}],"wp:attachment":[{"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=9633"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=9633"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.skyer9.pe.kr\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=9633"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}