PRD 신규 작성 (AI-Ready PRD) - Beupgo 유튜브용
1.요약 (Executive Summary)
1.1 앱이름
1.2 한 줄 가치 제안 (One-liner)
- “갈등의 언어를 공감의 언어로 바꾸는 NVC 기반 화해 편지 앱" (사용자가 겪는 관계 갈등을 단계별 질문으로 구조화하고, NVC 모델과 AI 제안으로 부드러운 화해 메시지를 만들어 가장 편한 방식 (텍스트)으로 전달할 수 있게 돕습니다.)
1.3 핵심 가치 키워드 (Core Value)
- NVC 템플릿, 단계별성찰(5Q), 오프라인우선, 프라이버시 제일주의, 즉시성(로그인불필요), 간결한공유(텍스트)
1.4 대상 사용자 (Target Users)
- 말다툼 후 먼저 화해하고 싶은 사람
- 친구 또는 동료, 배우자 또는 연인, 부모 또는 자녀, 형제 또는 자매, 시댁 또는 처가
- 상담은 부담스럽지만 검증된 대화기술(NVC)을 쉽게 적용하고 싶은 사람
- 개인정보 노출 없이 오프라인으로 쓰고 싶은 사람
1.5 타깃 플랫폼 & 기술원칙 (Target Platform & Guardrails)
- iOS 전용
- SwiftUI + MVVM, Swift Concurrency(async/await), 현대적 Error (throw/try/catch)
- Completion Handler 금지, NSError 금지
- 버튼 형식 강제 :
Button { /* action */ } label: { /* view */} 만 사용 - 현지화 강제 : 모든 사용자 노출 텍스트는 Localizable.xcstrings 의 Key로 참조 (기본 언어 영어, 한국어, 일본어, 중국어 총 4개국어 지원)
- 오프라인 우선 : 사용자 데이터(성찰 답변, 일기) 온디바이스 영구 저장(SwiftData)
- AI로 초안 생성시에는 gemini-2.5-flash-lite 모델 사용
- AI 연동은 Firebase AI Logic 사용
- Firebase AppCheck 를 사용하여 승인되지 않은 클라이언트로부터 API를 보호
- 로그는 import os.log 를 사용하여 Apple의 통합 로깅 시스템을 사용, print 사용 금지
AI를 위한 코딩 수칙 요약
SwiftUI·MVVM, async/await만, NSError 금지, 모든 문자열은 Localizable.xcstrings 키로, 버튼은 지정 패턴만, 로그는 import os.log 를 사용.
1.6 MVP 스코프 (What’s In for v1.0)
핵심 여정(한 번에 끝까지 가는 “관계 회복 세션")
1.6.1 관계선택(ConflictSectionView)
- Free: 친구 또는 동료
- Pro : 배우자 또는 연인, 부모 또는 자녀, 형제 또는 자매, 시댁 또는 처가 (Pro는 잠금 + Paywall 유도)
1.6.2 1단계 - 마음정리(GuidedJournalView, 5문항)
- 상황, 감정, 상처, 나의성찰, 진짜원했던것(최대 500자, 감정 태그 ≥8)
- 온디바이스 임시→최종 저장(SwiftData)
1.6.3 2단계 - 마음편지작성(MessageComposerView)
- 1단계 답변 → NVC 기반 초안 생성(Gemini AI) → 부드럽게/간결하게/다시 제안 등 어시스턴트 버튼
1.6.4 3단계 - 전달(Delivery)
- 텍스트복사, 이미저장, 링크공유(Firestore 익명 저장, TTL 7일)
1.6.5 수익화(PaywallView)
- 1회성 IAP 12,900원 평생 이용권(Pro)
- 결제 성공시 즉시 잠금 해제 및 UI 업데이트 (복원 포함)
Out (MVP 범위 외)
회원 시스템/실시간 협업/웹·안드로이드 클라이언트/AI 장문 코칭(지속 대화)/크로스 디바이스 동기화.
1.7 차별점 (Why KindVerb)
- 심리 기술의 즉시 사용성 : NVC를 질문-요약-초안으로 즉시 실행
- 프라이버시: 로그인 없이 전 기능 사용, 온디바이스 저장 기본
- 낮은 진입장벽 : Free 카테고리로 가치 체험 → Pro 전환이 자연스럽게 연결
- 전달 다양성: 텍스트/이미지/링크(TTL7일)까지, 상대 선호에 맞춘 선택
1.8 비즈니스 모델 요약 (Monetization Snapshot)
- Free : 친구 또는 동료 카테고리 사용가능, 핵심 여정 전부 사용 가능
- Pro(12,900원, 1회성 IAP, 평생): 배우자 또는 연인, 부모 또는 자녀, 형제 또는 자매, 시댁 또는 처가 카테고리 잠금해제 + 전문가 라이브러리 + 마음노트고급
- 광고없음(집중, 신뢰유지)
- 페이월 트리거 : Pro 카테고리 선택시 또는 AI 고급 리라이팅 요청시
- Gemini API 요청 제한 : Free 사용자(0시 부터 24시 까지 하루 최대 1회), Pro 사용자(0시 부터 24시 까지 하루 최대 10회)
1.9 성공지표 (Success Metrics)
| Metric | 정의 | 목표(MVP) | 측정 |
| Activation | 첫 실행 → 첫 편지 완성 비율
| ≥ 30%
| 이벤트 퍼널
|
| D1 Retention | 다음날 재방문율
| ≥ 35%
| 리텐션 코호트
|
| Crash-free | 크래시 없는 세션 비율
| ≥ 99.8%
| Crashlytics
|
| TTI | Cold Launch → 첫 터치 가능
| ≤ 2.0s
| 계측 로그
|
| Pro 전환 | 구매 전환율(MAU 대비)
| 3–5%
| IAP 이벤트
|
※ TTI(Time To Interactive) : 앱을 실행한 뒤, 사용자가 실제로 첫 터치를 할 수 있을 때까지 걸리는 시간을 의미
1.10 일정(알파 → 출시) & 마일스톤 (4주 MVP)
- 주1 : 프로젝트 스캐폴드, 화면골격/네비게이션, 공통 컴포넌트, 현지화구조
- 주2: GuideJournal(5문항) UI/로직, SwiftData 저장, 상태관리
- 주3: AI 초안 생성/리라이팅 워크숍, 텍스트/이미지/링크공유(링크 TTL 7일)
- 주4: IAP + Paywall, 아이콘/애니메이션 폴리싱, 전체 시나리오 E2E 테스트 → App Store 제출
1.11 리스크 & 가드레일(요약)
- AI 품질 변동: 초안 재요청(리트라이), 프롬프트 체계화, 로컬 템플릿 백업
- IAP 심사: 명확한 가치 제시, 복원제공, 가이드라인 준수(영수증 검증)
- 개인정보/보안: 기본 오프라인, 링크는 익명 + TTL 7일 자동 삭제
- 성능: 메모리/스크롤 FPS 모니터링, 이미지 렌더 최적화
2.문제정의&맥락 (Problem Statement & Context)
2.1 문제(Problem)
- 사용자상황 :
- 일상적인 대화 속에서 갈등•오해•상처가 발생했을 때, 대부분의 사람들은 즉각적으로 사과하거나 감정을 정리해서 표현하기가 어렵습니다.
- 구체적불편 :
- 감정이 격해져서 어떤 말을 해야 할지 떠오르지 않음
- 사과를 하고 싶지만 표현 방식을 잘못해 오히려 갈등이 커짐
- “나도 상처받았다"라는 마음을 부드럽게 전달할 방법을 찾기 힘듦
- 심리 상담•코칭 앱은 부담스럽거나 지속적인 비용이 듦
- 핵심 Pain Point 요약 :
- “갈등 이후, 따뜻하고 관계 회복적인 언어를 찾는 것이 어렵다.”
2.2 기존 대안 & 한계 (Existing Alternatives & Limitations)
- 메신저 바로 쓰기 : 충동적으로 보낸 메시지가 오해를 키움 → 후회 + 관계악화
- 검색 & 블로그 글 참고 : “사과 메시지 예시" 같은 글은 상황 맞춤형이 아님 → 진정성 부족
- 자필 편지/메모 : 따뜻하긴 하지만 형식•구조가 잡히지 않아 오해 여지 있음
- 심리 상담/커플 치료 : 효과적이지만 즉각적이지 않고, 비용•시간 장벽 큼
- 경쟁 앱/서비스 : 기존 저널링 앱은 **“나의 감정 기록”**에 집중, 상대방에게 건네는 언어까지 연결하지 못함
따라서 현재 시장에는 “갈등 후 즉시, 안전하고 공감적인 화해 언어를 만들어 전달” 해주는 적합한 솔루션이 부족합니다.
2.3 해결 가설 (Solution Hypothesis)
KindVerb는 다음과 같은 방식으로 문제를 해결합니다 :
- 구조화된 5단계 자기 성찰 질문(5Q Flow)
- 사용자가 감정을 정리하고 진짜 전하고 싶은 마음을 발견
- AI 기반 NVC 템플릿 메시지 생성
- “관찰 → 감정 → 필요 → 부탁" 구조로 안전한 문장 제공
- 사용자 주도 리라이팅 옵션
- “부드럽게", “간결하게" 등 맥락에 맞게 조정
- 다양한 전달 방식 지원
- 텍스트 복사, 이미지 저장, 링크 공유(7일 TTL)
- 프라이버시 중심 설계
👉 가설 : 사용자는 즉시 사용할 수 있는 화해 언어 도구를 원하고, 이 경험은 재방문율과 Pro 업그레이드로 이어질 것이다.
2.4 제품 경계 (Product Boundaries)
- 하지 않을 것 (이번 범위 아님 / Later)
- 실시간 채팅/상담 기능 : 전문가와 연결하거나, 사용자 간 익명 대화 지원 ❌
- 멀티 플랫폼(Web/Android): 초게이는 iOS 전용
- AI 장기 코칭/대화 세션 : MVP는 “편지 1회 완성"에 집중
- 지속 동기화·클라우드 백업 : 온디바이스 우선 전략 유지
- 이유 : MVP의 목표는 **단일 여정(Conflict → Reflection → Message → Delivery)**이 완성도 있게 동작하는 것
2.5 인사이트 (Contextual Insight)
- 심리학적 배경: NVC(Nonviolent Communication, 비폭력 대화)는 연구적으로 긴장 완화와 갈등 회복에 효과가 검증됨.
- 디지털 환경의 맥락:
- 사람들은 실시간 대화보다 비동기적 텍스트 메시지에서 더 솔직해질 수 있음
- 프라이버시 걱정 없는 즉시성 앱은 기존 상담 대비 장벽을 크게 낮춤
- 비즈니스 맥락:
- “셀프케어/마음 챙김” 앱 카테고리 성장 중
- 구독형보다는 저항이 적은 1회성 IAP가 관계 회복 툴에 더 적합
3.목표, 비목표, 성공지표 (Goals, Non-Goals, Success Metrics)
3.1 목표(Goals, 우리가 반드시 달성해야 할 것)
3.1.1 “간단하고 직관적인 감정 기록"
- 사용자가 3탭 이내로 감정을 기록할 수 있어야 한다.
- 오프라인 상황에서도 입력된 데이터는 로컬 큐에 저장되고, 온라인이 되면 자동 동기화한다.
3.1.2 “건강한 표현 제안"
- 기록된 감정을 기반으로, 앱은 비폭력 대화(NVC) 구조에 맞는 문장 템플릿을 제안해야 한다.
- 사용자는 추천된 문장을 복사・공유할 수 있어야 한다.
3.1.3 “안정성과 신뢰성 확보"
- 앱은 **Crash-Free 세션 비율 ≥ 99.8%**를 유지해야 한다.
- 데이터 손실 없는 동기화와 **안정적인 첫 실행 경험(TTI ≤ 2.0s)**을 보장한다.
3.2 비목표 (Non-Goals, 이번 범위에 포함하지 않는 것)
- "Android / Web 버전 지원" → 이번 MVP에서는 iOS만 지원한다.
- "심층 AI 코칭 기능" (예: 상담형 대화, AI 기반 대화 분석) → 차후 유료 구독 모델에서 고려.
- "소셜 네트워크 공유/피드" → MVP 범위에는 포함하지 않는다. 단, 문장 복사 & 기본 공유 시트는 허용.
- "사용자 맞춤형 알림/리마인더" → MVP 이후 버전에 포함.
3.3 성공지표 (Success Metrics, 정량적으로 측정 가능한 목표)
| Metric | 정의 | 목표 | 측정방법 |
| D1 Retention | 설치 후 다음 날 앱을 다시 연 사용자 비율 | ≥ 35% | Analytics 이벤트 (install → session_start) |
| Crash-Free Sessions | 크래시 없는 세션 비율 | ≥ 99.8% | Firebase Crashlytics |
| TTI (Time To Interactive) | 앱 실행 → 첫 입력 가능까지 걸린 시간 | ≤ 2.0s | 계측 로그 |
| Emotion Log Completion Rate | 감정 기록 플로우 완료율 (기록 시작 → 저장 완료) | ≥ 80% | 이벤트 퍼널 추적 |
| AI Suggestion Usage | 제안된 문장 복사/공유 비율 | ≥ 30% | 이벤트 로그 (copy_suggestion, share_suggestion) |
4.페르소나 & JTBD (Personas & Jobs To Be Done)
4.1 주요 페르소나 (Key Personas)
4.1.1 Persona1: “에밀리(28세, 직장인)”
- 상황/목표:
- 직장에서 상사와 갈등이 생길 때, 감정을 바로 기록하고 싶음.
- 퇴근 후 연인과의 대화에서 불필요한 싸움을 줄이고 싶음.
- 고통지점(Pain Points):
- 순간적으로 화가 나면 감정을 정리하지 못하고, 말로 표현하다가 관계가 악화됨.
- 기존 메모 앱은 감정 분류/표현 지원이 없어 활용하기 어려움.
- 성공의 모습 (Success State):
- **“감정을 빠르게 기록하고, 앱이 제안하는 부드러운 문장을 사용”**하여 대화 갈등을 줄임.
4.1.2 Persona2 : “민수(34세, 기혼, 두 아이의 아버지)”
- 상황/목표:
- 가족 대화에서 불필요한 언성이 오가는 것을 줄이고 싶음.
- 아이에게 화내지 않고 차분하게 의사 표현을 하고 싶음.
- 고통지점 (Pain Points):
- 순간적으로 감정을 표현하다가 아이와 배우자에게 상처 주는 말을 자주 함.
- 기존 일기/메모 앱은 단순 기록에 그쳐 실질적 도움을 주지 못함.
- 성공의 모습 (Success State):
- **“앱이 제공하는 NVC 기반 문장 템플릿”**으로 아이와 배우자에게 상처 없는 대화를 이어감.
4.1.3 Persona3: “소라(22세, 대학생)”
- 상황/목표:
- 기숙사에서 룸메이트와 생활하면서 스트레스가 많음.
- 연인과 장거리 연애 중이라, 감정 조절이 어렵고 표현 방법을 찾고 있음.
- 고통지점 (Pain Points):
- 친구/연인에게 감정을 전달하다가 의도치 않게 오해를 삼.
- SNS는 과하게 공개적이라 솔직한 감정 기록이 어려움.
- 성공의 모습 (Success State):
- **“앱에 비공개로 감정을 정리하고, 차분히 정제된 말”**을 건네 관계를 안정적으로 유지.
4.2 JTBD (Jobs To be Done)
JTBD 문장은 “When ___, I want to ___, so I can ___.” 형식으로 작성.
- When I feel frustrated during a conversation with my partner,
I want to record my emotions quickly in the app,
so I can avoid saying things that might hurt them.
- When I argue with my kids after work,
I want to see a suggested kind sentence template,
so I can calm the situation without raising my voice.
- When I experience stress in daily life (school, work, or relationships),
I want to log my feelings privately and safely,
so I can reflect later and improve how I express myself to others.
5.주요 사용 사례 & 사용자 스토리 (Top Use Cases & User Stories)
5.1 핵심 사용 사례 (Top Use Case)
앱 KindVerb의 MVP 단계에서 가장 중요한 3~4가지 시나리오를 정의합니다.
각각은 Gherkin 문법 (Given-When-Then) 으로 작성하여, AI가 바로 테스트 케이스로 변환할 수 있도록 합니다.
- UC-1: 감정 기록 (Emotion Logging)
- Given 사용자가 직장에서 상사에게 부정적인 피드백을 받고 기분이 상함
- When 앱을 열어 감정을 선택하고 간단한 메모를 남김
- Then 앱은 해당 감정을 저장하고, 나중에 돌아볼 수 있도록 시각화된 기록을 제공
- UC-2: 친절한 문장 제안 (Kind Sentence Suggestion)
- Given 사용자가 연인과 다툼 중 감정을 바로 표현하고 싶음
- When 앱에서 “감정 기록”을 완료했을 때
- Then 앱은 NVC(비폭력대화) 기반의 친절한 문장 템플릿을 추천하여, 사용자가 그대로 활용하거나 수정 가능
- UC-3: 오프라인 기록 & 동기화 (Offline Logging & Sync)
- Given 사용자가 지하철이나 비행기 모드로 오프라인 상태임
- When 감정을 기록하면
- Then 앱은 이를 로컬 큐에 저장하고, 온라인 상태가 되면 자동으로 서버에 동기화
- UC-4: 감정 리플렉션 & 성장 (Emotion Reflection & Growth)
5.2 사용자 스토리 (User Stories)
아래 사용자 스토리들은 제품의 감동 포인트를 잘 드러내어, 단순 기능을 넘어서는 정서적 가치를 보여줍니다.
(언론 인터뷰, 투자 피치덱, 앱스토어 스토리 등에서 바로 활용 가능)
5.2.1 Story1: “상처를 줄이지 못했던 말들이 줄어들었어요"
민수(34세, 아버지)는 아이들에게 자주 화를 냈습니다. 그러나 KindVerb를 사용한 뒤, 그는 화가 날 때마다 감정을 기록했고 앱이 제안한 친절한 문장을 선택했습니다. “예전 같으면 큰소리를 쳤을 상황에서, 지금은 ‘아빠가 피곤해서 목소리가 커졌어, 네가 잘못한 건 아니야.’라고 말할 수 있었어요.” 그의 아이들은 점점 아빠와 대화하는 시간이 더 안전하고 따뜻하다고 느끼게 되었습니다.
5.2.2 Story2: “사랑하는 사람을 지킬 수 있는 도구가 되었어요"
에밀리(28세, 직장인)는 연인과의 잦은 다툼으로 힘들어했습니다. 감정이 올라올 때마다 날카로운 말을 내뱉곤 했습니다. KindVerb를 사용하면서, 감정을 기록하는 과정이 잠시 멈추는 시간을 만들어 주었고, 앱이 제안하는 부드러운 문장을 통해 연인과 다시 소통할 수 있었습니다. “싸움이 줄어들고, 연인과 함께 미래를 이야기할 수 있는 여유가 생겼어요.”
5.2.3 Story3: “나 자신을 더 잘 알게 되었어요.”
소라(22세, 대학생)는 SNS에 솔직한 감정을 적기 부담스러워했습니다. KindVerb에만 자신의 감정을 기록하면서, 그녀는 자신이 반복적으로 불안해하는 순간들을 발견했습니다. 앱의 리플렉션 기능을 통해, 그녀는 자신의 감정 패턴을 차분히 들여다볼 수 있었고, “예전보다 더 나를 이해하게 되었고, 인간관계에서도 덜 불안해졌어요.” 라고 말했습니다.
5.3 감동 포인트 요약 (PR/인터뷰용)
- 즉각적인 감정 기록 → 관계 악화를 막는 예방 장치
- AI 추천 친절 문장 → 가족·연인·동료와의 대화 품질 향상
- 리플렉션 → 사용자가 자신의 성장과 변화를 확인
- 오프라인 동작 → 언제 어디서든 감정을 놓치지 않음
5.4 앱스토어 설명문 (App Store Description)
5.4.1 앱이름
5.4.2 한 줄 소개 (Subtitle)
- 말 한마디가 관계를 바꿉니다. 감정을 기록하고, 친절한 대화를 시작하세요.
5.4.3 주요소개 (Long Description)
KindVerb는 감정을 건강하게 기록하고, AI가 제안하는 친절한 문장으로 따뜻한 대화를 이어갈 수 있게 돕는 iOS 앱입니다.
- 빠른 감정 기록
- 순간의 감정을 선택하고 간단히 메모하세요. 앱은 기록을 안전하게 보관하며 언제든 돌아볼 수 있습니다.
- AI 기반 친절한 문장 추천
- 다툼이 커지기 전에, 상황에 맞는 부드러운 표현을 제안합니다. 그대로 사용할 수도 있고, 나만의 말로 다듬을 수도 있습니다.
- 오프라인 기록 & 동기화
- 지하철, 비행기 모드에서도 감정을 놓치지 않고 기록합니다. 온라인 상태가 되면 자동 동기화됩니다.
- 성장 리플랙션
- 지난 기록들을 차트와 패턴으로 확인하고, 점점 더 나은 대화를 만들어가는 자신을 발견하세요.
5.4.4 USP (Unique Selling Point)
- 관계를 지켜주는 즉각적인 감정 기록 + 친절한 문장 추천
- 오프라인에서도 놓치지 않는 안정성
- 나의 성장 과정을 시각화하는 리플렉션 기능
5.4.5 앱스토어 키워드 예시
감정 기록, 대화, 감정 관리, 커뮤니케이션, 연애, 자기계발, 멘탈헬스
5.4.6 앱스토어 미리보기 스크린샷 카피
- “단 3초, 감정을 기록하세요.”
- “AI가 제안하는 친절한 문장으로 대화를 이어가세요.”
- “지난 대화를 돌아보며 성장하세요.”
- “오프라인에서도 감정을 놓치지 마세요.”
5.5 마케팅 에셋 (AppStore, SNS, Landing Page용 카피)
5.5.1 앱스토어용 스토리 (스토어 피쳐 추천용)
“KindVerb는 단순한 감정 기록 앱이 아닙니다. 이 앱은 우리가 사랑하는 사람과 더 따뜻하게 연결될 수 있도록 돕는 작은 도구입니다. AI가 제안하는 ‘친절한 한마디'는, 당신의 하루와 관계를 바꿀 수 있습니다.”
5.5.2 SNS 홍보용 짧은 카피 (트위터/인스타/스레드)
- “말 한마디가 관계를 바꿉니다. KindVerb와 함께 대화를 더 따뜻하게.”
- “화가 날 때, 그냥 말하기 전에 잠시 기록하세요. KindVerb가 도와드립니다.”
- “사랑하는 사람과 더 좋은 대화를 나누고 싶나요? KindVerb가 답입니다.”
5.5.3 랜딩페이지 섹션 카피
- Hero Section (첫 화면)
- 말 한마디가 관계를 바꿉니다.
- KindVerb - 감정을 기록하고, 더 친절한 대화를 시작하세요.
- Features Section
- 감정 기록 → 단 3초면 충분합니다.
- AI 추천 → 상황에 맞는 친절한 문장을 제안합니다.
- 오프라인 모드 → 어디서든 감정을 놓치지 않습니다.
- 리플렉션 → 나의 성장과 변화를 차트로 확인하세요.
- Call to Action
- “오늘부터 더 따뜻한 대화를 시작하세요.”
- 👉 [앱스토어 다운로드 버튼]
5.5.4 PR/인터뷰용 키 메시지
- “KindVerb는 사람 사이의 오해와 상처를 줄이고, 따뜻한 대화를 가능하게 합니다.”
- “우리는 단순히 감정을 기록하는 것이 아니라, 관계를 지켜내는 기술을 만들고 있습니다.”
- “KindVerb의 미션은 더 많은 사람들이 ‘말 한마디로 사랑을 지킬 수 있도록 돕는 것’입니다.”
6.범위&우선순위 (Scope & Prioritization - Gherkin)
6.1 기능 목록 요약 (MoSCoW)
| ID |
기능명 |
요약 |
사용자 가치 |
우선순위 |
| F-01 |
관계 유형 선택 |
5개 관계 카드(Free 2, Pro 3)에서 여정 시작 |
적합한 여정으로 빠른 진입 |
Must |
| F-02 |
1단계: 마음 정리(5문항) |
상황·감정·상처·반성(선택)·진짜 원했던 것 입력 |
감정의 언어화·흥분 완화 |
Must |
| F-03 |
2단계: AI ‘마음 편지’ 작업실 |
NVC 기반 초안 생성/수정(부드럽게/간결하게/다시 제안/진솔하게) |
상처 줄이는 대화문 자동 생성 |
Must |
| F-04 |
3단계: 전달(복사/이미지/링크) |
편지 복사, 편지지 이미지 저장, 7일 만료 링크 공유 |
즉시 실사용 가능 |
Must |
| F-05 |
인앱 결제(Pro 평생권) |
Pro 카테고리 해제, 구매/복원, 실패 처리 |
수익화·지속가능성 |
Must |
| F-06 |
로컬 저장/오프라인 큐/재시도 |
SwiftData 저장, 오프라인 큐잉, 온라인 시 자동 재시도 |
네트워크 무관 안정성 |
Should |
| F-07 |
리플렉션(최근 기록 요약) |
최근 30일 감정 태그 빈도/간단 지표 카드 |
스스로의 변화 가시화 |
Should |
| F-08 |
알림(부드러운 리마인더) |
감사 노트/감정 기록 친절 알림(주 2회 이하, 사용자가 온/오프) |
습관화·리텐션 |
Could |
| F-09 |
라이브러리(전문가 칼럼 읽기) |
5~8개 핵심 글의 온디바이스 번들(외부 링크 無) |
교육 효과·브랜드 신뢰 |
Could |
| F-10 |
계정/로그인/클라우드 동기화 |
사용자 계정/원격 백업 |
개인정보·개발 복잡도 증가로 이번 범위 제외 |
Won’t |
| F-11 |
광고/추적 SDK |
서드파티 광고/트래킹 |
제품 신뢰/집중도 저해, 이번 범위 제외 |
Won’t |
6.2 RICE 우선순위 (MVP 기준 가정치)
- R(Reach) = MVP 3개월 예상 도달 사용자수
- I(Impact) = 전환/리텐션 기여(3=높음, 2=중, 1=낮음)
- C(Confidence) = 추정 신뢰도(0~1)
- E(Effort) = 주(엔지니어 1명 기준)
- RICE = (R × I × C) / E
| ID |
R |
I |
C |
E(weeks) |
RICE |
코멘트 |
| F-05 |
6k |
3 |
0.8 |
1.5 |
9,600 |
수익 임팩트 최상, 구현 난이도 중 |
| F-03 |
6k |
3 |
0.7 |
2.0 |
6,300 |
코어 가치, 모델 품질이 핵심 |
| F-04 |
6k |
2 |
0.9 |
1.0 |
10,800 |
공유/바이럴 직접 기여 |
| F-02 |
6k |
2 |
0.9 |
1.0 |
10,800 |
핵심 입력 파이프 |
| F-01 |
6k |
2 |
0.95 |
0.5 |
22,800 |
첫 진입 허들 제거 |
| F-06 |
6k |
2 |
0.8 |
1.0 |
9,600 |
신뢰/안정성 |
| F-07 |
4k |
2 |
0.7 |
1.0 |
5,600 |
리텐션 보조 |
| F-08 |
4k |
1 |
0.8 |
0.5 |
6,400 |
가벼운 노력 대비 효과 괜찮음 |
| F-09 |
3k |
1 |
0.7 |
0.8 |
2,625 |
신뢰 형성용 |
결론(개발 순서 추천): F-01 → F-02 → F-03 → F-04 → F-05 → F-06 → (F-07/F-08) → F-09
6.3 기능별 스펙 (Gherkin + 수용 기준)
6.3.1 F-01 유형 선택 (Conflict Selection)
- 비즈니스 규칙
- 카드 5종:
friend, partner = Free / parent_child, siblings, in_laws = Pro
- 선택 시 다음 화면으로
conflictType 전달
- 텍스트/버튼 라벨은 모두 현지화 키 사용(예:
conflict.title, conflict.button.start_free)
- Gherkin
Feature: Conflict type selection
As a user I want to start with a relationship context to receive tailored guidance.
Scenario: Show 5 conflict cards with Free/Pro badges
Given app launched first time
When the user reaches the conflict selection screen
Then 5 cards are visible
And "friend" and "partner" cards show a "Free" badge via key "conflict.badge.free"
And "parent_child", "siblings", "in_laws" cards show a "Pro" badge via key "conflict.badge.pro"
Scenario: Start Free journey
Given the user is on the conflict selection screen
When the user taps the "Start" button on "friend"
Then navigate to Guided Journal with conflictType="friend"
Scenario: Tap Pro card leads to paywall
Given the user is on the conflict selection screen
When the user taps the "Start Pro" button on "parent_child"
Then show Paywall screen
Scenario: Accessibility and localization keys are used
Given voiceover is on
Then card titles/CTA use localization keys only
- 수용기준
- 5개 카드/배지 정확 표시, Free→F-02 진입, Pro→F-05 진입
- 현지화 키 강제, 하드코딩 문자열 금지
- 포커스 이동/VoiceOver 레이블 정상
- 오류키(표시문구는 키로만)
error.navigation.missing_conflict_type
6.3.2 F-02 1단계: 마음정리(5문항)
- 문항
- 상황기록(텍스트 ≤500자)
- 감정선택(최소 8개 태그+직접입력, 복수 선택)
- 상처받은 지점(텍스트)
- 나의 반성(텍스트, 건너뛰기 허용)
- 진짜 원했던 것(텍스트 + 예시 안내)
- Gherkin
Feature: Guided journal (5 steps)
Scenario: Complete all steps and proceed
Given the user selected conflictType="friend"
When the user enters valid inputs for steps 1..5
Then tap "journal.complete" button
And navigate to JournalCompletion
Scenario Outline: Validation and limits
Given the user is on step <step>
When the user enters input exceeding the limit or invalid
Then show error with key <errorKey>
Examples:
| step | errorKey |
| 1 | error.journal.too_long |
| 2 | error.journal.emotion_empty |
| 5 | error.journal.want_empty |
Scenario: Skip reflection (step 4)
Given the user is on step 4
When the user taps "journal.skip.reflection"
Then proceed to step 5
Scenario: Offline saving
Given the device is offline
When the user completes step 5
Then data is saved locally (SwiftData)
And queued for sync when online (event "sync.queue_enqueued")
- 수용 기준
- 입력 검증/건너뛰기/오프라인 저장 동작
- 키보드 가림 방지, 진행 표시(1/5)
- 완료 시
Journal 엔티티 온디바이스 영구 저장
- 오류 키
error.journal.too_long, error.journal.emotion_empty, error.journal.want_empty, error.storage.failed
6.3.3 F-03 2단계: AI ‘마음 편지' 작업실
- 행동
- 화면 진입 시 NVC 프롬프트로 초안 자동 생성
- 어시스턴트 버튼 4종: 다시 제안/부드럽게/간결하게/더 진솔하게
- 평균 응답 ≤ 5초(타임아웃 시 재시도 UI)
- Gherkin
Feature: AI message composer
Scenario: Draft is generated on entry
Given the user completed Guided Journal
When opening MessageComposer
Then show loading indicator
And populate editor with AI draft within 5 seconds or show "composer.retry" action
Scenario: Assistant buttons transform text
Given the editor has a draft
When the user taps "composer.assist.soften"
Then the text updates with a softer tone
And event "ai.rewrite" is logged with variant="soften"
Scenario: Failure and recovery
Given the AI service is unreachable
When draft generation fails
Then show error key "error.ai.unavailable"
And expose "composer.retry" action
- 수용 기준
- 오류키
error.ai.unavailable, error.ai.timeout, error.ai.transform_failed
6.3.4 F-04 3단계: 전달(복사/이미지/링크)
- 행동
- 복사: 클립보드 성공 토스트
- 이미지: 편지지 배경 합성, 앨범 저장 권한 흐름 포함
- 링크: Firestore
letters 쓰기(익명), TTL 7일, URL 생성
- Gherkin
Feature: Delivery options
Scenario: Copy to clipboard
Given a finalized letter in editor
When the user taps "delivery.copy"
Then letter text is in clipboard
And toast with key "delivery.copied" is shown
Scenario: Save as image
Given photo permission is granted or requested
When the user taps "delivery.image"
Then save a high-resolution image to Photos
And show "delivery.image_saved"
Scenario: Share via link (online)
Given the device is online
When the user taps "delivery.link"
Then create Firestore document with { content, createdAt }
And return URL "https://kindverb.com/letter/<id>"
And show iOS share sheet
Scenario: Share via link (offline)
Given the device is offline
When the user taps "delivery.link"
Then show error "error.network.offline"
- 수용 기준
- 세 옵션 모두 정상 동작, 링크는 7일 후 자동 만료 전제
- 권한 거부/실패 케이스 명확 처리
- 오류키
error.delivery.copy_failed, error.delivery.image_failed, error.network.offline, error.link.create_failed
6.3.5 F-05 인앱 결제 (Pro 평생권)
- 행동
- Paywall: 장점·가격·구매/복원 버튼
- 성공 시 즉시 Pro 해제(자물쇠 제거), 실패/취소 분기
- Gherkin
Feature: In-App Purchase (Pro Lifetime)
Scenario: Purchase success unlocks Pro features
Given the user is on Paywall
When the user completes purchase successfully
Then unlock all Pro categories immediately
And replace Pro badges with unlocked state
And log "iap.purchase_succeeded"
Scenario: Restore purchases
Given the user taps "iap.restore"
When a valid receipt is found
Then set Pro state to true
And show "iap.restore_success"
Scenario: Purchase failure
When the purchase fails or is canceled
Then show error key "error.iap.failed" or "error.iap.canceled"
- 수용 기준
- 구매/복원 정상, UI 즉시 반영
- 스토어 가이드라인 100% 준수
- 오류 키
error.iap.failed, error.iap.canceled, error.iap.restore_failed
6.3.6 F-06 로컬 저장・오프라인 큐・자동 재시도
Feature: Offline-first storage and retry
Scenario: Save journal offline
Given no connectivity
When the user completes step 5
Then journal is persisted locally (SwiftData)
And a sync job is enqueued
Scenario: Auto-retry on reconnect
Given queued jobs exist
When connectivity is restored
Then jobs are sent
And success clears the queue
- 수용 기준
- 로컬 저장 실패율 < 0.1%, 재시도 지수백오프
- 앱 재시작 후에도 큐 유지
- 오류키
6.4 에지 케이스 (공통)
6.5 텔레메트리 이벤트(요약)
| 이벤트 ID |
설명 |
주요 속성(Properties) |
샘플 값 예시 |
목적 |
ai.draft_generated |
AI가 초안을 생성했을 때 |
latency_ms, tokens_in, tokens_out, model, conflict_type |
1240, 612, 284, "gpt-5", "friend" |
모델 성능 및 사용 맥락 측정 |
ai.rewrite |
사용자가 리라이팅 옵션 선택 |
variant(soften/concise/honest/retry), conflict_type, draft_length, latency_ms, reason(optional) |
"soften", "partner", 842, 980 |
리라이팅 패턴/선호도 분석 |
ai.accepted |
사용자가 AI 문안을 최종 채택 |
draft_id, time_to_accept_ms |
"d123", 4520 |
채택률, 사용자 신뢰도 측정 |
ai.discarded |
생성된 문안을 버림 |
draft_id, reason |
"d124", "too_generic" |
버려지는 이유 분석 및 품질 개선 |
user.note_saved |
감정 기록(노트) 저장됨 |
emotion, note_length, was_offline |
"angry", 240, false |
핵심 기능 사용률 추적 |
user.session_start |
앱 세션 시작 |
timestamp, entry_point |
2025-08-21T07:23:12Z, "push_notification" |
세션 빈도 및 유입 경로 파악 |
user.session_end |
앱 세션 종료 |
duration_ms, crash_free |
380000, true |
세션 길이, 안정성 측정 |
delivery.link_shared |
AI 문안을 링크로 공유 |
url_prefix, ttl_days, content_length, was_online |
"https://kindverb.com/letter/", 7, 1260, true |
공유 기능 효과 측정 |
iap.purchase_succeeded |
결제 성공 |
sku, price, currency, platform |
"kindverb.pro.monthly", 5900, "KRW", "ios" |
매출 분석 |
iap.purchase_failed |
결제 실패 |
sku, error_code, platform |
"kindverb.pro.monthly", "storekit_timeout", "ios" |
결제 실패율, 개선 포인트 |
setting.locale_changed |
사용자가 언어 설정 변경 |
old_locale, new_locale |
"en", "ko" |
다국어 사용 패턴 추적 |
6.6 AI-Ready 작성 가드레일
- 현지화 강제: 모든 사용자 노출 텍스트는 키만 사용(예:
Text("journal.title")).
- 버튼 규칙: SwiftUI 버튼은 반드시
Button { /* action */ } label: { /* view */ } 형식.
- 동시성: 모든 비동기 로직은 async/await,
NSError 금지(Swift Error + throw/try/catch).
- 아키텍처: MVVM( View ↔ ViewModel ↔ UseCase/Repository ↔ Service ) 경계 준수.
- 테스트 용이성: 각 시나리오는 Gherkin → UI Test / VM Unit Test로 1:1 매핑 가능.
7.앱플로우&정보구조 (App Flow & Information Architecture, IA)
7.1 네비게이션 모델 (MVP)
-
루트 컨테이너: NavigationStack
-
탭바: 없음(단일 핵심 여정 집중)
-
모달(Presented): V03_PaywallView(시트), iOS 공유 시트(시스템), 포토 권한 알림(시스템)
-
전체 흐름: Launch → Conflict 선택 → 1단계 성찰 → 1단계 완료 → 2단계 편지 작업실 → 전달(복사/이미지/링크) → 완료
V01_LaunchScreen
↓ auto
V02_ConflictSelectionView
├─[Free 선택]→ V04_GuidedJournalView
└─[Pro 선택] → V03_PaywallView (sheet)
├─[구매 성공]→ V04_GuidedJournalView
└─[취소/닫기]→ V02
V04_GuidedJournalView (5-step)
↓ [정리 완료]
V05_JournalCompletionView
↓ [2단계로]
V06_MessageComposerView
├─[텍스트 복사]
├─[이미지 저장]→ (포토 권한 요청·성공 시 저장)
└─[링크 공유]→ (온라인만 활성/성공 시 URL 발급)
↓
V07_CompletionView → V02 로 귀환
7.2 화면 인벤토리 (Screen Inventory)
| ID |
화면명 |
목적/설명 |
진입 |
주요 액션 → 다음 |
데이터 입/출력 계약 |
표시 텍스트 키(예) |
텔레메트리 |
| V01 |
LaunchScreen |
브랜딩·첫 인상, 1~1.5s 후 자동 전환 |
앱 실행 |
자동 → V02 |
입력: 없음 / 출력: 없음 |
launch.tagline, launch.accessibility_label |
user.session_start |
| V02 |
ConflictSelectionView |
관계 유형 선택(Free/Pro 배지) |
V01 |
[Free 시작] → V04, [Pro로 시작] → V03(sheet) |
입력: 없음 / 출력: conflictType |
conflict.title, conflict.cta.free, conflict.cta.pro |
ui.conflict_shown, ui.conflict_selected |
| V03 |
PaywallView (Sheet) |
Pro 가치 제시·구매/복원 |
V02 |
[구매]→영수증 검증 성공→V04, [복원], [닫기] |
입력: selectedConflictType / 출력: entitlement=pro |
paywall.title, paywall.benefits.*, paywall.buy, paywall.restore |
iap.paywall_shown, iap.purchase_succeeded/failed |
| V04 |
GuidedJournalView |
5개 질문으로 성찰(1단계) |
V02 또는 V03 |
[다음] 순회, [정리 완료]→V05 |
입력: conflictType / 출력: journalDraft (로컬 저장) |
journal.q1.*~journal.q5.*, journal.next, journal.complete |
journal.step_shown, journal.completed |
| V05 |
JournalCompletionView |
1단계 완료 피드백 |
V04 |
[2단계로]→V06 |
입력: journalDraft / 출력: 없음 |
journal.done.title, journal.done.cta |
journal.completion_shown |
| V06 |
MessageComposerView |
AI 초안/수정, 최종 본문 확정 |
V05 |
[다시 제안/부드럽게/간결/진솔], [텍스트 복사], [이미지 저장], [링크 공유]→V07 |
입력: journalDraft / 출력: messageDraft, sharedLink? |
composer.title, composer.tools.*, composer.share.* |
ai.draft_generated, ai.rewrite, delivery.link_shared, delivery.image_saved, delivery.text_copied |
| V07 |
CompletionView |
여정 마무리·격려 |
V06 |
[확인]→V02 |
입력: deliveryMethod / 출력: 없음 |
completion.title, completion.tip, completion.ok |
user.session_end (또는 홈 회귀 이벤트) |
주의: 모든 화면의 사용자 노출 텍스트는 직접 문자열 금지, 반드시 현지화 키 사용. 버튼은 Button { } label: { } 형식만.
7.3 상태 다이어그램 (요약)
7.3.1 앱 전역 상태 (오프라인/권한/Pro)
[앱 시작]
├─ Network: Online / Offline
│ ├─ Online: 링크 공유 가능
│ └─ Offline: 링크 공유 비활성 (토스트: error.network.offline)
├─ Entitlement: Free / Pro (paywall 성공·복원 시 Pro)
└─ Photo Permission: NotDetermined → Denied/Authorized
├─ 이미지 저장 시 요청
└─ Denied면 설정 이동 안내 키 노출
7.3.2 1단계 성찰(5-Step) 상태
Q1 → Q2 → Q3 → Q4(건너뛰기 허용) → Q5 → [정리 완료] → 완료화면
(각 스텝은 ViewModel 임시 상태에 저장, 완료 시 SwiftData 영구 저장)
7.3.3 Pro 결제 상태
Free → (구매 성공/복원) → Pro
실패/취소 → Free 유지 (에러 키/가이드 표시)
7.4 권한 플로우 (Permissions)
| 사용 시점 |
권한 |
트리거 화면 |
UX 요구사항(키) |
실패/거부 시 |
| 이미지 저장 버튼 탭 |
Photos AddOnly |
V06 |
perm.photos.rationale, perm.photos.go_settings |
버튼 비활성 또는 설정 이동 안내 |
| (선택) 알림 리마인더 |
Notifications |
(후속 버전) |
perm.push.rationale |
요청 스킵 가능(기본 거부 가정) |
MVP에서는 포토 권한만 필수. 알림은 후속 버전 고려.
7.5 딥링크 & 라우팅 (Deeplink / Routing)
| 유형 |
패턴 |
동작 |
| 앱 내부 라우트 |
kindverb://open/conflict/{type} |
V02를 열고 해당 카드 하이라이트 |
| (선택) 웹→앱 유니버설링크 |
https://kindverb.com/letter/{id} |
앱 설치 시 뷰어 장착 가능(초기 버전은 웹 전용 페이지 노출을 기본) |
| 인앱 네비 |
NavigationPath로 V02→V04→V05→V06→V07 |
뒤로가기 제스처 허용(데이터 보존) |
MVP에서는 링크 뷰어 웹 우선. 나중에 Universal Links를 앱에 매핑해 읽기 전용 뷰 추가 가능.
7.6 정보구조 (Information Architecture)
7.6.1 도메인 엔티티 & 소유권
-
ConflictType (enum): 화면 선택값. 소유: ViewModel(세션 메모리)
-
JournalDraft (struct): Q1~Q5 답변. 소유: SwiftData(영구), VM(임시)
-
MessageDraft (struct): AI 초안/수정 결과. 소유: VM(세션), 필요 시 로컬 저장 옵션
-
Entitlement (struct): free | pro. 소유: IAPService + VM 캐시
-
Settings (struct): 로컬화/접근성/테마. 소유: AppStorage/환경
7.6.2 레이어별 책임 (MVVM 가드레일)
-
View: 렌더링/유저 입력. 텍스트=현지화 키, 버튼 표준 형식.
-
ViewModel: 상태 관리, 검증, 유스케이스 호출, 에러 매핑(키).
-
UseCase/Repository: 도메인 로직, SwiftData CRUD, IAP/LLM/Firestore 호출 조정.
-
Service/DAO: 외부 API·스토리지 실제 호출(LLM, StoreKit, Firestore).
7.7 화면별 데이터 계약(요약)
| 화면 |
입력 |
출력 |
비고 |
| V02 |
- |
conflictType |
Free/Pro 배지에 따라 흐름 분기 |
| V03 |
selectedConflictType |
entitlement=pro(성공시) |
실패 시 에러 키·재시도 제공 |
| V04 |
conflictType |
journalDraft (SwiftData 저장) |
각 스텝별 입력 제한(문자 수 등) |
| V06 |
journalDraft |
messageDraft, sharedLink? |
AI 요청 파라미터: conflictType, 핵심 감정/욕구 요약 |
| V07 |
deliveryMethod |
- |
홈으로 리다이렉션 |
7.8 로딩/빈 상태/에러 패턴
| 상황 |
패턴 |
사용자 메시지(키) |
버튼/행동 |
| AI 초안 생성 중 |
스켈레톤 또는 인디케이터 |
state.loading.generating_draft |
취소 버튼 없음(짧은 대기 가정) |
| Journal 빈 입력 |
친절한 안내 + 예시 |
journal.empty.hint, journal.example.* |
[다음] 비활성(검증 실패 키) |
| 오프라인 링크 공유 |
비활성 + 토스트 |
error.network.offline |
[다시 시도] 키 |
| 포토 권한 거부 |
시트 안내 |
perm.photos.denied |
[설정으로 이동] |
| IAP 실패 |
명확한 사유/재시도 |
`iap.error.timeout |
canceled |
모든 에러는 NSError 금지, 현대적 Error → ViewModel에서 로컬라이즈드 키로 매핑.
7.9 디자인 토큰 적용 지점 (요약)
-
컬러: color.primary, color.bg, color.text.muted
-
간격: spacing.s/m/l (4pt 그리드)
-
라운드: radius.l(카드), radius.m(버튼)
-
타이포: 본문 가독성 우선, Dynamic Type 100% 지원
7.10 접근성(A11y) & 키보드 UX
-
모든 상호작용 요소 44pt 이상
-
VoiceOver 레이블: 키 기반(a11y.conflict.card.friend, …)
-
입력 폼: 키보드 회피, Return 키 행동 정의(다음 스텝으로 이동)
-
컬러 대비: WCAG AA 이상
7.11 로컬라이제이션 키 레지스트리(발췌)
예시 키만. 실제 문자열은 Localizable.xcstrings에서 관리.
-
Launch: launch.tagline, launch.accessibility_label
-
Conflict: conflict.title, conflict.card.friend.title, conflict.cta.free, conflict.cta.pro
-
Journal: journal.title, journal.q1.title, journal.q2.title, …, journal.next, journal.complete
-
Composer: composer.title, composer.tools.retry, composer.tools.soften, composer.share.copy, composer.share.image, composer.share.link
-
Completion: completion.title, completion.tip, completion.ok
-
Errors/State: error.network.offline, perm.photos.rationale, iap.error.timeout, …
7.12 텔레메트리(화면 매핑)
-
V02 진입: ui.conflict_shown
-
관계 선택: ui.conflict_selected { conflict_type }
-
1단계 각 스텝 노출: journal.step_shown { step_index }
-
1단계 완료: journal.completed
-
AI 초안 생성: ai.draft_generated
-
AI 리라이트: ai.rewrite { variant }
-
전달: delivery.text_copied | delivery.image_saved | delivery.link_shared
-
세션 종료: user.session_end { duration_ms, crash_free }
7.13 엣지 케이스 정의
-
앱 재시작 시 미완료 JournalDraft 자동 복원(사용자 동의 키 journal.resume.prompt)
-
링크 공유 실패 후 재시도 시, 중복 저장 방지(클라이언트 UUID로 멱등 키)
-
장문 편지(예: >4000자) 생성 시 스냅 성능 저하 방지(가상화 스크롤 또는 지연 렌더링 정책)
7.14 IA 요약 도식
[ConflictType]───┐
├──> [JournalDraft] ──> [MessageDraft] ──> [Delivery (copy|image|link)]
[Entitlement] ──┘ │
└──(SwiftData 저장)
Services: AI(Large Language Model), IAP(StoreKit), LinkShare(Firestore), Photos
8.UI/UX 요구사항 (UI/UX Requirements)
8.0 제품 UX 원칙 (Design Principles)
-
평온함(Composed): 시각적 소음 최소화, 글 읽기·쓰기 집중 지원
-
단순함(Focused): 한 화면 = 한 과업. 기본 선택지를 명확히 안내
-
친절함(Kind): 실패·거절에도 사용자를 비난하지 않음(격려형 카피)
-
안심(Private): 온디바이스 저장·오프라인 우선 UI로 신뢰 제공
-
예측 가능성(Consistent): 동일한 액션은 동일한 위치·모양·문구
8.1 디자인 토큰 (Design Tokens)
코드에서는 토큰 이름만 참조합니다. 실제 값은 테마 파일에서 관리.
8.1.1 컬러
| Token |
값(예시) |
용도 |
color.bg |
#FAFAFA |
기본 배경 |
color.surface |
#FFFFFF |
카드/시트 배경 |
color.primary |
#4F46E5 |
주요 액션/포커스 |
color.text.primary |
#111827 |
본문 텍스트 |
color.text.muted |
#6B7280 |
보조 텍스트 |
color.border |
#E5E7EB |
구분선/입력창 테두리 |
color.success |
#10B981 |
성공·완료 피드백 |
color.warning |
#F59E0B |
주의 |
color.error |
#EF4444 |
오류 강조 |
8.1.2 간격/라운드/그림자
| Token |
값(예시) |
설명 |
spacing.xs |
4 |
최소 간격(4pt 그리드) |
spacing.s |
8 |
|
spacing.m |
12 |
기본 텍스트 주변 |
spacing.l |
16 |
카드 내부 패딩 |
spacing.xl |
24 |
섹션 간 간격 |
radius.s |
8 |
입력/태그 |
radius.m |
12 |
버튼 |
radius.l |
16 |
카드/시트 |
shadow.card |
iOS default |
카드 미묘한 그림자 |
8.1.3 타이포 스케일 (Dynamic Type 대응)
| Role |
폰트(예시) |
크기(기본) |
사용처 |
type.title |
SF Pro / Serif Body |
22–28pt |
화면 타이틀 |
type.heading |
SF Pro |
18–20pt |
섹션 제목 |
type.body |
SF Pro |
15–17pt |
본문(가독성 최우선) |
type.caption |
SF Pro |
12–13pt |
도움말/설명 |
규칙: Dynamic Type 100% 지원. 대비 WCAG AA 이상 유지.
8.2 핵심 컴포넌트 (Components)
버튼 형식 강제: 모든 SwiftUI 버튼은 Button { /* action */ } label: { /* view */ } 형식만 사용.
문구는 반드시 현지화 키로 표기(예: Text("composer.share.link")).
8.2.1 PrimaryButton
모양: 가로폭 꽉 채움, 높이 48–52pt, radius.m, 배경 color.primary, 텍스트 Color.white
-
상태: 기본 / 비활성(불투명도 0.4) / 로딩(스피너) / 성공(체크 아이콘)
-
접근성: 최소 44pt 터치, VoiceOver 라벨 키 제공
-
예시 키: button.primary.start, button.primary.next, button.primary.buy
8.2.2 SecondaryButton (Outline)
테두리 color.border, 텍스트 color.text.primary, 배경 투명
-
Paywall의 “복원”, “다음에 하기” 등 보조 액션에 사용
-
키: button.secondary.restore, button.secondary.later
8.2.3 Tag/EmotionChip
Pill 형태, radius.s, 선택 시 color.primary 배경 + 흰색 텍스트
-
멀티 선택 허용, 선택/해제 애니메이션 120ms
-
키: emotion.happy, emotion.sad … + emotion.custom
8.2.4 Card
color.surface 배경, radius.l, shadow.card
-
Conflict 카드, “나의 마음” 요약 카드 등에 사용
-
타이틀·서브텍스트·배지(Free/Pro)·CTA 버튼 슬롯 제공
-
키: conflict.card.friend.title, conflict.badge.free, conflict.cta.free
8.2.5 InlineBanner
정보/경고/오류 3종. 좌측 아이콘 + 본문 + 선택적 CTA
-
오프라인 경고, 권한 가이드, 저장 성공 안내에 사용
-
키: banner.offline.title, banner.offline.body, banner.cta.retry
8.2.6 TextArea (Journal Input)
최소 높이 140pt, 자리표시자(키), 글자수 카운트(예: 0/500)
-
포커스 시 테두리 color.primary로 전환
-
키: journal.q1.placeholder, journal.char_count
8.2.7 Loading Indicator / Skeleton
8.3 UI 패턴 (Patterns)
8.3.1 빈 상태 (Empty States)
8.3.2 로딩 (Loading)
8.3.3 오류 (Errors) — 현대 Error → 키 매핑
네트워크 없음:
-
배너: error.network.offline.title, error.network.offline.body
-
CTA: banner.cta.retry
-
링크 공유 버튼 비활성 + 토스트 error.network.offline.toast
-
IAP 실패:
-
포토 권한 거부:
8.3.4 오프라인 UX
상단 인앱 배너 고정(필요 시): banner.offline.title/body
-
링크 공유 버튼 비활성 + 툴팁: delivery.link.disabled_offline
-
로컬 작업(기록/수정/이미지 저장)은 정상 허용
8.3.5 권한 UX (Just-in-time)
이미지 저장 시 최초 한 번 요청(거부 시 친절 가이드로 대체)
-
키: perm.photos.rationale, perm.photos.request.title, perm.photos.request.body
8.3.6 진행(Progress) & 단계 표시
8.3.7 제스처 & 뒤로가기
8.4 특수 화면 UX 스펙
8.4.1 ConflictSelectionView
카드 캐러셀: 1.05x 확대 중심 카드, 좌우 8pt 프리뷰
-
배지: Free = 녹색, Pro = 잠금 아이콘+회색
-
CTA 위치 고정: 하단 안전영역 위 16pt
-
텔레메트리: ui.conflict_selected { conflict_type }
8.4.2 PaywallView (Sheet)
구성: 헤드라인 → 혜택 3~4개(아이콘+짧은 문구) → 가격/정책 → CTA(구매) → 보조(복원/다음에)
-
신뢰 요소: “1회 구매 · 평생 이용”(키), 환불 정책 미니문구(키)
-
로드 상태: 구매 중 인터랙션 잠금 + 스피너
-
키: paywall.title, paywall.benefits.*, paywall.buy, paywall.one_time.lifetime, paywall.restore, paywall.later
8.4.3 GuidedJournalView (5 스텝)
한 화면 한 질문 원칙, 예시 문구 제공
-
글자수 제한 시 실시간 카운트 + 긍정 피드백(journal.counter.ok)
-
[다음] 비활성 조건 명확화(검증 실패 키 제공)
-
키: journal.q{n}.title, journal.q{n}.helper, journal.next, journal.complete
8.4.4 MessageComposerView
상단 “나의 마음 요약 카드”: 감정 태그+핵심 욕구 키워드 수평 스크롤
-
본문 에디터(고정 폭 + 충분한 라인 높이)
-
AI 도우미 4버튼: 다시 제안 / 더 부드럽게 / 더 간결하게 / 더 진솔하게
-
공유 섹션 3버튼: 복사 / 이미지 저장 / 링크 공유
-
상태 안내(로딩·성공 토스트): composer.toast.copied, composer.toast.image_saved, composer.toast.link_ready
8.4.5 CompletionView
큰 칭찬 문구 + “앞으로를 위한 팁” 1~2개
-
홈으로 돌아가는 명확한 CTA
-
키: completion.title, completion.tip, completion.ok
8.5 모션 & 햅틱 (Motion & Haptics)
-
모션 지침: 120–200ms 내 자연스러운 이징(easeInOut), 과도한 패럴랙스 금지
-
피드백 타이밍:
-
카드 선택, 구매 성공, 편지 저장 성공 = success 햅틱
-
오류 배너 표시 = warning 햅틱(약하게)
-
감정 태그 토글 = light 탭틱
-
애니메이션 예: AI 텍스트 갱신 시 “타자 효과” 400–800ms (사용자 취소 불필요)
8.6 접근성 (A11y)
-
터치 타겟: 최소 44×44pt
-
VoiceOver 라벨/힌트: 모든 버튼·칩·카드에 키 제공
-
다크 모드: 대비 AA 유지, 색의 의미에 텍스트/아이콘 보조(색상 단독 의존 금지)
-
동적 폰트: Large 이상에서도 레이아웃 파손 없는 자동 줄바꿈
-
감정 아이콘: 텍스트 라벨 병기(아이콘 단독 사용 금지)
8.7 현지화 규칙 (Localization)
-
절대 규칙: UI 내 직접 문자열 금지, 모두 Localizable.xcstrings 키로 참조
-
플레이스홀더/매개변수: ICU 형식 권장(예: 진행률 “{current}/{total}”)
-
복수형: 필요 시 언어별 규칙 반영 키 분리(.one/.other)
-
톤 & 보이스: 부드럽고 격려형(“괜찮아요”, “다시 해볼까요?”)
샘플 키 레지스트리(발췌)
-
Buttons: button.primary.start, button.primary.next, button.primary.buy, button.secondary.restore, button.secondary.later
-
States: state.loading.generating_draft, state.saved, state.network.checking
-
Errors: error.network.offline, error.unknown, iap.error.canceled, perm.photos.denied.title/body
-
Journal: journal.q1.title, journal.q1.placeholder, journal.complete, journal.example.1/2/3
-
Composer: composer.tools.retry/soften/concise/authentic, composer.share.copy/image/link, composer.toast.*
8.8 이미지 내보내기(“이미지로 저장”) 스펙
-
해상도: 최소 1080×1920px(2× 스케일), 여백 spacing.xl
-
배경: 차분한 종이 질감(Assets), 대비 충분
-
폰트: 본문 type.body 라인 높이 1.4–1.5
-
워터마크(선택): 하단 “KindVerb” 미세 그레이(접근성 침해 없도록)
-
성공 피드백: 토스트 composer.toast.image_saved + success 햅틱
8.9 링크 공유 UX (온라인)
-
버튼 활성 조건: 네트워크 Online + AI 본문 1자 이상
-
클릭 → 상태: 로딩 인디케이터 + “링크 생성 중”(delivery.link.creating)
-
성공: 시스템 공유 시트 표시, 토스트 composer.toast.link_ready
-
실패: 배너 error.network.offline 또는 error.link.create_failed
-
만료 안내: 텍스트 키 delivery.link.ttl_notice (예: “7일 후 자동 삭제”)
8.10 분석 이벤트(UX 관점 요약)
| 이벤트 |
시점 |
속성(예시) |
ui.conflict_shown |
V02 노출 |
— |
ui.conflict_selected |
카드 CTA 탭 |
conflict_type |
journal.step_shown |
Q1~Q5 노출 |
step_index |
journal.completed |
[정리 완료] |
elapsed_ms, text_length |
ai.draft_generated |
첫 초안 수신 |
latency_ms, tokens? |
ai.rewrite |
도우미 4종 사용 |
variant |
delivery.text_copied |
복사 |
length |
delivery.image_saved |
이미지 저장 |
duration_ms |
delivery.link_shared |
링크 공유 완료 |
id_hash?, channel? |
iap.paywall_shown |
Paywall 노출 |
source |
iap.purchase_succeeded/failed |
구매 결과 |
reason? |
8.11 수용 기준 체크리스트 (UI/UX)
-
모든 버튼은 표준 형식 사용(액션/라벨 분리)
-
모든 문구는 현지화 키로만 표기
-
터치 타겟 44pt 이상, Dynamic Type 완전 대응
-
AI 로딩 2초 초과 시 상태 문구 노출, 8초 초과 시 재시도 제공
-
오프라인 시 링크 공유 비활성 + 배너/토스트 안내
-
포토 권한 거부 시 설정 이동 시트 표준 제공
-
Paywall “1회 구매·평생 이용” 문구와 복원 버튼 노출
-
이미지 내보내기 결과 가독성 및 대비 AA 충족
-
텔레메트리 이벤트가 표에 맞게 전부 발화
-
다크 모드 대비·아이콘/색 의존 최소화
9.플랫폼 & 기술결정 + 에러 도메인 (Platform & Tech Decisions + Error Domain)
(iOS · SwiftUI · MVVM · async/await · 현대 Error · 현지화 · 오프라인 우선)
9.1 지원 환경 & 빌드 정책
-
Target OS: iOS 17+ (SwiftData 사용·현대 접근성·새로운 StoreKit UI 사용 목적)
-
Language/Runtime: Swift 6+, Swift Concurrency(Structured Concurrency) 100%
-
UI: SwiftUI 100% (UIKit 혼합 금지. 필요 시 wrapper 컴포넌트로 격리)
-
아키텍처: MVVM + UseCase + Repository + Service (의존성 역전 유지)
-
패키지 관리자: Swift Package Manager (SPM)
-
서드파티: 최소화. 필요 시 Firebase(Firestore) & StoreKit만 사용
-
현지화: Localizable.xcstrings 키 기반(기본 언어 en, ko/ja/zh-Hans 병행)
-
접근성: Dynamic Type/VoiceOver/색상대비 AA 이상
9.2 계층 구조(레이어) & 책임
원칙: View는 표시와 입력만, ViewModel은 상태·의도 관리, UseCase는 업무 규칙, Repository는 도메인 I/O, Service는 외부/플랫폼 호출.
View (SwiftUI)
↕ @Published / intents
ViewModel (ObservableObject)
↕ UseCase (async)
Repository (protocols → impl: Local/Remote)
↕
Services (SwiftData, File, Network(Firestore), IAP, ShareLink)
-
View: UI 그리기, 사용자 액션을 Intent로 위임, 문자열은 키만 사용
-
ViewModel: 상태머신(Idle/Loading/Success/Error), 취소(Cancellation) 관리
-
UseCase: 업무 규칙 캡슐화(예: “AI 초안 생성 후 텍스트 정제”)
-
Repository: 도메인 모델 중심 I/O(예: Journal 저장/조회, Letter 업로드)
-
Service: 플랫폼·라이브러리 상세 구현(교체 가능한 낮은 레벨)
의존성 주입(DI): Environment/Factory로 주입. 단위 테스트에서 Fake 교체 용이.
9.3 동시성 & 성능 가드레일 (async/await)
-
Structured Concurrency: Task {} / TaskGroup / withTimeout 활용
-
MainActor 규칙: UI state 변경은 @MainActor에서만
-
취소 전파: 화면 이탈 시 진행 중 작업 자동 취소(메모리·배터리 보호)
-
백오프 재시도: 네트워크 계열은 exponential backoff (max 3회)
-
타임아웃: AI/공유링크 8초 타임아웃 → 사용자 재시도 유도
-
메모리/스토리지: SwiftData로 로컬 우선, 대용량 텍스트는 파일 분리 고려
9.4 저장소 & 네트워크 결정
9.5 인앱결제(StoreKit 2)
-
상품: 일회성(Lifetime) kindverb.pro.lifetime
-
구매 흐름: Paywall → 구매/복원 → PurchaseState 갱신 → UI 리프레시
-
오류 처리: 사용자 메시지는 현지화 키로만 표기(IAP 전용 에러 키 별도)
9.6 로깅 & 텔레메트리
-
Logger: os.Logger 채택, 카테고리(“UI”, “IAP”, “AI”, “LINK”, “STORE”, “DATA”)
-
개인정보: 민감 데이터 로그 금지. 이벤트는 PRD 6.5/8.10 정의에 일치
-
레벨 규칙: debug(개발용), info(흐름), error(에러 도메인 코드 포함)
9.7 피처 플래그 & 구성
-
간단 플래그: AppConfig 구조체 / @Environment(\.appConfig) 주입
-
예시: enableTypingAnimation, enableLinkShare, aiModel="gpt-5"
-
원격 플래그: 초기 릴리스는 없음(단순성 유지). 필요 시 UserDefaults 동기화
9.8 테스트 전략
-
단위 테스트: ViewModel/UseCase/Repository에 집중(Mock Service 주입)
-
스냅샷: 핵심 View(Conflict/Journal/Composer/Paywall) 다크/라이트·동적폰트
-
E2E(선택): XCUITest로 핵심 플로우(선택→기록→초안→공유→완료)
-
회귀 방지: 에러 도메인 매핑·현지화 키 누락 체크
9.9 에러 도메인(분류 체계) — 사용자 메시지는 전부 “키”로
개발자는 타입 안전 Error로 원인·복구전략 결정 → ViewModel이 UI 상태로 매핑 → View는 키만 표시.
9.9.1 에러 최상위 분류
-
AppError (enum)
-
.network(NetworkError) — 연결·타임아웃·상태코드
-
.storage(StorageError) — SwiftData 읽기/쓰기/마이그레이션
-
.iap(IAPError) — 결제/영수증/복원
-
.permission(PermissionError) — 포토 등 권한 거부
-
.ai(AIError) — 초안 생성 실패·쿼터·포맷
-
.link(LinkError) — Firestore 쓰기/TTL 정책 위반
-
.validation(ValidationError) — 입력 검증 실패
-
.unknown(underlying: Error?) — 미분류(로깅만 상세)
9.9.2 서브 도메인 예시
-
NetworkError = .offline, .timeout, .unreachable, .http(code:Int)
-
StorageError = .saveFailed, .fetchFailed, .migrationRequired
-
IAPError = .canceled, .notFound, .verificationFailed, .unknown
-
PermissionError = .photosDenied, .photosRestricted, .notDetermined
-
AIError = .serviceUnavailable, .rateLimited, .invalidResponse, .timeout
-
LinkError = .createFailed, .unauthorizedRule, .quotaExceeded
-
ValidationError = .empty(field), .tooLong(field,max), .invalidFormat(field)
9.9.3 에러 → UI 매핑(현지화 키)
| Error 도메인 | 사용자 메시지 키(타이틀/본문/CTA 예시) | 기본 복구 전략 |
|---|
NetworkError.offline | error.network.offline.title, error.network.offline.body, banner.cta.retry | 재시도/오프라인 모드 안내 |
NetworkError.timeout | error.network.timeout.title, error.network.timeout.body, banner.cta.retry | 지수 백오프 재시도 |
IAPError.canceled | iap.error.canceled | 무시(토스트), 흐름 유지 |
IAPError.verificationFailed | iap.error.verify_failed, iap.cta.retry | 복원 시도/재결제 |
PermissionError.photosDenied | perm.photos.denied.title/body, perm.photos.go_settings | 설정 이동 |
AIError.rateLimited | ai.error.ratelimit, banner.cta.retry_later | 지연 후 재시도 |
LinkError.createFailed | error.link.create_failed, banner.cta.retry | 재시도/오프라인 저장 대안 제시 |
ValidationError.tooLong(.journal,500) | journal.error.too_long | 입력 길이 조정 가이드 |
직접 문자열 금지: 모든 사용자 문구는 키로만 표기.
개발자용 상세는 Logger에 남김(PII 금지).
9.10 실패-복구(UX) 표준 플로우
-
탐지: catch AppError
-
분류: 도메인별로 ErrorViewState 변환(배너/토스트/시트)
-
복구 옵션: 재시도, 설정 이동, 입력 수정, 오프라인 대체
-
기록: 텔레메트리 이벤트(error.domain, error.code, screen)
-
사용자 보장: “진행 중 입력/초안은 소실되지 않음” 원칙 준수
9.11 코드 스니펫(아주 작은 예시, 규칙 준수)
실제 구현은 다음 섹션에서 파일 단위로 제공합니다. 여기서는 원칙 준수 예시만 간단히 남깁니다. (문자열은 키, 버튼 형식 고정, async/await, 현대 Error)
// MARK: - Error 정의 (간단 예시)
// 한 줄 한 줄 주석: 왜 이렇게 구성하는지 설명합니다.
// App 전역에서 사용할 에러 최상위 타입을 정의합니다.
// 이유: 도메인별 세부 에러를 하나의 공통 타입으로 모아
// ViewModel에서 일관된 방식으로 UI 상태를 만들기 위함입니다.
enum AppError: Error {
// 네트워크 관련 에러(오프라인, 타임아웃 등)를 감쌉니다.
case network(NetworkError)
// 로컬 저장(SwiftData) 관련 에러를 감쌉니다.
case storage(StorageError)
// 인앱결제 관련 에러를 감쌉니다.
case iap(IAPError)
// 권한 거부 등 권한 관련 에러를 감쌉니다.
case permission(PermissionError)
// AI 초안 생성 실패 등 AI 관련 에러를 감쌉니다.
case ai(AIError)
// 링크 공유(Firestore) 관련 에러를 감쌉니다.
case link(LinkError)
// 입력값 검증 실패를 감쌉니다.
case validation(ValidationError)
// 분류되지 않은 모든 에러를 안전하게 수용합니다.
case unknown(underlying: Error?)
}
// 네트워크 하위 에러 예시입니다.
// 이유: 사용자 메시지는 동일하지만, 로깅과 정책은 코드별로 달라질 수 있기 때문입니다.
enum NetworkError: Error { case offline, timeout, unreachable, http(code: Int) }
// MARK: - 에러 → 현지화 키 매핑(뷰모델 내부 유틸 예시)
// 이 함수는 AppError를 받아서 화면에 표시할 "키"를 결정합니다.
// 이유: 사용자 문구는 코드에 직접 쓰지 않고, 항상 키를 반환하기 위함입니다.
func localizedKeys(for error: AppError) -> (titleKey: String, bodyKey: String?, ctaKey: String?) {
switch error {
case .network(.offline):
return ("error.network.offline.title", "error.network.offline.body", "banner.cta.retry")
case .network(.timeout):
return ("error.network.timeout.title", "error.network.timeout.body", "banner.cta.retry")
case .iap(.canceled):
return ("iap.error.canceled", nil, nil)
case .permission(.photosDenied):
return ("perm.photos.denied.title", "perm.photos.denied.body", "perm.photos.go_settings")
case .link(.createFailed):
return ("error.link.create_failed", nil, "banner.cta.retry")
default:
return ("error.unknown", nil, nil)
}
}
// MARK: - 버튼 형식·키 사용 예시(규칙 준수)
// 규칙 1: 버튼은 반드시 action/label 분리 형식을 사용합니다.
// 규칙 2: 사용자 노출 문자열은 키만 사용합니다.
Button {
// 액션: 재시도 같은 복구 로직을 실행합니다(비동기).
Task { await viewModel.retry() }
} label: {
// 라벨: 현지화 키를 Text에 넣습니다(직접 문자열 금지).
Text("banner.cta.retry")
}
.accessibilityLabel(Text("a11y.banner.retry")) // 접근성 라벨도 키 사용
9.12 보안·개인정보(플랫폼 차원 결정 요약)
-
데이터 경계: 온디바이스 99%, 링크 공유만 익명 원격 쓰기
-
민감데이터: 외부 전송 금지. 로그·이벤트에 원문 텍스트 금지(길이·해시만)
-
키 관리: API 키·IAP 상품 ID는 빌드 설정/환경 주입, 코드 하드코딩 금지
-
네트워크: TLS 1.2+, ATS 기본, 중간자 공격 방어(증명서 핀닝 불필요)
9.13 유지보수·확장성 고려
-
교체 가능성: AIResponseService는 프로토콜로 추상화(모델·벤더 교체 용이)
-
백엔드 축소: 기본은 오프라인. 링크 공유 외 서버 의존 제거(비용↓, 위험↓)
-
모듈화: Services/ 하위 컴포넌트는 독립 테스트 가능하도록 최소 결합
-
관측성: 실패율, 초안 지연 시간, IAP 전환율 핵심 지표 대시보드화 전제
9.14 이 섹션의 “확정/보류” 목록
-
확정: iOS17+, SwiftUI/MVVM, SwiftData, StoreKit2, async/await, Error 도메인, 키 기반 현지화, 링크 공유 Firestore(TTL 7일)
-
보류: 원격 피처 플래그(초기 릴리스 제외), 푸시 인앱 메시지 타게팅(추후 16단계에서 상세)
10. 데이터모델 (Entities · JSON · Relationships/States)
10.1 모델링 원칙 (Design Principles)
온디바이스 99%: 사용자의 모든 민감 데이터는 SwiftData 로컬 저장.
-
클라우드 1%: 선택 기능 “링크 공유”만 Firestore에 익명 저장(읽기 금지 + TTL 7일).
-
단순·명확: 엔티티는 “한 가지 책임”. 긴 텍스트는 최대 길이 제한(일관된 검증).
-
상태 머신: 주요 엔티티는 유한 상태(STATE) + 전이(TRANSITION) 를 명확히 정의.
-
버전 관리: schemaVersion 필드로 점진적 마이그레이션 대비.
-
테스트 가능성: ID는 UUID v4, 시간은 ISO8601/Z(저장 시 시스템 타임존→UTC 정규화).
10.2 엔티티 개요(요약 표)
| Entity |
Purpose |
저장소 |
주요 키 필드 |
관계 |
Journal |
5단계 자기 성찰 세션 |
SwiftData |
id, conflictType, status, createdAt |
1 — may have many → LetterDraft |
LetterDraft |
AI/사용자 편지 초안 |
SwiftData |
id, journalId, status, content |
N — 1 → Journal / 1 — 0..1 → ShareRecord |
ShareRecord |
링크 공유 이력(로컬 트래킹) |
SwiftData |
id, letterId, remoteId?, status |
1 — 1 ← LetterDraft |
PurchaseState |
Pro 구매 상태 |
SwiftData |
isPro, purchaseDate? |
— |
Settings |
앱 설정(언어/애니메이션 등) |
SwiftData |
id, 각종 토글 |
— |
ConflictType |
관계 유형(열거형) |
코드/Key |
friends, couple, parentChild, siblings, inLaws |
Journal.conflictType |
(Remote) letters |
공유용 원격 문서 |
Firestore |
id, content, createdAt |
← ShareRecord.remoteId |
10.3 상세 스펙 — 엔티티별 정의
A) ConflictType (열거형 · 코드 상수)
역할: 관계 시나리오 구분 + 무료/Pro 게이팅.
-
값 (고정, 키 문자열은 로컬라이즈 키와 매핑 가능):
-
friends (Free), couple (Free), parentChild (Pro), siblings (Pro), inLaws (Pro)
-
표시 키 예시: conflict.friends.title, conflict.couple.title, …
-
검증 규칙: Journal.conflictType ∈ {위 5개}
B) Journal (자기 성찰 세션)
목적: 5단계 질문의 사용자 입력을 저장하고, 편지 생성의 근거 데이터가 됨.
필드(표)
| 필드 |
타입 |
필수 |
기본값 |
제약/검증 |
인덱스 |
id | UUID | ✔︎ | uuid4 | PK | PK |
schemaVersion | Int | ✔︎ | 1 | 마이그레이션용 | ✔︎ |
conflictType | String | ✔︎ | — | ConflictType 값만 허용 | ✔︎ |
status | String | ✔︎ | draft | draft→completed | ✔︎ |
situationText | String | ✔︎ | — | 길이 ≤ 500 | — |
feelings | [String] | ✔︎ | [] | 각 항목 길이 ≤ 24, 최대 8개 | — |
hurtText | String | ✔︎ | — | 길이 ≤ 500 | — |
reflectionText | String? | — | nil | 길이 ≤ 500 | — |
desiredNeedText | String | ✔︎ | — | 길이 ≤ 500 | — |
createdAt | ISO8601 | ✔︎ | now | 과거/미래 허용(테스트 편의) | ✔︎ |
updatedAt | ISO8601 | ✔︎ | now | 저장 시 갱신 | ✔︎ |
상태(STATE) & 전이(TRANSITION)
JSON 스키마(간결형)
{
"Journal": {
"id": "uuid",
"schemaVersion": 1,
"conflictType": "friends | couple | parentChild | siblings | inLaws",
"status": "draft | completed",
"situationText": "string(max:500)",
"feelings": ["string(max:24)"],
"hurtText": "string(max:500)",
"reflectionText": "string(max:500)|null",
"desiredNeedText": "string(max:500)",
"createdAt": "2025-08-21T03:14:15Z",
"updatedAt": "2025-08-21T03:14:15Z"
}
}예시(JSON)
{
"id": "b8c7d5d7-4a8b-4f0f-9a64-6d78f1b8c1ab",
"schemaVersion": 1,
"conflictType": "couple",
"status": "completed",
"situationText": "주말 약속을 깜빡해서 다투었다.",
"feelings": ["guilty", "anxious"],
"hurtText": "내가 지키지 못한 약속 때문에 상대가 존중받지 못했다고 느꼈을 수 있다.",
"reflectionText": "변명보다 책임을 먼저 인정해야겠다.",
"desiredNeedText": "서로의 약속을 더 확실히 확인하고 싶은 마음.",
"createdAt": "2025-08-20T12:00:00Z",
"updatedAt": "2025-08-20T12:30:00Z"
}C) LetterDraft (편지 초안: AI + 사용자 편집)
목적: 하나의 Journal로부터 여러 버전의 초안을 만들 수 있으나, 최종 1개를 선택해 공유 가능.
필드(표)
| 필드 |
타입 |
필수 |
기본값 |
제약/검증 |
인덱스 |
id |
UUID |
✔︎ |
uuid4 |
PK |
PK |
journalId |
UUID |
✔︎ |
— |
FK(Journal.id) |
✔︎ |
schemaVersion |
Int |
✔︎ |
1 |
— |
✔︎ |
status |
String |
✔︎ |
drafted |
drafted→edited→finalized |
✔︎ |
content |
String |
✔︎ |
— |
길이 10000 이하 |
Full-text |
aiModel |
String |
✔︎ |
"gpt-5" |
로깅용(문자열) |
— |
promptVersion |
Int |
✔︎ |
1 |
프롬프트 변경 추적 |
— |
tone |
String |
✔︎ |
"neutral_warm" |
사전 정의 값 |
— |
iteration |
Int |
✔︎ |
1 |
1부터 증가 |
— |
createdAt |
ISO8601 |
✔︎ |
now |
— |
✔︎ |
updatedAt |
ISO8601 |
✔︎ |
now |
— |
✔︎ |
상태
JSON 스키마(간결형)
{
"LetterDraft": {
"id": "uuid",
"journalId": "uuid",
"schemaVersion": 1,
"status": "drafted | edited | finalized",
"content": "string(max:10000)",
"aiModel": "gpt-5",
"promptVersion": 1,
"tone": "neutral_warm | softer | concise | sincere",
"iteration": 1,
"createdAt": "2025-08-21T03:14:15Z",
"updatedAt": "2025-08-21T03:14:15Z"
}
}D) ShareRecord (링크 공유 로컬 추적)
목적: 원격 공유 결과/만료를 로컬에서 추적하고, 재시도/실패 UI를 제어.
필드(표)
| 필드 |
타입 |
필수 |
기본값 |
제약/검증 |
인덱스 |
id |
UUID |
✔︎ |
uuid4 |
PK |
PK |
letterId |
UUID |
✔︎ |
— |
FK(LetterDraft.id), 유일(편지당 1개) |
✔︎(unique) |
remoteId |
String? |
— |
nil |
Firestore 문서 ID |
✔︎ |
url |
String? |
— |
nil |
https://kindverb.com/letter/{remoteId} |
— |
status |
String |
✔︎ |
pending |
`pending |
success |
contentHash |
String |
✔︎ |
sha256 |
길이=64(hex) |
— |
expiresAt |
ISO8601? |
— |
nil |
Firestore TTL 기준 복제 |
— |
createdAt |
ISO8601 |
✔︎ |
now |
— |
✔︎ |
updatedAt |
ISO8601 |
✔︎ |
now |
— |
✔︎ |
상태
원격(파이어스토어) letters 컬렉션
E) PurchaseState (Pro 구매 상태)
| 필드 | 타입 | 필수 | 기본값 | 제약 |
|---|
id | UUID | ✔︎ | uuid4 | PK |
isPro | Bool | ✔︎ | false | — |
purchaseDate | ISO8601? | — | nil | Pro일 때만 존재 |
originalTransactionId | String? | — | nil | 검증 추적 |
lastVerifiedAt | ISO8601? | — | nil | 영수증 검증 타임스탬프 |
F) Settings (앱 설정)
| 필드 | 타입 | 기본값 | 설명 |
|---|
id | UUID | uuid4 | PK |
appLanguage | String | "system" | `"en" |
enableTypingAnimation | Bool | true | 편지 타이핑 애니메이션 |
reducedMotion | Bool | OS설정따름 | 접근성 반영 캐시 |
10.4 관계(ER 다이어그램 텍스트)
삭제 전파 규칙
-
Journal 삭제 → 관련 LetterDraft 전부 삭제 → 연결된 ShareRecord도 삭제
-
LetterDraft 삭제 → 연결 ShareRecord 있으면 함께 삭제
-
원격 Firestore 문서는 TTL로 자동 삭제(로컬에서 강제 삭제 동작 없음)
10.5 검증 규칙 (Validation)
-
길이 제한 (모두 “문자 수” 기준)
-
situationText, hurtText, reflectionText, desiredNeedText ≤ 500
-
LetterDraft.content ≤ 10000
-
feelings 배열 길이 ≤ 8, 각 항목 ≤ 24
-
필수 입력
-
Journal: conflictType, situationText, feelings(≥1 권장), hurtText, desiredNeedText
-
LetterDraft: journalId, content, status
-
상태 전이 시점 검사
-
무결성
-
ShareRecord: contentHash는 업로드 당시 LetterDraft.content의 SHA-256
-
ShareRecord.status == success이면 remoteId,url,expiresAt 존재해야 함
10.6 인덱싱 & 조회 패턴 (SwiftData)
-
주요 인덱스:
-
Journal: createdAt DESC, status, conflictType
-
LetterDraft: journalId, updatedAt DESC, status
-
ShareRecord: status, createdAt DESC
-
대표 쿼리
-
홈: 최근 Journal 5건 (completed 우선)
-
작업실: journalId로 최신 LetterDraft 1건
-
공유 히스토리: ShareRecord where status in (success,failed) order by createdAt DESC
10.7 상태 머신 정리(표)
Journal
| From | To | 조건 |
|---|
draft | completed | 모든 필수 필드 유효(검증 통과) |
LetterDraft
| From | To | 조건 |
|---|
drafted | edited | 사용자가 내용 수정 |
edited | finalized | 최종 검토 완료(길이·금칙어·Pro게이트) |
ShareRecord
| From | To | 조건 |
|---|
pending | success | Firestore create 성공, remoteId 수신 |
pending | failed | 네트워크/규칙 에러 |
pending | canceled | 사용자가 공유 취소 |
10.8 오류 메시지 키(데이터 계층 관련)
UI 문구는 키만 사용(실 문자열 금지). 아래 키는 9장에서 정의한 에러 도메인과 합치.
| 상황 | 키 예시 |
|---|
| 길이 초과 | journal.error.too_long |
| 필수 누락 | journal.error.required_missing |
| 전이 불가 | journal.error.invalid_state_transition |
| 외부 공유 실패 | error.link.create_failed |
10.9 샘플 픽스처 (End-to-End)
완료된 세션 → 초안 2개 중 1개 확정 → 공유 성공 흐름
{
"Journal": {
"id": "3c1f7a15-4b1c-4e94-a05f-9f2a4a6b0c11",
"schemaVersion": 1,
"conflictType": "parentChild",
"status": "completed",
"situationText": "숙제 문제로 언성을 높였다.",
"feelings": ["worried", "frustrated"],
"hurtText": "아이의 자율성을 침해했다고 느꼈을 수 있다.",
"reflectionText": "지적보다 공감을 먼저 하자.",
"desiredNeedText": "서로 일정을 합의하고 존중받고 싶은 마음.",
"createdAt": "2025-08-19T10:00:00Z",
"updatedAt": "2025-08-19T10:15:00Z"
},
"LetterDrafts": [
{
"id": "7e6f8d9a-3d1e-44a7-9b02-8e0f2e9d1234",
"journalId": "3c1f7a15-4b1c-4e94-a05f-9f2a4a6b0c11",
"schemaVersion": 1,
"status": "edited",
"content": "어제 내 말투가 상처가 되었을 것 같아...",
"aiModel": "gpt-5",
"promptVersion": 1,
"tone": "softer",
"iteration": 2,
"createdAt": "2025-08-19T10:16:00Z",
"updatedAt": "2025-08-19T10:20:00Z"
},
{
"id": "1a2b3c4d-55aa-66bb-77cc-8899ddeeff00",
"journalId": "3c1f7a15-4b1c-4e94-a05f-9f2a4a6b0c11",
"schemaVersion": 1,
"status": "finalized",
"content": "어제 네 마음을 충분히 듣지 못한 점 미안해...",
"aiModel": "gpt-5",
"promptVersion": 1,
"tone": "sincere",
"iteration": 3,
"createdAt": "2025-08-19T10:21:00Z",
"updatedAt": "2025-08-19T10:25:00Z"
}
],
"ShareRecord": {
"id": "0f9e8d7c-6b5a-4321-9abc-ef0123456789",
"letterId": "1a2b3c4d-55aa-66bb-77cc-8899ddeeff00",
"remoteId": "a7Xp2Rz0KLMn",
"url": "https://kindverb.com/letter/a7Xp2Rz0KLMn",
"status": "success",
"contentHash": "b1f79e2a5c2b1a...<총64자리sha256>...",
"expiresAt": "2025-08-26T10:25:00Z",
"createdAt": "2025-08-19T10:25:05Z",
"updatedAt": "2025-08-19T10:25:05Z"
}
}10.10 마이그레이션 전략
-
Schema v1: 본 문서의 필드 집합.
-
향후 변경 예: feelings를 구조화([{code,label}])하거나 content에 서식 추가.
-
원칙: 파괴적 변경 회피 → 새로운 필드는 옵셔널로 추가 → 마이그레이션 태스크에서 기본값 세팅.
10.11 성능·용량 가드
10.12 개인정보 & 보안
-
로컬 저장 암호화는 OS 보호 영역(File Protection) 신뢰.
-
로그/이벤트에 원문 텍스트 절대 기록 금지(길이·해시만).
-
Firestore는 읽기 금지 + TTL 7일로 자동 삭제.
11.API 개요 (API Overview - Auth, Endpoints, Errors, Rate)
11.1 철학 & 범위
-
기본 원칙: 로그인/회원가입 없음. 읽기 금지, 쓰기 전용 원격 저장. 사용자 원문은 원칙적으로 기기 밖으로 나가지 않음.
-
네 가지 통신만 허용
-
Firestore letters.create: “링크로 공유하기” 시 편지 원문을 익명 단발성으로 업로드(읽기 불가, TTL 7일).
-
(옵션) LLM Draft API: 사용자가 “클라우드 초안 향상” 기능을 켰을 때만, 비식별 프롬프트로 초안 생성.
-
Apple StoreKit: 인앱결제 영수증 온디바이스 검증(서버 불필요).
-
App Check/Attestation: 파이어스토어 남용 방지용 기기 무결성 검증.
-
버전 전략: 모든 원격 스키마/엔드포인트는 /v1 고정. 필드 추가 시 하위 호환 유지.
11.2 인증(Auth) & 보안(Sec)
11.3 엔드포인트 일람(요약)
| 영역 | 메서드/이름 | 경로/컬렉션 | 목적 | 인증/보안 | Rate(클라이언트 수단) |
|---|
| Firestore | letters.create | db.collection("letters").add({...}) | 편지 링크 공유(익명 쓰기) | App Check, 보안규칙 | 디바이스당 ≤20/일, ≤2/분 |
| LLM(옵션) | drafts.create | /v1/drafts (프록시 or 공급자 SDK) | 비식별 프롬프트로 초안 생성 | API Key(앱 내 보관 금지: 키체인/원격설정) | 디바이스당 ≤60/일, ≤6/분 |
| StoreKit | 영수증 검증(온디바이스) | — | Pro 구매 상태 확인/복원 | Apple 서명 | Apple 한도(기본 제한 無) |
| App Check | attest | — | 무결성 토큰 발급/검증 | Firebase App Check | 필요 시 재요청 backoff |
주의: Firestore “읽기”는 절대 제공하지 않습니다. 링크 페이지 조회는 웹(KindVerb.com) 에서만 가능하며 앱·서버는 해당 문서를 읽지 않습니다.
11.4 상세 스펙 — Firestore letters.create (유일한 원격 저장)
요청(Request) (클라이언트가 Firestore SDK로 add)
{
"content": "string (<=10000)", // 편지 원문 (최종본)
"createdAt": "serverTimestamp()" // 서버 타임스탬프 (클라이언트 입력 금지)
}-
전송 전 강제 규칙(클라이언트 측)
-
content 길이 ≤ 10000
-
contentHash = SHA-256(content) 계산 → 로컬 ShareRecord.contentHash에 저장(중복/재시도 판단용)
-
비속어 필터(선택): 금칙어 있으면 사용자 경고(서버로 전송 자체는 막지 않음—표현 자유, 단 앱 정책 경고)
보안 규칙(Sample)
-
allow create: if request.resource.data.keys().hasAll(['content', 'createdAt']) && request.resource.data.content is string && request.resource.data.content.size() < 10000;
-
allow read, update, delete: if false;
-
TTL: 콘솔에서 createdAt 필드에 7일 정책 지정.
응답(Response)
에러(클라이언트 맵핑)
-
permission-denied → error.link.permission_denied
-
resource-exhausted(쿼터/속도) → error.link.rate_limited
-
unavailable(네트워크/서버) → error.network.unavailable
-
invalid-argument(검증 실패) → error.link.invalid_content
-
unknown → error.common.unknown
11.5 (옵션) LLM Draft API — 비식별 프록시 계약
개요
-
기본값 OFF → 사용자가 “AI 초안 생성” 기능을 명시적으로 켰을 때만 호출.
-
모델 고정: Google Gemini 2.5 Flash Lite.
-
API Key 관리: 앱 내 Secrets.plist 파일에 "GeminiAPIKey" 항목으로 저장.
-
비식별화 원칙: 모든 사용자 입력(성찰 답변, 일기 텍스트)은 전송 전 온디바이스에서 고유명사/민감정보 토큰화(<PERSON_1>, <DATE_1> 등).
-
로깅: 네트워크 요청/응답은 import os.log를 이용한 Apple 통합 로깅에 메타데이터만 기록. (print 금지, PII 금지)
엔드포인트
요청(Request) JSON (비식별화된 상태)
{
"contents": [
{
"parts": [
{
"text": "Please draft a warm, empathetic message based on the following context:\nSituation: <PERSON_1> said something hurtful\nFeelings: [sad, disappointed]\nHurt: I felt ignored\nReflection: I may have overreacted\nNeed: I want to feel respected"
}
]
}
],
"generationConfig": {
"temperature": 0.7,
"topK": 40,
"topP": 0.95,
"maxOutputTokens": 800
}
}응답(Response) JSON
{
"candidates": [
{
"content": {
"parts": [
{
"text": "I felt hurt when I was ignored. I may have overreacted, but what I need most is to feel respected..."
}
]
},
"safetyRatings": [
{ "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE" }
]
}
],
"usageMetadata": {
"promptTokenCount": 120,
"candidatesTokenCount": 95,
"totalTokenCount": 215
}
}클라이언트 후처리
-
응답의 candidates[0].content.parts[0].text 추출.
-
토큰화된 엔터티(<PERSON_1>, <DATE_1>)를 역치환하여 원래 사용자 문맥 복원.
-
LetterDraft 엔티티에 저장 (status="drafted", aiModel="gemini-2.5-flash-lite").
Rate Limit (앱 차원에서 강제)
오류 매핑 (현지화 Key 기반)
| Gemini API 에러 | 앱 내부 에러 도메인 | 매핑 키 예시 |
|---|
| 401 Unauthorized (잘못된 API키) | ai | error.ai.unauthorized |
| 429 Rate limit | ai | error.ai.rate_limited |
| 400 Content policy violation | ai | error.ai.content_policy |
| 504 Timeout | network | error.network.timeout |
| 기타/알 수 없음 | common | error.common.unknown |
-
모든 사용자 표시 문구는 Localizable.xcstrings Key 참조 필수.
-
로깅은 os_log("Gemini API failed: %{public}@", log: .network, type: .error, error.localizedDescription) 형식 사용.
11.6 인앱결제(StoreKit) 검증
-
전략: 온디바이스 영수증 검증(서버 불필요).
-
상태 전파: 성공 시 PurchaseState.isPro = true, purchaseDate 세팅, originalTransactionId 저장.
-
구매 복원: Apple 영수증 재검증 → 동일 originalTransactionId 확인.
-
에러 키:
-
검증 실패: error.iap.verify_failed
-
복원 실패: error.iap.restore_failed
-
취소: error.iap.user_canceled
11.7 레이트 리밋 & 백오프 (클라이언트 관점)
| 기능 | 단위 | 제한 | 백오프 |
|---|
| letters.create | 디바이스당 | 20/일, 2/분 | 1s→2s→4s… 최대 32s, 5회 |
| drafts.create(옵션) | 디바이스당 | 20/일, 6/분 | 동일 |
| App Check 재시도 | 호출당 | 3회 | 2s 고정 간격 |
Firestore 자체 쿼터/스루풋 한계 발생 시에도 위 정책이 우선 적용되어 사용자 경험을 보호합니다.
11.8 에러 규약(통합 포맷)
{
"domain": "link|ai|iap|network|common",
"code": "permission_denied|rate_limited|timeout|verify_failed|unknown",
"messageKey": "error.link.rate_limited.message", // UI 표기용 현지화 키
"debugInfo": { "httpLike": 429, "provider": "firestore" } // 로그 전용(PII 금지)
}11.9 보안·프라이버시 가드레일
-
원격 전송 데이터는 최소 필드 집합만: content(letters), nvc 요약(옵션 LLM).
-
LLM 전송 전 개인정보 토큰화 필수. 토큰 목록은 디바이스 내에서만 보관.
-
읽기 금지 설계로, 앱·서버 모두 사용자의 편지 원문을 재다운로드할 수 없음.
-
텔레메트리에는 해시/길이/상태만 기록(원문 절대 금지).
11.10 계약 테스트(수용 기준)
letters.create
-
(성공) 문서 ID 수신 → ShareRecord.status=success, remoteId/url 채움, expiresAt=+7d 표시
-
(권한오류) App Check 미설정 시 permission-denied → error.link.permission_denied 매핑
-
(과다요청) 1분 내 3건 전송시 1건 블록 → error.link.rate_limited
-
drafts.create(옵션)
11.11 변경/확장 여지
12.오프라인・동기화・캐싱 (Offline・Sync・Caching)
12.1 원칙 (Principles)
-
오프라인 우선(Offline-First): 주요 기능(관계 선택, 성찰, 편지 작성/편집, 로컬 저장)은 네트워크 없이 100% 동작.
-
클라우드 1%만 사용: Firestore는 ‘링크로 공유’ 기능에 한하여 쓰기(Create)만 사용(읽기/수정/삭제 없음, TTL 7일).
-
로컬 영구 저장: 사용자 생성 데이터는 SwiftData(온디바이스)로 저장. iCloud/서버 백업 없음(프라이버시 최우선).
-
현대적 오류 처리: throw/try/catch 기반 도메인 에러. NSError/Completion Handler 금지.
-
UX 일관성: 연결 상태와 무관하게 같은 플로우를 제공하되, 온라인 기능은 명확한 상태 안내와 복구 경로 제공.
-
로깅: os.log 사용, PII/콘텐츠 본문 기록 금지(메타데이터만).
12.2 데이터 구분 (Data Residency & Classes)
| 데이터 | 저장소 | 동기화 | TTL/만료 | 비고 |
|---|
| Guided Journal(성찰 답변) | SwiftData | 없음 | 사용자가 삭제 시 | 완전 오프라인 |
| LetterDraft(편지 초안/완성) | SwiftData | 없음 | 사용자가 삭제 시 | 완전 오프라인 |
| LinkShare(공유 메타) | SwiftData | 아웃박스 큐 → Firestore Create | 링크 자체는 Firestore에서 7일 TTL | 본문은 Firestore에만 존재(읽기 불가 정책) |
| Design/AI 설정(토큰/선호) | UserDefaults(Keychain 아님) | 없음 | 앱 재설치 시 소멸 | 민감 정보 미보관 |
| Gemini 호출 로그 메타 | OSLog | 없음 | 시스템 보존 정책 | 토큰/본문 금지 |
12.3 로컬 저장 구조 (SwiftData 모델 스냅샷)
실제 코드 구현은 Xcode에서 진행. 여기서는 스키마/필수 필드만 정의.
-
Journal: id(UUID), createdAt(Date), relationType(Enum), situation(Text≤2000), feelings([String]≤20), hurt(Text≤2000), reflection(Text≤2000?), need(Text≤2000), version(Int).
-
LetterDraft: id(UUID), journalId(UUID?), stage(Enum: draft|edited|final), content(Text≤6000), aiModel(String?), updatedAt(Date), version(Int).
-
ShareTask(아웃박스): id(UUID), letterId(UUID), status(Enum: queued|uploading|succeeded|failed), attempts(Int), lastErrorCode(String?), shareURL(String?), expiresAt(Date?), createdAt(Date), updatedAt(Date).
버전 필드는 데이터 마이그레이션/충돌 방지를 위한 미래 대비.
12.4 읽기/쓰기 경로 (Read/Write Path)
12.4.1 쓰기(오프라인)
-
View → ViewModel이 Journal/LetterDraft를 즉시 로컬 저장
-
성공 시 UI 피드백(토스트/배지)
-
실패 시 도메인 에러 storage.write_failed를 발생 → 현지화 키로 사용자 안내
12.4.2 읽기
12.5 캐싱 정책 (Caching Policy)
-
뷰 캐시: 화면 전환 간 5분 이내 재진입 시, ViewModel 메모리 캐시 우선 → 백그라운드 진입 시 무효화.
-
이미지 렌더 캐시: 편지 이미지 저장 프리셋 렌더 결과를 파일 캐시에 저장(해시 키: letterId+스타일). 24시간 후 자동 삭제.
-
AI 결과 캐시: LetterDraft.content 자체가 캐시. 같은 입력으로 재요청 방지.
12.6 링크 공유 동기화 (Outbox Sync)
12.6.1 개념
12.6.2 상태 머신
queued → uploading → (succeeded | failed)
-
queued: 네트워크 불가/사용자 트리거 생성 직후
-
uploading: Firestore Create 실행 중
-
succeeded: shareURL·expiresAt(=createdAt+7d) 기록
-
failed: 재시도 가능, attempts 증가, lastErrorCode 기록
12.6.3 재시도 정책
12.6.4 트리거
12.7 연결성 및 복구 UX (Connectivity & Recovery)
| 상황 | UI 안내(현지화 키 예시) | 사용자 선택지 |
|---|
| 오프라인 상태에서 링크 공유 | share.offline.queue_info (대기열에 저장됨, 연결 시 자동 전송) | [확인] |
| 업로드 실패(재시도 예정) | share.retry.scheduled (곧 재시도) | [닫기] [지금 다시 시도] |
| 권한/정책 오류(400/401/403) | share.failed.policy | [가이드 보기] [닫기] |
| 업로드 성공 | share.success.copied (URL 클립보드/공유시트) | [공유하기] [닫기] |
모든 버튼은 Button { } label: { } 형식, 모든 문구는 Localizable.xcstrings Key로만.
12.8 백그라운드 실행 (Background Tasks)
12.9 저장 한도 & 정리 (Storage Limits & Purge)
-
편지 이미지 캐시: 50MB 상한 → LRU 방식으로 삭제.
-
아웃박스 큐: 100건 상한 → failed 7일 경과 항목 자동 정리.
-
로컬 데이터 내보내기/삭제: v1.1에서 “모든 데이터 초기화” 옵션 제공 예정(개인정보 보호 강화).
12.10 에러 도메인 & 매핑 (Error Domain & Mapping)
| 도메인 | 코드 | 의미 | 사용자 안내 키 |
|---|
network | offline | 네트워크 불가 | error.network.offline |
network | timeout | 요청 시간 초과 | error.network.timeout |
storage | write_failed | SwiftData 저장 실패 | error.storage.write_failed |
share | unauthorized | Firestore 인증/키 오류 | error.share.unauthorized |
share | rate_limited | 429 | error.share.rate_limited |
share | policy_blocked | 400/403 정책 위반 | error.share.policy_blocked |
common | unknown | 알 수 없음 | error.common.unknown |
12.11 접근성/현지화 (A11y & L10n) — 오프라인 맥락 특화
-
오프라인/대기열 상태는 VoiceOver로 명확히 읽히는 라벨 제공(예: “링크 공유 대기열에 저장됨”).
-
로더/스켈레톤 사용 시 진행률/상태 문구 키 필수(“업로드 중… 2/3”).
-
시간/만료 안내는 지역/캘린더/24h 설정을 따름. 예: “링크 만료: 2025-08-28 18:00”.
12.12 보안·프라이버시(오프라인 관점)
-
SwiftData 파일은 iOS 데이터보호 NSFileProtectionComplete 기본 준수.
-
백업: iCloud 백업 비권장(민감 데이터). 필요 시 isExcludedFromBackup.
-
Firestore 보안 규칙: letters 컬렉션 Create만 허용(읽기/수정/삭제 금지) + TTL 7일.
-
로그에 PII 금지, 에러 상세는 내부코드만.
12.13 성능 기준 (SLO) — 오프라인/캐시
| 항목 | 목표 | 비고 |
|---|
| 로컬 저장(write) | p95 ≤ 60ms | Journal/LetterDraft |
| 리스트 로드 | p95 ≤ 80ms | 50건 기준 |
| 이미지 렌더 캐시 조회 | p95 ≤ 40ms | 파일캐시 hit |
| 아웃박스 전송 성공률 | ≥ 99% | 24h 윈도우, 재시도 포함 |
12.14 텔레메트리(오프라인/동기화 관련)
| 이벤트 키 | 속성 | 트리거 |
|---|
share_task.created | letterId, queuedAt | 공유 버튼 탭 시 |
share_task.updated | taskId, status, attempts, httpCode? | 상태 변경 시 |
share_task.succeeded | taskId, expiresAt, urlHash | 성공 시 |
share_task.failed | taskId, lastErrorCode, attempts | 최종 실패 시 |
cache.pruned | bytesFreed, kind | 캐시 청소 시 |
실제 사용자 노출 문구는 xcstrings 키로만 표시, 텔레메트리는 내부 스키마.
12.15 수용 기준 (Acceptance Criteria)
-
완전 오프라인에서: 성찰 입력 → 편지 작성/수정/저장까지 모든 경로 수행 가능.
-
오프라인에서 “링크로 공유”: ShareTask(queued) 생성, UI에 대기열 안내 표출.
-
온라인 복귀 시: 백그라운드/포그라운드/수동 재시도 중 하나 이상의 경로로 전송 시도.
-
정책 오류(401/403/400) 시: 재시도 중지 + 사용자 안내 + 도움말 링크(내부 가이드) 노출.
-
성공 시: URL 생성·만료일 계산(7일 후)·클립보드/공유시트 연결.
-
캐시 상한 초과 시: LRU로 삭제되고, UI는 오류 없이 동작.
-
모든 에러는 도메인/코드/현지화 키로 매핑되어 사용자에게 명확히 전달.
12.16 테스트 계획(샘플)
-
오프라인 시나리오: 기내모드에서 전체 핵심 루프(성찰→작성→저장) 수행.
-
대기열→성공: 기내모드에서 공유 생성→네트워크 복귀→자동 업로드 확인.
-
429/5xx: 목 서버로 응답 조작→지수 백오프 로그/상태 전이 검증.
-
정책 오류: 401/403/400→즉시 실패/안내 확인.
-
캐시 LRU: 이미지 캐시 초과→오류 없이 자동 정리 확인.
-
접근성: VoiceOver로 상태/진행률/만료 텍스트 읽기 검증.
메모 (개발 시작용 체크)
-
SwiftData 모델에 ShareTask 포함
-
BGTaskScheduler 등록(ID 2개)
-
네트워크 리치어빌리티 옵저버 → 아웃박스 트리거
-
Firestore Create 전송 유스케이스 + 지수 백오프 공통 유틸
-
현지화 키: share.*, error.*, cache.* 세트 추가
-
OSLog 카테고리 분리: .storage, .network, .sync, .cache
13.보안・개인정보보호・컴플라이언스(Security・Privacy・Compliance)
13.1 원칙 & 범위
-
데이터 최소화: 계정/로그인/식별자 수집 없음. 모든 핵심 데이터는 온디바이스(SwiftData).
-
오프라인-우선: 네트워크가 없어도 핵심 여정(성찰→편지 작성→보관) 100% 동작.
-
클라우드 1%: Firestore는 **링크 공유(Create만)**에 한정, TTL 7일 자동 삭제.
-
현대적 오류 처리: throw/try/catch 기반 도메인 에러. NSError/Completion Handler 금지.
-
로깅 가드레일: os.log만 사용, 민감 내용/본문/토큰 미기록.
-
투명성 & 제어권: 로컬 데이터 내보내기/삭제(차기 버전 포함)와 명확한 안내.
13.2 데이터 분류표 (Data Classification)
| 분류 | 예시 | 저장 위치 | 보호 수준 | 유지 기간 |
|---|
| 민감 사용자 생성물 | 성찰 답변, 편지 초안/완성 | SwiftData(온디바이스) | iOS 데이터 보호(NSFileProtection) | 사용자가 삭제 시까지 |
| 공유 본문 | 링크로 공유된 편지 텍스트 | Firestore(서버) | 전송 중 TLS, 서버 저장 암호화 | 7일 TTL 후 자동 삭제 |
| 동기화 메타 | ShareTask 상태, URL 해시 | SwiftData | 기기 내 보호 | 상태 수명 + 7일 정리 |
| 설정/환경 | 언어, 테마 등 | UserDefaults | 기기 내 보호 | 앱 생명주기 |
| 비즈니스 로그(메타) | 전송 시도 횟수, HTTP 코드 | OSLog | 시스템 보호, 민감값 금지 | OS 정책 준수 |
설계 원칙: 이름/이메일/광고ID 등 식별자 비수집.
13.3 데이터 흐름(요약)
-
사용자가 성찰·편지를 작성 → SwiftData 로컬 저장
-
“링크로 공유” 선택 시 → ShareTask(아웃박스) 생성 → 온라인 시 Firestore에 Create
-
Firestore 문서 ID 기반 URL 발급 → 7일 후 자동 삭제(TTL)
-
서버에서 읽기/수정/삭제는 불가(규칙상 차단)
13.4 보안 통제(Technical Controls)
-
전송구간 암호화: ATS 강제(HTTP 금지), TLS 1.2+.
-
저장구간 암호화: SwiftData 파일은 iOS 기본 NSFileProtectionComplete 적용. 백업 제외 필요 시 isExcludedFromBackup 플래그 사용.
-
도메인 화이트리스트: 네트워크는 Firestore 엔드포인트 및 kindverb.com만 허용(ATS 예외 금지).
-
비밀관리:
-
가드레일 상 **Secrets.plist의 "GeminiAPIKey"`**를 사용. 클라이언트 내 비밀은 노출 가능하다는 전제에서 동작(완전 보호 불가).
-
완화책: 문자열 난독화, 키 롤테이션 계획, 최소 권한 API 키, 도메인-기반 제한(가능 시), 요청량 제한, 런타임 키 검증(형식/길이).
-
로깅: os_log의 프라이버시 레드액션 사용, 본문·개인정보·API 키 절대 기록 금지.
-
권한 최소화: 사진 저장 권한(옵트인)만 요청, 마이크/연락처/위치 권한 불사용.
-
무결성 가드(선택): 탈옥 휴리스틱, 디버거/프록시 탐지 등 소프트 가드(하드 차단 아님).
13.5 개인정보 보호(Privacy)
13.6 Firestore 보안 규칙 & TTL
-
보안 규칙(요지): letters 컬렉션 Create만 허용, Read/Update/Delete 전면 차단. content ≤ 5000자, createdAt 필수.
-
TTL: createdAt + 7일 지나면 자동 영구 삭제.
-
리스크 노트: URL은 타인 공유 시 누구나 열람 가능. 앱/웹에서는 광고/추적 없음.
13.7 권한(Privacy Prompts) & 현지화 키
-
사진 저장: 최초 사용 시 설명 → privacy.photos.purpose 키.
-
알림(선택): 리마인더 사용 시 사전 교육 화면 → privacy.notifications.purpose.
-
링크 공유 경고: privacy.share.link.warning_title, privacy.share.link.warning_body.
모든 노출 문자열은 Localizable.xcstrings 키만 사용.
13.8 규정 준수 매핑(간단 가이드)
-
GDPR/CCPA:
-
데이터 최소 수집, 목적 제한, 저장 제한(TTL), 투명성(프라이버시 정책) 준수.
-
식별자 미수집이므로 데이터 권리 처리 부담 최소. 온디바이스 삭제 기능 제공.
-
COPPA:
-
App Store Review:
-
개인정보 라벨(Data Nutrition) → “데이터 수집 안 함”, 단 링크 공유 시 서버에 일시 보관을 “앱 기능용 데이터(사용자 입력, 링크)”로 명확히 표기.
-
Health/Medical 카테고리 회피, “교육/자기개선” 톤. 치료/진단 주장 금지.
법률 자문이 필요한 경우 별도 검토. 위는 제품 설계 기준 준수 가이드.
13.9 인시던트 대응(Incident Response)
13.10 제3자 의존성 & 라이선스
-
프레임워크: Firebase(Firestore), Gemini SDK/HTTP.
-
관리: SPM 고정 버전, 취약점 공지 모니터링, 연 1회 OSS 감사.
-
라이선스 고지: Settings>About>Licenses 화면에 자동 생성 고지.
13.11 보안/프라이버시 수용기준(AC)
-
식별자/추적자 비수집이 코드/설정/스토어 라벨로 일치한다.
-
앱은 완전 오프라인에서 핵심 기능이 동작한다.
-
링크 공유 시 사전 고지가 현지화 키로 표시되고, 사용자 동의 후에만 진행된다.
-
Firestore는 Create만 성공, Read/Update/Delete는 실패한다(규칙 테스트 통과).
-
Firestore 문서는 7일 후 자동 삭제가 검증된다(스테이징 환경 시뮬레이션).
-
os.log에 민감 본문/토큰이 기록되지 않음이 점검된다.
-
ATS 우회 없음(HTTP 금지), 허용 도메인만 접속 가능.
-
사진 권한 프롬프트 목적 문구가 현지화되어 표시된다.
-
프라이버시 정책(웹)과 앱 내 고지가 기능/데이터 흐름과 일치한다.
-
키 노출 가정하더라도 남용 감지/차단/교체 플랜이 문서화되어 있다.
13.12 테스트 계획(샘플)
-
규칙 테스트: 스테이징 Firestore에 Create/Read/Update/Delete 시도 → C만 200, R/U/D 403 확인.
-
TTL 테스트: TTL=10분 환경에서 만료/삭제 관찰 → 프로덕션 7일 설정과 동등성 검증.
-
로그 스캔: 시나리오 실행 후 Console.app으로 민감값 노출 탐지(정규식: 키/URL/본문 길이).
-
권한 UX: 사진 저장/알림 프롬프트가 교육 화면 뒤에 등장, 거부→재요청 경로 검증.
-
오프라인 시나리오: 링크 공유 시 ShareTask 대기열 생성 및 온라인 복귀 후 전송 검증.
-
침투 표면 점검: ATS 설정 파일 점검, 디버그 플래그/개발자 메뉴 빌드 제외 확인.
13.13 위험 & 완화
| 위험 | 설명 | 완화 |
|---|
| 클라이언트 키 노출 | Secrets.plist는 리버스 엔지니어링에 취약 | 최소 권한 키, 난독화, 주기적 롤테이션, 트래픽 모니터링/알림, 쿼터 설정 |
| 공유 링크 오남용 | URL이 타인에게 전달될 수 있음 | 공유 전 경고·가이드, 민감정보 자제 권고, 7일 TTL, URL 난수 길이 확대 |
| 규칙 오구성 | Read 허용 등 오설정 위험 | IaC로 규칙 버전관리, CI에서 규칙 테스트, 변경 이중승인 |
| 로그 민감정보 유출 | 개발자가 무심코 본문 로깅 | 린터/리뷰 체크리스트, os_log 전용 래퍼로 민감값 차단 |
| 의료/치료 오인 | 사용자가 치료 앱으로 인식 | 앱/스토어 내 “정보성 가이드, 치료 아님” 고지, 문구 현지화 |
13.14 준비물 체크리스트(출시 전)
-
App Privacy 라벨: “데이터 수집 안 함(링크 공유 예외 설명)” 정합성 확인
-
프라이버시 정책(웹) 최신화: 오프라인-우선, TTL 7일, 식별자 비수집 명시
-
Firestore 규칙/TTL IaC 적용 + CI 테스트 통과
-
os.log 민감값 레드액션 래퍼 도입, print 탐지 린팅
-
권한 프롬프트 교육 화면/현지화 키 존재 확인
-
키 롤테이션 절차 문서/담당 지정
-
라이선스 고지 화면 동작 확인
14.성능목표 & 품질기준 (Performance SLOs & Quality Bars)
14.1 핵심 성능 SLO (앱 전반)
| 항목 | 정의 | 목표(각 pctl) | 측정 방법 | 도구/수집 주기 |
|---|
| Cold Start TTI | 프로세스 시작 → 첫 상호작용 가능 | p50 ≤ 2.0s, p95 ≤ 3.0s | XCTOSSignpostMetric.applicationLaunch, 실기기 리허설 | XCTest Performance, Instruments (매 스프린트) |
| Warm Start TTI | 백그라운드 → 포그라운드 인터랙션 가능 | p50 ≤ 0.9s, p95 ≤ 1.4s | Signpost 마킹 | Instruments (주간) |
| 스크롤 FPS | 주요 리스트/에디터 스크롤 프레임 | 평균 ≥ 55fps, 드롭 프레임율 ≤ 1.5% | Core Animation FPS, Hitch 추적 | Instruments (주간) |
| UI 응답성 | 탭→UI 반응(하이라이트/전환) | p95 ≤ 100ms | Custom signpost 구간 | UITest + Instruments |
| Crash-free 세션 | 크래시 없는 세션 비율 | ≥ 99.8% | MXCrashDiagnostic, Crashlytics | MetricKit 일간 집계 |
| Foreground Hang | 2s+ 메인스레드 정지 비율 | ≤ 0.20% 세션 | MXHangDiagnostic | MetricKit 주간 |
| 메모리 피크 | 세션 중 최대 RSS | p95 ≤ 350MB | XCTMemoryMetric, MXMemoryMetric | Perf 테스트/MetricKit |
| 에너지 영향 | 평균 에너지 등급 | Low 유지 | Energy Log 등급 | Instruments 리그(주간) |
| 앱 크기(압축 IPA) | App Store 배포 번들 | ≤ 80MB | Xcode Archive 결과 | CI 빌드 게이트 |
TTI는 탭 가능 상태 진입 시점(첫 Interaction 가능한 상태)을 기준.
14.2 화면별 퍼포먼스 예산 (View Budget)
| 화면 ID | 1st Paint | TTI | 추가 메모 |
|---|
| V02 ConflictSelection | ≤ 300ms | ≤ 500ms | 캐러셀 이미지/일러스트는 비동기 지연 로드 |
| V04 GuidedJournal (Q-step) | ≤ 200ms | ≤ 350ms | 키보드 표시 전 레이아웃 안정화 |
| V06 MessageComposer | ≤ 350ms | ≤ 600ms | 초안 로딩 스켈레톤 ≤ 120ms 내 표시 |
| V03 Paywall | ≤ 250ms | ≤ 400ms | 영수증 체크/상품 로드는 백그라운드 |
| V07 Completion | ≤ 200ms | ≤ 300ms | 애니메이션 200–300ms 내 종료 |
14.3 네트워크/AI SLO (클라우드 1%)
| 항목 | 정의 | 목표(각 pctl) | UX 정책 | 측정 |
|---|
| LLM 초안 생성 | V06 진입 후 첫 결과 수신 (Gemini 2.5 flash-lite) | p50 ≤ 5.0s, p95 ≤ 9.0s | 스켈레톤 즉시(≤120ms), 7s 초과 시 친절한 안내 키(ai.timeout.tip) | os_signpost, 이벤트 |
| AI 재생성 | “다시 제안/부드럽게” 클릭→결과 | p50 ≤ 3.5s, p95 ≤ 7.0s | 버튼 상태: 로딩/비활성, 실패 시 재시도 키 | 이벤트 |
| 링크 공유 Create | Firestore document 생성→URL 획득 | p50 ≤ 700ms, p95 ≤ 1.2s | 오프라인 시 아웃박스 큐, 온라인 시 자동 재전송 | 이벤트 + Signpost |
| 아웃박스 재전송 지연 | 네트워크 복귀→전송 완료 | p50 ≤ 10s, p95 ≤ 60s | 백오프(최대 60s), 재시도 토스트 | 이벤트 |
14.4 오프라인·저장소 SLO
| 항목 | 정의 | 목표 | 측정/검증 |
|---|
| SwiftData 저장 지연 | 편지 저장 save() 호출→커밋 | p95 ≤ 120ms | 단위테스트 + Signpost |
| 대용량 로컬셋 | 2,000개 저널, 500개 편지 | V04/V06 TTI 목표 유지 | 테스트 픽스처 리그 |
| 아웃박스 제한 | 대기 중 ShareTask 수 | ≤ 50개 (경고) | 상태 이벤트 |
| 백업 제외(선택) | 민감 데이터 iCloud 백업 | 정책에 맞춰 제외 여부 명시 | 파일 속성 점검 |
14.5 접근성·현지화 품질 바
| 영역 | 기준(출시 게이트) | 측정/검증 |
|---|
| A11y 라벨 | 모든 상호작용 컨트롤 100% VoiceOver 라벨 (키 기반) | XCUITest + 수동 점검 |
| Dynamic Type | L~XXXL까지 레이아웃 깨짐 없음 | Snapshot 테스트 |
| 컬러 대비 | 텍스트 대비 AA 이상 | Figma/Plugin, 수동 |
| 현지화 키 적용 | 사용자 노출 문자열 100% 키 참조 (누락=빌드 실패) | L10n 스크립트/CI |
| Pseudo-L10n | 길이 30% 증가 시 UI 유지 | 스냅샷 |
14.6 코드 품질 바 (CI 게이트)
| 항목 | 기준 |
|---|
| 컴파일 경고 | 0 |
print 사용 | 금지(검사 스크립트) |
| 동시성 경고 | Swift Concurrency 경고 0 |
| 테스트 커버리지 | ViewModel/UseCase ≥ 70%, Service ≥ 60% |
| Lint/Formatting | 팀 규칙 준수(파일당 300라인 가이드) |
| 스냅샷 테스트 | 주요 화면(라이트/다크, 크기 2종) 통과 |
| 성능 회귀 | 기준 대비 +10% 초과 증가 금지 (TTI/FPS/메모리) |
14.7 실측·모니터링 설계
-
계측 원칙: os_log + Signpost로 주요 구간(Launch, View Appear, Save, LLM Call, Share Create)을 마킹.
-
수집:
-
로컬 개발: Instruments 템플릿(Launch, Time Profiler, Animation, Energy).
-
배포 후: MetricKit(크래시/행/CPU/메모리/디스크/애니메이션) + Crashlytics(최소 메타만).
-
이벤트 스키마(요약):
-
perf.launch, perf.view_show{viewId}, perf.ai_draft{ms, promptType, outcome}, perf.share_create{ms, outcome}, perf.save_local{ms}
-
개인 콘텐츠/프롬프트 본문 비수집(키만).
14.8 테스트 리그 & 시나리오
디바이스군
-
Low: iPhone SE (3세대)
-
Mid: iPhone 12/13
-
High: 최신 Pro 라인 1종
OS 범위: iOS 최신 안정버전 ±1 메이저
네트워크
데이터 볼륨
핵심 시나리오
-
콜드 스타트→V02→V04 입력→저장→V06 LLM 생성→편집→링크 공유(오프라인/온라인 전환)
-
다국어 전환(영/한), Dynamic Type XXL, 다크 모드
-
연속 세션 30분 스크롤/편집(메모리/에너지 관찰)
14.9 회귀 방지(성능 가드)
14.10 에러 예산 & 완화 정책
| 지표 | 월간 에러 예산 | 초과 시 조치 |
|---|
| Crash-free 세션 | 0.2%p | 새 기능 동결, Hotfix 48h 내 배포 |
| Hang Rate | 0.2% | 메인스레드 트레이스 분석/해결 우선 |
| LLM p95 지연 | 9.0s | 프롬프트/토큰/네트워크 점검, 캐시/스트리밍 옵션 검토 |
| 링크 p95 지연 | 1.2s | Firestore 인덱스/리전/재시도 백오프 조정 |
14.11 출고 체크리스트 (Quality Bars)
-
콜드 스타트 p50 ≤ 2.0s, p95 ≤ 3.0s (SE 포함)
-
스크롤 FPS 평균 ≥ 55fps, 드롭 ≤ 1.5%
-
Crash-free ≥ 99.8%, Hang ≤ 0.2%
-
V06 LLM p50 ≤ 5s / p95 ≤ 9s (스켈레톤 즉시 표시)
-
링크 공유 p95 ≤ 1.2s, 오프라인 아웃박스 재전송 p95 ≤ 60s
-
메모리 피크 p95 ≤ 350MB, Energy 등급 Low
-
A11y/현지화/동시성/버튼 규칙 100% 준수
-
CI 성능 회귀 ≤ +10%, 경고 0, print 0, 테스트 커버리지 기준 충족
15.분석&텔레메트리 (Analytics & Telemetry)
15.1 목적 & 가드레일
15.2 데이터 등급 & 동의
| 등급 | 예시 | 수집/업로드 정책 |
|---|
| Level 0 (필수 진단) | 앱 버전, OS, 기기 등급 | 온디바이스 집계. 업로드는 Opt-in 시 익명화 후 전송 |
| Level 1 (행태 이벤트) | 화면 진입, 버튼 탭, 퍼널 단계 | 텍스트 키/코드만. 원문 텍스트 금지 |
| Level 2 (성능/품질) | TTI, FPS, Crash/Hang | MetricKit 표준 페이로드 |
| 금지(수집 안 함) | ‘마음 편지’ 원문, 감정 자유서술, 상대 실명/연락처 | 항상 금지 |
15.3 세션/사용자 식별 규칙
-
세션 정의: 포그라운드 진입부터 30분 무활동 또는 앱 종료까지.
-
익명 사용자 ID: 앱 최초 실행 시 로컬에 생성한 UUIDv4(해시). 서버 업로드 시 해시+솔트(앱 버전별 변경)로 재식별 곤란성 확보.
-
리텐션 측정용 키: install_ts, first_open_ts, last_open_ts (Epoch ms).
15.4 이벤트 택소노미 (마스터 목록)
A) 앱/세션 라이프사이클
-
app.install
-
app.first_open
-
session.start
-
session.end
B) 온보딩/관계 선택
-
onboarding.shown
-
relationship.card_view
-
relationship.select (props: type=friend|partner|parent|sibling|inlaw, locked=bool)
C) 저널(성찰 5단계)
-
journal.step_view (props: step=1..5)
-
journal.step_complete (props: step=1..5, input_type=tag|text, char_count)
-
journal.finish
D) 메시지 작업실(LLM)
-
draft.request (props: prompt_preset=initial|regenerate|softer|concise|sincere, estimated_tokens_in)
-
draft.result (props: latency_ms, success=bool, retry_count, estimated_tokens_out)
-
draft.edit_local (props: edit_chars, duration_ms)
E) 전달(공유)
-
share.copy_text
-
share.save_image (props: success=bool)
-
share.link_create (props: success=bool, latency_ms, offline_queued=bool)
-
share.link_open (웹(선택): 익명 파라미터만, 리퍼러/UA만)
F) 수익화
-
paywall.view
-
iap.purchase_attempt
-
iap.purchase_result (props: success=bool, error_code)
-
iap.restore_attempt
-
iap.restore_result (props: success=bool)
G) 성능/품질 (요약 이벤트: 14장에서 정의한 SLO와 호응)
-
perf.launch (cold/warm, tti_ms)
-
perf.view_show (view_id, tti_ms)
-
perf.ai_draft (preset, latency_ms, outcome)
-
perf.share_create (latency_ms, outcome)
-
perf.save_local (latency_ms, outcome)
H) 오류/복구
-
error.shown (props: domain, code, retriable=bool)
-
recovery.action (props: type=retry|offline_queue|open_settings)
I) 실험/플래그
-
exp.exposure (props: exp_key, variant)
중요: 어떤 이벤트에도 사용자 원문 문자열을 넣지 않는다. 감정/메시지 내용은 길이·선택코드만.
15.5 핵심 이벤트 스키마 (샘플 표)
15.5.1 relationship.select
| 필드 | 타입 | 필수 | 예시 | 설명 |
|---|
type | string(enum) | Y | "partner" | 관계 유형 코드 |
locked | bool | Y | false | Pro 잠금 여부 |
ab_test_variant | string | N | "A" | 실험군 라벨 |
ts | int64 | Y | 1724212345678 | 밀리초 타임스탬프 |
15.5.2 journal.step_complete
| 필드 | 타입 | 필수 | 예시 | 설명 |
|---|
step | int | Y | 2 | 진행 단계(1..5) |
input_type | string(enum) | Y | "tag" | 입력 타입 |
char_count | int | Y | 120 | 텍스트 길이(원문 미저장) |
selected_tags | string[] | N | ["angry","hurt"] | 사전정의 코드만 |
ts | int64 | Y |
|
|
15.5.3 draft.result
| 필드 | 타입 | 필수 | 예시 | 설명 |
|---|
prompt_preset | string(enum) | Y | "initial" | 요청 프리셋 |
latency_ms | int | Y | 3450 | 왕복 지연 |
success | bool | Y | true | 성공 여부 |
estimated_tokens_in | int | N | 420 | 요청 토큰 추정 |
estimated_tokens_out | int | N | 280 | 응답 토큰 추정 |
error_code | string | N | "timeout" | 실패 시만 |
15.5.4 share.link_create
| 필드 | 타입 | 필수 | 예시 | 설명 |
|---|
success | bool | Y | true | 생성 성공 |
latency_ms | int | Y | 612 | Firestore 생성 시간 |
offline_queued | bool | Y | false | 오프라인 큐 적재 여부 |
15.5.5 iap.purchase_result
| 필드 | 타입 | 필수 | 예시 | 설명 |
|---|
success | bool | Y | true | 결제 성공 |
error_code | string | N | "paymentCancelled" | 실패 시 StoreKit 코드 |
paywall_variant | string | N | "badgeA" | 페이월 실험군 |
15.6 퍼널 정의 (비즈니스 핵심)
15.6.1 핵심 가치 퍼널 (Activation)
session.start → relationship.select(type) →
journal.step_complete(step=5) → journal.finish →
draft.request(preset=initial) → draft.result(success=true) →
share.copy_text|share.save_image|share.link_create(success=true)
15.6.2 수익화 퍼널
paywall.view → iap.purchase_attempt → iap.purchase_result(success=true)
15.6.3 리텐션/재참여 퍼널
session.start (D+1/D+7/D+30 코호트) → journal.step_view ≥1 → draft.request ≥1
15.7 사용자 속성(익명) & 디바이스 속성
| 키 | 타입 | 예시 | 비고 |
|---|
app_version | string | "1.0.0" | 빌드/CFBundleShortVersionString |
build_number | string | "100" |
|
os_version | string | "iOS 18.0" |
|
device_tier | string(enum) | `"low | mid |
locale | string | "en-US" |
|
pro_status | bool | false | IAP 복원 후 업데이트 |
exp_bucket | string | "A" | 플래그 SDK 없이도 저장 |
15.8 계측 아키텍처 & 파이프
대안(서버 없이): MetricKit + App Store Connect만으로 크래시/성능을 추적하고, 기능 이벤트는 **온디바이스 집계(카운터/퍼널 완료 플래그)**로만 사용해도 됨. (완전 프라이버시 극대화 모드)
15.9 샘플링 & 보존
15.10 QA & 검증(출시 게이트)
-
스키마 린터: 이벤트/프로퍼티가 허용된 화이트리스트에 없으면 컴파일 실패(스위프트 제네릭 래퍼로 키 상수화).
-
페이크 시나리오: UI테스트에서 전체 퍼널 재현 → 각 단계 이벤트 수/순서 검증.
-
현지화 키 누락 차단: 사용자 노출 문자열은 키만 사용(분석 이벤트는 영문 코드만 사용).
-
리플레이 보호: 업로드 시 중복 방지 해시(session_id+seq_no) 포함.
15.11 대시보드(권장 차트)
-
Activation 퍼널: 단계별 전환/이탈, 평균 단계 소요.
-
LLM 품질판: draft.result 성공률, p50/p95 지연(프리셋별).
-
수익화: 페이월 뷰→구매 시도→성공, 실패 코드 분포.
-
성능 요약: TTI p50/p95, FPS 평균/드롭, Crash/Hang 추이(14장 SLO 연동).
-
언어/기기별 분해: locale, device_tier별 핵심지표.
15.12 거버넌스 & 버저닝
-
이벤트 키 네이밍 규칙: domain.action(소문자·점 표기). 속성은 snake_case.
-
변경 정책: 이벤트 삭제 금지(비활성만). 스키마 변경은 v2 suffixed 신규 키로 추가. 릴리스 노트에 이벤트 변경 섹션 필수.
-
권한: 스키마 변경은 PM(소유), iOS TL(리뷰), 보안 담당(승인) 3자 승인.
15.13 개인정보 고지(설정 화면 복사 문구 제안)
15.14 (선택) 웹 링크 열람 텔레메트리
-
링크 페이지는 광고/쿠키 배너 없음.
-
수집 가능 항목: share.link_open(문서ID 해시, 생성 후 경과시간 범주, 브라우저 UA 범주).
-
수집 금지: 발신자/수신자 텍스트, IP 저장(실시간 지오 추정 없음).
15.15 PRD 연계 맵
-
3장 지표: D1/D7 리텐션, Crash-free, TTI, 구매 전환 → 모두 이벤트/MetricKit로 계산 가능.
-
14장 SLO: perf.* 이벤트 + MetricKit으로 대시보드 자동 생성.
-
17장 수익화: paywall.*, iap.*로 전환 퍼널 구성.
-
12장 오프라인 정책: offline_queued로 재시도 품질 가시화.
16.알림&인앱메시징 (Notifications & In-App Messaging)
16.1 목적 & 전략
-
목적
-
(1) 사용자의 성찰 루틴(Daily Reflection)을 습관화
-
(2) 완성된 메시지를 공유 행동으로 이어지게
-
(3) **재참여(Retention)**와 수익화 전환을 유도
-
전략
-
iOS 네이티브 로컬 알림 중심 → 알림센터 과잉 사용 금지
-
개인 맞춤형 컨텍스트: “오늘 기록한 감정을 다시 읽어볼까요?” 등
-
인앱 메시징은 페이월·기능 안내 등 맥락적 트리거에서만 사용
16.2 가드레일
-
사용자 동의 (Opt-in): 앱 최초 실행 시 알림 권한 요청은 첫 성찰 완료 후에만 노출 → 사용자가 앱 가치를 체험한 후 요청.
-
빈도 제한
-
푸시: 1일 최대 1회(주간 5회 상한)
-
인앱 메시징: 세션당 최대 1회, 중복 차단
-
현지화 필수: 모든 텍스트는 Localizable.xcstrings 키 참조
-
민감정보 금지: 알림/배너에 감정 원문 텍스트 삽입 금지 → 태그 기반 요약, 일반적 톤으로만
16.3 알림 유형 (로컬)
| 유형 | 설명 | 트리거 조건 | 예시 메시지 키 |
|---|
| Daily Reflection Reminder | 매일 성찰 유도 | 매일 저녁 8시, 사용자 설정 시각 | notif.daily_reflection.body |
| Streak/N-Day Progress | 연속 기록 보상 | 성찰 3일, 7일, 30일 달성 시 | notif.streak.body |
| Draft Follow-up | 미완성 초안 리마인드 | draft.request 후 24시간 내 share.* 없음 | notif.draft_followup.body |
| Share Celebration | 공유 완료 축하 | share.* 발생 직후 | notif.share_celebration.body |
| System Upgrade Info | 버전 업데이트 안내 | 앱 버전 교체 후 최초 실행 시 | notif.upgrade_info.body |
16.4 인앱 메시징 유형
| 유형 | 설명 | 트리거 | UI 형식 |
|---|
| Onboarding Nudge | 알림 권한 요청 전 부드러운 안내 | 첫 성찰 완료 직후 | 하단 시트 + CTA |
| Feature Highlight | 새 기능 소개 | 새 릴리스 후 첫 3세션 | 풀스크린 모달 |
| Paywall Inline Prompt | 잠금 기능 진입 시 업셀 | 관계 카드 클릭 등 | 풀스크린 Paywall |
| Upgrade Celebration | Pro 업그레이드 후 환영 메시지 | iap.purchase_result(success=true) 직후 | 모달 배너 |
| Retention Win-back | 비활성 사용자 복귀 시 | 7일 이상 미로그인 후 재실행 | 배너 메시지 |
16.5 UX 설계 원칙
-
맥락적 호출(Contextual Triggers): 단순 “앱에 돌아오세요”가 아닌 진행 중인 여정과 연결.
-
A/B 테스트 가능성: 알림 문구, 시각, 인앱 배너 형식을 실험(→ 18. 실험 장과 연결).
-
취소/해제 용이성: 인앱 메시지에는 “다시 보지 않기” 버튼 제공. 알림은 설정에서 토글 가능.
16.6 동의 UX (Consent UX)
-
앱 설치 → 온보딩 완료 → 첫 성찰 성공
-
인앱 시트:
-
제목: consent.notif.title (“알림으로 더 꾸준히 기록해보세요”)
-
본문: consent.notif.body (“시간을 직접 설정할 수 있으며 언제든 끌 수 있어요”)
-
CTA 버튼: consent.notif.allow (→ iOS 시스템 권한 요청 호출)
-
Secondary CTA: consent.notif.later
16.7 기술 구현 (iOS)
-
프레임워크: UserNotifications (로컬 알림), SwiftUI 뷰 + MVVM
-
스케줄링:
-
오프라인 우선: 네트워크 필요 없음 (단, 업그레이드 안내 알림은 앱 내 로직으로만 제어)
-
인앱 메시징:
16.8 텔레메트리 & 측정 (15장 연계)
-
이벤트 로깅:
-
notif.permission.requested / notif.permission.granted
-
notif.scheduled (props: type, ts)
-
notif.tapped (props: type, delivered_ts, latency)
-
inapp.shown (props: type, screen_id)
-
inapp.cta_clicked (props: type, action=upgrade|dismiss|later)
-
지표
16.9 빈도 & 지능형 제어
16.10 개인정보 보호 고려사항
-
알림/인앱 메시지 내용은 일반적 톤으로만 (예: “당신의 기록” → O, “오늘 화났다고 쓴 글 다시 읽어보세요” → X)
-
알림 전달 여부 자체는 개인 식별 불가 이벤트(notif.scheduled/notif.tapped)로만 기록
-
사용자 거부/설정 변경은 즉시 반영 (다음 알림부터 중단)
16.11 향후 확장 (Roadmap)
-
WidgetKit: 홈 화면 위젯으로 성찰 리마인더 확장
-
Live Activity: Draft 생성 중 실시간 진행률 표시
-
Cross-platform (Optional): 현재는 iOS 전용. 향후 macOS 버전 고려 가능
17.수익화&가격정책 (Monetization & Pricing)
17.1 수익 철학 & 전략 요약
-
가치 증명형 Freemium: 첫 세션에서 “와, 이거 된다”를 느끼게 한 뒤, 고통-해결의 확신이 생긴 시점에 자연스럽게 결제.
-
광고 없음: 신뢰·몰입 최우선. 사용자의 감정 데이터를 상품화하지 않음.
-
오프라인 우선: 핵심 가치는 온디바이스. 결제 권한만 StoreKit 2로 관리.
-
단일 핵심 SKU(평생권) + 선택적 확장 SKU(콘텐츠 팩): 헷갈리지 않게 단순화하되, 고가치 유저에게는 추가 업셀 포인트 제공.
17.2 오퍼 구조 (Offer Structure)
17.2.1 무료(Free)
-
사용할 수 있는 것:
-
잠금(🔒) 표시: 배우자 또는 연인, 부모 또는 자녀, 형제 또는 자매, 시댁 또는 처가 4개 관계 카드, 전문가 라이브러리, 고급 노트
17.2.2 Pro (Lifetime – 1회성 결제)
17.2.3 선택적 확장 SKU(옵션, v1.1+)
출시 4~8주 뒤 ARPPU 상승을 위한 2차 업셀 포인트로 오픈
17.3 Paywall UX 설계 (윤리·전환 최적화)
-
노출 타이밍(권장 시나리오 3단계)
-
코어 가치 체험 직후: 첫 편지 초안이 화면에 나타난 순간 → 상단에 “Pro에서 모든 관계 열기” 미니 배너(비침습)
-
잠금 기능 진입 시: Pro 관계 카드 탭 → 풀스크린 Paywall
-
두 번째 성공 경험 후: 2번째 편지 공유 완료 직후, 축하 모달 → Pro 혜택 제안
-
페이월 핵심 요소
-
사회적 증거(키 메시지): paywall.hero.title, paywall.hero.subtitle
-
혜택 리스트(3~5개): 체크아이콘 + 한 줄 가치 키 paywall.benefit.*
-
가격/정책: “1회 결제, 평생 이용”, paywall.price_note.lifetime
-
행동 버튼(권장 2개): 구매, 나중에
-
보조링크: 구매 복원
-
다크패턴 금지: 타이머·가짜 할인가·과장 표현 금지
현지화 키 예시
-
paywall.hero.title, paywall.benefit.all_relations, paywall.benefit.library, paywall.benefit.advanced_notes, paywall.cta.buy, paywall.cta.later, paywall.cta.restore, paywall.price_note.lifetime
17.4 상품 정책 (App Store 가이드 준수)
-
비소모성 IAP (Lifetime) + Family Sharing 지원(선택)
-
Refund: Apple 정책 고지(인앱 어뷰징 방지 문구는 비노출)
-
구매 복원: Settings → settings.restore_purchases
-
구매 전 대가리뷰 유도 금지
17.5 가격 정책 & 실험 (Tiering & Tests)
-
초기 권장가: ₩12,900 (한국) / $9.99 (글로벌 레퍼런스)
-
앵커링 카드(페이월): “평생 이용(1회 결제) vs 매달 고민 Zero”
-
가격 밴드 실험(AB)
-
콘텐츠 팩: 1,900 / 2,900 / 3,900
-
프로모션
측정 지표: Paywall 뷰율 → 결제 클릭률 → 결제 성공률 → 환불률, ARPPU, LTV, 지역별 전환
17.6 기능 게이팅 매트릭스 (Free vs Pro)
| 기능 | Free | Pro |
|---|
| 관계 유형 | 1종(친구 또는 동료) | 5종 전체 |
| AI 초안 | 1일 10회 | 1일 20회 |
| 전문가 라이브러리 | 미리보기 3개 | 전체 |
| 마음 노트 고급 | 미지원 | 감정 통계/리마인더 커스텀 |
| 링크 공유 | 기본(만료 7일) | 3/7/30일 선택 |
17.7 StoreKit 2 권한·영수증(온디바이스) 설계
현지화 키(결제 상태)
17.8 업셀 트리거 & 윤리 가드레일
-
권장 트리거
-
Pro 관계 카드 탭
-
2회 이상 성공적 편지 공유 후
-
전문가 라이브러리 3개 이상 열람 시
-
차단 조건
-
24h 내 2회 이상 Paywall 금지
-
알림/인앱 메시지 중복 노출 방지
17.9 텔레메트리 이벤트 & KPI (15장 연계)
핵심 이벤트
-
paywall.viewed (props: source=card|share_success|library_lock, country, price_tier)
-
iap.purchase_tap (props: product_id)
-
iap.purchase_result (props: product_id, result=success|fail|cancel, duration_ms, reason_if_fail)
-
entitlement.changed (props: to=pro|free, cause=purchase|restore|revoke)
-
content_pack.purchase_result
-
share.completed → 업셀 경로 분석용
KPI
17.10 App Store 메타 & 외부 성장
-
스토어 스크린샷: “1회 결제, 평생 이용” 명확 표기(지역별 현지화)
-
인앱 이벤트 카드: “사과 편지 주간”, “명절 대화 팩 출시”
-
추천 키워드: 관계 회복, 사과 편지, NVC, 연인/가족 대화
17.11 수익 예측(샘플 모델 – 내부 참고)
-
가정: 월 신규 10K 설치, Paywall 노출 60%, 전환 3.5%, 가격 ₩12,900
-
월 결제건 ≈ 10,000 × 0.60 × 0.035 = 210건
-
매출(총) ≈ 210 × 12,900 = ₩2,709,000
-
Apple 수수료(가정 15%) 차감 후 순매출 ≈ ₩2.30M
실제는 지역·세율·환율에 영향 → AB 테스트로 전환 최적화 + 스토어 추천 노출로 탄력 확장
17.12 리스크 & 완화
-
가격 민감도: 초기 반응 저조 시 9,900 테스트 롤백 경로 사전 정의
-
환불 증가: 페이월 설명 명확화, 기대치 관리(기능 비교표 노출)
-
복제 앱 출현: 브랜드/도메인(KindVerb) 자산 강화, 고유 라이브러리 업데이트 주기 유지
-
어뷰징: 환불 후 사용 방지(엔타이틀먼트 즉시 해제 로직), 링크 공유는 TTL로 노출 최소화
17.13 수용 기준 (Acceptance Criteria)
-
v1.0: kindverb.pro.lifetime 비소모성 IAP로 결제·복원 100% 동작
-
Paywall 3 트리거 소스 구현: 관계 카드, 공유 완료, 라이브러리 잠금
-
Family Sharing 설정 시 동작 확인(선택)
-
결제 실패/취소/환불 경로 UX 제공(현지화 키 사용)
-
텔레메트리 이벤트(§17.9) 100% 발화 및 스키마 검증
-
알림·인앱 빈도 제한 로직 동작(§16 연계)
17.14 현지화 키 패키지(요약)
17.15 로드맵(수익 확장)
-
D+30~60: 시즌 팩(명절/기념일) 출시 → 콘텐츠 IAP 첫 실험
-
D+90: Pro 사용자 전용 “깊이 학습 코스” 파일럿(향후 구독화 검토)
-
파트너십: 가족상담/부부 교육기관 B2B ‘대량 코드’ 판매(외부 영업 채널)
부록 A) SKU 테이블 (초안)
| Product ID | 유형 | 현지화 이름 키 | 가격(권장) | 설명 키 |
|---|
kindverb.pro.lifetime | 비소모성 | sku.pro_lifetime.title | ₩12,900 | sku.pro_lifetime.desc |
kindverb.pack.season.2025q4 | 비소모성 | sku.season_q4.title | ₩2,900 | sku.season_q4.desc |
kindverb.pack.expert.family | 비소모성 | sku.expert_family.title | ₩3,900 | sku.expert_family.desc |
부록 B) Gherkin(결제 코어 시나리오)
Feature: Pro Lifetime Purchase
Scenario: User unlocks all relations via lifetime purchase
Given the user taps a Pro-locked relation card
When the paywall is shown and the user confirms purchase of "kindverb.pro.lifetime"
Then the purchase succeeds and entitlement becomes "pro"
And the locked relations become accessible immediately
And an "entitlement.changed" event is logged with to=pro
Scenario: Restore purchases on new device
Given the user installs the app on a new device
When the user taps "Restore Purchases"
Then the app verifies transactions and sets entitlement to "pro" if found
And a success toast appears
실행 메모 (iOS 기술 가드레일 재확인)
-
StoreKit 2 + Swift Concurrency(async/await)
-
현대적 Error (throw/try/catch), NSError/CompletionHandler 금지
-
SwiftUI 버튼 형식 강제: Button { } label: { }
-
모든 텍스트 키 참조: Localizable.xcstrings
-
오프라인 우선: 엔타이틀먼트 캐시+서명 검증, 서버 불필요
18.실험(Experimentation - A/B, Feature Flags)
18.1 목적 & 원칙
18.2 실험 타입
-
오프라인 A/B / A/B/n (기본): 디바이스 단위 무작위 배정, 결과는 온디바이스 집계 → TestFlight/내부 QA용 내보내기(수기 분석) + App Store Connect 지표와 교차 확인.
-
점진적 롤아웃(퍼센트 플래그): 신규 UX의 안정성 검증(5% → 25% → 100%).
-
시퀀셜 테스트(내부용): 기간을 분리해 전후 비교(데이터 적을 때 유용).
※ 서버 없는 구조이므로 글로벌 자동 밴딧은 사용하지 않음(디바이스 로컬 밴딧은 제품 전체 최적화에 불리).
18.3 코호트 배정(온디바이스, 결정적 해시)
-
Seed: identifierForVendor(UUID) + experiment_id + app_version.
-
Hash → Bucket: 0–99 버킷.
-
할당 규칙: 예) A=0–49, B=50–99 (50:50).
-
지속성: 배정 결과는 SwiftData에 저장(앱 재설치 전까지 유지).
-
타기팅: 언어/지역/앱버전/Pro 여부 필터.
PRD 키: exp.id, exp.name, exp.variant_keys = ["A","B","C"], exp.target = { region:"KR", lang:"ko|en", appVersion: ">=1.0.0" }, exp.rollout: 0..100.
18.4 기능 플래그(Feature Flags) 스키마
-
정의 위치: 번들 내 Resources/Experiments.json (정적), v1.x는 앱 업데이트로 변경.
-
런타임 오브젝트 예
-
flag.paywall.copy.variant ∈ {A_plain, B_empathy}
-
flag.ai.tone.variant ∈ {calm, concise, warm}
-
flag.share.order ∈ {text_first, image_first}
-
가드레일: 플래그 평가 실패 시 안전 기본값 사용. 크래시/에러 3회↑ 세션에서 자동 Off.
18.5 우선 실험 목록(가설·변형·지표)
E-01 페이월 카피 톤
-
가설: 공감형 카피가 일반형 대비 결제 전환↑.
-
변형:
-
A 일반형: paywall.hero.title=A1.title, subtitle=A1.subtitle
-
B 공감형: paywall.hero.title=B1.title, subtitle=B1.subtitle
-
대상: KR, 앱 v1.0+, Free 유저.
-
1차 지표: iap.purchase_result=success / paywall.viewed (전환율).
-
가드레일: paywall.dismiss_rate, session.exit_after_paywall 증가 금지.
E-02 페이월 노출 타이밍
-
가설: “첫 편지 초안 표시 직후 미니 배너”가 잠금 화면 직행 대비 반감 없이 거부감↓.
-
변형:
-
1차 지표: iap.purchase_result 전환, 보조: paywall.viewed/세션, D1 리텐션.
E-03 가격 밴드 (KR)
-
가설: ₩12,900이 ₩9,900·₩15,000 대비 매출/전환 균형 최적.
-
변형: 9,900 / 12,900 / 15,000 (각 33%).
-
1차 지표: ARPPU, 전환율, 환불률.
E-04 AI 초안 톤
-
가설: warm 톤이 공유 완료율↑.
-
변형: calm vs concise vs warm (초안 내부 문체만 다름).
-
1차 지표: share.completed / message_drafted.
-
가드레일: 편지 길이 과도 증가로 TTI↑ 금지.
E-05 공유 버튼 순서
-
가설: image_first가 저장/재열람 증가로 관계 개선 체감↑.
-
변형: text_first vs image_first.
-
1차 지표: share.image_saved, 보조: 재방문률(D7).
E-06 감정 선택 UI 레이아웃
-
가설: 그리드+검색이 스크롤 리스트 대비 선택 완료 시간↓, 이탈↓.
-
변형: list vs grid_search.
-
1차 지표: journal.step2.time_to_complete, step2.abandon_rate.
모든 실험은 동시에 최대 2개만 노출(간섭 최소화).
18.6 측정 & 분석(§15 이벤트 매핑)
-
필수 이벤트(요약):
-
experiment.assigned(id, variant, seed)
-
paywall.viewed / iap.purchase_result / entitlement.changed
-
message.drafted / share.completed / share.image_saved
-
journal.step2.completed / journal.step2.abandoned
-
세션/유저 식별: 온디바이스 임의키(PII 없음).
-
집계: SwiftData에 일/주 단위 롤업 스냅샷.
-
내보내기: 설정>개발자 메뉴에서 CSV 내보내기(테스트/리뷰용).
-
외부 벤치마크: App Store Connect 전환/리텐션과 교차확인.
18.7 윤리·컴플라이언스 가드레일
-
금지: 가짜 희소성·타이머·숨은 가격.
-
표시: 가격·평생권·복원 경로 명확 표기(§17 키 사용).
-
개인정보: 실험 배정·로그에 PII 저장 금지, 네트워크 전송 없음.
-
접근성: 각 변형 모두 A11y 준수(폰트/대비/VO 레이블).
18.8 운영 절차(End-to-End)
-
설계: 가설-지표-기간-표본 크기 정의(최소 7일, 주말 포함).
-
구현: Experiments.json 정의→플래그/배정 로직 연결→키 텍스트 추가(Localizable.xcstrings).
-
QA: 디버그 화면에서 강제 배정/변형 미리보기, 로깅 확인.
-
롤아웃: 5% 퍼센트 플래그로 크래시/에러 모니터→25%→100%.
-
판정: 전환(주지표) + 가드레일 지표 동시 충족 시 승자 승격.
-
정리: 승자 변형을 기본값으로 고정, 실패 실험 플래그 제거.
18.9 수용 기준(AC)
-
Experiments.json 스키마/유효성 검사 통과(없는 키 참조 시 앱이 기본값으로 안전 복귀).
-
코호트 배정이 결정적이며 앱 재실행/재부팅 후에도 변하지 않음.
-
디버그 메뉴에서 실험 강제 배정/해제 가능(내부 테스트용).
-
§15 이벤트가 실험 속성(experiment_id, variant)을 포함해 100% 발화.
-
접근성·현지화 키 누락 없음(A/B 모든 화면).
-
퍼센트 플래그(5→25→100) 시 크래시/에러 증가 없음.
18.10 현지화 키(예시)
-
exp.paywall.copy.A1.title, exp.paywall.copy.A1.subtitle
-
exp.paywall.copy.B1.title, exp.paywall.copy.B1.subtitle
-
exp.ai.tone.calm.label, exp.ai.tone.concise.label, exp.ai.tone.warm.label
-
exp.share.order.text_first, exp.share.order.image_first
18.11 Gherkin 시나리오(샘플)
Feature: On-device AB assignment
Scenario: Deterministic cohort on same device
Given a device with identifierForVendor = X
And experiment "exp_paywall_tone_v1" is active with split A:50 B:50
When the user starts the app
Then the app assigns a variant deterministically for this device
And the same variant is used across sessions
Feature: Paywall timing experiment
Scenario: Mini banner vs full screen
Given the user is Free and completes AI draft
When experiment "exp_paywall_timing_v1" assigns variant B
Then a mini banner is shown instead of immediate full-screen paywall
And analytics "paywall.viewed" includes experiment metadata
18.12 리스크 & 완화
-
표본 부족: 기간 연장 또는 시퀀셜 테스트로 전환.
-
변형 교차 간섭: 동일 세그먼트에 동시 노출 2개 이하로 제한.
-
현지화 누락: 실험용 문구도 반드시 키 사용(런타임 검증).
-
버전 파편화: 실험은 최신 버전 한정으로 운영.
19.수용기준&테스트계획 (Acceptance Criteria & Test Plan + Converage Mapping)
19.1 목적
-
명확한 품질 기준: 기능이 "완료"되었음을 판단하는 측정 가능한 조건 정의.
-
테스트 구조화: 기능별 테스트 케이스와 커버리지 매핑을 통해 QA가 누락 없이 점검.
-
자동화 기반: 가능한 범위는 XCTest + async/await으로 자동화, 나머지는 QA 매뉴얼 체크리스트.
19.2 수용기준 (Acceptance Criteria)
공통 AC
-
앱은 SwiftUI + MVVM + Swift Concurrency(async/await) 원칙으로 작성되어야 한다.
-
모든 사용자 노출 텍스트는 Localizable.xcstrings 키를 통해 참조된다.
-
모든 네트워크/AI 호출은 try/throw/catch 기반의 현대적 에러 처리를 따른다.
-
print() 사용 금지, 로그는 os.log를 통해 기록된다.
-
오프라인 모드에서도 핵심 루프(성찰 기록 → AI 초안 → 저장)는 완전하게 동작한다.
-
Firestore 연동은 링크 공유 시, TTL 7일 한정으로 동작한다.
-
Gemini API 호출은 Secrets.plist의 GeminiAPIKey를 통해 안전하게 인증된다.
기능별 AC (예시)
저널링(성찰 기록)
-
사용자는 감정·상황·상처·자기성찰·욕구 단계를 순서대로 기록할 수 있다.
-
각 단계는 건너뛰기 불가, 진행률 표시 UI가 작동한다.
-
데이터는 SwiftData에 영구 저장되며, 앱 재실행 후에도 유지된다.
AI 초안 생성
-
사용자가 성찰 완료 후 "AI 초안 받기"를 선택하면 Gemini 2.5 Flash Lite API가 호출된다.
-
AI 응답은 비식별화된 텍스트 기반으로 수신·저장된다.
-
응답 실패 시, 앱은 error.ai.* 현지화 키를 사용해 오류 메시지를 노출한다.
공유 & 페이월
-
Free 유저는 작성 초안을 읽을 수 있지만 공유 시 Pro 페이월을 만난다.
-
Pro 구독 성공 시 즉시 entitlement=pro 로 상태가 변경된다.
-
공유 링크는 Firestore에 저장되며 7일 후 자동 만료된다.
알림 & 인앱 메시징
-
사용자는 성찰 리마인더를 하루 1회 이상 받을 수 있다.
-
알림은 로컬 푸시 기반으로 작동하며 오프라인에서도 예약된다.
-
인앱 배너는 Experiments.json 플래그에 따라 변형이 다르게 나타난다.
19.3 테스트계획 (Test Plan)
1) 단위 테스트 (XCTest)
-
대상: ViewModel 로직, 데이터 모델 직렬화/역직렬화, AI 응답 파서, 에러 매핑.
-
도구: XCTest, XCTExpect (async/await 지원).
-
커버리지 목표: 핵심 비즈니스 로직(감정 → 초안 → 공유) 90% 이상.
2) UI 테스트 (XCUITest)
-
대상: 성찰 플로우, 페이월 전환, 공유 플로우, 알림 수신.
-
도구: XCUITest 스냅샷 비교, 접근성 라벨 확인.
-
커버리지 목표: 주요 사용자 시나리오(Gherkin 기반) 100%.
3) 수동 테스트 (QA)
-
대상: 다국어 현지화, 접근성(VoiceOver/다크모드), 네트워크 끊김 시나리오, iAP 결제.
-
체크리스트: PRD 각 섹션의 Acceptance Criteria와 매핑된 항목.
4) 실험 검증
19.4 커버리지 매핑 (Coverage Mapping)
| 기능 영역 | Acceptance Criteria ID | 단위 테스트 | UI 테스트 | 수동 QA | 자동화율 |
|---|
| 저널링(성찰 기록) | JRN-01~05 | ✅ | ✅ | ✅ | 80% |
| AI 초안 생성 | AI-01~04 | ✅ | ✅ | ✅ | 90% |
| 공유 & 페이월 | PAY-01~06 | ✅ | ✅ | ✅ | 85% |
| 알림 & 인앱 메시징 | NOTI-01~03 | ⚪️ | ✅ | ✅ | 70% |
| 데이터 모델 & 저장소 | DATA-01~04 | ✅ | ⚪️ | ✅ | 95% |
| 보안 & 개인정보 | SEC-01~03 | ✅ | ⚪️ | ✅ | 75% |
19.5 Definition of Done (DoD)
-
모든 Acceptance Criteria 체크박스가 통과해야만 기능은 완료.
-
모든 신규 기능은 단위 테스트 & UI 테스트가 포함되어야만 머지 가능.
-
테스트 실패 시 CI 파이프라인이 빌드 차단.
-
QA 승인 + PO 리뷰 이후에만 출시 가능.
20.위험・가정・의존성 (Risks・Assumptions・Dependencies)
20.1 목적 & 범위
20.2 리스크 분류 & 점수 체계
-
분류: 제품(P), 기술(T), 보안·컴플라이언스(SC), 운영(O), 시장·수익(M)
-
평가: 가능성(L)=Low/Med/High, 영향(I)=Low/Med/High → 점수(Severity)=L×I
-
상태: Open / Mitigating / Closed
-
소유자: 기능적 오너 1명 지정(중복 금지)
20.3 최상위 리스크 레지스터 (Top Risks Register)
| ID | 유형 | 리스크 설명 | L | I | 점수 | 조기 신호(Leading) | 완화책(Mitigation) | 비상대응(Playbook) | 소유자 | 상태 |
|---|
| R-01 | P | 핵심 가치 미전달로 D1/D7 리텐션 미달 | Med | High | High | 1세션 내 AI초안 생성률 < 60%, 온보딩 이탈 > 35% | 온보딩 마찰 제거, 템플릿(오프라인) 즉시 노출, 빈 상태 가이드 강화 | 페이월 노출 타이밍 연기, “즉시 결과 미리보기” 실험을 긴급 상시화 | PM | Open |
| R-02 | M | Pro 전환율 < 목표(3–5%) | Med | High | High | 페이월 노출 대비 결제 시도율 < 8%, 장바구니 이탈↑ | 가격·베네핏 카피 A/B, 3일 내 ‘두 번째 기회’ 페이월, 번들 혜택 추가 | 임시 10% 할인 캠페인(14일), Pro 체험 48h 제공 후 자동락 | Growth | Open |
| R-03 | T | LLM 지연/불가(Gemini 장애/쿼터) | Med | High | High | AI 응답 TTP > 6s, 오류율 > 5% | 오프라인 템플릿 즉시 대체, 재시도 지수백오프, 실패 키 UX | AI 토글 Kill-switch, “오프라인 모드” 배너 및 템플릿로 대체 | iOS Lead | Open |
| R-04 | T | Firestore TTL/한도 문제로 링크 만료/생성 실패 | Low | Med | Med | 링크 생성 실패율 > 1%, TTL 미적용 알림 | 사전 쿼터 알람, TTL 규칙 e2e 테스트, 재시도 큐 | 링크 기능 자동 비활성 + “텍스트 복사/이미지 저장”만 안내 | Backend | Mitigating |
| R-05 | SC | 민감 주제 앱 심사 리젝(정신건강 주장) | Med | High | High | 리젝 사유 4.8/1.1 언급, 메디컬 클레임 감지 | 비치료/비진단 고지, 자기성찰 도구 명시, 위기 리소스 링크 | 메디컬 표현 일괄 치환 스크립트, 앱 설명·스크린샷 즉시 수정 | PM | Open |
| R-06 | SC | 로컬 데이터 유출 위험(분실·탈옥) | Low | High | Med | 외부 백업 앱에서 DB 탐지 | AppTransportSecurity/데이터 보호 속성, Face/Touch ID 잠금 옵션 | 긴급 보안 공지, 버그픽스 패치(핫픽스) + 사용법 가이드 | Security | Open |
| R-07 | T | SwiftData 손상/마이그레이션 실패 | Low | High | Med | 크래시 로그에 스키마 오류, 복구 실패 케이스 | 마이그레이션 테스트 매트릭스/샌드박스 백업, 저널 자동 백업 | 복구 마법사(백업→새 스키마 복원), 실패 시 내보내기 안내 | iOS Lead | Open |
| R-08 | O | 릴리스 지연(의존 태스크 슬립) | Med | Med | Med | 번다운 차트 편차 > 25%, 리뷰 지연 > 3일 | 모듈화·특성 플래그, 컷라인 관리, 위험기능 뒤로 밀기 | 기능 플래그 Off로 빌드 고정, Hotfix 창구 분리 | PM | Mitigating |
| R-09 | M | 가격 민감도 과대평가/과소평가 | Med | Med | Med | 환불율 > 5%, 결제 전 이탈↑ | 3단계 가격 테스트(₩9,900/12,900/15,000), 결제 전 가치 슬라이드 | 단기 가격 롤백 + 구매자 보상(콘텐츠 추가/업그레이드) | Growth | Open |
| R-10 | SC | 현지화로 인한 오인/정서적 위해 | Low | Med | Low | CS에 표현 오해 케이스 누적 | 민감 키워드 리뷰, 톤·매뉴얼, 지역별 가이드 | 문제 문구 핫패치(원격 키값 롤백), CS 스크립트 배포 | PM | Open |
| R-11 | T | 알림 권한 거부 → 리텐션 저하 | Med | Med | Med | 첫 24h 알림 허용률 < 35% | 맥락형 프리프롬프트, 가치 설명, 늦은 권한 재요청 UX | 푸시 의존 이벤트를 인앱 배너로 대체, 리마인더는 캘린더 내보내기 | iOS | Open |
| R-12 | T | 접근성(A11y) 미흡로 심사·리텐션 영향 | Low | Med | Low | VoiceOver 이슈 리포트 | 라벨·포커스 체계 테스트, 다크모드 대비 체크 | 이슈 있는 뷰 임시 비노출/간소화 Hotfix | QA | Open |
메모: 모든 리스크는 **실험(18장)**과 **분석(15장)**의 이벤트로 모니터링되어야 합니다. 각 R-ID는 이벤트 속성 risk_id로 태깅하여 대시보드에서 필터링 가능하게 설계합니다.
20.4 가정(Assumptions) & 검증 계획
| ID | 가정(검증 필요 사실) | 근거/의도 | 검증 방법 & 마감 | 위반 시 대안 |
|---|
| A-01 | iOS 16+ 점유율이 90%+ | SwiftUI 최신 구성요소 활용 | TestFlight 디바이스 분포 체크 (T-7) | 최소 타깃 iOS 15로 하향 고려(기능 일부 폴백) |
| A-02 | 오프라인 우선으로도 핵심 루프 완결 가능 | 프라이버시·속도 | “네트워크 끊김” 시나리오 QA 체크리스트 | 템플릿 품질 보강 + 오프라인 편집 UX 강화 |
| A-03 | ₩12,900 가격의 수용성 | 동종군 가격대 | A/B(₩9,900/12,900/15,000) 2주 | 수익/전환 최적 포인트로 즉시 조정 |
| A-04 | Firestore TTL 7일이 일관 동작 | 문서 스펙 | 스테이징에서 일 단위 TTL 삭제 확인 | 앱 단에서 만료 태그 + 클라이언트 차단 로직 |
| A-05 | Gemini 2.5 Flash Lite 안정성 | 벤치/문서 | p95 응답 < 5s 모니터 | 오프라인 템플릿 자동대체 + 큐 재시도 |
| A-06 | 심사 가이드라인 준수 시 승인 | 사례/가이드 | 프리심사 리뷰(문구 체크리스트) | 메디컬·치료 표현 일괄 수정 스크립트 가동 |
| A-07 | 로깅은 os.log만으로 충분 | 운영 정책 | 에러/성능지표 수집 확인 | 추가 계측이 필요하면 Privacy-preserving counters 적용 |
20.5 의존성(Dependencies)
외부 의존성
-
Apple 생태계: App Store 심사/StoreKit(영수증 검증), UserNotifications, ATS 정책
-
Gemini API: 모델 gemini-2.5-flash-lite, Secrets.plist → “GeminiAPIKey”
-
Firebase Firestore: letters 컬렉션, TTL 7일, 무료 쿼터·한도
-
도메인/웹: kindverb.com/letter/<id> 렌더(광고/추적 없음), TLS
내부 의존성
-
아키텍처 가드레일: SwiftUI + MVVM, async/await, modern Error, 버튼/현지화 규칙 강제
-
리소스 체계: Localizable.xcstrings 키 스키마 / 접근성 라벨 키
-
실험/플래그: Experiments.json 원격 플래그 → 안전한 기본값 필요
-
관찰성: os.log 채널/카테고리 표준화, 크래시 수집(예: PLCrashReporter/Apple 자체)
20.6 비상대응(Contingency) 플레이북
시나리오 S-01: LLM 장애/지연
-
즉시 조치: AI 버튼 비활성 + “오프라인 템플릿” 자동 노출, 배너 공지(현지화 키)
-
대체 경로: 템플릿→초안 편집→공유(텍스트/이미지)
-
복구 후: 장애 시간 알림 & “추가 템플릿” 보상 제공
시나리오 S-02: Firestore 문제(링크 생성 실패/TTL 불가)
시나리오 S-03: 심사 리젝(표현/포지셔닝 문제)
시나리오 S-04: 데이터 마이그레이션 실패
-
즉시 조치: 읽기 전용 세이프 모드, 백업→복원 마법사
-
사용자 선택: 내보내기(plaintext) 제공
-
후속: 장애 케이스 수집 후 마이그레이션 스크립트 보강
20.7 리스크 모니터링 지표 & 임계치(알람 기준)
| 지표 | 임계치(경고/치명) | 소스 | 액션 |
|---|
| AI p95 응답시간 | 4s / 6s | 성능 로그 | 오프라인 템플릿 fallback 토글 |
| AI 실패율 | 3% / 5% | 에러 로그 | 재시도 파라미터 상향 + 배너 공지 |
| 링크 생성 실패율 | 0.5% / 1% | 기능 이벤트 | 링크 비활성 + 대체 안내 |
| D1 Retention | 30% / 25% | 분석 | 온보딩·빈상태 가설 실험 가속 |
| Pro 전환율 | 3% / 2% | 결제 이벤트 | 가격/카피 A/B 즉시 스케줄 |
| 알림 허용률 | 45% / 35% | 시스템 이벤트 | 권한 프리프롬프트 카피 교체 |
20.8 RAID(리스크·가정·이슈·의존성) 운영 템플릿
| 유형 | ID | 제목 | 상태 | 소유자 | 업데이트(YYYY-MM-DD) | 다음 액션/마감 |
|---|
| Risk | R-03 | LLM 지연 | Mitigating | iOS Lead | 2025-08-21 | Fallback 자동화 점검(08-22) |
| Assump. | A-04 | TTL 7일 | Open | Backend | 2025-08-21 | 스테이징 TTL 삭제 로그 캡처(08-23) |
| Issue | I-02 | 알림 허용률↓ | Open | PM | 2025-08-21 | 권한 카피 A/B 배포(08-24) |
| Depend. | D-01 | StoreKit | Open | iOS Lead | 2025-08-21 | 영수증 재검증 시나리오 테스트(08-25) |
20.9 승인 기준(Exit Criteria)
-
상위 High 점수 리스크(R-01/R-02/R-03/R-05) 완화책 배포 완료 및 지표 안정화(임계치 하회 7일 연속)
-
가정 A-01~A-06 검증 완료 또는 대안 적용
-
핵심 의존성(LLM, Firestore, StoreKit) e2e 테스트 통과 로그 확보
21.출시계획&마일스톤(Release Plan & Milestones)
21.1 전략적 목표
-
**최소 기능 세트(MVP)**를 빠르게 출시하여 D1/D7 리텐션과 첫 결제 전환율을 검증
-
App Store 리뷰 승인을 원활히 통과 → 심사 리스크 최소화
-
**수익화 실험(A/B)**을 조기 삽입하여 가격·전환 데이터 확보
-
90일 내 안정적 운영 체계 완성 (AI 프록시, 오프라인 데이터 무결성, 결제·심사 대응 자동화)
21.2 단계별 계획
🟢 Phase 1 — 준비 단계 (T-60 ~ T-30)
-
개발
-
SwiftUI + MVVM 뼈대 구현 (온보딩, 홈, AI Draft 화면)
-
SwiftData 모델 안정화 (로컬 영구 저장)
-
Firestore TTL 7일 링크 공유 기본 기능
-
Secrets.plist → GeminiAPIKey 연동 (LLM 프록시)
-
디자인
-
로고/아이콘/App Store 스크린샷 제작
-
접근성(A11y) 컬러 대비 테스트
-
운영
📌 마일스톤 M1 (T-30)
-
MVP 빌드(App Flow 70% 완료) TestFlight 내부 배포
-
주요 기능: 감정 기록, AI 초안, 공유 링크, 페이월 기본 동작
🟡 Phase 2 — 알파 & 베타 (T-30 ~ T-14)
-
알파 테스트 (내부 10명)
-
크래시/데이터 손실/AI 응답 속도 QA
-
오프라인 모드 UX 시나리오 점검
-
베타 테스트 (TestFlight 50명)
-
실제 사용자 데이터 기반 리텐션 확인 (D1, D7)
-
결제 UX 검증 (₩9,900 / ₩12,900 / ₩15,000 A/B)
-
운영
-
GDPR/개인정보 문서 현지화 완료
-
심사 대비 “비의료 자기성찰” 가이드 정리
📌 마일스톤 M2 (T-14)
-
외부 베타 런칭 (TestFlight)
-
수익화 전환율 초기 데이터 확보
🟠 Phase 3 — 심사 & 출시 준비 (T-14 ~ T-0)
-
기술
-
앱 사이즈 최적화 (< 50MB 목표)
-
성능 튜닝 (p95 응답 < 5s)
-
심사 패키지
-
App Store 메타데이터 (제목, 설명, 스크린샷, 키워드)
-
App Store Description (5.4 항목과 연계)
-
심사 대응용 내부 FAQ (메디컬 표현 차단 강조)
-
마케팅
-
최소 홍보 웹사이트(kindverb.com)
-
SNS 티저 3건 발행
📌 마일스톤 M3 (T-7)
📌 마일스톤 M4 (T-0)
-
공식 출시 (App Store Live)
-
초기 500 다운로드 목표
-
결제 전환율/리텐션 리얼타임 모니터링
🔵 Phase 4 — 안정화 & 성장 (T+1일 ~ T+90일)
📌 마일스톤 M5 (T+30)
📌 마일스톤 M6 (T+90)
21.3 주요 KPI 타임라인
-
T+7: D1 Retention ≥ 35%, 결제 시도율 ≥ 5%
-
T+30: Pro 전환율 ≥ 3%, 환불율 ≤ 3%
-
T+90: 누적 매출 ≥ ₩10,000,000, D30 Retention ≥ 20%
21.4 실행 원칙
-
출시일 고정 → 기능 플래그 조정 (컷라인 관리)
-
심사 리스크 대비 → 메디컬 표현 제거 자동화
-
수익화 조기 검증 → 결제·가격 A/B 빠르게
-
오프라인 완결 UX 확보 → LLM 장애와 무관하게 사용 가능
22.범위 외 항목 (Out of Scope)
22.1 이번 릴리스에서 제외되는 기능
-
안드로이드/웹 지원
-
실시간 채팅/대화형 커뮤니티 기능
-
고급 AI 생성 (심리 상담·의료 진단 수준)
-
서드파티 앱 직접 연동 (예: 캘린더, 헬스킷, 웨어러블)
-
Apple HealthKit, Google Fit, 캘린더 연동 등 외부 생태계 통합은 제외.
-
MVP에서는 오직 로컬 기록 + Firestore 공유 링크만 지원.
-
완전한 다국어 지원
-
고급 데이터 분석 대시보드
22.2 의도적으로 제외한 UX 영역
22.3 수익화 관련 제외 범위
22.4 기술적 범위 외
-
서버사이드 사용자 데이터 보관
-
백엔드 AI 모델 직접 호스팅
22.5 정리
-
Out of Scope 선언은 단순히 “제외”가 아니라, 우선순위와 전략적 선택을 의미합니다.
-
이렇게 해두면 투자자, 팀원, AI 모델 모두 **“이번에 하지 않을 일”**을 명확히 이해하여 스코프 크리프(scope creep) 를 방지합니다.
23. 용어집 (Glossary)
| 용어 | 정의 | 비고 |
|---|
| KindVerb | 앱의 정식 명칭. “친절한 말하기”를 돕는 iOS 전용 앱. | 코드/문서에서 프로젝트명으로 사용. |
| MVP (Minimum Viable Product) | 최소 기능 제품. 빠른 출시와 학습을 위해 꼭 필요한 기능만 담은 첫 버전. | 1.0 출시 목표. |
| SwiftUI | Apple의 선언형 UI 프레임워크. 모든 화면(View)은 SwiftUI로 작성. | UIKit 사용 금지. |
| MVVM (Model-View-ViewModel) | 앱 아키텍처 패턴. View ↔ ViewModel ↔ Model 레이어 구분. | iOS Best Practice 강제. |
| Swift Concurrency | async/await 기반의 최신 비동기 처리 방식. | completion handler 금지. |
| Error Handling | NSError 대신 Swift Error 프로토콜 기반의 throw/try/catch. | 표준화된 도메인 에러 키 사용. |
| Localization (현지화) | 모든 사용자 노출 텍스트는 Localizable.xcstrings 키 기반. | 기본 언어: 영어. |
| Telemetry (텔레메트리) | 앱 내 이벤트/지표 로깅. 사용자 행동/성능 지표 수집. | Analytics와 함께 사용. |
| SLO (Service Level Objective) | 성능 및 안정성 목표치. 예: 앱 실행 ≤ 2초. | QA와 출시 심사 기준. |
| Offline-First | 네트워크 연결 여부와 무관하게 앱이 정상 작동하도록 설계. | SwiftData 로컬 저장 필수. |
| Firestore (TTL 7일) | 구글 Firebase Firestore. KindVerb에서는 **공유 링크 저장소(익명)**로만 사용. | 7일 후 자동 만료. |
| LLM (Large Language Model) | 대규모 언어 모델. KindVerb에서는 Gemini 2.5 Flash-Lite를 사용. | 프록시 계약 통해 익명 호출. |
| Secrets.plist | 민감 API 키 저장용 설정 파일. Git에 포함되지 않음. | GeminiAPIKey 필수. |
| Button Format Rule | SwiftUI 버튼 작성 규칙: Button { /* action */ } label: { /* view */ } | 다른 형식 금지. |
| JTBD (Jobs To Be Done) | “When ___, I want to ___, so I can ___” 형태의 사용자 과업 정의. | 항상 영문으로 유지. |
| App Flow (앱플로우) | 앱 화면 간 이동/전환 경로 정의. | 정보구조(IA)와 연결. |
| Scope Creep | 계획되지 않은 기능이 개발 범위에 추가되는 현상. | Out of Scope 선언으로 방지. |
| In-App Purchase (IAP) | 앱 내 결제 기능. 구독/일회성 결제 포함. | KindVerb의 핵심 수익화 모델. |
| Feature Flags | 기능의 On/Off를 제어하는 스위치. 점진적 출시(A/B 실험)에 활용. | 빠른 롤백 가능. |
| Acceptance Criteria (수용 기준) | 특정 기능이 완료되었음을 판단할 수 있는 구체적 문장. | QA 및 테스트 기준. |
| Milestones (마일스톤) | 프로젝트 주요 시점/단계별 목표. | Alpha → Beta → Launch. |
📌 특징
-
기술 용어 + 비즈니스 용어를 모두 포함 → 개발자·기획자·마케터 모두 같은 언어 사용 가능.
-
AI가 헷갈리기 쉬운 규칙(예: 버튼 형식, 현지화, 에러 처리)은 강조해서 명시.
-
“모호성 0%” 원칙에 따라, 단어 하나로도 팀 전체가 같은 이미지를 공유할 수 있도록 설계.
24.AI프롬프트 엔벨로프 (AI Prompt Envelope - GPT5 Optimized Block)
24.1 목적
-
ChatGPT/GPT-5, Claude, Gemini 등 생성형 AI가 바로 개발 산출물(코드, 설계, 테스트)을 생성할 수 있도록 AI 친화적으로 패키징된 지침.
-
**모호성 0%**를 목표로, KindVerb 프로젝트의 개발 규칙, 아키텍처, 현지화, 버튼 규칙 등을 반드시 준수하게 강제.
-
AI가 빠뜨리기 쉬운 부분(버튼 형식, 문자열 현지화, 에러 처리 방식)을 시스템 레벨에서 차단.
24.2 System Instruction (AI에게 전달할 시스템 역할)
You are an expert iOS engineer.
Generate SwiftUI + MVVM code using Swift Concurrency (async/await) only.
Do not use completion handlers. Do not use NSError.
Use Swift Error protocol with throw/try/catch for error handling.
All user-facing strings must use localization keys from Localizable.xcstrings.
In code, reference keys only (e.g., Text("home.title")).
All Buttons must be written strictly as:
Button { /* action */ } label: { /* view */ }.
Never use Button("label") { ... } form.
Adopt Offline-first principle: All user data must persist on-device using SwiftData.
Cloud communication allowed only for link sharing (Firestore TTL=7 days).
When AI drafting is needed, use gemini-2.5-flash-lite model via Secrets.plist with key "GeminiAPIKey".
Logging must use os.log (unified logging), never print().
Provide production-ready, modular code, with folder structure and per-file explanations.
Ask clarification questions before coding if any PRD detail is missing.
24.3 Inputs (AI가 받을 입력)
24.4 Outputs (AI가 반드시 생성해야 할 결과물)
-
Source Code
-
SwiftUI + MVVM 구조의 화면/컴포넌트
-
UseCase / Repository / Service 계층 분리
-
async/await 기반 네트워크·스토리지 연동
-
Swift Error 처리 포함
-
한 줄 한 줄 주석 (초보자도 이해할 수 있도록 Why + How 설명)
-
Folder Tree
-
프로젝트 구조도 (예: /View, /ViewModel, /UseCase, /Repository, /Service, /Resources)
-
각 폴더/파일 역할 설명
-
Unit Tests
-
ViewModel/UseCase 단위 테스트
-
Mock Repository 활용
-
에러 처리 케이스 포함
-
App Flow Glue
24.5 Example Prompt Block (AI 호출 시 붙여넣기용)
SYSTEM / INSTRUCTION
[위의 System Instruction 블록 붙여넣기]
INPUTS
- Full PRD (sections 1–26)
- Design tokens (Section 8)
- Data models & API schemas (Sections 10–11)
OUTPUTS
- SwiftUI + MVVM code (with async/await)
- Full folder tree & explanations
- Unit tests (ViewModel, UseCase)
- All code must follow: Button rule, Localization rule, Error rule
24.6 왜 중요한가? (JOHS 관점에서)
-
AI가 실수하기 쉬운 부분을 강제 → 코드 검수 시간이 줄어듦.
-
사람/AI 모두 같은 개발 규칙 기반으로 움직임 → 협업 효율 극대화.
-
코드 품질 균일화 → 빠른 출시 후에도 유지보수가 용이.
-
투자자/언론 대응 시 “우린 AI 기반 자동개발 파이프라인을 구축했다” 라는 강력한 메시지 전달 가능.
25.핸드오프 템플릿 (Handoffs Templates)
25.1 Backend Structure Document (백엔드 구조 문서)
목적: KindVerb 앱이 의존하는 서버 API를 팀이 일관되게 구현/확장할 수 있도록 표준 구조 제공
템플릿 구조 예시
-
아키텍처: (예: BFF + Clean Architecture, Firebase 기반)
-
서비스 모듈: Auth / User / Journal / Sharing / AI Proxy
-
DB 스키마: 테이블·컬렉션 정의, 인덱스, FK 규칙
-
API 상세:
| Method | Path | Req JSON | Res JSON | Errors(키) | Rate Limit |
|---|
| POST | /v1/journals | { userId, emotion, note } | { id } | error.journal.create_failed | 10/min |
-
관찰성: 로그, 메트릭, 트레이싱 방식
-
보안: 키 관리, 역할 기반 권한, RLS 규칙
25.2 Frontend Guidelines Document (프론트엔드 가이드라인)
목적: SwiftUI + MVVM 기반으로 개발자 전원이 동일한 코드 규칙을 준수하도록 강제
템플릿 구조 예시
-
아키텍처 레이어: View / ViewModel / UseCase / Repository / Service
-
코딩 규칙: async/await 필수, Error 처리 throw/try/catch, Completion/NSError 금지
-
UI 규칙:
-
버튼 형식: 반드시 Button { } label: { }
-
현지화: 모든 Text/Label은 Localizable.xcstrings key 참조
-
디자인 토큰 반영 (Color, Spacing, Radius 등)
-
리소스 규칙: xcassets, xcstrings, SwiftData 모델 정의
-
리뷰 체크리스트: PR마다 확인해야 할 항목 (Localization, Error 처리, Telemetry 등)
25.3 Feature Specification Document (기능 명세서)
목적: PRD의 Feature Spec을 더 상세화하여 에지 케이스 & 복구 전략까지 반영
템플릿 구조 예시
-
기능 ID: F-01
-
문제 정의: (이 기능이 해결하는 문제)
-
정상 플로우: (예: 감정 기록 → 로컬 저장 → 백그라운드 동기화)
-
예외 플로우: 오프라인 시 로컬 큐 저장, 충돌 시 최신 기록 우선
-
입력 검증: JSON Schema 기반
-
에러 처리: 키 정의 (예: error.network.offline)
-
수용 기준: 테스트 가능한 문장으로
25.4 App Flow Document (앱 플로우 문서)
목적: 팀원 모두가 앱의 전체 네비게이션·상태 전환을 한눈에 볼 수 있도록
템플릿 구조 예시
-
화면 ID 목록: (로그인, 홈, 기록 작성, 기록 목록, 공유 화면)
-
네비게이션 구조: 탭/스택/모달 정의
-
상태 다이어그램: 로그인 전/후, 오프라인/온라인
-
권한 플로우: 알림/마이크/위치 허용 → 거부 → 재시도
-
딥링크: 특정 공유 링크 → 앱 내 특정 화면 연결
25.5 Implementation Plan (구현 계획)
목적: 주차별 일정/담당/위험/디펜던시를 명확히 하여 실제 실행 단계에서 혼선을 방지
템플릿 구조 예시
25.6 QA & Test Handoff (QA 전달 문서)
목적: QA가 “무엇을 어떻게 테스트해야 하는지”를 명확히 알 수 있도록
템플릿 구조 예시
-
기능별 수용 기준 → 테스트 케이스 매핑
-
테스트 케이스: ID, 전제, 단계, 기대 결과
-
자동화 범위: 단위 테스트, UI 테스트, Snapshot 테스트
-
버그 리포트 규칙: ID, 심각도, 재현 경로, 로그 첨부
25.7 Marketing & Store Handoff (마케팅/스토어 전달 문서)
목적: 마케팅팀/스토어 운영팀이 앱스토어 등록 및 언론 대응을 할 수 있도록
템플릿 구조 예시
-
App Store Description (5.4)
-
스크린샷 가이드라인: 필수 화면, 해상도, 지역별 텍스트
-
키워드 전략: ASO (검색 최적화 키워드)
-
출시 알림 플로우: 이메일/푸시/소셜 미디어
-
언론 Q&A: “왜 KindVerb인가?”, “차별점은 무엇인가?”
26.PRD 품질 체크리스트 (AI & Human)
26.1 Human Review Checklist (인간 검수용)
| 카테고리 | 질문 | 체크 여부 |
|---|
| 목표 | PRD의 문제 정의와 목표가 명확히 구분되어 있는가? | ☐ |
| 범위 | In-Scope / Out-of-Scope가 구체적으로 작성되어 있는가? | ☐ |
| 사용자 | Personas와 JTBD가 현실성 있고 구체적으로 서술되어 있는가? | ☐ |
| 스토리 | User Story가 감정적으로 설득력 있으며, App Store Description까지 이어졌는가? | ☐ |
| 데이터 모델 | 모든 엔티티(Entity)와 관계(1:N, N:N)가 정의되었는가? | ☐ |
| API | Auth, Endpoints, Error Domain, Rate Limit이 명확히 서술되었는가? | ☐ |
| UI/UX | 모든 사용자 노출 텍스트가 Localization 키 기반으로 작성되었는가? | ☐ |
| 기술 가드레일 | SwiftUI, MVVM, async/await, Error Handling 규칙이 명시되었는가? | ☐ |
| 보안/개인정보 | 암호화, TTL 정책, 로컬 저장, 컴플라이언스 대응이 있는가? | ☐ |
| 수익화 | Pricing 모델과 결제 흐름이 App Store 정책에 맞게 정의되었는가? | ☐ |
| 실험 설계 | A/B 테스트와 Feature Flags 조건이 정의되었는가? | ☐ |
| 출시 계획 | 마일스톤과 QA/스토어 핸드오프 플랜이 포함되어 있는가? | ☐ |
26.2 AI Review Checklist (AI 친화성 검수용)
| 카테고리 | 질문 | 체크 여부 |
|---|
| 명확성 | 모든 주요 기능은 Gherkin 문법(Scenario: Given-When-Then) 또는 Acceptance Criteria로 표현되었는가? | ☐ |
| 일관성 | 버튼 작성 규칙, 에러 처리 방식, 현지화 규칙이 문서 전체에서 일관되게 적용되었는가? | ☐ |
| 데이터 | 모든 JSON 예시가 유효한 구조(올바른 key:value)로 제공되는가? | ☐ |
| LLM 지침 | AI Prompt Envelope(24번)이 명확히 정의되어 있는가? | ☐ |
| 오프라인 우선 | SwiftData 저장, Firestore TTL, Sync 정책이 누락 없이 기술되었는가? | ☐ |
| 성능 기준 | SLO(SLOs)와 Quality Bars가 수치형으로 정의되어 있는가? | ☐ |
| 테스트 가능성 | 모든 Acceptance Criteria는 QA 테스트 케이스로 변환 가능하게 작성되었는가? | ☐ |
| 코드 자동 생성 | AI가 PRD만 보고 폴더 트리 + SwiftUI 코드 + 테스트 코드를 자동 생성할 수 있는가? | ☐ |
| 중복성 제거 | 동일 항목이 여러 곳에서 다르게 기술되지 않았는가? | ☐ |
26.3 Meta Review Checklist (최종 메타 검증)
-
PRD는 1~26까지 모든 섹션이 완결되었는가?
-
각 섹션은 “왜(Why)–무엇(What)–어떻게(How)” 순서로 설명되었는가?
-
비즈니스·기술·AI 친화성 세 축을 모두 만족하는가?
-
이 PRD만으로도 새로운 팀원이 추가 설명 없이 개발에 착수할 수 있는가?
-
AI에게 이 PRD를 입력했을 때 코드·테스트·스토어 설명문까지 자동 생성 가능한가?
26.4 활용 방법
-
PRD 최종 작성 후, Human Reviewer(기획자, 개발자, 디자이너, 마케터)가 26.1 체크리스트를 검토
-
AI(GPT-5 또는 Gemini)에게 26.2 체크리스트 기준으로 “PRD 평가”를 요청 → AI가 누락/모호성을 지적
-
PRD 보완 → 다시 Human & AI가 교차 검토 → Meta Review 체크 완료
-
최종 승인 시 **“AI Prompt Envelope”**와 함께 코드 생성 단계로 진입