@@ -421,4 +421,147 @@ class _IapTestPageState extends State<IapTestPage> {
421421
422422![ alt text] ( /assets/img/in_app_purchase/result_screen.png )
423423
424+ ---
425+
426+ ## Google Play 실시간 구독 알림 (RTDN) 설정
427+
428+ 구독이 갱신되거나 취소·만료될 때마다 서버가 자동으로 이를 감지하려면
429+ ** Real-Time Developer Notifications (RTDN)** 기능을 활성화해야 합니다.
430+ Google Play → Cloud Pub/Sub → 서버로 이벤트가 전달되는 구조입니다.
431+
432+ ``` mermaid
433+ sequenceDiagram
434+ participant Play as "Google Play"
435+ participant PubSub as "Cloud Pub/Sub"
436+ participant BE as "서버(FastAPI)"
437+ participant DevAPI as "Google Play Developer API"
438+ participant DB as "Database"
439+
440+ Play->>PubSub: "RTDN 이벤트 게시 (base64 JSON)"
441+ PubSub->>BE: "HTTP POST /rtdn/google"
442+ BE->>BE: "base64 디코딩 및 알림 파싱"
443+ BE->>DevAPI: "subscriptionsv2.get 호출"
444+ DevAPI-->>BE: "현재 구독 상태 응답"
445+ BE->>DB: "상태 업데이트 (갱신·취소·만료)"
446+ BE-->>PubSub: "200 OK (처리 완료 응답)"
447+ ```
448+
449+ ---
450+
451+ ### 5-1. Cloud Pub/Sub 설정
452+
453+ #### ① API 활성화
454+
455+ 1 . [ Google Cloud Console] ( https://console.cloud.google.com )
456+ 2 . ** Pub/Sub API** 검색 → ** Enable**
457+ ![ alt text] ( /assets/img/in_app_purchase/cloud_pub_sub_api.png )
458+
459+ #### ② 토픽(Topic) 생성
460+
461+ 1 . ** Pub/Sub → Topics → Create topic**
462+ 2 . ID: ` rtdn-notifications ` 등
463+ 3 . 생성 후 표시되는 전체 경로를 복사해둡니다.
464+ ![ alt text] ( /assets/img/in_app_purchase/pub_sub.png )
465+ ![ alt text] ( /assets/img/in_app_purchase/pub_sub_create_topic.png )
466+ ```
467+ projects/<PROJECT_ID>/topics/rtdn-notifications
468+ ```
469+
470+ #### ③ 권한 부여
471+
472+ 1 . 생성한 토픽 → ** Permissions** 탭 → ** Add principal**
473+ ![ alt text] ( /assets/img/in_app_purchase/add_principal.png )
474+ 2 . 아래 이메일을 추가하고 ** Pub/Sub Publisher** 역할 부여
475+ ```
476+ 477+ ```
478+ ![ alt text] ( /assets/img/in_app_purchase/pub_sub_publisher.png )
479+ 3 . 저장 후 1~ 2분 정도 기다리면 반영됩니다.
480+
481+ > 이 과정을 통해 Google Play가 해당 토픽으로 알림 메시지를 발행할 수 있게 됩니다.
482+ {: .prompt-info}
483+
484+ #### ④ 구독(Subscription) 생성
485+
486+ 1 . 토픽 상세 페이지 → ** Create subscription**
487+ ![ alt text] ( /assets/img/in_app_purchase/make_subscription.png )
488+ 2 . ID: ` rtdn-subscription `
489+ 3 . ** Delivery type** : Push
490+ 4 . ** Push endpoint URL** :
491+
492+ ```
493+ https://{서버도메인}/rtdn/google
494+ ```
495+ 5 . 나머지는 기본값 그대로 두고 ** Create**
496+
497+ ---
498+
499+ ### 5-2. Play Console 연결
500+
501+ 1 . [ Play Console] ( https://play.google.com/console ) → 앱 선택
502+ 2 . ** Monetize → Monetization setup → Real-time developer notifications**
503+ 3 . ** Enable real-time notifications** 체크
504+ 4 . Topic name 입력:
505+
506+ ```
507+ projects/<PROJECT_ID>/topics/rtdn-notifications
508+ ```
509+ 5 . ** Save changes**
510+ ![ alt text] ( /assets/img/in_app_purchase/play_console_monetization_rtdn_setup.png )
511+ 6 . ** Send test message** 버튼으로 연결 성공 여부를 확인합니다.
424512
513+ ---
514+
515+ ### 5-3. 서버 구현 (FastAPI 예시)
516+
517+ 아래 코드는 Pub/Sub이 전송한 RTDN 알림을 받는 ` /rtdn/google ` 엔드포인트 예시입니다.
518+ 알림 본문은 base64 인코딩된 JSON이므로, 디코딩 후 구독 상태를 다시 조회해야 합니다.
519+
520+ ``` python
521+ # iap/rtdn.py
522+ from fastapi import APIRouter, Request, HTTPException
523+ import base64, json
524+ from google.oauth2 import service_account
525+ from googleapiclient.discovery import build
526+
527+ router = APIRouter()
528+ SCOPES = [" https://www.googleapis.com/auth/androidpublisher" ]
529+
530+ def _android_publisher ():
531+ creds = service_account.Credentials.from_service_account_file(
532+ " service-account.json" , scopes = SCOPES
533+ )
534+ return build(" androidpublisher" , " v3" , credentials = creds, cache_discovery = False )
535+
536+ @router.post (" /rtdn/google" )
537+ async def handle_rtdn (req : Request):
538+ body = await req.json()
539+ msg = body.get(" message" )
540+ if not msg or " data" not in msg:
541+ raise HTTPException(status_code = 400 , detail = " invalid format" )
542+
543+ decoded = base64.b64decode(msg[" data" ]).decode()
544+ data = json.loads(decoded)
545+
546+ if " subscriptionNotification" not in data:
547+ return {" ok" : True , " msg" : " ignored (non-subscription event)" }
548+
549+ n = data[" subscriptionNotification" ]
550+ purchase_token = n[" purchaseToken" ]
551+ notif_type = n[" notificationType" ]
552+
553+ pub = _android_publisher()
554+ result = pub.purchases().subscriptionsv2().get(
555+ packageName = data[" packageName" ], token = purchase_token
556+ ).execute()
557+
558+ # 구독 상태(result["subscriptionState"])에 따라 DB 업데이트 수행
559+ # 예: ACTIVE → 활성화, CANCELED/EXPIRED → 권한 회수 등
560+
561+ return {" ok" : True , " notificationType" : notif_type}
562+ ```
563+
564+ > RTDN은 “상태 변경”만 알리므로, 반드시 ` subscriptionsv2.get ` 으로 ** 실제 구독 상태를 재조회** 해야 합니다.
565+ > {: .prompt-info}
566+
567+ ---
0 commit comments