docs(chat): 날짜 구분선 요구사항과 검증을 기록한다

This commit is contained in:
Yu Sung
2026-10-19 10:18:36 +09:00
parent 7a6d12e871
commit b0cabf97f6
20 changed files with 610 additions and 0 deletions
@@ -0,0 +1,255 @@
# 유저-크리에이터 채팅 날짜 구분선 구현 계획
> 상태: 구현 완료 / P1-GATE 실서버·접근성 QA 대기
| 문서 항목 | 내용 |
|---|---|
| 작성일 | `2026-10-16` |
| 요구사항 기준 | [prd.md](prd.md), DATE-001~008 |
| API 기준 | 해당 없음, 기존 메시지 timestamp 활용 |
| 현재 Phase | Phase 1, 구현 완료 / 최종 QA 대기 |
| 현재 활성 Goal | 없음. P1-GATE 외부 조건 대기 |
## 목표
유저-크리에이터 DM에서 표시되는 TEXT 메시지를 열람 기기의 현지 날짜 및 앱 언어로 구분하고, 페이지 병합과 ACK 갱신 후에도 구분이 정확하며 메시지 스크롤과 이전 메시지 로드를 유지한다.
## 현재 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 외부 조건 대기 | `2/2` | `P1-GATE` | 실제 DM 계정·방 데이터와 VoiceOver 수동 검증 필요 |
- 2026-10-16 사용자 구현 요청으로 P1-T1을 시작한다. 완료한 항목만 검증 증거와 함께 갱신한다.
- Goal 실행 체계나 token budget은 설정하지 않는다.
## 범위
### 포함
- 표시 TEXT 메시지 기준 날짜 행 파생, 인접한 현지 날짜 변경 지점과 현재 첫 메시지 위에 날짜 구분 항목 표시.
- 앱 환경 locale에 맞춘 연·월·일·요일 문자열과 기기 시간대 기준 그룹핑.
- 시간대, DST, locale, 과거 페이지 병합, 보류 메시지 상태 스냅샷 검증 및 SwiftUI 접근성 표시.
- `SodaLive`와 `SodaLive-dev` 타깃의 신규 Swift 파일 등록과 두 타깃 빌드, 지정 수동 QA.
### 제외
- 요청 범위 밖 제품 코드·테스트·프로젝트 설정 변경. 이전 문서 전용 제한은 사용자 구현 요청으로 해제했다.
- API 변경, 메시지별 기존 시각 포맷 변경, 언어 서비스 또는 `DateExtension` 변경.
- VOICE 표시, 날짜 고정 헤더, 별도 ViewModel/DTO, 메시지 정렬·중복 제거·ACK 로직 변경, 네트워크 모킹 및 재시도 UI.
- 사용자에게 확정받지 않은 별도 디자인 시스템이나 픽셀 사양 확장. 구체 스타일은 기존 앱 디자인 관례를 따르는 구현 판단이다.
## 기술적 제약과 구조
- 기술 스택: iOS 16.6 이상, Swift language version 5.0, Foundation, SwiftUI, 기존 `work/tests`의 `swiftc` 기반 계약 검사 패턴. 테스트 타깃이나 외부 의존성은 추가하지 않는다.
- locale은 RoomView의 `@Environment(\.locale)`에서 소비한다. 앱 루트의 `LanguageContainer.environment.locale` 주입을 이용하며 언어 매핑을 복제하거나 `Locale.current`를 새 날짜 구현에 사용하지 않는다.
- RoomView는 `@Environment(\.timeZone)`을 사용한다. 그룹핑과 포맷 양쪽에서 전달된 `Calendar`의 사본을 지정 timeZone으로 정규화한다.
- formatter는 호출마다 설정된 `DateFormatter`를 쓰고 locale, calendar, timeZone을 적용한 뒤 `setLocalizedDateFormatFromTemplate("yMMMMdEEEE")`를 호출한다. 상대 날짜, 하드코딩된 언어 조각, 공유 mutable cache를 추가하지 않는다.
- 행은 현재 표시 목록에서 파생한다. TEXT만 필터링한 뒤 원래 순서와 ID를 유지하고, `Date(timeIntervalSince1970: TimeInterval(createdAt) / 1000)`으로 표시 메시지마다 한 번 변환한다. 정렬, 별도 날짜 섹션, 상태 저장은 하지 않는다.
- 첫 표시 행은 항상 날짜를 갖고, 후속 행은 직전 표시 TEXT와 `Calendar.isDate(_:inSameDayAs:)`가 false일 때만 날짜를 갖는다. 빈 입력에는 행이 없다.
- 확인된 앱 파일 책임: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatModels.swift`, `Models/UserCreatorChatDisplayMessage.swift`, `UserCreatorChatRoomView.swift`, `UserCreatorChatRoomViewModel.swift`, 기존 `Components/UserCreatorChatTextMessageItemView.swift`. 루트 locale 주입은 `SodaLive/Sources/App/SodaLiveApp.swift`에서 확인한다.
### 집중 검증 항목
| 위험 입력·조건 | 확인할 동작 | 담당 검증 |
|---|---|---|
| 앱 언어와 기기 지역 불일치 | 언어는 앱 locale, 날짜 경계는 기기 timeZone | P1-T1, Gate 언어 QA |
| UTC 자정·현지 자정·DST | 24시간 차이나 UTC 날짜가 아닌 같은 현지 일자 비교 | P1-T2 날짜 경계 assertion |
| 과거 페이지가 같은 날짜에 이어짐 | 첫 날짜 항목 이동, 중복 없음, 읽던 위치 유지 | P1-T2 prepend assertion, Gate 스크롤 QA |
| pending이 서버의 다른 날짜로 확정됨 | 기존 빈 날짜 제거와 새 묶음 중복 방지 | P1-T2 ACK 스냅샷, Gate 서버 QA |
| 날짜 항목 높이가 기존 viewport를 채움 | 첫 메시지 paging·짧은 목록 추가 로드·바닥 이동 보존 | P1-T2 hook 연결 확인, Gate 실제 화면 QA |
## Phase 1
**Phase 결과:** TEXT 메시지의 날짜 문자열과 파생 행·비고정 UI가 정확히 동작하며 기존 채팅 열람 흐름을 보존한다.
**선행조건:** 사용자 구현 착수 요청과 본 PRD의 확정 요구사항.
**Phase 완료 조건:** `P1-T1`, `P1-T2`, `P1-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 1.1 날짜 표현 formatter
**Goal `P1-T1`:** 지정된 앱 locale, calendar, 기기 timeZone에 따라 상대 표현 없이 날짜 전체를 문자열로 만든다.
- **시작 조건:** DATE-002~004 확정, `P1-T2` 전에 완료.
- **완료 증거:** RED/GREEN/REFACTOR 체크 전체, 셸 검사 성공, 양쪽 앱 타깃에 신규 소스 등록.
- **범위 밖:** 날짜 묶음 계산과 UI.
**Files:**
- Create: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDatePresentation.swift` (향후 신규)
- Modify: `SodaLive.xcodeproj/project.pbxproj` (신규 파일을 `SodaLive`와 `SodaLive-dev` Sources 양쪽에 등록)
- Create: `work/tests/UserCreatorChatDatePresentationCheck.swift` (향후 신규)
- Create: `work/tests/check-user-creator-chat-date-presentation.sh` (향후 신규)
- Reference: `work/tests/check-gift-decoding-contract.sh` (임시 바이너리 컴파일 및 정리 패턴)
Runner는 `swiftc -swift-version 5`로 `Models/UserCreatorChatModels.swift`, `Models/UserCreatorChatDisplayMessage.swift`, formatter helper와 checker를 함께 컴파일하고 임시 실행 파일을 종료 시 정리한다.
**Interfaces:**
- Consumes: `Foundation.Locale`, `Foundation.Calendar`, `Foundation.TimeZone`, `Foundation.Date`.
- Produces: `enum UserCreatorChatDatePresentation`의 `static func title(for date: Date, locale: Locale, calendar: Calendar, timeZone: TimeZone) -> String`.
- [x] **RED:** checker에 Gregorian UTC fixture `2026-10-16T12:00:00Z`의 한국어 정확 문자열 `2026년 10월 16일 금요일`, 영어 full weekday/month/day/year 포함, 일본어 연·월·일·요일 포함 검사를 작성한다. 오늘·어제·이전 연도도 모든 필드 유지, locale과 timeZone 독립성, 전달 calendar의 상이한 timeZone 정규화 검사를 포함한다.
- [x] **RED 확인:** `sh work/tests/check-user-creator-chat-date-presentation.sh`를 실행한다. 최소 컴파일 가능한 임시 구현이 빈 문자열을 반환하도록 두어 한국어 exact-string assertion이 실패함을 확인한다. 컴파일/링크 오류는 의도한 RED가 아니다.
- [x] **GREEN:** 위 인터페이스를 구현한다. formatter에 locale, calendar, timeZone을 지정하고 localized template `yMMMMdEEEE`를 사용한다.
- [x] **GREEN 확인:** 같은 `sh work/tests/check-user-creator-chat-date-presentation.sh` 실행이 exit 0이고 `PASS:` 뒤에 통과 assertion 수와 `assertions`가 출력되는지 확인한다.
- [x] **REFACTOR:** 이번 Task가 만든 중복만 정리한 후 checker를 다시 실행한다. 불필요한 공유 캐시·언어별 문자열은 만들지 않는다.
#### Task 1.2 날짜 행과 비고정 UI
**Goal `P1-T2`:** 현재 TEXT 표시 메시지로 날짜 구분 행을 파생하고 원래 메시지 목록과 함께 스크롤한다.
- **시작 조건:** `P1-T1` 완료, DATE-001·005~008 확정.
- **완료 증거:** 행 경계/병합/전송 상태 테스트와 checker 통과, 수동 UI 검증 기록.
- **범위 밖:** 정렬·pagination·ACK 구현 변경 및 비TEXT 표시.
**Files:**
- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDatePresentation.swift` (행 파생 계약 추가)
- Create: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Components/UserCreatorChatDateSeparatorView.swift` (향후 신규)
- Modify: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift`
- Modify: `SodaLive.xcodeproj/project.pbxproj` (separator view를 두 앱 타깃 Sources에 등록)
- Modify: `work/tests/UserCreatorChatDatePresentationCheck.swift` (행/스냅샷 검사 추가)
**Interfaces:**
- Consumes: `UserCreatorChatDatePresentation.title(for:locale:calendar:timeZone:)`, `[UserCreatorChatDisplayMessage]`.
- Produces: `struct UserCreatorChatDatedMessage: Identifiable { let message: UserCreatorChatDisplayMessage; let separatorDate: Date?; var id: String { message.id } }` 및 `static func rows(from messages: [UserCreatorChatDisplayMessage], calendar: Calendar, timeZone: TimeZone) -> [UserCreatorChatDatedMessage]`.
- Produces: `UserCreatorChatDateSeparatorView.init(title: String)`.
- [x] **RED:** checker에 빈 목록·단일 행·동일 날짜, 동일/상이 날짜 history prepend 후 첫 날짜 행 이동·중복 없음, VOICE-only 및 혼합 목록의 orphan 없음 검사를 추가한다. Gregorian calendar를 쓰고 서울 `2026-10-15T23:30Z`→`2026-10-16T00:30Z`는 구분 1개, 서울 `2026-10-16T14:30Z`→`15:30Z`는 2개, LA `2026-11-01T08:30Z`→`09:30Z`는 1개이며 다음 현지 자정에는 추가되는지 검증한다. pending→failed 날짜 유지, pending→server 동일/상이 날짜의 빈 항목 제거·중복 없음, 같은 timestamp의 순서·ID 유지와 기존 pending→server ID 변경도 스냅샷 assertion으로 먼저 작성한다.
- [x] **RED 확인:** `sh work/tests/check-user-creator-chat-date-presentation.sh`를 실행한다. 행 생성의 컴파일 가능한 빈 결과 scaffold에서 단일 표시 메시지의 첫 `separatorDate` assertion이 실패함을 확인한다.
- [x] **GREEN:** `rows(from:calendar:timeZone:)`는 TEXT 필터 후 순서와 message ID를 유지하며 첫 행과 현지 날짜 변경 행에만 날짜를 설정한다. RoomView는 환경 locale/timeZone 및 정규화 calendar로 행을 파생하고 `textMessages` projection 및 기존 first/last hook을 보존한다. separator를 `VStack(spacing: SodaSpacing.s16)`에서 해당 메시지 앞에 표시하며, 중앙 appFont12 regular gray400, 전체 폭, 다중 행, truncation 없음, VoiceOver 날짜 1회 읽기를 제공한다. 기존 메시지 `.id(message.id)`, 첫 메시지 `.onAppear -> loadMore`, content height preference, viewport auto-fill, 마지막 TEXT id 및 join의 `scrollTo(lastMessage.id, .bottom)`을 유지하고 날짜 행은 scroll/paging target으로 쓰지 않는다.
- [x] **GREEN 확인:** checker 명령이 exit 0이고 RED 단계의 날짜 경계·병합·전송 상태·ID assertion이 모두 통과함을 확인한다. 스냅샷 검증을 실제 소켓 통합 검증으로 주장하지 않는다.
- [x] **REFACTOR:** 신규 계산의 중복만 정리하고 checker를 다시 실행한다. 기존 메시지 item, ViewModel, 입력/헤더, 언어 서비스와 `DateExtension`은 수정하지 않는다.
P1-T2의 UI 증거는 원본 컴포넌트 네이티브 캡처 범위다. 실제 RoomView의 paging·ACK·스크롤 및 VoiceOver 상호작용은 아래 P1-GATE 미완료 항목으로 남긴다.
### 완료 조건
- [x] `P1-T1`, `P1-T2`의 구현 체크박스와 자동 검사·컴포넌트 렌더링 증거를 기록했다. 전체 사용자 흐름 승인은 Gate와 구분한다.
- [x] DATE-001~008이 요구사항 추적 표의 Task 및 Gate 증거와 연결된다.
- [x] `SodaLive` 및 `SodaLive-dev` 양쪽 타깃에 필요한 신규 Swift 소스가 포함된다.
- [ ] P1-GATE의 실제 DM 흐름과 VoiceOver QA가 완료됐다.
### Phase 1 Gate
**Goal `P1-GATE`:** 자동 검사, 두 타깃 빌드와 대화 UI 수동 검증을 종합해 Phase 결과를 판정한다.
- **시작 조건:** `P1-T1`, `P1-T2` 완료.
- **완료 증거:** 아래 명령 성공, 수동 QA 기록, 미해결 제한 명시.
- **범위 밖:** 검증을 통과시키기 위한 테스트 완화 또는 관련 없는 코드 수정.
- **TDD 예외 사유:** 기존 구현의 검증과 문서 기록만 수행하는 Gate다. **대체 검증 방법:** 아래 focused checker, 두 타깃 빌드와 수동 QA 결과를 요구사항별 완료 증거로 남긴다.
```bash
sh work/tests/check-user-creator-chat-date-presentation.sh
plutil -lint SodaLive.xcodeproj/project.pbxproj
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build
```
**Expected:** checker assertion 전부 PASS, plist lint `OK`, 두 build 모두 exit code 0. 신규 소스의 양쪽 타깃 멤버십을 확인한다.
수동 검증:
- [ ] 앱 언어 ko/en/ja 및 System, 기기 언어와 앱 locale 불일치에서 앱 언어 표시와 기기 timeZone 날짜 경계가 각각 적용된다.
- [ ] 초기 진입, join, incoming, send, ACK 후 바닥 위치, 첫 메시지 paging, 짧은 viewport auto-fill에서 구분선 높이가 스크롤 흐름을 깨지 않는다.
- [ ] history 로드 전후 스크롤 연속성을 확인한다. 날짜 항목은 함께 스크롤되고 고정되지 않는다.
- [ ] 사용 가능한 서버·테스트 데이터로 실제 전송 중·실패·ACK 후 날짜 변경을 확인한다. 스냅샷 검증과 구분해 기록하며 재현할 수 없으면 관련 QA를 미완료로 유지한다.
- [ ] 좁은 폭, 큰 텍스트 크기, VoiceOver에서 잘림 없이 날짜 전체가 한 번 읽힌다.
- [x] 구현 완료 후 변경 범위의 코드 리뷰 결과를 `docs/sample/sample-review.md`에 맞춰 `docs/20261016_유저_크리에이터_채팅_날짜_구분선/reviews/phase1-chat-date-separators.md`에 기록했다. 코드 범위 APPROVE와 전체 Gate BLOCKED를 구분한다.
## 요구사항 추적성
| 요구사항 | 실행 Task | 검증 증거 |
|---|---|---|
| DATE-001 | P1-T2 | 비어 있지 않은 표시 날짜 행과 UI 표시, Gate 초기 진입 |
| DATE-002 | P1-T1, P1-T2 | ko/en/ja formatter, 앱 locale 환경 주입 및 불일치 QA |
| DATE-003 | P1-T1, P1-T2 | calendar/timeZone 정규화, 서울 경계 및 LA DST 케이스 |
| DATE-004 | P1-T1 | 연·월·일·요일, 오늘·어제·과거 연도 검사 |
| DATE-005 | P1-T2 | 빈/단일/연속 날짜, history prepend 병합 스냅샷 |
| DATE-006 | P1-T2, P1-GATE | 비고정 component와 스크롤 QA |
| DATE-007 | P1-T2 | pending→failed 및 pending→server 날짜 변경/불변 스냅샷 |
| DATE-008 | P1-T2 | TEXT 필터, VOICE-only 및 혼합 날짜 orphan 검사 |
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P1-T1` | 사용자 구현 착수 요청 | 아니요 | formatter assertion 또는 빌드 계약 문제를 기록하고 멈춤 |
| 2 | `P1-T2` | `P1-T1` | 아니요 | 행 테스트 실패 원인을 수정 후 같은 Task 검증 재개 |
| 3 | `P1-GATE` | 두 Task 완료 | 아니요 | 실패 소유 Task에 계획된 회귀 수정을 추가한 뒤 재검증 |
```text
P1-T1 → P1-T2 → P1-GATE
```
## 변경 금지와 중단 규칙
- PRD의 날짜 정책, 사용자 확정 사항, API 계약을 근거 없이 변경하지 않는다.
- 날짜 sort/dedup, 별도 section state, synthetic ID, DTO, ViewModel 수정, sticky header, VOICE 표시를 추가하지 않는다.
- 요구 범위 밖 리팩터링, dependency, 테스트 타깃을 만들지 않는다. `Pods/**`, `generated/**`, `build/**`도 수정하지 않는다.
- 빌드 또는 테스트 오류를 감추기 위해 테스트를 삭제·완화하지 않는다. 소켓 연결 결과를 스냅샷 검증으로 과장하지 않는다.
- fixture 또는 서버 응답이 필요한 라이브 QA를 사용할 수 없으면 수동 확인을 미완료로 기록한다. 서버/날짜 fixture를 위한 production mock이나 새 재시도 UI를 만들지 않는다.
- 요구사항과 구현 경계가 충돌하거나 세 번의 서로 다른 수정 시도 뒤에도 차단되면 추가 코드를 작성하지 않고 원인, 관찰 결과, 남은 검증을 기록해 사용자 판단을 기다린다.
- 리뷰에서 수정 필요가 확정되면 고치기 전에 리뷰 근거와 실행 가능한 회귀 수정 Task를 이 Phase에 추가한다.
## Progress
실제 문서 작업과 향후 구현 검증은 구분한다. 아래 체크박스는 미래 실행 항목이다.
### 문서 작성 실행, 2026-10-16
- 상태: 계획 작성 완료 / 구현 미착수.
- 무엇을: 문서 정책, 계획 샘플, 현재 PRD를 읽고 본 계획의 Phase, 요구사항 매핑 및 PRD 링크/범위 결정 기록을 작성했다.
- 어떻게: `docs/agent-guides/documentation-policy.md`, `docs/sample/sample-plan-task.md`, 현재 `prd.md`, DM View·모델·ViewModel, 앱 locale 주입, 기존 standalone checker와 Xcode 소스 등록 방식을 읽었다. `rg`로 두 앱 타깃, 최소 iOS 버전과 Swift 버전, XCTest 타깃 부재를 확인했다. 문서 작업 외의 빌드·테스트·앱 실행은 수행하지 않았다.
- 문서 검증: 두 문서를 다시 읽고 `rg -n '^\| DATE-00[1-8]' docs/20261016_유저_크리에이터_채팅_날짜_구분선/plan-task.md`의 추적표 8행을 대조해 모든 요구사항이 Task와 검증 증거에 연결됨을 확인했다. 별도 문서 검색에서 미래 구현의 완료 표시·미치환 placeholder·행 끝 공백은 없었다. 두 구현 Task의 RED→GREEN→REFACTOR, Gate 예외 사유, PRD의 상대 링크와 DEC-013도 대조했다.
- 미실행: `sh work/tests/check-user-creator-chat-date-presentation.sh`, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, 두 xcodebuild 명령, 모든 UI 수동 QA. 계획 문서에 기재한 명령은 실행 증거가 아니다.
- 남은 항목: 모든 구현 체크박스, 코드 및 Gate 검증.
- 다음 행동: 구현은 별도 사용자 요청이 있을 때만 시작한다.
### P1-T1 실행 기록, 2026-10-16
- 상태: 완료. formatter, standalone checker 및 두 타깃 등록을 commit `e765c3c`에 기록했다.
- RED: 컴파일 가능한 빈 문자열 scaffold에서 `expected '2026년 10월 16일 금요일', got ''`, exit 1을 확인했다.
- GREEN: `sh work/tests/check-user-creator-chat-date-presentation.sh`를 구현자와 상위 에이전트가 각각 실행해 `PASS: 27 assertions`, exit 0을 확인했다. `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `git diff --check` 통과.
- 최초 GREEN의 일본어 기대 문자열에는 요일 앞 locale 공백이 누락되어 실패했다. locale 생성 결과를 확인하고 fixture만 교정했으며 필드 검사 요구를 완화하지 않았다.
- 운영 타깃: `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -destination 'generic/platform=iOS' -derivedDataPath "$RUN_TMP/standard-build" CODE_SIGNING_ALLOWED=NO build` exit 0. 기존 SDK 경고·중복 소스 등록 경고는 범위 밖으로 유지했다. T2 이후 다시 빌드한다.
- 기존 회귀 기준선: 11개 기존 work/tests 스크립트, 지원 모드 포함 18회 실행 모두 exit 0. DM 화면 변경 후의 완료 증거와는 구분한다.
- 커밋 검증: 한글 conventional subject 사전 검사와 커밋 직후 `work/scripts/check-commit-message-rules.sh` 통과. 실제 스크립트는 `--message`를 지원하지 않아 별도 subject 사전 검사 사용.
- LSP 도구 없음: Foundation compiler 검사와 앱 빌드 결과를 기록하며 LSP 성공으로 대체 표기하지 않는다.
### P1-T2 및 Gate 실행 기록, 2026-10-16
- 구현: TEXT별 파생 날짜 행, 원본 message ID·순서 보존, 앱 locale·기기 timeZone 연결, 날짜 컴포넌트와 두 타깃 소스 등록을 완료했다. 기존 메시지 시각·ViewModel·API·ACK 로직은 수정하지 않았다.
- 커밋: 화면 연결·파생 행·직접 회귀 테스트·두 타깃 등록을 `7a6d12e`로 기록했다. 한글 conventional subject 사전 검사와 커밋 직후 저장소 검사 스크립트가 통과했다.
- RED: 빈 rows scaffold에서 `FAIL: single TEXT message must have its first separatorDate`를 확인했다. 이후 `sh work/tests/check-user-creator-chat-date-presentation.sh`는 `PASS: 113 assertions`, exit 0. 부모가 최종 소스로 재실행해 동일 결과를 확인했다.
- 구현 판단: `textMessages`는 기존 필터를 유지하고 messageListView 내부에서 한 번 캡처해 rows를 계산한다. 계획의 rows→textMessages projection 대신 기존 scroll helper를 유지하는 최소 변경이다. custom caption은 `.custom(..., relativeTo: .caption)`, 영어는 시스템 `.caption`을 사용해 locale별 Dynamic Type을 지원한다.
- 전체 빌드가 기존 앱 `Font` enum과 새 속성 타입 이름의 충돌을 검출했다. `SwiftUI.Font`로 한 줄 한정한 뒤 동일 명령 두 개가 모두 exit 0으로 통과했다. parse 검사는 이 충돌을 잡지 못했으므로 전체 컴파일 증거와 구별한다.
- 운영 빌드: `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -destination 'generic/platform=iOS' -derivedDataPath "$RUN_TMP/standard-build" CODE_SIGNING_ALLOWED=NO build` 성공.
- 개발 빌드: `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath "$RUN_TMP/simulator-build" CODE_SIGNING_ALLOWED=NO build` 성공. 최초 전체 의존성 빌드는 600초 timeout 뒤 캐시를 이용해 재개했다.
- 최종 회귀: 기존 11개 스크립트의 지원 모드 18회와 신규 날짜 검사 1회, 총 19회 모두 exit 0. 기존 테스트 파일은 수정하지 않았다. XCTest 타깃과 공식 lint 명령이 없어 이를 실행했다고 주장하지 않는다.
- 네이티브 QA: 원본 컴포넌트와 폰트를 임시 앱에서 직접 컴파일해 iPhone SE3/iOS18.3의 ko/en/ja × normal/accessibility3 × light/dark 12장 촬영. 폰트 타입 수정 이후 재촬영하고 production 소스 9개 SHA-256 일치를 확인했다. 캡처는 [evidence](reviews/evidence/README.md)에 보존한다.
- 앱 실행: 개발 앱을 소유된 시뮬레이터에 설치·실행해 홈 화면까지 확인했다. 인증된 DM 방은 확인하지 않았고 서버 데이터를 만들거나 개인 인증 정보를 읽지 않았다.
- 코드 리뷰: Oracle이 코드 범위 APPROVE, 구체적인 잔여 코드 차단 사항 없음으로 판정했다. 최종 Gate는 실제 DM 및 VoiceOver QA 부족으로 BLOCKED다.
- 최종 시각 재검토: 소스 해시 불일치와 시스템 추적 권한 팝업 가림이 있었던 중간 캡처는 사용하지 않았다. 최종 소스와 팝업 없는 환경으로 12장 재촬영 후 독립 검토자가 12/12 PASS, 잔여 시각 차단 사항 없음으로 판정했다. 이는 컴포넌트 한정 승인이며 전체 DM Gate 승인이 아니다.
- 남은 항목: 실제 계정·roomId와 날짜가 다른 이력으로 paging·ACK·스크롤·System 언어를 검증하고 VoiceOver 읽기를 확인한다. LSP 도구 부재도 별도로 남긴다.
- 정리: 최종 증거를 reviews/evidence에 보존하고 소유된 시뮬레이터 shutdown/delete, custom set 빈 목록과 하네스 프로세스 부재, 임시 루트·캐시·작업 메모 제거를 확인했다. 사용자 시뮬레이터·공유 캐시는 유지했다.
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 |
|---|---|---|---|---|---|
| 2026-10-16 | DEC-PLAN-001 | 확정 | 현재 작업은 구현 계획 문서 작성과 PRD 연결 수정만 수행한다. 구현 체크박스는 전부 미완료로 두고 활성 Goal은 두지 않는다. | 사용자 명시 범위 및 최신 요청 | 전체 Phase, PRD DEC-013 |
| 2026-10-16 | DEC-PLAN-002 | 에이전트 기술 설계 | Foundation formatter와 파생 행을 기존 DM 경계에서 구현하고 별도 XCTest target/dependency를 추가하지 않는다. | 기존 소스 확인과 저장소 checker 패턴에 따른 계획 판단, 사용자 별도 기술 승인 아님 | P1-T1, P1-T2 |
| 2026-10-16 | DEC-PLAN-003 | 구현 판단 | separator는 중앙 appFont12 regular gray400, 전체 폭 다중 행, Dynamic Type 및 VoiceOver 지원으로 구현한다. | PRD가 위임한 시각 상세, 기존 앱 관례에 따른 계획 기준이며 사용자 확정 픽셀 사양은 아님 | P1-T2, P1-GATE |
## 발견된 문제와 제한
| ID | 상태 | 관찰 및 영향 | 처리 계획 |
|---|---|---|---|
| LIMIT-001 | 미검증 | Foundation checker는 날짜 계산과 formatter를 검사하지만 SwiftUI 레이아웃, 실제 viewport와 네트워크 ACK를 증명하지 않는다. | 빌드와 기기/시뮬레이터 수동 QA를 Gate에서 별도 기록한다. |
| LIMIT-002 | 외부 조건 | 실제 incoming/history/ACK 흐름을 재현할 서버 계정 또는 날짜 fixture가 없을 수 있다. | 접근 불가 시 관련 QA를 미완료로 명시하고 production mock은 추가하지 않는다. |
@@ -0,0 +1,209 @@
# 유저-크리에이터 채팅 날짜 구분선 PRD
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현 완료 / 실서버·접근성 QA 대기 |
| 작성일 | 2026-10-16 |
| 최종 수정일 | 2026-10-16 |
| 대상 제품 | SodaLive iOS 유저-크리에이터 DM 채팅 |
| 작성자·결정권자 | 작성: 에이전트 / 제품 결정: 사용자, 6차 인터뷰 완료 |
| 관련 API Contract | 해당 없음. 기존 메시지 timestamp 활용을 전제로 하며 API 변경 요청 없음 |
| 관련 구현 계획 | [plan-task.md](plan-task.md), P1-T1·T2 구현 완료, P1-GATE 외부 조건 대기 |
| 관련 review | [Phase 1 리뷰](reviews/phase1-chat-date-separators.md) |
### 요구사항 상태
| 상태 | 의미 |
|---|---|
| 확정 | 사용자가 확인한 목표 또는 현재 코드에서 확인한 사실 |
| 미결 | 인터뷰에서 결정해야 하며 임의로 구현 기준 삼지 않음 |
| 제외 | 이번 문서 작성 세션의 제외 범위 |
## 1. 개요
유저-크리에이터 DM 대화에서 메시지 사이에 날짜 구분 항목을 표시해 사용자가 메시지가 오간 날짜를 파악할 수 있게 한다. 날짜 경계는 채팅을 보는 사람의 기기 현재 시간대를 따른다. 날짜 표기의 언어·지역은 앱 언어 설정에 대응하는 locale을 따르며, 항상 연·월·일·요일을 포함하고 오늘·어제 같은 상대 날짜 표현은 사용하지 않는다.
## 2. 문제와 배경
- **확정:** 유저-크리에이터 채팅에는 날짜 표시가 없어 메시지가 언제 오갔는지 파악하기 어렵다.
- **확정:** 다른 채팅 앱과 같이 대화 중 날짜 구분선 또는 항목을 추가하는 것이 요청된 개선 방향이다.
- **문서 작성 판단:** 별도 정량 계측 목표는 두지 않고, 날짜별 메시지 구분과 아래 기능 수용 사례로 해결 여부를 판정한다. 사용자가 별도 계측 기능을 요청한 것은 아니다.
## 3. 목표
### 3.1 제품 목표
- 메시지 대화 흐름에서 날짜별 구분을 제공한다.
- 날짜 표기는 앱 언어 설정의 locale을 따른다. 현재 앱 매핑인 한국어 `ko_KR`, 영어 `en_US`, 일본어 `ja_JP`에 맞는 순서와 표현으로 항상 연·월·일·요일을 표시한다. 오늘·어제로 대체하지 않는다.
### 3.2 사용자 경험 목표
- 대화 내 날짜 구분 항목으로 메시지의 날짜를 확인할 수 있다.
- 기존 시간순 대화 열람과 이전 메시지 로드 흐름을 유지한다.
## 4. 이번 세션의 제외 범위
- 제품 코드, 테스트, 미리보기, 목업, UI 설계 변경을 하지 않는다.
- 위 문서 전용 제한은 초기 인터뷰 당시 범위다. 후속 사용자의 `구현 진행` 요청에 따라 확정 계획 내 코드·테스트·프로젝트 등록 및 검증을 수행한다. 기존 기록은 당시 이력으로 보존한다.
- 빌드, 의존성 변경, 커밋을 하지 않는다.
- 초기 인터뷰 세션에서는 `plan-task.md`를 작성하지 않았다. 요구사항 확정 후 2026-10-16 사용자 요청에 따라 현재 PRD와 같은 폴더에 구현 계획만 추가한다. 기능 구현은 이번 범위에 포함하지 않는다.
- API Contract 문서는 작성하지 않는다. API 변경 요청은 없고 기존 timestamp가 존재한다.
- 기존 7월 DM 문서와 9월 수신자 문서는 별개 선행 기능 문서로 참조만 하며 수정하지 않는다.
- 향후 기능 범위에서도 날짜 고정 헤더와 음성 메시지 표시 추가는 제외한다. 음성 표시 제외는 기존 TEXT 표시 범위를 유지하기 위한 에이전트의 최소 범위 판단이며, 사용자 별도 결정으로 간주하지 않는다.
## 5. 대상 사용자와 흐름
| 사용자 | 목표 | 주요 작업 |
|---|---|---|
| SodaLive 유저-크리에이터 DM 사용자 | 메시지가 오간 날짜 파악 | 대화 진입, 메시지와 날짜 구분 항목 열람, 과거 메시지 탐색 |
### 핵심 흐름
1. 사용자가 유저-크리에이터 DM 대화에 진입한다.
2. 메시지를 시간순으로 읽으며 각 날짜의 구분 항목을 확인한다.
3. 위로 스크롤해 과거 메시지를 로드하고 날짜별 흐름을 이어서 확인한다.
## 6. 현재 구현에서 확인한 사실
- 대화 화면은 `TEXT` 메시지만 표시하며 `createdAt` 기준 오름차순으로 정렬된 메시지 목록을 사용한다.
- 가장 첫 메시지가 나타날 때 이전 페이지를 로드하고, 화면 높이보다 콘텐츠가 짧으면 추가 로드를 시도한다.
- `UserCreatorChatDisplayMessage.createdAt`은 `Int64`이며 서버 메시지 timestamp를 보존한다.
- 보류 메시지는 현재 epoch milliseconds로 생성되고, ACK 수신 시 서버 메시지로 교체된다. 실패한 보류 메시지도 존재할 수 있다.
- 각 텍스트 메시지 항목은 `createdAt`을 epoch milliseconds로 해석해 시각을 표시한다.
- 기존 메시지 시각은 `Date.convertDateFormat`을 통해 기기 현재 시간대와 자동 갱신 locale을 사용한다. 새 날짜 구분선은 기기 현재 시간대와 앱 언어 locale을 따르기로 확정했다. 기존 메시지별 시각의 locale까지 변경하기로 결정한 것은 아니다.
- 앱 언어 설정은 별도로 존재하며 한국어 `ko_KR`, 영어 `en_US`, 일본어 `ja_JP` locale로 매핑한다. 새 날짜 구분선은 기기 언어·지역을 직접 따르는 대신 이 앱 언어 정책을 따른다.
## 7. 기능 요구사항
| ID | 상태 | 요구사항 | 수용 기준 | 추적 |
|---|---|---|---|---|
| DATE-001 | 확정 | 유저-크리에이터 DM 대화에 메시지 사이 날짜 구분 항목을 제공한다. | 대화에서 날짜 단위의 구분을 인식할 수 있으며, 삽입 위치는 DATE-005를 따른다. | 목표, OQ-004, DEC-009 |
| DATE-002 | 확정 | 날짜 표기의 언어·지역은 앱 언어 설정의 locale을 따른다. | 한국어는 `ko_KR`, 영어는 `en_US`, 일본어는 `ja_JP` 형식으로 표시한다. 기기 설정과 앱 언어가 다르면 앱 언어를 우선하며 시스템 기본 선택도 기존 앱 언어 해석 정책을 따른다. 포함 필드는 DATE-004를 따른다. | OQ-002, OQ-003, DEC-007, DEC-008 |
| DATE-003 | 확정 | 날짜 구분과 일자 경계는 채팅을 보는 사람의 기기 현재 시간대를 따른다. | 기기 현지 날짜가 바뀌면 서로 다른 날짜로 구분한다. 같은 메시지도 열람자의 시간대에 따라 다른 날짜에 속할 수 있으며 한국 시간으로 고정하지 않는다. | OQ-001, DEC-006 |
| DATE-004 | 확정 | 날짜 구분 항목은 항상 연·월·일·요일을 표시한다. 오늘·어제 같은 상대 날짜 표현은 사용하지 않는다. | 한국어 예시는 `2026년 10월 16일 금요일`이다. 오늘과 어제에 해당하는 날짜도 같은 포함 필드를 유지하며, 영어·일본어는 동일 필드를 해당 locale의 순서와 표현으로 표시한다. 한국어 순서와 접미사를 다른 언어에 강제하지 않는다. | OQ-003, DEC-008 |
| DATE-005 | 확정 | 현재 불러온 첫 메시지 위와 인접 메시지의 현지 날짜가 바뀌는 지점에 날짜 구분 항목을 표시한다. | 같은 날짜의 연속 메시지 사이에는 반복하지 않는다. 과거 페이지 병합 후 같은 날짜가 이어지면 페이지 경계의 구분선은 중복하지 않고 합쳐진 날짜 묶음의 첫 메시지 위에만 남긴다. 메시지가 없으면 날짜 항목도 표시하지 않는다. | OQ-004, DEC-009 |
| DATE-006 | 확정 | 날짜 구분 항목은 대화 목록 안에서 메시지와 함께 스크롤된다. | 화면 밖으로 스크롤되면 보이지 않고, 화면 상단에 고정하거나 별도 날짜 헤더로 중복 표시하지 않는다. | OQ-005, DEC-010 |
| DATE-007 | 확정 | 전송 중·실패 메시지는 로컬 생성 시각으로 날짜를 구분하고, 서버 메시지로 확정되면 서버 createdAt으로 날짜 구분을 갱신한다. | ACK 전후 현지 날짜가 달라지면 서버 날짜 묶음으로 이동한다. 이후 DATE-005를 다시 적용해 비어버린 날짜 항목과 중복 날짜 항목을 남기지 않는다. 실패 상태만으로는 로컬 생성 날짜를 바꾸지 않는다. | OQ-006, DEC-012 |
| DATE-008 | 확정, 에이전트 범위 판단 | 기존 화면에 표시하는 TEXT 메시지를 대상으로 날짜 구분을 추가한다. | 표시하지 않는 VOICE 메시지만 있는 날짜에는 날짜 항목을 만들지 않는다. 날짜 구분을 위해 음성 표시 기능을 새로 추가하지 않는다. 보류·실패 TEXT의 날짜 처리는 DATE-007을 따른다. | OQ-007, DEC-011 |
## 8. 기술 제약과 확인 범위
- 현재 대화 화면 파일: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomView.swift`
- 표시 메시지 모델: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDisplayMessage.swift`
- 정렬, pagination 병합, 보류 메시지 및 ACK 교체: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/UserCreatorChatRoomViewModel.swift`
- 메시지별 시각 표시: `SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Components/UserCreatorChatTextMessageItemView.swift`
- 기존 날짜 formatter: `SodaLive/Sources/Extensions/DateExtension.swift`
- 앱 언어 locale 정책: `SodaLive/Sources/Settings/Language/Services/LanguageService.swift`
- 현재 확인된 timestamp는 milliseconds 단위다. 새 날짜 구분선은 기기 현재 시간대와 앱 언어 locale에 따라 항상 연·월·일·요일을 표시한다. 기존 메시지별 시각 formatter 변경으로 범위를 확장하지 않는다.
- 초기 인터뷰에서는 기술 설계를 확정하지 않았다. 후속 구현 계획 요청에 따라 기술 구성과 검증 절차를 [plan-task.md](plan-task.md)에 정리했다. API 변경은 없으며 계획 작성은 기능 구현 착수 승인이 아니다.
- 구현 시 세부 색상, 글꼴, 간격, 장식선 여부와 컴포넌트 분리는 기존 채팅 디자인 및 저장소 관례 안에서 구현자가 정한다. 이 문서는 해당 시각 상세를 사용자 확정 사항으로 간주하지 않으며, 목업이나 제품 코드를 만들지 않는다.
## 9. 성공 기준
### 9.1 문서 완료 기준, 이번 세션
- [x] 초기 PRD가 샘플의 관련 구조를 따르고 확정 사실과 미결 결정을 구분한다.
- [x] 요구사항, 기술 제약, 성공 기준, Open Questions, 추적성, Decision Log를 기록한다.
- [x] 요청된 경로에 PRD 한 파일만 작성하고 계획 문서는 만들지 않는다.
- [x] 인터뷰로 중요한 미결 사항을 해소하고 결정 근거를 누적한다.
- [x] 확정 요구사항, 제외 범위, 수용 사례를 사용자가 확인할 수 있게 정리한다.
위 문서 완료 체크는 초기 인터뷰 세션의 이력이다. 후속 사용자 요청으로 구현 계획 문서를 추가한 현재 범위는 DEC-013과 `plan-task.md`를 따른다.
### 9.2 기능 수용 기준, 전체 사용자 흐름의 최종 QA 대기
- [ ] 확정된 날짜 경계에 따라 대화 중 날짜 구분 항목이 표시된다. (`DATE-001`, `DATE-003`, `DATE-005`)
- [ ] 날짜 문자열이 합의된 locale 및 표시 형식을 따른다. (`DATE-002`, `DATE-004`)
- [ ] 첫 메시지 위와 날짜 변경 지점에만 구분선이 표시되고, 같은 날짜의 과거 페이지를 붙이면 중복 날짜 항목이 남지 않는다. 빈 목록에는 날짜 항목이 없다. (`DATE-005`)
- [ ] 초기 로드, 과거 페이지 추가, 보류 메시지의 ACK 교체를 포함해 합의한 메시지 범위에서 날짜 구분이 일관된다. (`DATE-005`~`DATE-008`)
- [ ] 날짜 구분 항목은 메시지와 함께 스크롤되며 상단에 고정되지 않는다. 기존 화면에서 제외된 메시지 유형 때문에 빈 날짜 항목이 생기지 않는다. (`DATE-006`, `DATE-008`)
위 기능 수용 기준의 미완료 표시는 초기 문서 작성 이력 및 아직 남은 실제 대화 QA를 반영한다. 후속 구현에서 날짜 계산 자동 검사와 컴포넌트 렌더링·두 타깃 빌드는 통과했지만, 전체 대화 사용자 흐름의 수용 완료와 동일하게 처리하지 않는다. 현재 증거와 남은 항목은 계획 및 리뷰 문서에 기록한다.
### 9.3 기능 검증 사례
날짜 계산·formatter 스냅샷 자동 검사는 통과했다. 실제 DM의 페이지 로드·ACK·스크롤 상호작용은 인증된 테스트 방 부재로 대기하며, 원본 날짜 컴포넌트의 네이티브 렌더링만 별도 확인했다.
| 사례 | 기대 결과 | 요구사항 |
|---|---|---|
| 빈 목록 / 메시지 1개 | 날짜 항목 없음 / 첫 메시지 위에 해당 날짜 항목 1개 | DATE-005 |
| 같은 날 연속 메시지 / 현지 자정 전후 메시지 | 날짜 묶음당 첫 메시지 위에 1개 / 날짜 변경 시 새 항목 | DATE-003, DATE-005 |
| 과거 페이지가 같은 날짜로 연결됨 / 다른 날짜로 연결됨 | 합쳐진 같은 날짜 묶음에는 1개만 유지 / 실제 날짜 경계에만 추가 | DATE-005 |
| 앱 한국어·영어·일본어, 기기 언어·지역과 불일치 | 앱 locale 형식으로 연·월·일·요일 모두 표시, 기기 시간대로 날짜 판정 | DATE-002~004 |
| 오늘·어제·과거 연도 메시지 | 상대 날짜로 대체하거나 연도·요일을 생략하지 않음 | DATE-004 |
| 전송 중 / 실패 / 같은 날짜의 서버 확정 | 로컬 생성 날짜 유지 / 로컬 생성 날짜 유지 / 같은 날짜 묶음 유지 | DATE-007 |
| 서버 확정 시 날짜가 변경됨 | 서버 createdAt 기준으로 다시 구분, 기존 날짜 묶음이 비면 항목 제거, 새 묶음의 날짜 항목 중복 없음 | DATE-005, DATE-007 |
| 날짜 항목을 화면 밖으로 스크롤 / VOICE만 있는 날짜 | 상단 고정 표시 없음 / 빈 날짜 항목 생성 없음 | DATE-006, DATE-008 |
## 10. Open Questions
| ID | 상태 | 결정 필요 사항 | 결정 시 확인할 내용 | 연결 요구사항 |
|---|---|---|---|---|
| OQ-001 | 해결 | 날짜 구분의 timezone 및 하루 경계는 무엇을 기준으로 할까? | 기기 현재 시간대 기준으로 확정. 첫 질문의 1번 선택, DEC-006 | DATE-003, DATE-005 |
| OQ-002 | 해결 | locale은 기기 언어+지역과 앱 언어 중 무엇을 따를까? | 앱 언어 설정 기준으로 확정. 두 번째 질문의 1번 선택, DEC-007 | DATE-002 |
| OQ-003 | 해결 | 날짜 문구에 연도·요일을 포함할까? 오늘·어제는 별도 표현할까? | 항상 연·월·일·요일을 표시하고 상대 날짜 표현은 사용하지 않기로 확정. 세 번째 질문의 1번 선택, DEC-008 | DATE-002, DATE-004 |
| OQ-004 | 해결 | 구분 항목은 어떤 경계에 삽입할까? | 현재 불러온 첫 메시지 위와 날짜 변경 지점에 표시하고 같은 날짜의 페이지 경계 중복은 제거. 네 번째 질문의 1번 선택, DEC-009 | DATE-001, DATE-005 |
| OQ-005 | 해결 | 구분 항목은 대화 안의 정적 항목인가, 스크롤 중 고정 표시인가? | 메시지와 함께 스크롤하고 상단에 고정하지 않기로 확정. 다섯 번째 질문의 1번 선택, DEC-010 | DATE-006 |
| OQ-006 | 해결 | 보류·실패 메시지와 ACK 뒤 서버 timestamp가 달라 날짜가 바뀌면 어떻게 할까? | 전송 중·실패는 로컬 생성 시각, 전송 완료 후 서버 시각으로 날짜를 다시 구분. 여섯 번째 질문의 1번 선택, DEC-012 | DATE-007 |
| OQ-007 | 해소, 에이전트 범위 판단 | 날짜 구분은 TEXT 이외의 메시지 유형에도 적용할까? | 기존 TEXT 표시 범위를 유지하고 VOICE 표시 추가는 제외한다. 요청하지 않은 신규 메시지 표시를 만들지 않는 최소 범위 판단이며 사용자 별도 결정 아님, DEC-011 | DATE-008 |
현재 열린 제품 결정은 없다. OQ-007의 범위 유지와 시각 상세의 구현자 위임은 사용자 답변과 구별된 에이전트 판단이다. 후속 사용자 요청으로 구현했으며, 현재 대기는 제품 결정이 아닌 실서버·접근성 QA 환경이다.
## 11. 요구사항 추적성
| 요구사항 | 근거 / 미결 결정 | 문서 완료 증거 | 기능 검증 |
|---|---|---|---|
| DATE-001 | 사용자 확인 목표, DEC-009 | 문제, 목표, 요구사항과 확정 삽입 규칙 기록 | P1-T2: 첫 메시지·날짜 변경 자동 검사와 컴포넌트 표시 확인, 실제 DM 대기 |
| DATE-002 | OQ-002, DEC-007 | 앱 언어 locale 기준 반영 | P1-T1·T2: ko/en/ja 자동 검사 및 환경 주입 코드 확인, 실제 앱 System 설정 QA 대기 |
| DATE-003 | OQ-001, DEC-006 | 열람자 기기 시간대 기준을 요구사항과 결정 기록에 반영 | P1-T1·T2: 서울/UTC 경계, LA DST 자동 검사 통과 |
| DATE-004 | OQ-003, DEC-008 | 연·월·일·요일 필수 및 상대 날짜 표현 제외 반영 | P1-T1: 오늘·어제·과거 연도 전체 필드 검사 통과 |
| DATE-005 | OQ-004, DEC-009 | 첫 메시지, 날짜 전환, 과거 페이지 중복 제거 규칙 반영 | P1-T2: 빈/단일/연속·prepend 스냅샷 검사 통과, 실제 paging 대기 |
| DATE-006 | OQ-005, DEC-010 | 목록 내부에서 함께 스크롤하고 고정하지 않는 동작 반영 | P1-T2: 비고정 구조 코드 리뷰 승인, 실제 스크롤 제스처 QA 대기 |
| DATE-007 | OQ-006, DEC-012 | 전송 상태별 시각 출처와 서버 확정 후 날짜 재구분 반영 | P1-T2: pending/failed/server 교체 스냅샷 검사 통과, 실제 ACK 대기 |
| DATE-008 | OQ-007, DEC-011 | 기존 TEXT 표시 범위를 유지하는 에이전트 판단 명시 | P1-T2: VOICE-only·TEXT 혼합 날짜 검사 통과 |
## 12. Decision Log
| 날짜 | ID | 상태 | 결정 또는 확인 사실 | 근거 | 영향 |
|---|---|---|---|---|---|
| 2026-10-16 | DEC-001 | 확정 | 유저-크리에이터 채팅에서 날짜가 없어 메시지 시점을 파악하기 어렵고, 대화에 날짜 구분 항목을 추가한다. | 사용자 확인 | DATE-001, DATE-002 |
| 2026-10-16 | DEC-002 | 확정 | 이번 세션은 인터뷰와 문서 작성만 수행한다. 제품 코드, 빌드, 의존성, 커밋은 수행하지 않는다. 초기 PRD 이후 인터뷰 결과를 같은 문서에 보강하고, 계획 문서는 필요 시 요구사항 확정 후 작성한다. | 사용자의 문서 전용 지시, 저장소 문서 정책에 따른 에이전트 작업 방식 | 문서 상태 및 이번 세션 Non-Goals |
| 2026-10-16 | DEC-003 | 확정 | 현 단계에서 별도 API Contract 문서는 만들지 않는다. 기존 timestamp가 있으며 API 변경이 요청되지 않아 최소 문서로 진행한다. | 확인된 모델과 저장소 최소 문서 정책에 따른 에이전트 판단, 사용자 별도 결정 아님 | 기술 제약 |
| 2026-10-16 | DEC-004 | 확정 | 기존 7월 DM 문서와 9월 수신자 문서는 선행 기능 맥락으로 참조하고, 이번 신규 날짜 구분선 요구사항은 별도 PRD로 관리한다. | 기존 문서 범위 확인 및 에이전트 문서 구성 판단, 사용자 별도 결정 아님 | 이번 세션 Non-Goals |
| 2026-10-16 | DEC-005 | 미결 | timezone, locale 출처, 날짜 필드와 표현, 삽입 경계, 고정 여부, ACK 처리, 메시지 유형 범위는 미확정이다. | 사용자 인터뷰 전 | OQ-001~007, DATE-003~008 |
| 2026-10-16 | DEC-006 | 확정 | DEC-005 중 timezone 결정을 해소한다. 날짜 경계는 채팅을 보는 사람의 기기 현재 시간대를 따르고, 한국 시간으로 고정하지 않는다. locale 출처 등 나머지 사항은 미결이다. | 첫 인터뷰에서 사용자가 1번 선택 | DATE-003, OQ-001 |
| 2026-10-16 | DEC-007 | 확정 | DEC-005와 DEC-006에 남은 locale 출처 결정을 해소한다. 날짜 구분선은 앱 언어 설정의 locale을 따른다. 현재 앱 정책인 한국어 `ko_KR`, 영어 `en_US`, 일본어 `ja_JP`를 적용한다. 기기 시간대 기준은 유지하며 포함 필드와 상대 날짜 표현은 아직 미결이다. | 두 번째 인터뷰에서 사용자가 1번 선택 | DATE-002, OQ-002 |
| 2026-10-16 | DEC-008 | 확정 | DEC-005와 DEC-007에 남은 포함 필드와 상대 날짜 표현 결정을 해소한다. 항상 연·월·일·요일을 앱 언어 locale에 맞게 표시하고 오늘·어제 표현은 사용하지 않는다. 삽입 경계, 표시 방식, ACK 처리와 메시지 유형 범위는 아직 미결이다. | 세 번째 인터뷰에서 사용자가 1번 선택 | DATE-004, OQ-003 |
| 2026-10-16 | DEC-009 | 확정 | DEC-005와 DEC-008에 남은 삽입 경계 결정을 해소한다. 현재 불러온 첫 메시지 위와 날짜가 바뀌는 지점에 표시하고 같은 날짜의 연속 메시지에는 반복하지 않는다. 과거 페이지를 붙여도 같은 날짜 구분선은 중복하지 않는다. 빈 목록에 날짜 항목을 만들지 않는 것은 이 규칙에 따른 에이전트의 파생 기준이다. | 네 번째 인터뷰에서 사용자가 1번 선택 | DATE-001, DATE-005, OQ-004 |
| 2026-10-16 | DEC-010 | 확정 | DEC-005 등에 남은 고정 여부를 해소한다. 날짜 구분 항목은 메시지와 함께 스크롤되며 화면 상단에 고정하지 않는다. | 다섯 번째 인터뷰에서 사용자가 1번 선택 | DATE-006, OQ-005 |
| 2026-10-16 | DEC-011 | 확정, 에이전트 범위 판단 | 기존 TEXT 표시 범위를 유지하며 VOICE 메시지 표시 추가는 제외한다. 날짜 그룹은 실제 표시 대상 메시지를 기준으로 하므로 숨겨진 메시지만 있는 날짜에 빈 날짜 항목을 만들지 않는다. 기존 화면의 표시 유형을 확장하라는 요청이 없어 해소한 항목으로, 사용자에게 별도 승인받은 결정은 아니다. | 기존 View의 TEXT 필터와 최소 변경 원칙 | DATE-008, OQ-007 |
| 2026-10-16 | DEC-012 | 확정 | DEC-005 등에 남은 ACK 날짜 처리를 해소한다. 전송 중·실패는 로컬 생성 시각, 전송 완료 후는 서버 확정 시각을 사용한다. 날짜가 달라지면 날짜 묶음을 다시 계산하고 DATE-005에 따라 빈 날짜 및 중복 날짜 항목을 남기지 않는다. | 여섯 번째 인터뷰에서 사용자가 1번 선택 | DATE-005, DATE-007, OQ-006 |
| 2026-10-16 | DEC-013 | 확정 | 앞선 문서 전용 인터뷰 세션의 계획 작성 보류(DEC-002)는 당시 범위의 기록으로 보존한다. 사용자의 최신 명시 요청에 따라 이번 범위는 `plan-task.md` 작성과 PRD 연결 및 추적성 갱신으로 변경하며, 기능 구현은 계속 제외한다. | 사용자 직접 요청, 문서 정책에 따른 인터뷰 완료 후 계획 작성 | `plan-task.md`, DATE-001~008 |
| 2026-10-16 | DEC-014 | 확정 | 후속 사용자 요청 `구현 진행`, `ulw`에 따라 확정 계획 구현과 검증을 시작한다. 문서 전용 제한을 해제하되 제품 요구사항은 바꾸지 않는다. | 사용자 직접 요청 | P1-T1, P1-T2, P1-GATE |
## 13. 문서 검증 기록
- 2026-10-16: 문서 정책과 PRD 샘플을 읽고 초기 요구사항을 기록했다. 기존 대화 View, 표시 모델, ViewModel, 메시지 시각 컴포넌트, 날짜 extension과 언어 서비스의 해당 동작을 읽어 현재 사실과 신규 제품 결정을 분리했다.
- 제품 코드와 프로젝트 설정은 수정하지 않았다. 문서만 작성하므로 빌드, 자동 테스트, 앱 실행 검증은 수행하지 않았다. 날짜 구분선 기능은 미구현이며 인터뷰 진행 중이다.
- 2026-10-16, 인터뷰 1차: 사용자 답변 `1`을 기기 시간대 선택으로 반영했다. 개요, 현재 동작과 신규 결정 구분, DATE-003, OQ-001, 추적표, Decision Log의 일치 여부를 문서 검색으로 확인한다. 제품 코드는 변경하지 않는다.
- 2026-10-16, 인터뷰 2차: 사용자 답변 `1`을 앱 언어 설정 선택으로 반영했다. 개요, 목표, 기존 동작과 신규 결정 구분, DATE-002, OQ-002, 기술 제약, 추적표, DEC-007의 정합성을 문서 검색으로 확인한다. 기존 메시지별 시각 표시 변경은 추가하지 않았다.
- 2026-10-16, 인터뷰 3차: 사용자 답변 `1`을 항상 연·월·일·요일 표시로 반영했다. 개요, 목표, DATE-002·004, 기술 제약, OQ-003, 추적표와 DEC-008의 정합성을 문서 검색으로 확인한다. 코드 변경, 빌드 및 앱 실행 검증은 수행하지 않는다.
- 2026-10-16, 인터뷰 4차: 사용자 답변 `1`을 첫 메시지 위와 날짜 변경 지점 표시 및 과거 페이지의 같은 날짜 중복 제거로 반영했다. DATE-001·005, 기능 수용 기준, OQ-004, 추적표와 DEC-009의 정합성을 문서 검색으로 확인한다. 표시 방식, ACK 처리, 메시지 유형 범위는 아직 미결이며 코드 변경 및 앱 검증은 수행하지 않는다.
- 2026-10-16, 인터뷰 5차: 사용자 답변 `1`을 메시지와 함께 스크롤하고 상단에 고정하지 않는 방식으로 반영했다. 요청 범위를 확장하지 않도록 기존 TEXT 표시 유지도 에이전트 판단으로 구분해 기록했다. DATE-006·008, OQ-005·007, 추적표 및 DEC-010·011의 정합성을 문서 검색으로 확인한다. 남은 사용자 결정은 보류·실패 메시지와 서버 ACK의 날짜 처리다. 코드 변경 및 앱 검증은 수행하지 않는다.
- 2026-10-16, 인터뷰 6차: 사용자 답변 `1`을 전송 완료 후 서버 확정 날짜로 갱신하는 규칙으로 반영했다. DATE-007, OQ-006, 추적표 및 DEC-012를 일치시키고 기능 검증 사례를 정리했다. 과거 미결 기록은 당시 이력으로 보존하고 현재 상태만 해소했다. 문서만 작성했으며 빌드, 테스트, 앱 실행은 하지 않았다.
- 2026-10-16, 최종 문서 확인: 독립 검토자가 PRD만 읽고 향후 계획 수립에 중요한 미답변 질문이 없다고 판정했다(`nothing important left`). 최초 일반 검토자 실행은 지원되지 않는 모델 오류로 시작하지 못했으며 문서 전문 검토자로 재시도해 확인했다. 요구사항 8개, 질문 7개의 현재 상태와 DEC-006~012 연결을 `rg -n '^\| DATE-|^\| OQ-|DEC-012' docs/20261016_유저_크리에이터_채팅_날짜_구분선/prd.md`로 대조했다. 기능 테스트를 실행하거나 완료 처리한 것은 아니다.
- 2026-10-16, 범위 변경 및 추적성 보강: 사용자 요청에 따라 [plan-task.md](plan-task.md)를 작성하고 DATE-001→P1-T2, DATE-002→P1-T1/P1-T2, DATE-003→P1-T1/P1-T2, DATE-004→P1-T1, DATE-005→P1-T2, DATE-006→P1-T2/P1-GATE, DATE-007→P1-T2, DATE-008→P1-T2로 연결했다. 이 기록은 문서 작업만 허용하며 제품 코드, 테스트 파일, 프로젝트 설정, 빌드 및 앱 실행은 허용하지 않는다. 계획의 모든 구현 체크박스는 미실행 상태다.
## 14. 인터뷰 종료 판단
| 항목 | 최종 판단 |
|---|---|
| Goal | 1.00: 메시지가 오간 날짜를 대화에서 확인 |
| Scope | 0.95: 기존 DM의 표시 TEXT에 날짜 항목 추가, 이번 세션은 문서만 작성 |
| Constraints | 1.00: 기기 시간대, 앱 locale, 연·월·일·요일, 비고정 표시, 서버 시각 확정 정책 결정 |
| Success | 0.95: 삽입·병합·표기·전송 상태별 수용 사례를 명시, 실행 검증은 구현 이후 |
| Context | 0.95: 기존 View·모델·병합·시각·언어 정책 확인, 시각 상세는 기존 관례 내 구현자 결정 |
| 최종 모호성 | 0.03: 기존 코드베이스 가중식의 0.0275를 반올림한 주관적 점검값 |
| 열린 질문 | 제품 결정 없음. 후속 구현 완료, 실서버·접근성 QA 대기 |
@@ -0,0 +1,22 @@
# 네이티브 날짜 구분선 캡처
- 환경: iPhone SE (3rd generation), iOS 18.3, 375×667pt / PNG 750×1334px.
- 캡처: 실제 SwiftUI 날짜 구분선·formatter·모델·앱 폰트·테마를 직접 컴파일한 임시 네이티브 하네스. `xcrun simctl io screenshot`으로 촬영했다.
- 조합: `ko_KR`, `en_US`, `ja_JP` × `normal`, `accessibility3` × `dark`, `light`, 총 12장.
- 가용 폭: 기존 채팅과 같은 좌우 24pt 여백, 날짜 폭 327pt.
- 최종 소스의 `SwiftUI.Font` 명시 후 다시 컴파일하고 12장 모두 재촬영했다. `production-after.sha256`의 원본 소스 9개와 현재 소스 일치를 확인했다.
- 실제 앱 smoke 실행의 추적 권한 팝업이 남은 중간 캡처는 폐기했다. 작업 소유 시뮬레이터의 실제 앱 제거·재부팅으로 가림을 없애고 다시 촬영한 현재 12장 모두 독립 시각 재검토 PASS다.
- 원본 폰트: Pretendard-Regular, NotoSansJP-Regular. 영어는 시스템 caption. locale별 폰트 등록과 날짜별 프레임을 실행 중 검사했다.
- 날짜 항목과 날짜 계산은 원본 production 코드다. 주변 말풍선·상단 설명은 합성 QA UI이며 실제 `UserCreatorChatRoomView`가 아니다.
- 전체 앱도 별도로 빌드·설치·실행했으나, 이 캡처로 실제 DM 로그인·paging·ACK·VoiceOver·스크롤 연속성의 통과를 주장하지 않는다.
- 실제 채팅은 검정 배경을 사용하므로 light/dark 모두 같은 검정 배경이다. 별도 시각 목업이 없어 pixel-diff의 동일성 목표는 해당 없다.
## 관찰값
| locale | normal 날짜 높이 | accessibility3 날짜 높이 | 결과 |
|---|---:|---:|---|
| ko_KR | 14.5pt | 35pt | 전체 날짜 한 줄 |
| en_US | 14.5pt | 77.5pt | 큰 글자에서 두 줄, 전체 필드 유지 |
| ja_JP | 17.5pt | 42pt | 전체 날짜 한 줄 |
기능 판정·미검증 항목·정리 결과는 [리뷰 리포트](../phase1-chat-date-separators.md)에 기록한다. 임시 앱·시뮬레이터·컴파일 캐시는 증거를 옮긴 뒤 제거한다.
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

@@ -0,0 +1,2 @@
FAIL: Gregorian UTC fixture ko_KR: expected '2026년 10월 16일 금요일', got ''
EXIT_CODE: 1
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -0,0 +1,9 @@
b7428e768adf7fbe2ebb356505594e5ba1325213d2d53aa624651dc230c4d05e /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Components/UserCreatorChatDateSeparatorView.swift
a5c0de9b1b1fdb682708035b1f5fde6e3e2e231a6696ba6f40134caff25b647e /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDatePresentation.swift
34bddad5cb9cc1791fad5b0d3c84461d4d1f536035cdfd2b184a6989330ac80c /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatDisplayMessage.swift
dba97c4663dd64673d79953b5687ef10fbd4cd358e3b2cdcd5c07ab9f26ee5c3 /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/V2/Main/Chat/UserCreatorChat/Models/UserCreatorChatModels.swift
086fb0a19d884124a4cca638f7c8483de724ee31a61603b2cbd489aabf26a6c6 /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/Extensions/FontModifier.swift
747347d86ade888fe9c1d30629285b4a71991bc0f21189a0d4b76ad314c32364 /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/Extensions/ColorExtension.swift
0f50ec7d3e77e0813fd1a806440c01baf96f8fdfa2a41faccd6dc4df52cf647a /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/UI/Theme/Color.swift
372a73b821236c15449fa022cd628f61556f1381a16f234a4b77e833c2e588c1 /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/UI/Theme/Spacing.swift
cbb8e24dc17b352d30adf3a6cd2b190532f059a02801ecabf7392a013914352c /Users/klaus/Develop/sodalive/iOS/SodaLive/SodaLive/Sources/Settings/Language/Models/LanguageOption.swift
@@ -0,0 +1 @@
FAIL: single TEXT message must have its first separatorDate
@@ -0,0 +1,111 @@
# Phase 1 채팅 날짜 구분선 검증 및 리뷰
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 대상 | P1-T1, P1-T2, P1-GATE |
| 기준 | feat/dm-date-separators, 시작 3bd75f1 / formatter e765c3c / 화면 연결 7a6d12e |
| 일자 | 2026-10-16 |
| 기준 문서 | [PRD](../prd.md), [계획](../plan-task.md) |
| 상태 | 코드 검토 승인 / 실제 DM·접근성 Gate 외부 조건 대기 |
## 2. 목적과 범위
DATE-001~008의 날짜 문자열, 현지 날짜 경계, 페이지 병합, ACK 시각 교체, 표시 TEXT 범위와 비고정 UI를 검증한다. 네트워크·인증·기존 메시지 시각·VOICE UI 변경은 제외한다.
## 3. 판정 기준
요구사항 위반과 기존 paging/scroll 회귀는 차단 항목이다. 자동 스냅샷 검사와 실제 서버 QA를 구분하며, 수행하지 못한 확인은 통과로 기록하지 않는다. 구현 결함이 확정되면 수정 전에 계획에 회귀 Task를 추가한다.
## 4. 검토 근거와 환경
- Xcode 26.0 (17A324), macOS, Swift 5 language mode, 최소 iOS 16.6.
- 실제 앱 타깃: SodaLive / SodaLive-dev. XCTest 타깃 없음. 기존 work/tests standalone Swift checker 사용.
- 앱 루트의 locale 주입, DM View의 첫/마지막 메시지 paging·scroll hook, ViewModel의 기존 ACK 교체 로직을 기준으로 한다.
- 현재 도구 세트에 LSP diagnostics와 네이티브 GUI 입력 드라이버가 없다. 컴파일러 검증과 가능한 네이티브 렌더 캡처를 사용하되 동등하지 않은 검증은 구분한다.
## 5. 실행한 검증
| 검증 | 결과 | 근거 |
|---|---|---|
| xcodebuild -version | 통과 | Xcode 26.0, 17A324 |
| xcodebuild -workspace SodaLive.xcworkspace -list -disableAutomaticPackageResolution | 통과 | 두 앱 스킴 확인, exit 0. DVTDeviceOperation build-number 경고 있음 |
| 날짜 formatter / grouping checker | 통과 | 의도한 RED 이후 최종 `PASS: 113 assertions`, exit 0. 재개 후 재실행도 동일 |
| 기존 work/tests 회귀 | 통과 | 기존 11개 스크립트 지원 모드 18회 + 신규 날짜 checker 1회, 최종 19회 모두 exit 0 |
| 두 앱 타깃 빌드 | 통과 | SodaLive generic iOS / SodaLive-dev simulator 최종 빌드 모두 exit 0. CODE_SIGNING_ALLOWED=NO |
| plutil / diff whitespace | 통과 | project.pbxproj OK, git diff --check exit 0 |
| 네이티브 렌더링 | 컴포넌트 한정 통과 | 실제 production 소스·폰트 사용, 3언어×2글자크기×2테마 12장. 최종 소스9개 해시 일치 확인 |
| 실제 앱 설치·실행 | 통과 | 소유된 iPhone SE3/iOS18.3에 개발 앱 설치, launch 성공 및 홈 화면 표시 확인 |
| 실제 DM 서버·paging·ACK QA | 외부 조건 대기 | 안전하게 사용할 테스트 계정과 접근 가능한 roomId가 제공되지 않음 |
## 6. 발견 사항
잔여 확정 코드 결함 없음. Oracle 코드 리뷰는 APPROVE다. 이것은 실서버 통합 및 VoiceOver 상호작용까지 검증한 승인이 아니다.
| ID | 상태 | 내용 | 재개 조건 |
|---|---|---|---|
| QA-001 | 보류 | 실제 DM 대화 진입과 서버 ACK/과거 페이지 검증에는 인증과 테스트 방 필요 | 사용자가 승인한 테스트 계정·roomId 및 날짜가 다른 이력 제공 |
| QA-002 | 보류 | lsp_diagnostics 도구 없음 | native Swift compiler/build 결과와 별도로 도구 가용 시 실행 |
| QA-003 | 보류 | 실제 VoiceOver 읽기와 화면 스크롤 제스처 미실행 | GUI 접근 가능한 기기에서 날짜 1회 읽기·비고정 스크롤 확인 |
| BUILD-001 | 수정 검증 완료 | 새 dateFont의 `Font`가 기존 앱 enum과 충돌, 두 타깃 컴파일 실패 | T2 진행 중 `SwiftUI.Font` 한정 후 두 타깃 동일 명령 exit 0 |
| EVIDENCE-001 | 교정 완료 | 초기 캡처 소스 해시가 최종 폰트 타입 한정 전 소스와 일치 | 최종 소스로 12장 재촬영, 재개 시 소스9개 SHA-256 모두 OK |
| EVIDENCE-002 | 수정 검증 완료 | 실제 앱 smoke 실행의 시스템 추적 권한 팝업이 후속 캡처의 날짜를 가림 | 소유된 시뮬레이터에서 실제 앱 제거·재부팅 후 12장 전체 재촬영, 독립 재검토 12/12 PASS |
## 7. 계획 연결
독립 코드 리뷰에서 신규 회귀 수정이 필요한 발견 사항은 없었다. BUILD-001은 P1-T2 미완료 상태에서 컴파일로 발견해 수정한 항목이다. 필수 실서버 QA가 미완료이므로 P1-GATE를 완료 처리하지 않는다.
## 8. 종료 판정
구현·자동 검사·두 타깃 빌드 완료, 코드 범위 APPROVE. 전체 P1-GATE는 QA-001·003과 LSP 도구 가용성 확인이 남아 BLOCKED다. 테스트 방을 제공받지 않았으므로 개인 토큰을 읽거나 임의 roomId에 접근해 검증을 우회하지 않았다.
## 9. 후속 검증 기록
새 실행 결과와 리뷰 판정은 이 문서에 누적한다.
### 자동 검사 및 빌드
- formatter RED: `expected '2026년 10월 16일 금요일', got ''`, exit 1. rows RED: `FAIL: single TEXT message must have its first separatorDate`. 증거는 [formatter-red.log](evidence/formatter-red.log), [rows-red.log](evidence/rows-red.log).
- GREEN: `sh work/tests/check-user-creator-chat-date-presentation.sh` → 113 assertions, exit 0. [최종 로그](evidence/checker-final.log).
- `plutil -lint SodaLive.xcodeproj/project.pbxproj` → OK. 신규 소스 두 개는 양쪽 앱 타깃에 각각 등록됐다.
- `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -destination 'generic/platform=iOS' -derivedDataPath "$RUN_TMP/standard-build" CODE_SIGNING_ALLOWED=NO build` → 최종 exit 0.
- `xcodebuild -quiet -workspace SodaLive.xcworkspace -scheme SodaLive-dev -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath "$RUN_TMP/simulator-build" CODE_SIGNING_ALLOWED=NO build` → 최종 exit 0.
- 최초 simulator 빌드는 600초 제한으로 중단됐다. 캐시를 유지해 재개한 전체 컴파일이 BUILD-001을 발견했고, 타입 한정 수정 뒤 두 빌드가 성공했다.
- 기존 SDK deprecated/Sendable 경고, 기존 중복 Sources 등록 경고, Firebase dSYM 설정 경고, DVTDeviceOperation 경고는 변경 범위 밖이다. 이번 작업에서 임의 수정하지 않았다.
최종 회귀 실행 목록(모두 exit 0):
| 스크립트 | 실행 모드 |
|---|---|
| check-app-deep-link-gift-detail.sh | 기본 |
| check-auth-content-settings-exit.sh | 기본 |
| check-auth-verification-mock.sh | 기본, 내부 Debug/Release 심볼 검사 포함 |
| check-gift-address-search.sh | 기본, baseline |
| check-gift-contact-input.sh | 기본 |
| check-gift-decoding-contract.sh | 기본 |
| check-gift-detail-cancel-loading.sh | 기본 |
| check-gift-detail-talk-inquiry.sh | 기본 |
| check-gift-phase11.sh | 기본 all, profile, policy, button |
| check-gift-recipient-address.sh | 기본 |
| check-received-gift-detail.sh | 기본 privacy, foundation, recipient-form, received-states |
| check-user-creator-chat-date-presentation.sh | 기본, 113 assertions |
모든 스크립트는 `bash work/tests/이름.sh 모드`로 실행했다. 기본 모드는 인자를 생략했다. Auth 검사에 사용되는 HOME·CFFIXED_USER_HOME 및 compiler cache는 소유된 임시 폴더로 격리했다. 기존 스크립트/fixture21개는 기준선 단계 SHA-256 불변 확인, 최종 git diff에서도 변경 없음이다.
### 네이티브 증거 범위
- [캡처 매트릭스와 관찰값](evidence/README.md): 12개 PNG와 [원본 소스 해시](evidence/production-after.sha256)를 보존한다.
- 실제 `UserCreatorChatDateSeparatorView`, formatter, 모델, `FontModifier`, 테마·간격, LanguageOption을 직접 컴파일했다. I18n shim은 사용하지 않는 언어 설정 이름 네 개뿐이다. 날짜 코드를 대체하지 않았다.
- Native harness는 UIKit 윈도우에 SwiftUI를 올리고 `simctl io screenshot`으로 촬영했다. 원본 폰트 등록·날짜 프레임 폭327pt·3개 합성 메시지/2개 날짜 구분을 실행 중 확인했다.
- 큰 영어 날짜는 두 줄로 확장하고, 한·일 날짜는 한 줄이다. 날짜 잘림·대체 글리프는 관찰되지 않았다. 실제 채팅 배경과 동일한 검정 배경을 light/dark 모두 유지한다.
- 주변 메시지와 상단 QA 설명은 합성 UI다. 실제 RoomView의 bottom anchor, 페이지 추가, ACK 이동 및 VoiceOver 조작을 검증했다고 주장하지 않는다.
- 이전 독립 코드/시각 리뷰는 코드 APPROVE 및 컴포넌트 한정 PASS였다. 최종 소스 해시와 불일치했던 초기 캡처는 최종 증거로 사용하지 않고 재촬영했다.
- 재개 후 독립 검토는 새 캡처 12장에 실제 앱의 추적 권한 팝업이 남아 날짜가 가려진 증거 결함을 발견했다. 앱 프로세스 종료만으로 팝업이 사라지지 않아, 사용자가 아닌 이 작업 소유의 시뮬레이터에서 실제 앱을 제거하고 재부팅했다. 권한 허용을 임의 선택하지 않았다. 팝업 없는 영어 큰 글자 probe를 직접 확인한 뒤 같은 하네스의 12장을 모두 다시 촬영했다. 제품 소스는 추가 변경하지 않았다.
- 최종 독립 재검토(`ses_eae4fc5c2ffe9q4b4Mh24gmGn7`): 수정된 PNG 12/12를 직접 다시 열고 모든 프레임의 두 날짜가 온전하게 보임을 확인해 컴포넌트 시각 PASS 판정. 잘림·말줄임·깨진 문자·고립 음절·합성 말풍선과의 겹침 없음. 잔여 시각 차단 사항과 제품 코드 수정 요구 없음. 실제 RoomView·paging·ACK·VoiceOver는 승인 범위 밖이다.
### 자원 정리
- 하네스 앱은 촬영 스크립트 종료 시 terminate했다. 실제 개발 앱은 팝업 복구 과정에서 작업 소유 시뮬레이터에서만 제거했다.
- 소유된 simulator `809AF3D0-7644-4AF0-A773-4E0E487406A4`를 shutdown/delete한 뒤 custom set의 devices 목록에 기기가 없음을 확인했다. `pgrep -x NativeDateQA`도 프로세스 없음을 확인했다.
- 최종 리뷰 결과와 캡처·검사 증거를 저장소에 보존한 뒤 작업 전용 `dm-run.OwYrK5` 전체(두 DerivedData, checker 캐시, 하네스 앱/스크립트, custom simulator set)를 제거했다. 경로 부재 검사 PASS, 관련 xcodebuild/swift-frontend/NativeDateQA 실행 프로세스 없음. 작업 메모도 삭제했다. 사용자 시뮬레이터·공유 캐시는 제거하지 않았다.