기준 문서:
docs/planning/steppie_planning_document.md
버전: 0.1.0
범위: iOS SwiftData와 Android Room은 같은 데이터 의미, 기본값, 검증 규칙을 구현한다.
- 영속 저장되는 모든 ID는 UUID v4 문자열을 사용한다.
- 모든 타임스탬프는 timezone offset을 포함한 ISO 8601 문자열을 사용한다.
- 로컬 날짜는
YYYY-MM-DD형식을 사용한다. - 로컬 시간은 24시간제
HH:mm형식을 사용한다. - 화면 표시 텍스트는 플랫폼 리소스 ID가 아니라
LocalizedText로 저장한다. - 진행 기록에 남을 수 있는 사용자 생성 데이터는 물리 삭제보다 소프트 삭제를 우선한다.
- 양 플랫폼에 영향을 주는 데이터 변경은 구현보다 이 문서를 먼저 수정한다.
| 계약 타입 | iOS | Android | JSON |
|---|---|---|---|
| UUID | UUID |
String 또는 UUID wrapper |
string |
| DateTime | Date |
Instant 또는 epoch millis wrapper |
ISO 8601 string |
| LocalDate | DateComponents 또는 string wrapper |
LocalDate 또는 string wrapper |
YYYY-MM-DD |
| LocalTime | string wrapper | string wrapper | HH:mm |
| Bool | Bool |
Boolean |
boolean |
| Int | Int |
Int |
number |
| String | String |
String |
string |
RoutineSet은 순서가 있는 루틴 카드 묶음이다. 초기 설정 이후에는 활성 RoutineSet이 정확히 하나 있어야 한다.
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
id |
UUID | 예 | generated | 백업/복원 후에도 유지되는 ID |
name |
LocalizedText | 예 | 없음 | 사용자에게 보이는 루틴 세트 이름 |
isActive |
Bool | 예 | false |
활성 세트는 하나만 허용 |
createdAt |
DateTime | 예 | now | 생성 시각 |
updatedAt |
DateTime | 예 | now | 하위 루틴 순서/내용 변경 시 갱신 |
deletedAt |
DateTime? | 아니오 | null |
소프트 삭제 표시 |
검증 규칙:
- 활성 RoutineSet은 소프트 삭제될 수 없다.
- 빈 RoutineSet은 생성/편집 중에만 허용한다.
- 사용자에게 보이는 목록에서는
deletedAt != null인 레코드를 숨긴다.
Routine은 아이 모드에 표시되는 단일 활동 카드다.
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
id |
UUID | 예 | generated | 안정적인 루틴 ID |
routineSetId |
UUID | 예 | 없음 | 상위 RoutineSet |
titleKey |
String? | 아니오 | null |
템플릿 기반 루틴의 내장 현지화 키 |
title |
LocalizedText | 예 | 없음 | 사용자가 수정할 수 있는 제목 |
icon |
IconRef | 예 | default icon | 내장 아이콘 또는 로컬 사진 참조 |
colorToken |
String | 예 | color.card.sky |
디자인 토큰에 존재해야 함 |
order |
Int | 예 | next index | 세트 안의 0 기반 순서 |
scheduledTime |
LocalTime? | 아니오 | null |
선택 예정 시각 |
isActive |
Bool | 예 | true |
비활성 루틴은 아이 흐름에서 숨김 |
createdAt |
DateTime | 예 | now | 생성 시각 |
updatedAt |
DateTime | 예 | now | 마지막 수정 시각 |
deletedAt |
DateTime? | 아니오 | null |
소프트 삭제 표시 |
검증 규칙:
order는 같은 세트의 활성/미삭제 루틴 사이에서 유일해야 한다.title은 최소 하나 이상의 비어 있지 않은 locale 값을 가져야 한다.colorToken은 WCAG AA를 만족하는 카드 색상 조합으로 해석되어야 한다.scheduledTime은 알림 설정이 켜진 경우에만 알림을 생성할 수 있다.
DailyLog는 특정 로컬 날짜의 루틴 상태를 기록한다.
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
id |
UUID | 예 | generated | 기록 행 ID |
date |
LocalDate | 예 | today | 기기 로컬 날짜 |
routineId |
UUID | 예 | 없음 | 완료 당시의 Routine |
routineSetId |
UUID | 예 | 없음 | 상위 세트 스냅샷 키 |
status |
LogStatus | 예 | undone |
completed 또는 undone |
completedAt |
DateTime? | 아니오 | null |
완료 상태일 때 필수 |
createdAt |
DateTime | 예 | now | 행 생성 시각 |
updatedAt |
DateTime | 예 | now | 행 수정 시각 |
검증 규칙:
date + routineId조합당 DailyLog는 최대 하나다.status == completed이면completedAt은 null이 아니어야 한다.status == undone이면completedAt은 null이어야 한다.- 하루 리셋은 UTC 날짜가 아니라 로컬 날짜 기준이다.
AppSettings는 기기 로컬 동작 설정을 저장한다. 플랫폼 전용 권한 상태를 제외하고 백업에 포함한다.
| 필드 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
id |
String | 예 | singleton |
단일 행 |
guardianPinHash |
String | 설정 후 예 | 없음 | 원본 PIN 저장 금지 |
recoveryCodeHash |
String | 설정 후 예 | 없음 | 6자리 복구 코드 해시 |
feedbackIntensity |
FeedbackIntensity | 예 | normal |
strong, normal, quiet, off |
soundEnabled |
Bool | 예 | true |
효과음 |
ttsEnabled |
Bool | 예 | true |
음성 안내 |
ttsRate |
Double | 예 | 1.0 |
범위 0.5...1.5 |
ttsVolume |
Double | 예 | 1.0 |
범위 0.0...1.0 |
hapticEnabled |
Bool | 예 | true |
햅틱/진동 |
undoDurationSeconds |
Int | 예 | 5 |
허용값: 3, 5, 10 |
notificationLeadTimes |
Int[] | 예 | [10, 5] |
예정 시각 전 알림 시간(분) |
quietHoursStart |
LocalTime? | 아니오 | null |
선택 방해 금지 시작 |
quietHoursEnd |
LocalTime? | 아니오 | null |
선택 방해 금지 종료 |
locale |
String? | 아니오 | null |
null이면 시스템 locale 사용 |
createdAt |
DateTime | 예 | now | 생성 시각 |
updatedAt |
DateTime | 예 | now | 수정 시각 |
보안 규칙:
- PIN은 해시 전 항상 정확히 4자리 숫자다.
- 복구 코드는 해시 전 항상 정확히 6자리 숫자다.
- 플랫폼에 적합한 비밀번호 해시 또는 Keychain/Keystore 기반 비밀 저장 방식을 사용한다.
{
"ko": "양치하기",
"en": "Brush teeth"
}규칙:
- Locale 키는 BCP 47 언어 태그를 사용한다.
- 내장 템플릿은
ko와en을 필수로 가진다. - 사용자 생성 커스텀 텍스트는 현재 앱 locale만 저장할 수 있다.
- fallback 순서: 현재 앱 locale -> 시스템 언어 ->
ko-> 첫 번째 사용 가능 값.
{
"type": "builtin",
"name": "toothbrush"
}{
"type": "photo",
"localAssetId": "8A5B6B1A-1C47-4F43-A4A3-8B8A64B56921",
"backupAssetName": "routine-photo-8A5B6B1A.jpg"
}규칙:
type은builtin또는photo다.- 내장 아이콘 이름은
docs/design-tokens.md를 따른다. - 사진 에셋은 로컬 우선으로 동작하며, 가능하면 백업에 포함한다.
| Enum | 값 |
|---|---|
LogStatus |
completed, undone |
FeedbackIntensity |
strong, normal, quiet, off |
Mode |
child, guardian |
다음 값은 원천 데이터로 저장하지 않고 파생한다.
isCompletedToday: 현재 로컬 날짜의 DailyLog에서 계산한다.currentRoutine: 활성 RoutineSet 안에서 오늘 완료되지 않은 첫 번째 활성 루틴.progressCount: 오늘 완료된 활성 루틴 수.progressTotal: 활성 RoutineSet 안의 활성/미삭제 루틴 수.
양 플랫폼은 같은 초기 데이터를 제공해야 한다.
- 내장 템플릿 3종: morning, school, bedtime.
- 출시 전 내장 아이콘 60개 이상.
- 모든 내장 템플릿 루틴에 한국어/영어 라벨 제공.
- 온보딩 중 기본 활성 RoutineSet은 morning 템플릿으로 만들 수 있다.
- 스키마 버전은
1에서 시작한다. - 마이그레이션은 결정적이고 오프라인에서 가능해야 한다.
- 하위 호환 필드는 nullable 필드 또는 기본값을 사용한다.
- breaking schema change는
backup-contract.md를 갱신하고 복원 테스트를 추가해야 한다.