DateP

월간 갱신 로직 변경 및 문제 요약 04 26

Solo.dev 2025. 4. 27. 02:36
월간 갱신 로직 변경 및 문제 요약

월간 갱신 로직 변경 및 문제 요약

개요

  • 프로젝트: DateP
  • 대상 파일: purchase.tsx, startPurchase.ts, SubscriptionProvider.tsx
  • 변경 초점: 월간 갱신 로직 (basic: 50회, premium: 100회 리셋) 안정성 및 명확성 개선
  • 추가 문제: 구독 취소 후 복구, Firebase update 에러

변경된 주요 부분

  • purchase.tsx:
    • handleStartPurchase:
      • Firebase 저장 로직 개선: snapshot.exists()로 데이터 확인 후 update/set.
      • free 플랜 선택 시 모달 닫기 추가 (setModalVisible(false)).
      const snapshot = await get(subscriptionRef);
      const existingData = snapshot.val();
      if (existingData) {
          await update(subscriptionRef, subscriptionData);
      } else {
          await set(subscriptionRef, subscriptionData);
      }
    • handleRestorePurchases:
      • Firebase 저장 로직 동일 개선.
      • 에러 처리에서 기본값 리셋 (free, remainingUses: 1) 강화.
  • startPurchase.ts:
    • startPurchase:
      • 월간 리셋 조건 명확화: isExpired로 월 변경 확인.
      • Alert.alert 호출 제거 제안 (미적용).
      • 수정: transaction 선언을 조기에 이동하여 TS2448, TS2454 오류 해결.
      • 수정: Firebase set에서 undefined 값 제거로 transactionId 에러 해결.
      • 수정: subscriptionData 객체 명시적 정의 및 유효성 검사 추가, 디버깅 로그 삽입.
      • 수정: finishTransaction 조건부 호출 추가 (isAcknowledgedAndroid 확인).
      if (Platform.OS !== 'android' || !extendedPurchase.isAcknowledgedAndroid) {
          await RNIap.finishTransaction({
              purchase: extendedPurchase,
              isConsumable: false,
          });
          console.log('✅ 구독 구매 완료');
      } else {
          console.log('✅ 이미 확인된 구매, finishTransaction 생략');
      }
      const transaction: Transaction = {
          purchaseToken: Platform.OS === 'ios' ? undefined : extendedPurchase.purchaseToken,
          productId: extendedPurchase.productId || targetProductId,
          transactionId: Platform.OS === 'ios' ? extendedPurchase.transactionId : undefined,
          purchaseDate: extendedPurchase.transactionDate
              ? new Date(extendedPurchase.transactionDate).toISOString()
              : new Date().toISOString(),
      };
      const subscriptionData: SubscriptionData = {
          ...(Platform.OS === 'android' && extendedPurchase.purchaseToken && { purchaseToken: extendedPurchase.purchaseToken }),
          ...(Platform.OS === 'ios' && extendedPurchase.transactionId && { transactionId: extendedPurchase.transactionId }),
          plan,
          limit,
          remainingUses: limit,
          purchaseDate: transaction.purchaseDate,
      };
      console.log('저장할 구독 데이터:', JSON.stringify(subscriptionData, null, 2));
      await set(subscriptionRef, subscriptionData);
    • checkSubscriptionStatus:
      • 월간 리셋 조건 강화: isExpiredOrMissing.
      • 수정: undefined 값 제거로 Firebase set 에러 해결.
      • 수정: 구독 취소 시 Firebase 데이터 삭제 로직 추가.
      if (!isSubscribed || !activePurchase?.autoRenewing) {
          const key = Platform.OS === 'ios' ? activePurchase?.transactionId : activePurchase?.purchaseToken;
          if (key) {
              const sanitizedKey = sanitizePath(key);
              await set(ref(db, `subscriptions/${sanitizedKey}`), null);
              console.log('✅ 취소된 구독 데이터 삭제');
          }
          return [];
      }
      const updatedSubscription: SubscriptionData = {
          ...(Platform.OS === 'android' && activePurchase.purchaseToken && { purchaseToken: activePurchase.purchaseToken }),
          ...(Platform.OS === 'ios' && activePurchase.transactionId && { transactionId: activePurchase.transactionId }),
          plan,
          limit,
          remainingUses: limit,
          purchaseDate,
      };
  • SubscriptionProvider.tsx:
    • checkAndResetUsage:
      • 월간 리셋 조건 명확화: shouldReset.
      • storedKey 누락 시 기본값 리셋 제안 (미적용).
      const shouldReset = !lastReset || (
          plan !== 'free' && now.getMonth() !== new Date(lastReset).getMonth()
      );

추가 문제 및 해결

  • 문제 1: 구독 취소 후 Basic 플랜 복구:
    • 원인: getAvailablePurchases가 캐시된 구매 반환, 구독 취소 처리 부족, basic 플랜 기본값 설정.
    • 해결: checkSubscriptionStatus에서 구독 상태 검증 강화, 취소 시 Firebase 데이터 삭제, 기본값 free로 변경.
    const activePurchase = purchases.find((p) => {
        const isTargetProduct = targetProductIds.includes(p.productId);
        const isValid = Platform.OS === 'android' ? p.autoRenewing && p.purchaseStateAndroid === 0 : true;
        return isTargetProduct && isValid;
    });
    if (!isSubscribed || !activePurchase?.autoRenewing) {
        const key = Platform.OS === 'ios' ? activePurchase?.transactionId : activePurchase?.purchaseToken;
        if (key) {
            const sanitizedKey = sanitizePath(key);
            await set(ref(db, `subscriptions/${sanitizedKey}`), null);
        }
    }
  • 문제 2: Firebase update 에러:
    • 원인: handleStartPurchase에서 subscriptionDatatransactionId: undefined 포함.
    • 해결: subscriptionData 조건부 구성, undefined 값 필터링.
    const subscriptionData: SubscriptionData = {
        ...(Platform.OS === 'android' && result.data.purchaseToken && { purchaseToken: result.data.purchaseToken }),
        ...(Platform.OS === 'ios' && result.data.transactionId && { transactionId: result.data.transactionId }),
        plan: result.data.plan,
        limit: result.data.limit,
        remainingUses: result.data.remainingUses,
        purchaseDate: result.data.purchaseDate,
    };
    const cleanSubscriptionData = Object.fromEntries(
        Object.entries(subscriptionData).filter(([_, value]) => value !== undefined)
    );
  • 문제 3: finishTransaction 에러:
    • 원인: 이미 확인된 구매(isAcknowledgedAndroid: true)에 대해 finishTransaction 호출.
    • 해결: isAcknowledgedAndroid 확인 후 조건부 호출, 에러를 경고로 처리.
    if (Platform.OS !== 'android' || !extendedPurchase.isAcknowledgedAndroid) {
        try {
            await RNIap.finishTransaction({
                purchase: extendedPurchase,
                isConsumable: false,
            });
            console.log('✅ 구독 구매 완료');
        } catch (finishError: unknown) {
            console.error('❌ finishTransaction 실패:', finishError);
            const error = finishError as Error;
            if (error.message?.includes('purchase is not suitable to be purchased')) {
                console.warn('⚠️ 구매가 이미 완료된 상태로 간주됨');
            } else {
                throw error;
            }
        }
    }

요약

  • 주요 변경:
    • Firebase 저장 로직 개선: snapshot.exists()로 데이터 확인.
    • 월간 리셋 조건 명확화: purchaseDate와 현재 월 비교.
    • 에러 처리 개선:
      • checkSubscriptionStatusstartPurchase에서 undefined 값 제거.
      • startPurchase에서 transaction 선언 순서 수정.
      • startPurchase에서 subscriptionData 명시적 정의 및 유효성 검사 추가.
      • finishTransaction 조건부 호출로 에러 방지.
  • 문제 여부: 월간 리셋 로직은 basic/premium 플랜에서 정확히 동작 (remainingUses50/100).
  • 권장사항:
    • Firebase 트랜잭션 도입.
    • Alert.alertpurchase.tsx로 위임.
    • Google Play 캐시 동기화 확인.

테스트 권장

  1. 수동 테스트:
    • basic 플랜 (purchaseDate: '2025-03-15') → 날짜 2025-04-01로 변경 → remainingUses: 50 확인.
    • premium 플랜 → remainingUses: 100 확인.
    • Firebase 연결 끊김 모의 → AsyncStorage 기본값 저장 확인.
    • 추가: Android에서 startPurchase 호출 → transactionId 없이 purchaseToken 정상 저장 확인.
    • 추가: 구독 취소 후 checkSubscriptionStatus 호출 → Firebase 데이터 삭제 및 free 플랜 확인.
    • 추가: 재결제 시 handleStartPurchasetransactionId: undefined 에러 없음 확인.
  2. 단위 테스트:
    • Jest로 checkAndResetUsage 테스트: lastResetMonthly: '2025-03-15', 현재 날짜 2025-04-01remainingUses 리셋.
    • 추가: startPurchase 테스트: Android에서 transactionId 제외, set 성공 확인.
    • 추가: checkSubscriptionStatus 테스트: 취소된 구독 → Firebase 데이터 삭제 확인.
    • 추가: handleStartPurchase 테스트: subscriptionDataundefined 값 제외 확인.