출퇴근 영어 회화 앱 — 개발 계획서 v0.2
작성일: 2026-09-13 · 기준 문서: PRD v0.2, 제품검토 회의록(2026-09-13) 표기: [결정] 확정 · [제안] 검토 필요 · [확인 필요] 정보 요청 개발 방식: 구현은 Claude(Claude Code), 리뷰·실기기 테스트·배포 승인은 오너(개발자)
0. v0.1 대비 변경 요약
| # | 변경 | 절 |
|---|---|---|
| C1 | 마일스톤 재배치 — 걷기 모드를 M6 → M3으로 전진 | 7 |
| C2 | M1 스파이크 1주 → 2주, 블루투스 오디오 매트릭스 추가 | 6.1, 7 |
| C3 | M1 게이트 신설 — 6개 조건 미충족 시 스코프 재설계 | 8 |
| C4 | 선행 검증 2주를 개발 전 단계로 편입 (M0과 병렬) | 7 |
| C5 | 로컬 알림을 MVP 범위로 승격 | 2 |
| C6 | STT를 인터페이스로 추상화, 엔진 교체 가능 구조 | 3, 6.3 |
| C7 | 오디오 상태 머신 명세 추가 (인터럽트 정책 표 포함) | 6.2 |
| C8 | Android 14 포그라운드 서비스 타입·권한 요건 명시 | 6.5 |
| C9 | SRS(간격 반복) 모듈 신설 — 순수 함수, 단위 테스트 대상 | 6.6 |
| C10 | 인앱 결제 모듈 추가 | 2, 3 |
| C11 | 일정 산정 전제 명문화 — 오너 주당 10시간 | 7.4 |
| C12 | 데이터 모델에 Curveball / Mastery / StreakState / Subscription 추가 | 5 |
1. 개발 방향
- [결정] React Native CLI (bare) + TypeScript
- [결정] 처음부터 네이티브로 개발 → TestFlight / Play 내부 테스트로 검증
- [신규 v0.2] 불확실한 것을 먼저 만든다. v0.1은 리스크가 가장 큰 걷기 모드를 M6(9주차)에 두었다. 9주를 쓰고 나서 블루투스 오디오 전환 때문에 불가능하다는 걸 발견하면 프로젝트가 무너진다. 반대로 지하철 모드(카드·퀴즈)는 기술 리스크가 사실상 0이므로 언제 만들어도 된다. → M1 스파이크 → M3 걷기 모드를 앞에 배치하고, 3주차에 진행/중단 판정 게이트를 둔다.
2. MVP 범위 [변경 v0.2]
| 기능 | v0.1 | v0.2 | 비고 |
|---|---|---|---|
| 온보딩 (출퇴근 정보, 상황 선택, 여행 목표) | ✅ | ✅ | 레벨 진단 제거 |
| 데일리 루틴 자동 생성 | ✅ | ✅ | |
| 간격 반복(SRS) | — | ✅ 신규 | 루틴 생성기 내장 |
| 지하철 모드 — 화면 | ✅ | ✅ | 스와이프 → 큰 탭 타겟 |
| 지하철 모드 — 오디오 모드 | — | ✅ 신규 | 혼잡 시 분기 |
| Curveball 훈련 | — | ✅ 신규 | 리스닝 강화 |
| 오프라인 다운로드·동기화 | ✅ | ✅ | |
| 걷기 모드: 백그라운드 재생 + 이어폰 제어 | ✅ | ✅ | M3으로 전진 |
| 걷기 모드: 따라 말하기 + 판정 | ✅ | ✅ | 점수 → 이진 판정 |
| 걷기 모드: 화면 잠금 상태 녹음 | ⏳ 2단계 | ⏳ 2단계 | M1에서 가능성만 검증 |
| 스트릭 + 보호 장치 | ✅ | ✅ | 프리즈·대체 모드 추가 |
| 이어폰 없는 날 모드 | — | ✅ 신규 | |
| 로컬 알림 | ⏳ 2단계 | ✅ 승격 | 서버 불필요, 반나절 작업 |
| 원격 푸시 (FCM/APNs) | ⏳ 2단계 | ⏳ 2단계 | |
| 인앱 결제 | — | ✅ 신규 | 무료/Pro/여행 패스 |
| AI 롤플레이 | ⏳ 3단계 | ⏳ 3단계 |
사용 환경 범위 [2026-09-19 결정 반영] — 만드는 이동 환경은 지하철 2모드(M5) + 걷기(M3) 둘뿐이다. 차량(운전 중)은 재논의 트리거 없이 제외, 러닝은 트리거를 남긴 채 제외한다. 정본은 PRD 2장 "사용 환경 범위" 이고(2026-09-19 #제품 회의·백승현 부대표 결재), 이 계획서는 옮겨 적은 것이다 — 값이 어긋나면 PRD가 이긴다. 러닝 트리거가 M1 게이트 당일 발동하면 일정에 M1 매트릭스 재실측 2주 + 무입력 조작 설계가 추가된다(7.1 일정에 아직 안 들어 있다).
3. 기술 스택
라이브러리는 착수 시점에 New Architecture 호환 여부, 최근 유지보수 상태, 최신 RN 버전 지원을 확인한 뒤 확정한다.
| 영역 | 선택 | 선택 이유 |
|---|---|---|
| 프레임워크 | React Native CLI (최신 안정), TypeScript strict | 결정 사항 |
| 네비게이션 | React Navigation (native-stack) | 사실상 표준 |
| 서버 상태 | TanStack Query | 캐싱·재시도·오프라인 대응 |
| 클라이언트 상태 | Zustand | 가볍고 보일러플레이트 적음 |
| 로컬 저장 (KV) | react-native-mmkv | 설정·세션·진행 상태 |
| 로컬 DB | op-sqlite | 오프라인 콘텐츠·학습 기록 |
| 파일 다운로드 | react-native-blob-util | 음성 파일 백그라운드 다운로드 |
| 오디오 재생 | react-native-track-player | 백그라운드 재생, 잠금 화면·이어폰 리모트 |
| 녹음 | 후보 비교 후 확정 (M1에서 오디오 세션 전환 동작이 선택 기준) | 따라 말하기 녹음 |
| 음성 인식 | SpeechRecognizer 인터페이스 + 구현체 3종 [변경] |
엔진 교체 가능성 확보 → 6.3 |
| 알림 | notifee (로컬 알림) [신규] | 출퇴근 시각 트리거 |
| 인앱 결제 | react-native-iap 또는 RevenueCat [신규] | 구독 + 단품, 영수증 검증 |
| 백엔드 | Supabase (Auth, Postgres, Storage) | 인증·DB·파일 통합 |
| TTS (콘텐츠 제작) | 클라우드 TTS 1종 [확인 필요] — 다국 억양 보이스 지원 필수 | 억양 믹스 요건 |
| 원어민 녹음 | 성우 세션 2~3시간, 핵심 100문장 [신규] | TTS로는 대화 리듬 재현 불가 |
| 분석 | PostHog 또는 Firebase Analytics | 리텐션·퍼널·WSU |
| 에러 모니터링 | Sentry | 크래시·네이티브 오류 |
| 테스트 | Jest, RNTL, Maestro | 단위·컴포넌트·E2E |
| CI/CD | GitHub Actions + Fastlane | 빌드·TestFlight·Play 업로드 |
4. 아키텍처
4.1 원칙
- 오프라인 우선: 화면은 항상 로컬 DB를 읽는다. 서버는 동기화 대상일 뿐이다.
- 오디오는 앱 전체에서 단일 서비스: 재생·녹음·인식은
audio모듈 하나가 담당하고 화면은 상태만 구독한다. 상태 전이는 명시적 상태 머신으로 정의한다 → 6.2 - 기능 단위 폴더 구조: 기능 하나를 독립적으로 작업·리뷰할 수 있게 한다.
- [신규] 불확실한 의존성은 인터페이스 뒤에 둔다: STT 엔진, TTS 제공자, 결제 스토어는 교체 가능해야 한다.
- [신규] 학습 로직은 순수 함수로: 루틴 생성, SRS 스케줄링, 발음 판정은 부수 효과 없이 작성해 단위 테스트로 검증한다.
4.2 폴더 구조 [변경 v0.2]
src/
app/ # 네비게이션, 프로바이더, 앱 진입점
features/
onboarding/
routine/ # 루틴 생성 + 오늘 화면
subway/ # 지하철 모드 (화면 / 오디오 2모드)
walking/ # 걷기 모드 (오디오 플로우)
progress/ # 스트릭, 여행 진행도
paywall/ # 결제·구독 [신규]
domain/ # [신규]
srs/ # 간격 반복 스케줄러 (순수 함수)
routine-planner/ # 루틴 배치 알고리즘 (순수 함수)
scoring/ # 발음 판정 규칙 (순수 함수)
services/
audio/
player.ts # TrackPlayer 래퍼
recorder.ts # 녹음
session.ts # 오디오 세션 전환 (iOS/Android) [신규]
machine.ts # 오디오 상태 머신 [신규]
speech/ # [신규]
SpeechRecognizer.ts # 인터페이스
ios-speech.ts # SFSpeechRecognizer
android-speech.ts # SpeechRecognizer
whisper.ts # 폴백 후보 (벤치마크용)
notifications/ # 로컬 알림 스케줄링 [신규]
billing/ # 인앱 결제 [신규]
sync/ # 콘텐츠 다운로드, 기록 업로드
db/ # op-sqlite 스키마, 마이그레이션, 쿼리
api/ # Supabase 클라이언트
analytics/
shared/
ui/ # 공통 컴포넌트, 디자인 토큰
utils/
native/ # 커스텀 네이티브 모듈 (STT 브리지 등)
scripts/
content/ # 콘텐츠 생성·검증·TTS 변환·업로드
핵심 변경: domain/을 신설해 학습 로직을 UI·플랫폼에서 완전히 분리한다. SRS·루틴 배치·판정 규칙은 React도 네이티브도 모르는 순수 TypeScript로, Node에서 바로 테스트한다.
4.3 데이터 흐름
[Supabase] ── 콘텐츠 패키지(JSON + 음성) ──▶ [다운로드 큐] ──▶ [로컬 DB + 파일]
│
┌────────────┴────────────┐
▼ ▼
[domain/routine-planner] [화면·오디오]
▲ │
│ │
[domain/srs] ◀── Mastery ◀──────────┘
│
[Supabase] ◀── 학습 기록 배치 업로드 ── [미전송 기록 큐] ◀────────────────────┘
- 콘텐츠는 레슨 단위 패키지(JSON 1개 + mp3 여러 개)로 배포하고 버전 번호로 갱신 여부를 판단한다.
- 학습 기록은 로컬에 먼저 쓰고, 네트워크 복구 시 배치로 업로드한다. 충돌 시 "완료 상태는 되돌리지 않는다" 규칙.
- [신규] 발화 결과는
Mastery를 갱신하고, 다음 루틴 생성 시 SRS가 이를 읽어 복습 큐를 만든다. 이 루프가 제품의 학습 엔진이다.
5. 데이터 모델 [변경 v0.2]
| 테이블 | 서버 | 로컬 | 비고 |
|---|---|---|---|
| users / profiles | ✅ | 일부 캐시 | 출퇴근 정보, 레벨 추정치, 모드 선호 |
| travel_goals | ✅ | ✅ | |
| courses / situations / lessons | ✅ | ✅ | 읽기 전용, 패키지로 내려받음. tier(free\|pro) 추가 |
| expressions | ✅ | ✅ | accent, register 추가 |
| curveballs | ✅ | ✅ | 신규 — 예상 밖 되물음 + 대응 |
| dialogues / quiz_items | ✅ | ✅ | 음성은 파일 경로로 저장 |
| progress | ✅ | ✅ | 로컬 우선 기록 |
| mastery | ✅ | ✅ | 신규 — SRS 상태(level, next_review_at) |
| speaking_logs | ✅ | ✅ | 녹음 원본은 서버 전송 안 함. result(pass\|unclear), snr_db |
| daily_routines | ✅ | ✅ | mode, review_expression_ids 추가 |
| streak_state | ✅ | ✅ | 신규 — 프리즈 잔량 포함 |
| subscriptions | ✅ | 캐시 | 신규 — 플랜, 만료일, 스토어 영수증 |
| sync_queue | — | ✅ | 미전송 기록 |
- Supabase는 RLS(행 수준 보안)로 사용자 본인 기록만 접근 가능하게 설정한다.
- 콘텐츠 테이블은 모든 로그인 사용자 읽기 전용.
tier가 pro인 콘텐츠는 구독 상태를 RLS 정책에서 확인한다. - [신규] 녹음 원본은 기기를 떠나지 않는다. 개인정보 처리방침에 명시하고, 앱 소개에서도 강조한다(마케팅 자산).
6. 핵심 기술 과제
각 과제는 해당 마일스톤 시작 전에 스파이크로 먼저 확인한다.
6.1 블루투스 오디오 매트릭스 [신규 v0.2 — 최우선 리스크]
문제: 블루투스 이어폰에서 마이크를 켜는 순간 코덱이 A2DP(스테레오, 고음질)에서 HFP/SCO(모노 16kHz, 전화 음질)로 전환된다. 원어민 음성이 갑자기 전화 통화 음질이 되고, 전환에 0.5~2초 무음이 발생한다. AirPods, 갤럭시 버즈 모두 해당된다.
영향: 발음을 따라 하는 앱에서 들려주는 소리가 전화 음질이면 제품의 존재 이유가 없어진다. v0.1에는 "세션 전환 시 끊김·지연 확인" 한 줄로만 있었다.
정본 — 12케이스의 축·케이스 식별자·현장 순서·예외 처리·기록 양식·판정 규칙의 정본은
ogamyeo-app docs/m1-r2-case-matrix.md 이고, 기계가 읽는 같은 표가 같은 저장소
src/lib/m1/cases.ts(R2_CASES)에 있다(두 표가 어긋나면 cases.test.ts 가 실패한다).
아래는 그 정본에서 옮겨 온 것이고, 값이 어긋나면 정본이 이긴다.
회차 당일 손으로 채우는 칸은 이 저장소 docs/실기기_실측_회차_기록양식.md 에 있다.
[이력] 축은 2026-09-16 ogamyeo-app #19(PR #27)로 재확정됐다 — v0.2 초안이 축으로 쓴
이어폰 모델 3종과 조합 4종(A~D)은 폐기되고 OS 2 × 오디오 세션 구성 2 × 상황 3 이 됐다.
분모 12는 그대로다(M1 게이트 판정문이 R2 실패 조합 N / 12 로 나간다).
검증 매트릭스 (OS 2 × 오디오 세션 구성 2 × 상황 3 = 12케이스 · C01~C12)
| 케이스 | OS | 세션 구성 | 상황 | 반복 |
|---|---|---|---|---|
| C01 | iOS | A2DP유지 | 재생만 | 1 |
| C02 | iOS | A2DP유지 | 녹음 전환 왕복 | 3 |
| C03 | iOS | A2DP유지 | 통화 인터럽트 | 2 |
| C04 | iOS | HFP허용 | 재생만 | 1 |
| C05 | iOS | HFP허용 | 녹음 전환 왕복 | 3 |
| C06 | iOS | HFP허용 | 통화 인터럽트 | 2 |
| C07 | Android | A2DP유지 | 재생만 | 1 |
| C08 | Android | A2DP유지 | 녹음 전환 왕복 | 3 |
| C09 | Android | A2DP유지 | 통화 인터럽트 | 2 |
| C10 | Android | HFP허용 | 재생만 | 1 |
| C11 | Android | HFP허용 | 녹음 전환 왕복 | 3 |
| C12 | Android | HFP허용 | 통화 인터럽트 | 2 |
측정은 24회, 칸은 12개다. 케이스 이름 C01~C12 는 정본과 같은 글자로만 쓴다 —
현장 기록이 이 식별자로 코드·DB(mode_switch)에 붙는다.
세션 구성 2종 (축은 결과가 아니라 앱이 요청하는 구성이다. 요청대로 되는지가 R2 의 본문이다)
| 이름 | 뜻 |
|---|---|
| A2DP유지 | 재생은 A2DP 유지 · 녹음은 폰 내장 마이크. HFP 를 아예 요청하지 않는다 |
| HFP허용 | 녹음을 블루투스 헤드셋 마이크로. HFP/SCO 전환을 허용한다 |
OS별 세션 설정값(iOS AVAudioSession 옵션 · Android setCommunicationDevice/SCO)은 정본
§1 에 있다. 여기 옮겨 적지 않는다 — 두 벌이 되면 갈라진다.
상황 3종: 재생만 — 세션 구성만으로 경로가 내려앉는지 보는 기준선 ·
녹음 전환 왕복 — 재생 중 녹음 시작 → 발화 → 녹음 종료 → 재생 지속 ·
통화 인터럽트 — 재생·녹음 중 수신 전화.
이어폰은 축이 아니다 — 한 벌 = 회차 하나. 이어폰 모델을 축에 넣으면 12칸이 24·36칸이 되고
N / 12 분모가 깨진다. 그래서 이어폰은 회차 파라미터로 올렸다: 이어폰을 바꾸면 같은 12칸을
한 벌 더 돌린다. 한 칸에 실측이 여러 건이면 하나라도 내려앉으면 그 칸은 실패다(judgeR2 규칙).
A2DP 코덱 종류(SBC/AAC/LDAC)·볼륨·걷는 중인지도 축이 아니라 회차 헤더에 적는 값이고, 화면 꺼짐·
백그라운드는 축이 아니라 별도 관찰 항목이다(정본 §1·§7-3).
v0.2 초안의 A~D 조합은 어디로 갔나 — A → 세션 구성 A2DP유지, B → HFP허용,
C(전환 지연) → 상황 녹음 전환 왕복(전환 ms 는 축이 아니라 기록 칸으로 잰다).
D(LE Audio)는 새 축에 없다 — 정본이 LE Audio 를 다루지 않으므로 여기서 지어내지 않는다.
재려면 정본(m1-r2-case-matrix.md §10 '아직 안 닫힌 것')에 미결로 올려야 한다.
[결정] 이 결과가 걷기 모드의 최종 설계를 결정한다. M1 게이트 조건에 포함.
6.2 오디오 상태 머신 [신규 v0.2]
"오디오는 단일 서비스" 원칙만으로는 구현이 안 된다. 상태 전이를 명세로 못 박는다.
IDLE ──▶ LOADING ──▶ PLAYING_PROMPT ──▶ BEEP ──▶ RECORDING ──▶ EVALUATING ──┐
▲ │ │ │ │
│ ▼ ▼ ▼ │
│ INTERRUPTED ◀──────────────────┴─────────────┘ │
│ │ │
│ ├── 자동 재개 ──▶ (직전 상태로 복귀) │
│ └── 사용자 재개 대기 ──▶ PAUSED │
└──────────────────────── 세션 종료 ◀─────────────────────────────────────┘
인터럽트 정책 표 [확정 필요 — M1에서 실측 후]
| 인터럽트 소스 | 재생 중 | 녹음 중 | 종료 후 |
|---|---|---|---|
| 전화 수신 | 일시정지 | 녹음 폐기 | 사용자 확인 후 재개 |
| 타 앱 재생 (음악·영상) | 일시정지 | 녹음 폐기 | 자동 재개 안 함 |
| 알람 | 일시정지 | 녹음 폐기 | 자동 재개 |
| 내비 안내음 (ducking) | 볼륨 낮춤 | 녹음 유지 | 자동 복원 |
| 이어폰 분리 | 즉시 일시정지 | 녹음 폐기 | 재연결 시 확인 후 재개 |
| 블루투스 끊김 | 일시정지 | 녹음 폐기 | 재연결 대기 30초 |
| 앱 백그라운드 전환 | 재생 유지 | 녹음 유지 | — |
| 화면 켜짐 (걷기 모드) | 일시정지 | 일시정지 | 명시적 탭으로 재개 (안전) |
6.3 음성 인식 — 엔진 추상화 [변경 v0.2]
엔진별 제약
| 엔진 | 장점 | 제약 |
|---|---|---|
iOS SFSpeechRecognizer |
무료, 통합 쉬움 | 온디바이스 지원이 기기·언어별로 다름 (supportsOnDeviceRecognition 확인 필수). 네트워크 모드는 1분/요청 및 시간당 요청 제한이 있어 상시 사용 부적합 |
Android SpeechRecognizer |
무료 | 제조사 구현 편차 큼 (삼성/구글/샤오미 동작 상이). 오프라인 인식은 언어팩 설치 여부에 의존 |
| whisper.cpp + CoreML/NNAPI | 정확도 우수, 완전 오프라인 | 앱 용량 +40~150MB, 발열·배터리 부담. 30분 걷기 기준 배터리 실측 필요 |
[결정] 인터페이스로 추상화한다.
interface SpeechRecognizer {
isAvailable(): Promise<boolean>
supportsOnDevice(): Promise<boolean>
recognize(opts: { maxDurationMs: number; locale: string }): Promise<{
transcript: string
confidence: number // 엔진이 미제공 시 -1
snrDb: number // 자체 측정
}>
cancel(): void
}
MVP는 네이티브 엔진, Whisper는 폴백 후보로 벤치마크만 수행한다(M1). 제품 코드는 엔진을 모른다.
6.4 따라 말하기 판정 [변경 v0.2]
흐름: 원어민 음성 재생 → 비프 → 녹음·인식(최대 N초) → 판정 → 음성 피드백
판정 로직 (domain/scoring, 순수 함수)
1. SNR 측정 (RMS 기반)
2. SNR < 임계값(소음 환경) OR confidence < 0.4
→ 이진 판정만. unclear면 "통과" 처리 (관대하게)
3. SNR >= 임계값 (조용한 환경) AND Pro 구독
→ 목표 문장과 토큰 단위 비교, 단어별 하이라이트 제공
4. 어떤 경우에도 숫자 점수는 표시하지 않는다
설계 근거: 도심 인도 70~80dB(버스 지나가면 85dB), 걷기 중 불규칙 호흡, 비원어민 발음 → 온디바이스 STT의 WER이 조용한 환경에서도 15~25%, 소음 환경에서 40% 이상. 사용자가 잘 말해도 틀렸다고 나온다. 점수를 보여주면 그 순간 신뢰가 무너지고, 무조건 관대하면 "대충 본다"가 된다. 점수라는 형식 자체가 이 환경에 맞지 않는다.
[결정] false negative(잘 말했는데 실패 처리)를 false positive보다 훨씬 나쁜 오류로 취급한다. 목표는 평가가 아니라 발화량이다.
6.5 백그라운드 오디오 및 플랫폼 제약 [보강 v0.2]
iOS
- Background Modes의 Audio 활성화
- AVAudioSession 카테고리: 재생 전용(.playback) ↔ 재생+녹음(.playAndRecord) 전환 정책 확정 필요
- .playAndRecord 고정 시 블루투스 음질이 계속 HFP에 묶임 → 6.1 매트릭스로 결정
- 심사: 백그라운드 오디오 사용 사유, 마이크 권한 문구 명확화
Android [신규 v0.2]
- 포그라운드 서비스 + 미디어 알림
- Android 14 (API 34) 필수 요건:
- 서비스 타입 선언: mediaPlayback, microphone
- 전용 권한: FOREGROUND_SERVICE_MEDIA_PLAYBACK, FOREGROUND_SERVICE_MICROPHONE
- 백그라운드에서 시작한 서비스의 마이크 접근 제한 → 잠금 화면 녹음이 여기서 막힐 가능성 높음. M1 확인 대상
- Doze 모드·배터리 최적화 예외 요청 여부 검토
6.6 SRS (간격 반복) 모듈 [신규 v0.2]
domain/srs — 순수 함수, 플랫폼 무관.
type MasteryLevel = 0 | 1 | 2 | 3 | 4 | 5
const INTERVALS_DAYS = [0, 1, 3, 7, 16, 35] // level → 다음 복습까지
function schedule(
current: MasteryLevel,
result: 'pass' | 'unclear',
now: Date
): { level: MasteryLevel; nextReviewAt: Date }
function buildReviewQueue(
mastery: MasteryRecord[],
now: Date,
capacity: number
): ExpressionId[] // 기한 지난 것 우선, 오래된 순
루틴 배분 규칙 - 기본: 신규 40% / 복습 60% - 첫 주: 신규 70%에서 시작해 4주에 걸쳐 40%로 이동 - 여행 D-day 페이스가 부족하면 복습 비중을 40%까지 축소 (진도 우선) - 걷기 구간 문장은 복습 큐에서 우선 추출 (걷기 = 출력 연습 구간)
테스트: 간격 진급·강등, 큐 우선순위, 용량 초과 시 절단, D-day 페이스 조정 — 모두 단위 테스트.
6.7 루틴 생성 알고리즘
- 입력: 탑승 시간, 도보 시간, 혼잡도 모드, 레벨 추정치, 복습 큐, 여행 목표, 진행 상황
- 레슨 요소별 예상 소요 시간 상수 (표현 카드 1개 ≈ 60초, 따라 말하기 1문장 ≈ 20초, Curveball 1개 ≈ 30초)
- 오디오 모드는 퀴즈를 제외하고 재생 시간 기준으로만 채운다
- 여행 목표: 남은 출퇴근 횟수 ÷ 남은 상황 수로 페이스 계산
- 순수 함수(
domain/routine-planner), 단위 테스트로 검증
6.8 오프라인 다운로드
- 트리거: 앱 실행 시, Wi-Fi 연결 시, 전날 밤 [제안]
- 항상 "오늘 + 내일" 분량 보관, 7일 지난 음성 파일 정리
- Pro는 코스 전체 선다운로드 허용
- Supabase Storage 서명 URL 만료 처리 (다운로드 재시도 시 URL 재발급)
6.9 로컬 알림 [신규 v0.2]
- 출근 시각 10분 전 / 퇴근 시각 10분 전, 반복 스케줄
- 문구에 오늘 상황 포함 → 알림 스케줄 시점에 다음 레슨 제목을 미리 계산해 넣는다
- 주말·공휴일 제외 로직
- 권한 요청 시점: 첫 레슨 완료 직후 (온보딩 중 요청 대비 수락률 차이 큼)
- 구현 비용 낮음 (반나절 내외). 서버 불필요
7. 마일스톤 [재구성 v0.2]
7.1 전체 일정
| 마일스톤 | 기간 | 산출물 | 완료 기준 |
|---|---|---|---|
| V. 선행 검증 | 2주 | 랜딩·목업, 설문 30명, 인터뷰 5건, 가격 민감도, 대기자 명단 | 발화 수용도 60%↑, 대기자 30명↑ |
| M0. 환경 세팅 (V와 병렬) | 1주 | RN 프로젝트, 린트·포맷, CI, Supabase, 디자인 토큰, CLAUDE.md | 실기기에서 빈 앱 실행, CI 통과 |
| M1. 스파이크 | 2주 | 블루투스 매트릭스 12케이스, 백그라운드 재생, 녹음+STT 전환, 소음 인식률, 배터리 실측, Whisper 벤치마크 | 게이트 6개 조건 → 8장 |
| ⛳ 게이트 리뷰 | — | 진행 / 대안 전환 / 중단 판정 | 전원 참석 |
| M2. 콘텐츠 파이프라인 | 1주 | DB 스키마, 스타일 가이드, 콘텐츠 JSON 형식, TTS·녹음 파이프라인, 샘플 10상황 | 스크립트 1회 실행으로 레슨 패키지 배포 |
| M3. 걷기 모드 (핵심) | 2주 | 오디오 상태 머신, 따라 말하기, 이진 판정, Curveball, 이어폰 제어, 안전 로직 | 화면 안 보고 걷기 루틴 완주 |
| M4. 온보딩·루틴·SRS | 1.5주 | 온보딩, 루틴 생성기, SRS 모듈, 오늘 화면 | 입력에 따라 루틴 변화, 복습 큐 동작, 단위 테스트 통과 |
| M5. 지하철 모드 | 2주 | 화면 모드(카드·대화·Curveball·퀴즈), 오디오 모드, 중단 복구 | 두 모드 모두 레슨 1개 완주 |
| M6. 오프라인·동기화 | 1주 | 다운로드 큐, 로컬 DB, 배치 업로드 | 비행기 모드 완료 → 복구 후 서버 반영 |
| M7. 기록·알림·결제·분석 | 1.5주 | 스트릭+보호장치, 이어폰 없는 날 모드, 로컬 알림, 인앱 결제, 분석 이벤트, Sentry | 알림 동작, 결제 완주, WSU 수집 확인 |
| M8. 베타 | 2주 | TestFlight / Play 내부 테스트 | 테스터 30명 2주 사용, 지표 수집 |
합계: 약 16주 (선행 검증 2주 + 개발 14주. M0는 V와 병렬)
7.2 v0.1 대비 배치 변화
v0.1: M0 → M1 → M2 → M3 온보딩 → M4 지하철 → M5 오프라인 → M6 걷기 → M7 → M8
↑ 9주차에 최대 리스크
v0.2: V ∥ M0 → M1(2주) → ⛳게이트 → M2 → M3 걷기 → M4 → M5 → M6 → M7 → M8
↑ 3주차에 최대 리스크 판정
7.3 2주 늘어난 것에 대한 근거
선행 검증 2주 + M1 1주 확대 = 4주 증가(M0 병렬로 실질 +3주). 대신: - 9주차에 프로젝트가 무너질 위험을 3주차로 앞당겨 확인한다. - 게이트에서 조기 전환하면 3주만 쓰고 방향을 튼다 (v0.1이라면 9주 소모). - 수요·가격이 개발 전에 확정되어 콘텐츠 분할과 페이월 설계를 다시 하지 않는다.
7.4 일정 산정의 전제 [신규 v0.2 — 확인 필요]
이 구성(구현=Claude, 검증=오너)의 병목은 코드 작성이 아니라 실기기 검증 루프다.
- 오디오 버그는 로그로 잡히지 않는다. 직접 들어봐야 한다.
- 블루투스 이슈는 기기마다 다르다. 이어폰 한 벌 = 회차 하나이고, 한 회차가 12칸 24건 —
기기 2대 · 셋업 포함 90분 안팎이다(6.1 · 정본
cases.ts산정). 이어폰을 바꾸면 회차가 하나 더 붙는다. - 지하철·길거리 실환경 테스트는 실제로 출퇴근하며 해야 한다.
전제: 오너가 주당 리뷰·실기기 테스트에 최소 10시간을 쓸 수 있어야 위 일정이 성립한다. 주 5시간이면 20주로 재산정해야 한다. 착수 전 확정 필요.
7.5 이후
- 2단계: 잠금 화면 녹음(M1 결과에 따라), 원격 푸시, 스토어 정식 출시
- 3단계: AI 롤플레이, 노선 연동, 여행 모드 고도화
- 차량은 3단계 후보에서도 뺐다 (2026-09-19 결정 → PRD 2장 "사용 환경 범위"). 러닝은 M1 게이트 당일 대표 판정에 달려 있다.
8. M1 게이트 — 진행 판정 기준 [신규 v0.2]
M1 스파이크 종료 시 아래를 충족하지 못하면 걷기 모드 설계를 재검토한다. 이 게이트를 통과하지 못한 채 M2 이후로 진행하지 않는다.
| # | 조건 | 기준 |
|---|---|---|
| G1 | 블루투스 재생 음질 유지 | 12케이스 중 최소 1개 조합에서 녹음 전환 후에도 A2DP급 재생 유지 |
| G2 | 세션 전환 지연 | 재생 → 녹음 전환 1초 이내 |
| G3 | 소음 환경 인식률 | 70dB 배경음에서 이진 판정 정확도 85% 이상 |
| G4 | 백그라운드 재생 | iOS·Android 모두 잠금 화면에서 30분 연속 재생 |
| G5 | 이어폰 버튼 | 재생/일시정지 + 다음, 최소 2종 동작 |
| G6 | 배터리 | 30분 걷기 모드에서 소모 8% 이하 |
[확인 필요] G1·G2 의 기준 문장이 R2 정본과 다르다. 2026-09-19 정정에서 6.1 의 축만 정본에
맞췄고, 게이트 조건은 내용 결정이라 건드리지 않았다. 어긋난 자리는 둘이다 —
① G1 "12케이스 중 최소 1개 조합" vs 정본 §6-2 의 회차 판정 G(재생만 칸 + C02·C08 이 두 OS
모두 통과 · 재생만 4칸에서 C04·C10 을 뺄지는 정본 §10 미결),
② G2 "전환 1초 이내" vs 정본 §6-1(이번 회차는 전환 ms 를 판정에 쓰지 않음 · transitionMsBudget: null).
어느 쪽으로 맞출지는 신우섭 CTO 가 M1 게이트(W3) 판정 전에 정한다. 그때까지 회차 판정은
정본 §6-2 가 이긴다(정본은 측정 시작 뒤 변경 금지).
미충족 시 대안 (우선순위 순)
| 순위 | 대안 | 영향 |
|---|---|---|
| 1 | 유선 이어폰 권장 + 블루투스는 재생 전용 모드로 축소 | 타깃 축소, 온보딩에 이어폰 안내 추가 |
| 2 | 녹음 생략 → "따라 말하기(판정 없음)" + 자기 평가 탭 | 발화는 유지, 피드백 상실. WSU 측정은 자가 보고로 |
| 3 | 걷기 모드를 듣기 전용으로 축소, 발화 연습은 귀가 후 홈 세션으로 이동 | 핵심 차별점 상실 → 제품 포지셔닝 재정의 필요 |
대안 3에 도달하면 이 제품은 "출퇴근 리스닝 앱"이 된다. 그 시장에서 이길 수 있는지를 별도로 판단해야 한다. 사전에 인지하는 것이 목적이다.
9. Claude와의 작업 방식
역할 분담
| Claude | 오너 |
|---|---|
| 코드 작성, 테스트 작성, 리팩토링 | 요구사항 확정, 코드 리뷰 |
| 콘텐츠 초안·스크립트 작성 | 실기기 테스트 (오디오·이어폰·지하철·길거리) |
| 문서(CLAUDE.md, README) 유지 | 계정·키 관리, 스토어 배포 승인 |
| 순수 로직(SRS·루틴·판정) 단위 테스트 | 원어민 성우 섭외·녹음 세션 |
규칙
- 저장소 루트에 CLAUDE.md: 스택, 폴더 구조, 코딩 규칙, 명령어, 금지 사항(비밀 키 커밋 금지 등)
- 작업 단위 = 이슈 1개 = PR 1개, 한 PR은 한 기능·300줄 내외
- 모든 PR은 타입 체크·린트·테스트 통과 후 리뷰 요청
- 네이티브 설정 변경(Info.plist, AndroidManifest, Podfile, Gradle)은 PR 설명에 변경 이유 명시
- 브랜치:
main보호,feat/*,fix/* - [신규]
domain/아래 코드는 React·네이티브 import 금지 (순수성 유지, lint 규칙으로 강제)
한계 인지
- Claude는 실기기에서 직접 실행·청취할 수 없으므로 오디오·이어폰·백그라운드 동작은 반드시 오너가 실기기로 검증
- iOS 빌드·서명은 Mac + Xcode 환경 필요
- [신규] 블루투스 코덱 전환, 소음 환경 인식률, 배터리 소모는 오너 실측 외에 확인 수단이 없다. M1의 실질 작업자는 오너다
10. 테스트 전략
자동화
- 단위 (
domain/): SRS 스케줄링, 루틴 배치, 여행 페이스 계산, 발음 판정 규칙, 동기화 충돌 규칙, 스트릭 계산(주말 제외·프리즈) - 컴포넌트: 지하철 모드 카드·퀴즈 상호작용, 페이월
- E2E (Maestro): 온보딩 → 루틴 → 레슨 완료 핵심 경로, 결제 플로우
실기기 체크리스트 (수동) [보강 v0.2]
오디오 - [ ] 화면 잠금 후 30분 연속 재생 유지 - [ ] 이어폰 버튼 3종 동작 (재생/일시정지, 다음, 이전) - [ ] 블루투스 매트릭스 12케이스 (OS 2 × 세션 구성 2 × 상황 3 = C01~C12 · 이어폰은 축이 아니라 한 벌 = 회차 하나) — 6.1 - [ ] 재생 → 녹음 전환 지연 실측 - [ ] 통화 수신 → 종료 후 재개 - [ ] 블루투스 연결 끊김·재연결 - [ ] 타 앱 재생 / 알람 / 내비 안내음 인터럽트 — 6.2 정책 표 검증
환경 - [ ] 지하철 소음 환경 인식률 (실제 탑승) - [ ] 도심 인도 70~80dB 환경 인식률 (실제 보행) - [ ] 속삭임 볼륨 인식률 — 사회적 장벽 대응의 전제 - [ ] 비행기 모드(터널 가정)에서 레슨 진행 - [ ] 만원 지하철에서 오디오 모드 조작 가능 여부
성능·안전 - [ ] 배터리 소모 (30분 걷기 모드, 목표 8% 이하) - [ ] 발열 (Whisper 사용 시) - [ ] 걷기 모드 중 화면 켜짐 → 자동 일시정지 동작
알림·결제 - [ ] 출퇴근 시각 알림 정확도, 주말 제외 - [ ] 인앱 결제 구매·복원·만료
11. 배포
- iOS: Apple Developer 계정, TestFlight 내부·외부 테스트
- Android: Google Play Console, 내부 테스트 트랙
- Fastlane으로 빌드·업로드 자동화, 버전은 태그 기반
- 심사 대비:
- 백그라운드 오디오 사용 사유 명시
- 마이크·음성 인식 권한 안내 문구
- [신규] 구독 상품 정보 (가격, 갱신 조건, 해지 방법) — 스토어 심사 필수 항목
- 개인정보처리방침 페이지 필요
- [신규] "녹음 원본은 기기를 떠나지 않습니다"를 명시 — 심사 통과에 유리하고 마케팅 자산이 된다
12. 리스크 [보강 v0.2]
| # | 리스크 | 영향 | 대응 |
|---|---|---|---|
| R1 | [신규] 블루투스 녹음 전환 시 재생 음질 급락 (A2DP → HFP) | 치명 | M1 매트릭스 12케이스(C01~C12 · 6.1) 실측, G1 게이트 조건. 미충족 시 8장 대안 |
| R2 | [신규] 길에서 발화하는 사회적 장벽으로 걷기 모드 미사용 | 치명 | 선행 검증 2주. "통화하는 척" UX, 속삭임 허용 |
| R3 | 소음 환경 STT 품질·기기 편차 | 높음 | 이진 판정 + 관대한 통과, SNR 기반 분기, 엔진 추상화로 교체 가능 |
| R4 | [신규] Android 14 백그라운드 마이크 제한 | 높음 | M1에서 확인. FGS 타입·권한 선언. 불가 시 잠금 녹음은 2단계에서도 제외 |
| R5 | [신규] 오너 실기기 검증 시간 부족 | 높음 | 주당 10시간 전제 명시. 미달 시 20주로 재산정 |
| R6 | RN 버전 업·New Architecture 라이브러리 비호환 | 중간 | M0에서 핵심 라이브러리 호환 확인, 버전 고정 |
| R7 | 네이티브 이슈를 Claude가 실기기 없이 디버깅 | 중간 | 오너가 로그·재현 절차 공유, Sentry 활용 |
| R8 | 콘텐츠 제작이 개발보다 늦어짐 | 중간 | M2 이후 콘텐츠 제작을 개발과 병행. 스타일 가이드로 검수 속도 확보 |
| R9 | iOS SFSpeechRecognizer 네트워크 모드 요청 제한 | 중간 | 온디바이스 우선, 미지원 기기는 Whisper 폴백 검토 |
| R10 | 인앱 결제 심사 반려 (구독 정보 미비) | 낮음 | 스토어 요구 항목 체크리스트화 |
13. 착수 전 확인 필요
최우선 (일정·범위를 좌우)
- [ ] 오너 주당 리뷰·테스트 가능 시간 — 10시간 미달 시 일정 전면 재산정
- [ ] 테스트 이어폰 3종 확보 (AirPods급 / 갤럭시 버즈급 / 저가) — M1 전제. 12케이스의 축이 아니라 회차 파라미터다(6.1): 한 벌 = 회차 하나이므로 3종이면 같은 12칸을 세 벌 돌린다. 한 벌만 있어도 회차 1은 돌아가므로, 3종이 다 모이는 것이 M1 착수 조건은 아니다 — 확보 순서가 회차 순서이고, 어느 벌로 돈 회차인지는 회차 헤더에 모델명으로 적는다
- [ ] 테스트 기기: iPhone·Android 기종 (Android는 삼성 + 비삼성 각 1대 권장, 제조사별 STT 편차)
예산
- [ ] 월 API 비용 허용 범위 (TTS 초기 제작 + 상시 STT 폴백)
- [ ] 원어민 성우 녹음 30~50만원 승인 여부
- [ ] 선행 검증 광고비 (랜딩 테스트용, 10~30만원 수준)
계정·환경
- [ ] Mac 보유 여부 (iOS 빌드 필수)
- [ ] Apple Developer, Google Play Console, Supabase, GitHub 계정
- [ ] TTS 서비스 선택 (다국 억양 보이스 지원 여부가 선택 기준)
제품 결정
- [x] 앱 이름 — '오가며' 확정(2026-09-16, 오너 지시). 상표·도메인·앱스토어 중복 확인(PRD §13 U9)은 2026-10-09 충돌 없음으로 해소됐다 (PRD §13 "해소됨" 참고)
- [ ] 우선 플랫폼: iOS·Android 동시 vs 한쪽 먼저
- [ ] 최소 지원 OS 버전 (Android 14 FGS 요건 고려)