역량 UP!/Business

7) aw_project_🏀 팀플 : 안드로이드의 PUSH가 이상하다! 🏀

태하팍 2026. 3. 31. 09:57
반응형
안드로이드의 PUSH가 0건???

TEAM-PL 앱 개발 기록 — 푸시 알림 시스템 구축

프로젝트 개요
  농구 동호회 관리 앱 (Expo React Native + Supabase)의 푸시 알림 시스템을 처음부터 구축하고,
  Android FCM 설정 문제를 해결하며, 안정적인 서버 기반 알림 아키텍처로 전환한 과정.

  1. Google Play 정식 출시
  - 내부 테스트 → 공개 테스트 → 프로덕션 트랙 승격
  - 기존 빌드(versionCode 42)를 "라이브러리에서 추가"로 재사용하여 재심사 없이 진행
  - ProGuard 매핑 파일 경고는 Expo 관리형 빌드에서 정상 (무시 가능)
  - iOS/Android 동시 출시 완료

  2. 초대코드 버그 수정
  - 문제: 사용자 입력을 toUpperCase() → toLowerCase()로 변환하는 과정에서 DB 저장값과 불일치하여
  "Invalid code" 에러
  - 해결: RPC 함수에 LOWER(invite_code) = LOWER(code) 적용, 대소문자 무관하게 매칭

  3. 일정 탭 타임존 버그 수정
  - 문제: new Date().toISOString()이 KST → UTC 변환하면서 월말 날짜가 하루 밀림 (3/31 → 3/30)
  - 증상: 31일 경기가 일정 탭에서 안 보임 (홈에서는 보임)
  - 해결: toISOString() 대신 직접 YYYY-MM-DD 문자열 생성으로 타임존 영향 제거

  4. 경기 삭제 권한 버그 수정
  - 문제: 부회장(co_captain)이 앱에서 경기 삭제 시 에러가 조용히 무시됨
  - 원인 1: 모바일에서 에러 핸들링이 window.alert() (웹 전용)으로 되어있어 에러 메시지 미표시
  - 원인 2: 삭제 확인 다이얼로그가 웹에서만 동작
  - 해결: 모바일용 Alert.alert() 확인 다이얼로그 및 에러 핸들링 추가

  5. 팀원 내보내기 시 데이터 정리 누락
  - 문제: 게스트를 내보내기 했는데 attendance 레코드가 남아있어서 재참가 불가
  - 원인: remove_team_member RPC에 attendance/join_requests 정리 로직 없음 (leave_team에만
  있었음)
  - 해결: remove_team_member에 leave_team과 동일한 정리 로직 추가 (attendance 삭제, join_requests
   삭제)

  6. 경기 생성 시 게스트 설정 미반영
  - 문제: 팀 설정에서 게스트 모집을 켜놔도 크론으로 생성되는 경기에 guest_open: false,
  guest_needed: 0으로 하드코딩
  - 해결: 경기 생성 시 팀의 looking_for_guest, guest_count, guest_description 설정값을 자동 상속

  7. 푸시 알림 시스템 설계 — 가장 큰 작업

  DB 트리거 코드 예시 (게스트 참가)

  CREATE OR REPLACE FUNCTION notify_on_guest_join()
  RETURNS TRIGGER AS $$
  BEGIN
    IF NEW.is_guest = false THEN RETURN NEW; END IF;

    PERFORM net.http_post(
      url := 'https://xxx.supabase.co/functions/v1/notify-team-leaders',
      headers := jsonb_build_object(...),
      body := jsonb_build_object(
        'team_id', NEW.team_id,
        'title', '게스트 참가 알림',
        'body', NEW.player_name || '님이 게스트로 참가했습니다.'
      )
    );
    RETURN NEW;
  END;
  $$ LANGUAGE plpgsql SECURITY DEFINER;

  CREATE TRIGGER trigger_notify_guest_join
    AFTER INSERT ON team_members
    FOR EACH ROW EXECUTE FUNCTION notify_on_guest_join();

  시간 기반 알림 (5시간/1시간 전)
  pg_cron (*/5 * * * *)
    → send-game-notifications Edge Function
      → game_sessions에서 start_time 기준 시간 윈도우 매칭
      → 5h 전: attendance(status='pending') → push_token 조회 → Expo Push API
      → 1h 전: 여전히 pending인 멤버만 → 재알림

  8. Android FCM 설정 — 가장 삽질한 부분

  증상
  - iOS 알림 정상, Android 0건 발송
  - Expo 대시보드: All Platforms 39, Android 0, iOS 39

  원인
  - iOS는 EAS Build 시 APNs 키가 자동 설정
  - Android는 FCM 서비스 계정 키를 Expo 프로젝트에 수동 등록 필요
  - androidAppCredentials.googleServiceAccountKeyForFcmV1이 null 상태

  삽질 과정

  1. ❌ Expo 대시보드 Push Notifications 페이지 — 업로드 UI 없음
  2. ❌ Expo Credentials 페이지 — FCM 업로드 옵션 없음
  3. ❌ eas credentials CLI — 업로드했지만 빌드 인증서에만 저장됨 (Push용 아님)
  4. ❌ Google Cloud Console 레거시 API — 페이지 로드 에러
  5. ❌ Expo REST API — 인증 차단 (403)
  6. ✅ Expo GraphQL API 직접 호출로 해결!

  해결 코드
  # Step 1: 서비스 계정 키 생성
  mutation {
    googleServiceAccountKey {
      createGoogleServiceAccountKey(
        googleServiceAccountKeyInput: { jsonKey: $serviceAccountJson },
        accountId: $accountId
      ) { id }
    }
  }

  # Step 2: FCM V1에 연결 (이 mutation이 핵심!)
  mutation {
    androidAppCredentials {
      setGoogleServiceAccountKeyForFcmV1(
        id: $androidAppCredentialsId,
        googleServiceAccountKeyId: $keyId
      ) { id }
    }
  }

  GraphQL 스키마 탐색 방법

  # AndroidAppCredentialsMutation에서 사용 가능한 필드 찾기
  { __type(name: "AndroidAppCredentialsMutation") {
    fields { name args { name } }
  } }
  → setGoogleServiceAccountKeyForFcmV1 발견!

 9. pg_cron 장애 — 크론이 전부 실패하고 있었던 이유

  증상
  - 경기 자동 생성 안 됨, 5시간/1시간 전 알림 안 감
  - 크론 로그: ERROR: unrecognized configuration parameter "app.settings.supabase_url"

  원인
  - 크론 명령에서 current_setting('app.settings.supabase_url') 참조
  - 해당 DB 설정값이 어느 시점에 사라짐 (Supabase 인프라 변경 추정)
  - ALTER DATABASE 로 재설정 시도 → 권한 부족 (permission denied)

  해결
  - current_setting() 대신 URL/키를 크론 명령에 직접 하드코딩

  SELECT cron.schedule('create-game-sessions', '30 7 * * *', $$
    SELECT net.http_post(
      url := 'https://xxx.supabase.co/functions/v1/create-game-sessions',
      headers := jsonb_build_object(
        'Authorization', 'Bearer <service_role_key>',
        'Content-Type', 'application/json'
      ),
      body := '{}'::jsonb
    );
  $$);

 10. 하네스(Harness) 구성 — AI 코딩 도구의 맥락 유실 방지

  문제

  - 새 대화마다 이전 맥락을 잃어 Edge Function이 잘못 수정되는 문제 반복
  - "내일" 경기 생성이 "오늘"로 변경되거나, 알림 로직이 깨지는 등

  해결

  - 프로젝트 전용 에이전트 3개 정의 (supabase-engineer, app-engineer, deployer)
  - 핵심 규칙을 에이전트에 명시: "create-game-sessions는 반드시 내일 경기 생성"
  - 스킬 4개 (deploy, db-ops, notification-debug, orchestrator)
  - Superpowers 플러그인 설치로 개발 프로세스 체계화

  11. 배포 프로세스


 체크!
  1. Expo Android 푸시는 FCM 키 수동 등록 필수 — iOS와 달리 자동 안 됨, 
     eas credentials 빌드용과 푸시용이 다름
  2. 클라이언트 알림 호출보다 DB 트리거가 안정적 — OTA 의존성 제거, 에러 추적 용이
  3. toISOString()은 타임존 함정 — KST 환경에서 날짜 밀림 주의
  4. pg_cron의 current_setting()은 사라질 수 있다 — 하드코딩이 더 안전
  5. AI 코딩 도구는 맥락을 잃는다 — 하네스/메모리로 핵심 규칙 영속화 필수
  6. 같은 폰에서 계정 전환 테스트는 부정확 — push_token이 마지막 로그인 기기로 덮어씌워짐
  
  [pg_cron 16:30 KST]
    → create-game-sessions Edge Function
      → game_sessions INSERT
        → DB 트리거: 전체 멤버 알림 🔔

  [pg_cron 매 5분]
    → send-game-notifications Edge Function
      → 5h 전: 미정 멤버 알림 🔔
      → 1h 전: 미정 멤버 재알림 🔔

  [사용자 액션]
    → 게스트 참가 → team_members INSERT
      → DB 트리거: 회장/부회장 알림 🔔
    → 팀 가입 신청 → join_requests INSERT
      → DB 트리거: 회장/부회장 알림 🔔

작업요약

  • 팀원 내보내기 데이터 정리 - attendance/join_requests 동시 삭제 (leave_team과 동일)
  • 게스트 수락/거절 시스템
  • 신규 게스트 승인제, 기존 게스트 자동 참가, 액션 카드
  • 화면 액션 카드
  • 가입 신청/게스트 신청을 홈에서 바로 수락/거절
  • 게스트 모집 설정 가이드
  • 설정 미완료 경고 + 설정 이동 안내
  • 게스트 토글 가시성 개선
  • OFF 진한 회색으로 구분 가능하게
  • 게스트 참가 UI 개선
    • "GUEST 입력" 힌트바로 이동 버튼으로 변경
  • 생성 화면
    • 사각형 로고아이콘으로 변경
  • 선수정보 출생년도 선택
    • 나이 직접 입력년생 선택 + 자동 나이 계산 (선수정보 수정 + 팀원정보 수정 모두)
  • 하네스 구성 - 에이전트 3 + 스킬 4 + Superpowers 설치
  • Firebase 서비스 계정 교체 - 노출된 삭제 + 등록

 

반응형