[한글번역] WeChat Pay – APP端开发步骤说明

WeChat Pay App Dev Menual

원문 : APP端开发步骤说明

참고 : 아래의 번역문은 제가 직접 번역을 한 것은 아닙니다. Flitto에서 직접 한국어, 영어 번역을 참여해서 획득한 포인트로 위챗페이 개발에 필요한 문서를 번역하기 위해 번역요청(중국어->한국어)을 보내서 받은 번역문입니다. 혹시라도 위챗개발 문서가 필요한 개발자분들에게 도움이 되고자 공유합니다. 다수의 번역가가 참여했기 때문에 전체 문장이 다소 어색할 수 있음을 알려드립니다. 그리고 안드로이드 메뉴얼만 번역하였고, 4000원 비용이 들었습니다.

Android 개발 요점 설명

1. 백그라운드 설정

위챗 오픈플랫폼에서 개발 어플리케이션을 신청하시면 위챗 오픈플랫폼에서는 어플리케이션의 고유 식별 아이디를 생성해 드립니다. 안전한 결제를 위해 오픈플랫폼에 업체의 어플리케이션 패키지 이름과 어플리케이션 서명(signature)을 연동시키셔야 합니다. 연동 후에 정상적으로 결제가 가능합니다. 설정화면은 <오픈플랫폼>의 <관리센터/어플리케이션 수정/개발데이터 수정>메뉴에 있으며 그림 8.8의 빨간 네모로 표시되어 있는 부분입니다.

8.8

패키지명: 어플리케이션 프로젝트의 AndroidManifest.xml에 설정한 package 이름값이다. 예를들어 DEMO에서 package값은 net.sourceforge.simcpux.

어플리케이션 서명: 프로젝트의 패키지명 및 컴파일에 사용된 keystore을 signing툴을 사용하여 생성한 32비트길이의 MD5문자열입니다. 테스트기기에 signing툴을 설치하고 실행하면 application의 서명을 생성합니다. 그림8.9와 같이 녹색문자열이 곧 어플리케이션의 서명이다. signing툴 다운로드 url https://open.weixin.qq.com/zh_CN/htmledition/res/dev/download/sdk/Gen_Signature_Android.apk

8.9

2. APP ID 등록하기

WeChat jar패킷을 개발자의 앱프로젝트로 가져옵니다. 하지만, API를 사용하려면 우선 위챗으로 APPID를 등록해야 합니다. 코드는 아래와 같습니다:

final IWXAPI msgApi = WXAPIFactory.createWXAPI(context, null);
// app id를 위챗에 등록하기
msgApi.registerApp("wxd930ea5d5a258f4f");

3. 결제 호출

업체서버로부터 결제 청구를 생성합니다. 먼저 통합구매 API를 호출해(자세한 내용은 7장 참조) 구매리스트를 생성 후, prepay_id 값을 받아서 다시 사인 후 앱에 전송하여 결제 청구를 제출한다. 아래는 웨이신 결제를 호출하는 메인 코드입니다.

IWXAPI api;
PayReq request = new PayReq();
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.packageValue = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "7FFECB600D7157C5AA49810D2D8F28BC2811827B";
api.sendReq(req);

주의: sign 필드명으로 생성되는 리스트는 API 발급 설정을 참고하세요.

4. 지불결과 콜백

위쳇 SDK Sample을 참조하여, net.sourceforge.simcpux.wxapi 패캣경로에서 WXPayEntryActivity클래스를 생성합니다.(패킷명 혹은 클래스명이 일치하지 않으면 콜백이 불가능합니다), WXPayEntryActivity클래스에서 onResp 함수 구현, 지불완료 후 위쳇 엡은 판매자 어플리케이션으로 돌아가면서 onResp 함수를 콜백합니다. 개발자는 해당 함수내에서 알림메세지를 받아야하며 기존으로 돌아가기 위한 에러코드를 판단해야 합니다. 지불 성공 후 백오피스로 가서 지불결과를 체크한 후, 유저의 실제 지불결과를 보여줍니다. 클라이언트의 복귀를 유저 결제결과로 적용하지 않도록 꼭 주의해야 합니다. 서버사이드에서 받은 결제 알림메세지 혹은 API복귀결과를 체크하여 기준으로 해야 합니다, 코드예시는 다음과 같습니다:

@Override
public void onResp(BaseResp resp) {
if (resp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX){
Log.d(TAG,"onPayFinish,errCode=" + resp.errCode);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.app_tip);
}
}

콜백한 errCode 리스트:

명칭 기술 솔루션
0 성공 성공화면
-1 오류 발생가능한이유 : 사인(sign)오류, APPID 미등록, 프로젝트상에서 APPID설정오류, 등록된APPID와 세팅한것 맞지 않는 경우, 그 외
-2 사용자취소 발생가능한이유 : 사용자취소버튼 누르고 APP으로 복귀

ListView의 addHeaderView(Footer)를 RecyclerView 에서 구현하기

안드로이드 앱 개발시 가장 많이 사용하던 위젯은 ListView이다. 그리고 RecyclerView는 Android 5 Lollipop 버전이 업데이트 되었을 때, Support Library V7에 추가되었고 이를 사용하기 위해서는 build.gradle에 다음과 같이 추가하면 사용할 수 있다.

compile 'com.android.support:recyclerview-v7:${supportLibraryVersion}'

기존에 ListView에서 사용하던 상, 하단에 뷰 추가를 지원하던 addHeaderViewaddFooterView가 RecyclerView에서는 더이상 지원하지 않는다. 그래서 대부분의 안드로이드 개발자들은 StackOverFlow에 ‘How to add header view to recyclerview’ 다음과 같이 검색했을 것이다. 이미 RecyclerView에 HeaderView나 FooterView를 추가해주는 오픈 소스는 많이 공개되어 있다. 필요하다면 찾아서 사용하면 쉽게 뷰를 상하단에 추가할 수 있다. 결과적으로 기존 ListView에서는 addHeaderView가 어떻게 동작 했으며 RecyclerView에서는 그 기능을 동작하도록 할 수 있는지 한번 알아보려고 한다.

HeaderViewListAdapter (android.widget)
public void addHeaderView(View v, Object data, boolean isSelectable) {
    ...
    // Wrap the adapter if it wasn't already wrapped.
    if (mAdapter != null) {
        if (!(mAdapter instanceof HeaderViewListAdapter)) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
        }
    …
    }
}

ListView의 addHeaderView의 내부를 들여다 보면 기존의 ListAdapter 대신에 HeaderViewListAdapter 어댑터 클래스를 사용하며 이 위젯은 래핑에 필요한 WrapperListAdapter 인터페이스를 상속하고 있으며 Adapter를 가지고 있는 껍데기가 된다. 기존의 Adapter를 HeaderViewListAdapter로 재생성(Wrapping)하면서 mAdapter를 파라미터로 넘겨주는 이유가 바로 이 때문이다. 따라서 HeaderViewListAdapter에서는 기존의 Adapter를 그대로 가지고 있으면서 화면에 상하단에 추가된 뷰를 노출시켜주는 함수를 더 가지고 있다. 다음으로 getView함수를 살펴보자.

public View getView(int position, View convertView, ViewGroup parent) {
    // Header (negative positions will throw an IndexOutOfBoundsException)
    int numHeaders = getHeadersCount();
    if (position < numHeaders) {
        return mHeaderViewInfos.get(position).view;
    }

    // Adapter
    final int adjPosition = position - numHeaders;
    int adapterCount = 0;
    if (mAdapter != null) {
        adapterCount = mAdapter.getCount();
        if (adjPosition < adapterCount) {
            return mAdapter.getView(adjPosition, convertView, parent);
        }
    }

    // Footer (off-limits positions will throw an IndexOutOfBoundsException)
    return mFooterViewInfos.get(adjPosition - adapterCount).view;
}

Header로 추가된 상단뷰의 수보다 position이 작은 경우에는 HeaderView를 리턴해주고 ListView에서 Row Item의 position은 Header 수 만큼 빼서 그려준다. 상하단에 추가된 뷰가 있을 경우, 그것을 보여주고 그게 아니라면 Adapter의 기본 로직으로 동작하게 된다.

HeaderRecyclerViewAdapter 구현하기
public abstract class HeaderRecyclerViewAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {

개발 중인 서비스에서 다양한 데이터 타입이나 뷰를 사용 한다면 일일이 대응되는 클래스(RecyclerView.Adapter 상속 받는)를 생성하기 보다는 HeaderRecyclerView를 사용하거나 구현 방법을 한번 보는 것을 추천한다. 목록을 보여주는 ListView와 RecyclerView는 기본 개념이 같이 때문에 내부 동작 또한 크게 차이는 없을 것이다.

달라진 것이 있다면 ListView에서 종종 findViewById의 비용을 줄이기 위해 사용하던 ViewHolder가 RecyclerView.ViewHolder로 추가되어 예전에는 Adapter에서 getView를 통해 View를 생성했다면 이제는 ViewHolder를 생성하고(onCreateViewHolder) 데이터 바인딩할 때(onBindViewHolder), 파라미터로 다시 받아 사용할 수 있다.

public class HeaderRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

  private int TYPE_HEADER = -1;
  private int TYPE_ROW_ITEM = -2;

…
  public void addHeaderView(View header) {
    mHeaderViews.add(header);
  }

  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (isHeaderPosition(position)) {
      return new ViewHolder(mHeaderViews.get(position));
    } else {
      return mInnerAdapter.onCreateViewHolder(parent, position);
    }
  }

  @Override
  public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderPosition(position)) {
      //  bind HeaderViewHolder
    } else {
      //  bind ItemViewHolder
    }
  }

  @Override
  public int getItemViewType(int position) {
    // return 3 item type by position
  }
…

ListView의 addHeaderView 함수를 HeaderRecyclerViewAdapter에 추가한다. 그리고 RecyclerView.Adapter가 position과 ViewHolder의 타입에 따라 동작해야 하므로 위의 3가지의 함수가 동작에 있어서 가장 중요하다. 마찬가지로 만약에 FooterView를 RecyclerView에 추가하고 싶을 경우, 뷰의 식별자를 선언하고 ViewHolder를 생성하는 부분과 데이터를 바인딩하는 부분에 추가한다. 그리고 getItemViewType에서 화면상에 FooterView가 노출되는 position일 때 리턴해주면 된다. 마지막으로 앞으로 자주 사용할 RecyclerView와 많이 사용했던 ListView를 자유롭게 다뤄보자.

decompile apk (APK 디컴파일하기)

가끔 진행하는 앱의 코드 난독화 확인을 위해서 기록해 두려고 한다. APK 디컴파일(Decompile)을 악용하지 말았으면 한다. 안드로이드 애플리케이션이 컴파일되면 .dex 파일이 생성된다. dex는 Dalvik Executable로 Dalvik에서 실행할 수 있는 파일이며 컴파일된 코드 파일을 의미한다.

Download dex2jar tool

1. dex파일을 jar(.class files)로 변환하기 위해서 dex2jar를 다운로드 한다.

2. Signed APK 파일의 확장명을 apk에서 zip으로 변경한다.

3. zip 압축을 풀면 classes.dex 파일을 다운받은 dex2jar 폴더로 복사한다.

$ ./d2j-dex2jar.sh classes.dex
sh: ./d2j-dex2jar.sh: Permission denied

4. 위처럼 dex2jar.sh 실행하면 권한 거부로 실행할 수 없게 된다.

$ chmod 764 *.sh
$ ./d2j-dex2jar.sh classes.dex
dex2jar classes.dex -> ./classes-dex2jar.jar

실행 권한을 변경한 후 다시 실행하면 classes-dex2jar.jar 파일이 생성된다.

Download JD_GUI

JD(Java Decompiler)-GUIjar파일 안에 있는 class파일들을 보기 위해서 다운로드 한다.

생성된 classes-dex2jar.jar를 열면 모든 class파일들을 볼 수 있다.

CachePot으로 Activity 또는 Fragment 사이 간단한 데이터 통신

Java에서 흔히 객체로 데이터 통신 하기 위해 객체에 Serializable 인터페이스를 상속 받은 뒤 직렬화된 객체를 바이트 단위로 분해하여 전송한다. Android에서도 마찬가지로 Serializable을 사용하긴 하지만 보다 Parcelable을 많이 사용하고 있다. 하지만 Parcelable을 사용하기 위해서는 객체에 선언된 데이터가 추가 될 때마다 writeToParcelreadFromParcel을 통해 Parcel 객체에 읽고 쓰는 작업을 추가해야 데이터 전송이 가능하다. CachePot은 간단한 앱을 만들거나 Intent를 통한 다른 애플리케이션과 통신이 없다면 간단하게 데이터 캐시를 할 수 있는 인스턴스가 있으면 좋겠다고 생각되어 Generic을 사용해 간단하게 만든 안드로이드 라이브러리이다.

Download

현재 Gradle을 사용하는 중이라면 build.gradle에 아래를 추가하자.

repositories {
      jcenter()
}

dependencies {
      compile 'com.github.kimkevin:cachepot:1.0.0'
}

How To Use?

동기식 데이터 전달하기

  • Between Activity and A ctivity
  • Between Activity and Fragment
  • Between Fragment and Fragment

데이터가 메모리에 올라가기 때문에 결국은 사용자 인테페이스를 구성하는 Activity와 Fragment 등 아무대서나 동기적으로 데이터 전달이 가능하다.

1. Model 객체 전달

새로운 화면 생성 전 데이터 저장하기

KoreanFood foodItem = new KoreanFood(1, "Kimchi", "Traditional fermented Korean side dish made of vegetables");
CachePot.getInstance().push(foodItem);

새로운 화면 생성 후 데이터 가져오기

public class MainFragment extends Fragment{
    private KoreanFood foodItem;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        foodItem = CachePot.getInstance().pop(KoreanFood.class);
    }
}

2. Collection 또는 Map 객체 전달

새로운 화면 생성 전 데이터 저장하기

List<KoreanFood> foodItems = new ArrayList<>();
foodItems.add(new KoreanFood(1, "Kimchi", "Traditional fermented Korean side dish made of vegetables"));
foodItems.add(new KoreanFood(2, "Kkakdugi", "A variety of kimchi in Korean cuisine"));
CachePot.getInstance().push(foodItems);

새로운 화면 생성 후 데이터 가져오기

List<KoreanFood> foodItems = CachePot.getInstance().pop(ArrayList.class);

ArrayList나 HashMap, Stack, LinkedList 등과 같이 구현 클래스도 일반적인 모델 데이터를 전달하는 것과 같은 방식으로 데이터 전송이 가능하다.

비동기식 데이터 전달하기

ViewPager에서 position별로 Fragment에 객체 전달

요즘 앱에서 흔하게 ViewPagerFragmentStatePagerAdapter를 사용하고 있다. 그리고 Adapter에서는 getItem(int position)이 호출 될 때 Fragment를 생성하게 되는데 새로운 Fragment가 생성되고 CachePot 인스턴스에 저장된 데이터를 가져오기 전에 새로운 데이터를 캐시하게 되어 데이터를 가져올 때 오류가 발생하게 된다. 이를 방지하기 위해서 Fragment의 postiion을 함께 저장할 수 있는 기능을 제공한다.

Framgnet 생성 전에 position과 데이터 저장하기

private class PagerAdapter extends FragmentStatePagerAdapter {
    ...
    public Fragment getItem(int position) {
        CachePot.getInstance().push(position, foodItems.get(position));
        return FoodFragment.newInstance(position);
    }
}

생성된 Fragment에서 데이터 가져오기

public static FoodFragment newInstance(int position) {
    FoodFragment fragment = new FoodFragment();
    Bundle args = new Bundle();
    args.putInt(ARG_POSITION, position);
    fragment.setArguments(args);
    return fragment;
}
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
        final int position = getArguments().getInt(ARG_POSITION);
        koreanFoodItem = CachePot.getInstance().pop(position);
    }
}

제3회 부산모바일앱공모전 대상

부산모바일앱공모전?

부산지역 모바일 앱분야 우수개발자를 발굴, 육성함으로써 지역 모바일 앱산업 및 창업 활성화를 도모, 부산시민을 대상으로 편의성 제공 및 부산경제에 도움이 되는 공공 서비스용 앱 개발을 위한 매년 개최되는 모바일 어플리케이션 공모전이다.

작년에 참가 했던 제3회 부산모바일앱공모전에서 “터틀넥 목디스크” 라는 애플리케이션으로 대상이라는 큰 상이 나에게 왔다. 상금도 작은 금액이 아니었기에 사실 너무 과분하다는 생각이 들기도 했다. 공모전 참여 과정은 프로젝트 신청서를 먼저 제출하고, 개발 한 뒤에 결과물을 홈페이지에 등록하면, 심사 기간을 거쳐 공지된다. 공지된 사람들에 한에서 IT 전문가들 6~7명 정도 앞에서 작품에 대한 프리젠테이션을 해야되고, 최종적으로 심사 발표가 난다. 지금 돌이켜 생각해보면 많은 IT전문가들 앞이라 대답할 때, 그 만큼 긴장을 했기에 대답을 시원하게 하지는 못했기 때문에 상을 받기 어려울 것 같다는 생각을 했었다. 지금 생각나는 질문은 “배터리 문제는 어떻게 생각하시나요?”, “올바른 스마트폰 자세의 의학적 근거는 있나요?” 등 30 ~ 40분 가량 equipment 하나같이 폭풍같은 질문이었던 것으로 기억한다.

목디스크 앱을 만든 이유는?

목디스크를 관련한 아이디어로 앱을 만들 수 있었던건, 학교 가던 버스안에서 고개 숙여 스마트폰을 뚫어져라 보던 사람들, 우연히 뉴스 기사로 읽었던 스마트폰으로 인한 목디스크의 심각성을 알게 되면서 ‘어떻게 하면 저 사람들의 고개를 덜 숙이게 할 수 있을까?’ 라는 문제를 제기하게 되었고, 해결 방법으로 스마트폰의 기울기 센서를 이용했다. 사실 그 땐, 기울기 센서를 이용한 게임이 많이 출시되고 있었고, 당시 게임 개발을 하고 있었기 때문에, 이것을 이용하면 사람들의 자세 교정에 도움이 될 것 같다는 생각을 했다. 사실 우연히 발견한 작은 문제와 호기심으로 시작했고 이렇게 큰 결과를 가질 것이라고 예상을 하지 못했다.

부산 시민 모두가 참여할 수 있는 대화라 많은 분들이 팀 단위로 공모전에 참여했었고, 발표를 대기할 때 혼자라 외롭기도 했었다. 팀으로 참여를 한 것이 아니기에 혼자 기획, 개발, 디자인할 때는 많이 힘들었지만 지나고 나면 좋은 추억이 되듯이 개발자로 성장하는데 좋은 믿거름이 될 것 같다. 우리 주위에 찾아보면 항상 좋은 기회들은 많이 있고, 얼마만큼 자신이 하고 싶고 즐길 수 있느냐 잘 생각해본다면, 결과는 나쁘더라도 좋은 경험이 Beautiful 될 것같다. 그리고 마지막으로 이렇게 큰 상을 주신 부산광역시와 부산정보산업진흥원에 감사합니다.

미디어

“제3회 부산 모바일 앱 공모전 수상작” 부산정보산업진흥원
“부산 모바일 앱 공모전 ‘터틀넥- 자라목’ 대상” 부산일보
“부산 모바일 앱 공모전 ‘자라목’ 대상” 국제신문
“터틀넥 등 18편 선정 부산모바일앱 공모전” 전자신문

[Android] JNI – java.lang.UnsatisfiedLinkError

 java.lang.UnsatisfiedLinkError



 오랫만에 JNI를 사용하던 SA3D 도중에 [Android] 컴파일을 cheap nfl jerseys 하고 난 후 에러가 발생했습니다. 라이브러리 호출을 담당하는 System.loadLibrary 메소드 호출 시 발생하는 에러네요. 이때 이 에러는 라이브러리를 찾지 못할 때 Türk 발생한다. wholesale mlb jerseys  그래서 먼저 경로 확인을 [Android/Error] 했더니 경로는 이상이 Ima 없었고 다름 아닌 cheap jerseys from China 함수의 이름이었다.

 그러나! 함수 이름을 수정한 후에 cheap jerseys 다시 컴파일을 하고 ring 실행을 했더니 다시 에러가 발생했다. 이 문제는 이렇게 해결! JNI를 컴파일한 후 생성되는 폴더(obj)를 삭제하고 다시 컴파일을 하면 된다.