Android는 내부적으로 결제를 할 수 있도록, API를 제공하고 있습니다. 이 문서는 인앱 결제에 대한 설명만을 포함하고 있습니다. 우선 앱이 프로덕션 혹은 베타테스트 출시 상태에서 보시면 좋으리라 생각합니다.

 

[선행작업]

출시용 APK 파일 만들기 : https://nicgoon.tistory.com/198
베타 버전으로 앱 등록하기 : https://nicgoon.tistory.com/199

 

[공식 문서]

안드로이드 개발자 사이트 : https://developer.android.com/
안드로이드 개발자 사이트 공식 문서 루트 : https://developer.android.com/docs
안드로이드 빌링 공식 문서 개요 : https://developer.android.com/google/play/billing/billing_overview

 

[주요 사이트]

구글 플레이 콘솔 : https://play.google.com/apps/publish

 

[참고 사이트]

샘플 소스 : https://github.com/googlesamples/android-play-billing/tree/master/TrivialDrive_v2
안드로이드 인앱 결제 구현 : https://jizard.tistory.com/137

 

 

 

1. 인앱 결제 종류.

구글 에서는 인앱 결제에 대해서, 3가지 종류를 지원 합니다. 첫 번째는 1회성 결제를 하는 상품 결제, 구독을 하는 결제, 그리고, 중간 광고등을 보고 그 에 대한 보상을 처리하는 보상형 결제 등이 있습니다. 이 문서는 틈틈히 수정되고 업데이트 될 것 입니다. 첫 번째로, 1회성 결제를 설명할 것이고, 그 외에 것들은 차차 업데이트를 하게 될 것 같습니다.

 

2. 인앱 결제 권한 추가 및 결제 라이브러리 추가.

먼저 위 선행작업을 완료하였거나, 그에 준하는 경험을 가지고 있다고, 보고 설명을 시작합니다. 

먼저, 구글 플레이 콘솔에가 APK 파일을 업로드 하고, 알파, 베타, 프로덕션 어느 버전이든, 출시를 시작합니다. 앱을 선택하고 왼쪽 메뉴 (출시 하기전 왼쪽 메뉴와 모습이 틀립니다.) 중 앱정보 > 인앱 상품 항목을 선택하면, 다음과 같은 문구를 볼 수 있습니다. 이미 APK 파일을 업로드 했지만, 인앱 결제 권한이 없어 항목을 사용할 수 없습니다. 

그런 이유로, 인앱 결제 권한을 가진 APK 파일을 업로드 한 뒤에야 (알파나 베타 출시까지 할 필요는 없고, apk 파일만 업로드 하면됨) 비로서, 인앱 상품을 등록할 수 있습니다.

 

먼저, 앱레벨 build.gradle 파일을 열어 dependecy 에 독립 모듈이 추가될 수 있도록, 아래와 같은 구문을 추가합니다.

dependencies {
    ...
    implementation 'com.android.billingclient:billing:1.2.2'
}

 

그 다음, AndroidManifest.xml 파일을 열어, 아래의 권한을 추가합니다.

<uses-permission android:name="com.android.vending.BILLING" />

 

빌드 후 APK 파일을 새로 등록하고, 새 버전으로 출시 합니다.

 

3. 관리되는 제품 등록.(왼쪽 메뉴의 앱 정보 > 인앱 상품, 선택)

 

관리되는 제품 > 관리되는 제품 만들기를 차례로 눌러, 아래와 같이 1회성 상품을 등록할 수 있는 페이지를 호출 합니다. 호출된 페이지에서, 적당한 값을 넣어, 상품을 만들어 줍니다. 주의 할 점은 상태 값인데, 활성 APK 상태로 두어야 상품이 구매 가능한 상태가 됩니다. 모든 값을 입력했다면, 저장 버튼을 눌러 줍니다.

 

다시 왼쪽 메뉴의 앱정보 > 인앱 상품을 선택하면, 상품이 추가된 것을 볼 수 있습니다.

 

4. 결제 클래스 생성.

관련문서 : https://developer.android.com/google/play/billing/billing_library_overview?hl=ko

결제 관련 모듈을 클래스를 따로 생성해서 호출 하는 식으로 사용해야 합니다. 꼭 예제를 보면서 만드세요. 예제속 Shared-module > BillingManager.java 파일속에 해당 내용이 들어 있습니다.

PurchasesUpdatedListener 를 상속한 결제 메니저, BillingManager 클래스를 생성합니다. (implements 로 다중 상속을 합니다. - 그렇게 안하면 오류남. ) 그리고, 빌링 클라이언트 객체를 생성하면, onBillingSetupFinished 메소드가 호출 됩니다. 문서에는 이 부분이 완료되면, 이 후 결제 작업을 준비 한다고 되어 있습니다.

해당 클래스에 일단 onPurchasesUpdated 메소드를 재정의 해 줍니다. (필수 재정의 메소드).

public class BillingManager implements PurchasesUpdatedListener {
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {

    }
}

아래는 billingClient를 생성하고, 구글 플레이 스토어와 연결이 되었을 때, 처리하는 코드 구글 플레이 스토어와 상태가 해제 되었을 때 처리하는 코드 입니다. 상속한 onBillingServiceDisconnected 메소드는 구글 플레이 스토어와의 접속을 잃었을 때 호출된다고 합니다. 이 때는 startConnection() 메소드를 재 호출 해 줘야 한다고 합니다. 그래서 접속이 끊어 졌는 지 확인할 수 있는 변수, isConnected 가 필요합니다. 공식예제 에서는 결제 모듈을 쓰레드에서 Rennable에서 처리를 하고 있습니다.

일단 저는 메뉴얼 대로 메인 쓰레드에서 처리하도록 구현할 것 입니다. 그래서 아래와 같은 소스가 됩니다. 여기까지 소스를 입력하고, 한 번 실행해 보면, 오류 코드3을 만나고 접속에 실패할 수 있습니다. 이 이유는 실제 기기에서만, 결제 모듈이 동작 하기 때 문입니다. (이 것 때문에 1시간을 헤메임).

public class BillingManager implements PurchasesUpdatedListener {


    // 필요 상수들.
    final String TAG = "IN-APP-BILLING";


    // 초기화 시 입력 받거나 생성되는 멤버 변수들.
    private BillingClient mBillingClient;
    private Activity mActivity;


    // 현재 접속 상태를 나타 냅니다.
    public enum connectStatusTypes { waiting, connected, fail, disconnected }
    public connectStatusTypes connectStatus = connectStatusTypes.waiting;



    // 생성자.
    public BillingManager( Activity _activity )
    {


        // 초기화 시 입력 받은 값들을 넣어 줍니다.
        mActivity = _activity;


        Log.d(TAG, "구글 결제 매니저를 초기화 하고 있습니다.");

        // 결제를 위한, 빌링 클라이언트를 생성합니다.
        mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();


        // 구글 플레이와 연결을 시도합니다.
        mBillingClient.startConnection(new BillingClientStateListener() {

            // 결제 작업 완료 가능한 상태.
            @Override
            public void onBillingSetupFinished(int responseCode) {

                // 접속이 성공한 경우, 처리.
                if (responseCode == BillingClient.BillingResponse.OK) {
                    connectStatus = connectStatusTypes.connected;
                    Log.d(TAG, "구글 결제 서버에 접속을 성공하였습니다.");
                }

                // 접속이 실패한 경우, 처리.
                else {

                    connectStatus = connectStatusTypes.fail;
                    Log.d(TAG, "구글 결제 서버 접속에 실패하였습니다.\n오류코드:" + responseCode);
                }

            }

            // 결제 작업 중, 구글 서버와 연결이 끊어진 상태.
            @Override
            public void onBillingServiceDisconnected() {

                connectStatus = connectStatusTypes.disconnected;
                Log.d(TAG, "구글 결제 서버와 접속이 끊어졌습니다.");

            }


        });


    }


    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {

    }
}

위 소스를 이용해 클래스를 만들 고 있을 액티비티에서 호출 하면됩니다.

 

구글 결제 시스템이 초기화 되면, 구글에 등록된 상품의 정보를 요청할 수 있습니다. 이를 통해 해당 아이디들이 유효한지 확인할 수 있습니다. String List를 이용해 Sku 배열을 만들고, 요청할 상품들의 아이디를 추가해 줍니다. 그리고, 해당 상품들의 자세한 정보를 받아 오면됩니다.

구글 플레이 콘솔로가 등록된 상품들의 정보를 확인합니다. (물론 자신이 등록했으므로, 알고 있을 경우, 굳이 확인하지 않아도 됩니다.) 구글 플레이 콘솔 접속, 왼쪽메뉴에서 앱 정보 > 인앱 상품 > 관리되는 제품을 차례로 선택하면 등록된 상품을 확인할 수 있습니다.

그리고, 지금까지 만들어 왔던 빌드 매니저에, 구입 가능한 상품의 정보를 받아 오는 코드를 추가 합니다.


    // 구입 가능한 상품의 리스트를 받아 오는 메소드 입니다.
    public void get_Sku_Detail_List()
    {


        // 구글의 상품 정보들의 ID를 만들어 줍니다.
        List<String> Sku_ID_List = new ArrayList<>();
        Sku_ID_List.add( "gold_100" );


        // SkuDetailsParam 객체를 만들어 줍니다. (1회성 소모품에 대한)
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList( Sku_ID_List ).setType(BillingClient.SkuType.INAPP);


        // 비동기 상태로 앱의, 정보를 가지고 옵니다.
        mBillingClient.querySkuDetailsAsync(params.build() , new SkuDetailsResponseListener()
                {

                    @Override
                    public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {


                        // 상품 정보를 가지고 오지 못한 경우, 오류를 반환하고 종료합니다.
                        if( responseCode != BillingClient.BillingResponse.OK)
                        {
                            Log.d(TAG, "상품 정보를 가지고 오던 중 오류를 만났습니다. 오류코드 : " + responseCode);
                            return;
                        }

                        // 하나의 상품 정보도 가지고 오지 못했습니다.
                        if( skuDetailsList == null )
                        {
                            Log.d(TAG, "상품 정보가 존재하지 않습니다.");
                            return;
                        }


                        // 응답 받은 데이터들의 숫자를 출력 합니다.
                        Log.d(TAG, "응답 받은 데이터 숫자:" + skuDetailsList.size());

                        // 받아 온 상품 정보를 차례로 호출 합니다.
                        for( int _sku_index = 0;_sku_index < skuDetailsList.size(); _sku_index++)
                        {

                            // 해당 인덱스의 객체를 가지고 옵니다.
                            SkuDetails _skuDetail = skuDetailsList.get( _sku_index );

                            // 해당 인덱스의 상품 정보를 출력하도록 합니다.
                            Log.d( TAG, _skuDetail.getSku() + ": " + _skuDetail.getTitle() + ", " + _skuDetail.getPrice() + ", " + _skuDetail.getDescription() );

                        }


                        // 받은 값을 멤버 변수로 저장합니다.
                        mSkuDetailsList = skuDetailsList;


                    }
                }
        );



    }

그리고, 실행해 보면, 지정한 상품의 정보를 받아 올 수 있습니다. 당연한 이야기 지만 구글 결제 시스템과 접속이 성공적으로 이뤄진 상태에서 실행해야 합니다.

 

드디어 결제를 요청하는 부분 입니다. 당연한 이야기지만, 우리는 콜백만 받으면, 되므로, 결제는 사실상 끝입니다. 앞서 받았던, List<SkuDetails> 에서 구매를 원하는 혹은 유저가 선택한, SkuDetails 을 선택해 결제 요청창을 띄워 주시면 됩니다. 공식 메뉴얼에는 launchBillingFlow 에 1개의 인수만 넣도록 되어 있지만, 실제로 해 보면, 첫 인수에 Activity를 요청하고 있습니다. 위 단계들을 모두 거친뒤 요청해야 합니다.

    // 실제 구입 처리를 하는 메소드 입니다.
    public void purchase( SkuDetails skuDetails )
    {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        int responseCode = mBillingClient.launchBillingFlow(mActivity, flowParams);
    }

이 코드를 실행해 보면 아래와 같은 결제 창을 보실 수 있을 것 입니다.

 

처음 결제 클래스를 만들 때, PurchasesUpdatedListener 클래스를 상속하기 위해, 기본적으로 정의 해 두었던, onPurchasesUpdated 메소드가 생각이 나시나요? 이 메소드는 결제 처리 결과를 받는 메소드이며, 이제 이 것을 정의 하므로써, 결제 부분은 코드가 끝이 납니다. 이 메소드가 호출되는 시점은, 유저가 결제를 하던, 결제 실패를 하던, 어떤 이유로 결제가 실패해 결제 창이 닫히는 경우 발생합니다. 코드는 간단하므로, 더 설명 드리지 않겠습니다.

    // 결제 처리를 하는 메소드.
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {


        // 결제에 성공한 경우.
        if( responseCode == BillingClient.BillingResponse.OK && purchases != null )
        {

            Log.d( TAG, "결제에 성공했으며, 아래에 구매한 상품들이 나열될 것 입니다." );

            for( Purchase _pur : purchases )
            {
                Log.d( TAG, "결제에 대해 응답 받은 데이터 :"+ _pur.getOriginalJson() );
            }
        }

        // 사용자가 결제를 취소한 경우.
        else if( responseCode == BillingClient.BillingResponse.USER_CANCELED )
        {
            Log.d( TAG, "사용자에 의해 결제가 취소 되었습니다." );
        }

        // 그 외에 다른 결제 실패 이유.
        else
        {
            Log.d( TAG, "결제가 취소 되었습니다. 종료코드 : " + responseCode );
        }



    }

 

5. 소모품 소모.

한 번 구매후 효과가 영구히 지속되는 경우, 아이템을 한 번만 구매해, 이 과정이 필요 없습니다. 하지만 대부분의 경우, 아이템을 재 구매해야 하므로, 소모품을 소모 하지않으면 아이템의 재 구매가 불가능합니다. 그냥 아이템 구매후 소모품을 소모하도록 하면되며, 비동기 처리를 하는 메소드 이므로, 아주 편리 합니다.

1) 소모품 소모후 콜백 메소드.

콜백 메소드를 처리하는 ConsumeResponseListener 객체를 멤벼 번수로 선언하고, 이것을 생성자에서 정의해 주도록 합니다.

// 멤버 변수로 선언해 줍니다.
private ConsumeResponseListener mConsumResListnere;



// 생성자에 리스너를 정의해 변수에 저장합니다.
mConsumResListnere = new ConsumeResponseListener() {
    @Override
    public void onConsumeResponse(int responseCode, String purchaseToken) {

        // 성공적으로 아이템을 소모한 경우.
        if( responseCode == BillingClient.BillingResponse.OK )
        {
            Log.d( TAG, "상품을 성공적으로 소모하였습니다. 소모된 상품=>" + purchaseToken );
            return;
        }

        // 성공적으로 아이템을 소모한 경우.
        else
        {
            Log.d( TAG, "상품 소모에 실패하였습니다. 오류코드 ("+responseCode+"), 대상 상품 코드:" + purchaseToken );
            return;
        }



    }
};

 

2) 결제 후 소모 메소드 호출 하기.

기존의 결제 리스너에, 성공 메시지가 오면, 소모품으로 아이템을 소모 하도록 호출합니다.

    // 결제 처리를 하는 메소드.
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {


        // 결제에 성공한 경우.
        if( responseCode == BillingClient.BillingResponse.OK && purchases != null )
        {

            Log.d( TAG, "결제에 성공했으며, 아래에 구매한 상품들이 나열될 것 입니다." );

            for( Purchase _pur : purchases )
            {
                Log.d( TAG, "결제에 대해 응답 받은 데이터 :"+ _pur.getOriginalJson() );


                mBillingClient.consumeAsync( _pur.getPurchaseToken(), mConsumResListnere );

            }
        }

        // 사용자가 결제를 취소한 경우.
        else if( responseCode == BillingClient.BillingResponse.USER_CANCELED )
        {
            Log.d( TAG, "사용자에 의해 결제가 취소 되었습니다." );
        }

        // 그 외에 다른 결제 실패 이유.
        else
        {
            Log.d( TAG, "결제가 취소 되었습니다. 종료코드 : " + responseCode );
        }



    }

 

실행을 해 보면, 매우 결제가 잘 됩니다. 이 후 이 결제가 유효한 결제인지 서버 혹은 이 앱내에서 하게 됩니다. 앱내에서 검증은 신뢰할 수 없고, 저는 필요치 않아 다음에 서버 검증에 대한 글을 쓰게 될 것 같습니다. 앱내 검증이 필요하신 분은 아래 문서의 후반부를 참고 하시기 바랍니다.

 

Use the Google Play Billing Library  |  Android Developers

This document explains how to add Google Play Billing into your app using the Google Play Library. Specifically, this document covers how to add Google Play Billing functionality that's common to all in-app product types: one-time products, rewarded produc

developer.android.com

 

 

그럼 빌링매니저 풀 소스를 남겨 드립니다. 많은 분들에게 도움이 되었으면 좋겠습니다.

package com.tistory.nicgoon.android.inapptest.billing;

import android.app.Activity;
import android.support.annotation.Nullable;
import android.util.Log;

import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;

import java.util.ArrayList;
import java.util.List;

public class BillingManager implements PurchasesUpdatedListener {


    // 필요 상수들.
    final String TAG = "IN-APP-BILLING";


    // 초기화 시 입력 받거나 생성되는 멤버 변수들.
    private BillingClient mBillingClient;
    private Activity mActivity;
    private ConsumeResponseListener mConsumResListnere;


    // 현재 접속 상태를 나타 냅니다.
    public enum connectStatusTypes { waiting, connected, fail, disconnected }
    public connectStatusTypes connectStatus = connectStatusTypes.waiting;


    // 결제를 위해 가지온 상품 정보를 저장한 변수 입니다.
    public List<SkuDetails> mSkuDetailsList;


    // 생성자.
    public BillingManager( Activity _activity )
    {


        // 초기화 시 입력 받은 값들을 넣어 줍니다.
        mActivity = _activity;


        Log.d(TAG, "구글 결제 매니저를 초기화 하고 있습니다.");

        // 결제를 위한, 빌링 클라이언트를 생성합니다.
        mBillingClient = BillingClient.newBuilder(mActivity).setListener(this).build();


        // 구글 플레이와 연결을 시도합니다.
        mBillingClient.startConnection(new BillingClientStateListener() {

            // 결제 작업 완료 가능한 상태.
            @Override
            public void onBillingSetupFinished(int responseCode) {

                // 접속이 성공한 경우, 처리.
                if (responseCode == BillingClient.BillingResponse.OK) {
                    connectStatus = connectStatusTypes.connected;
                    Log.d(TAG, "구글 결제 서버에 접속을 성공하였습니다.");
                }

                // 접속이 실패한 경우, 처리.
                else {

                    connectStatus = connectStatusTypes.fail;
                    Log.d(TAG, "구글 결제 서버 접속에 실패하였습니다.\n오류코드:" + responseCode);
                }

            }

            // 결제 작업 중, 구글 서버와 연결이 끊어진 상태.
            @Override
            public void onBillingServiceDisconnected() {

                connectStatus = connectStatusTypes.disconnected;
                Log.d(TAG, "구글 결제 서버와 접속이 끊어졌습니다.");

            }


        });



        // 소모성 상품을 소모한 후, 응답 받는 메소드 입니다.
        mConsumResListnere = new ConsumeResponseListener() {
            @Override
            public void onConsumeResponse(int responseCode, String purchaseToken) {

                // 성공적으로 아이템을 소모한 경우.
                if( responseCode == BillingClient.BillingResponse.OK )
                {
                    Log.d( TAG, "상품을 성공적으로 소모하였습니다. 소모된 상품=>" + purchaseToken );
                    return;
                }

                // 성공적으로 아이템을 소모한 경우.
                else
                {
                    Log.d( TAG, "상품 소모에 실패하였습니다. 오류코드 ("+responseCode+"), 대상 상품 코드:" + purchaseToken );
                    return;
                }



            }
        };

    }


    // 구입 가능한 상품의 리스트를 받아 오는 메소드 입니다.
    public void get_Sku_Detail_List()
    {


        // 구글의 상품 정보들의 ID를 만들어 줍니다.
        List<String> Sku_ID_List = new ArrayList<>();
        Sku_ID_List.add( "gold_500" );


        // SkuDetailsParam 객체를 만들어 줍니다. (1회성 소모품에 대한)
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList( Sku_ID_List ).setType(BillingClient.SkuType.INAPP);


        // 비동기 상태로 앱의, 정보를 가지고 옵니다.
        mBillingClient.querySkuDetailsAsync(params.build() , new SkuDetailsResponseListener()
                {

                    @Override
                    public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList) {


                        // 상품 정보를 가지고 오지 못한 경우, 오류를 반환하고 종료합니다.
                        if( responseCode != BillingClient.BillingResponse.OK)
                        {
                            Log.d(TAG, "상품 정보를 가지고 오던 중 오류를 만났습니다. 오류코드 : " + responseCode);
                            return;
                        }

                        // 하나의 상품 정보도 가지고 오지 못했습니다.
                        if( skuDetailsList == null )
                        {
                            Log.d(TAG, "상품 정보가 존재하지 않습니다.");
                            return;
                        }


                        // 응답 받은 데이터들의 숫자를 출력 합니다.
                        Log.d(TAG, "응답 받은 데이터 숫자:" + skuDetailsList.size());

                        // 받아 온 상품 정보를 차례로 호출 합니다.
                        for( int _sku_index = 0;_sku_index < skuDetailsList.size(); _sku_index++)
                        {

                            // 해당 인덱스의 객체를 가지고 옵니다.
                            SkuDetails _skuDetail = skuDetailsList.get( _sku_index );

                            // 해당 인덱스의 상품 정보를 출력하도록 합니다.
                            Log.d( TAG, _skuDetail.getSku() + ": " + _skuDetail.getTitle() + ", " + _skuDetail.getPrice() + ", " + _skuDetail.getDescription() );
                            Log.d( TAG, _skuDetail.getOriginalJson() );


                        }


                        // 받은 값을 멤버 변수로 저장합니다.
                        mSkuDetailsList = skuDetailsList;


                    }
                }
        );



    }


    // 실제 구입 처리를 하는 메소드 입니다.
    public void purchase( SkuDetails skuDetails )
    {
        BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                .setSkuDetails(skuDetails)
                .build();
        int responseCode = mBillingClient.launchBillingFlow(mActivity, flowParams);
    }


    // 결제 처리를 하는 메소드.
    @Override
    public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases) {


        // 결제에 성공한 경우.
        if( responseCode == BillingClient.BillingResponse.OK && purchases != null )
        {

            Log.d( TAG, "결제에 성공했으며, 아래에 구매한 상품들이 나열될 것 입니다." );

            for( Purchase _pur : purchases )
            {
                Log.d( TAG, "결제에 대해 응답 받은 데이터 :"+ _pur.getOriginalJson() );


                mBillingClient.consumeAsync( _pur.getPurchaseToken(), mConsumResListnere );

            }
        }

        // 사용자가 결제를 취소한 경우.
        else if( responseCode == BillingClient.BillingResponse.USER_CANCELED )
        {
            Log.d( TAG, "사용자에 의해 결제가 취소 되었습니다." );
        }

        // 그 외에 다른 결제 실패 이유.
        else
        {
            Log.d( TAG, "결제가 취소 되었습니다. 종료코드 : " + responseCode );
        }



    }







}

 

 

 

 

 

Posted by 창업닉군
,