feat(fantalk): 쓰기 입력 제한을 적용한다

This commit is contained in:
Yu Sung
2026-08-25 17:36:44 +09:00
parent 425099955b
commit cb3778a76f
5 changed files with 166 additions and 6 deletions

View File

@@ -38,6 +38,10 @@
- 팬Talk 쓰기 화면은 크리에이터 채널 팬Talk 탭의 우측하단 plus button, empty state의 `응원남기기` 버튼, 내가 쓴 글의 `수정하기`에서 진입한다.
- 팬Talk 쓰기 화면 진입 시 키보드가 올라온 상태를 기본으로 하며, 입력 값 유무에 따라 보내기 버튼 disabled/enabled 상태를 분리한다.
- 입력 값이 있는 상태에서 취소 버튼을 터치하면 입력 종료 안내 모달을 표시한다. 입력 값이 없으면 별도 모달 없이 이전 화면으로 닫는다.
- 취소 문구의 고정 `width: 64`, `height: 56`만 제거하고 내비게이션 높이 `56`과 보내기 capsule 높이 `44`는 유지한다.
- 입력 영역은 `I18n.CreatorChannelFanTalk`의 ko/en/ja placeholder를 사용한다.
- 입력은 Swift `String.count` 기준 500자까지 허용하고, 501번째 이후 사용자 입력은 `prefix(500)`으로 반영하지 않는다.
- 예외적으로 content가 500자를 초과하면 메시지 본문과 현재 글자 수를 `Color.red400`으로 표시하고 보내기 버튼을 비활성화한다.
- 공용 모달은 요청 명칭상 `V2/Components` 대상이지만, 현재 저장소 경로 관례에 맞춰 `SodaLive/Sources/V2/Component/Modal/**` 아래에 작성한다.
## 주요 대상 파일
@@ -489,3 +493,95 @@
- 2026-08-14: 사용자 인터뷰로 팬레터 탭 empty 문구와 작성 버튼의 ko/en/ja 값을 확정하고 Phase 11에 미완료 후속 Task로 추가했다. 기존 작성 route와 내 채널 버튼 숨김 규칙은 유지하며 이번 작업은 문서만 보강했다.
- 2026-08-14: Phase 11 구현을 완료했다. 팬레터 탭 empty 문구와 작성 버튼을 ko/en/ja I18n으로 교체하고 기존 작성 route 및 내 채널 버튼 숨김 규칙을 유지했다. 사용자 후속 요청에 따라 작성 버튼은 `Color.gray800` 배경과 white 아이콘·텍스트로 변경했다. 23개 정적 계약, `git diff --check`, iPhone 16 Pro 시뮬레이터 Debug 빌드가 통과했고, 세 언어 재캡처와 두 독립 색상·CJK 리뷰에서 차단 항목 없이 PASS를 확인했다.
### Phase 12: 팬Talk 쓰기 입력 UX 보강
**Phase 결과:** 팬Talk 쓰기 화면에서 취소 문구가 잘리지 않고, 다국어 placeholder와 500자 입력 제한 및 초과 예외 상태가 일관되게 동작한다.
**선행조건:** PRD `§5.10`, `DEC-FANTALK-WRITE-001~003` 확정.
**Phase 완료 조건:** `P12-T1`, `P12-T2`, `P12-GATE` 완료와 검증 기록 누적.
#### Task 12.1 다국어 placeholder와 입력 상태 계약
**Goal 실행 `P12-T1`:** placeholder 문구와 500자 제한에 필요한 ViewModel 상태를 기존 타입에 최소 추가한다.
- **시작 조건:** Phase 12 선행조건 충족.
- **완료 증거:** ko/en/ja 문구, 500자 허용 경계, 초과 상태 및 전송 비활성화 조건이 코드와 정적 검사에서 확인된다.
- **범위 밖:** 신규 입력 컴포넌트, dependency, route, API 변경.
- **TDD 예외 사유:** 현재 프로젝트에 unit test bundle target이 없으며 이번 범위에서 신규 test target을 추가하지 않는다.
- **대체 검증 방법:** `rg` 정적 계약 검사, Swift 표준 문자열 경계 확인, Phase Gate의 앱 빌드와 수동 입력 검증을 사용한다.
**Files:**
- Modify: `SodaLive/Sources/I18n/I18n.swift`
- Modify: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift`
**Interfaces:**
- Consumes: 기존 `I18n.pick(ko:en:ja:)`, `content`, `maxContentCount`, `canSend`.
- Produces: `I18n.CreatorChannelFanTalk.writePlaceholder`, `content.count > maxContentCount` 초과 상태, 초과 상태를 포함한 `canSend`.
- [x] `I18n.CreatorChannelFanTalk.writePlaceholder`에 한국어 `응원 메시지를 입력해 주세요.`, 영어 `Enter a message of support.`, 일본어 `応援メッセージを入力してください。`를 추가한다.
- [x] `maxContentCount``500`을 유지하고 500자는 유효, 500자 초과만 예외인 상태를 제공한다.
- [x] `canSend`는 기존 공백·줄바꿈 검증에 더해 content가 500자를 초과하면 `false`가 되게 한다.
- [x] 수정 모드의 초기 content는 임의로 자르지 않아 500자 초과 예외 상태를 화면에서 판정할 수 있게 한다.
- [x] `rg -n "writePlaceholder|maxContentCount|content.count|canSend|whitespacesAndNewlines" SodaLive/Sources/I18n/I18n.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift`로 상태 계약을 확인한다.
#### Task 12.2 내비게이션·placeholder·500자 UI 적용
**Goal 실행 `P12-T2`:** 기존 쓰기 View 안에서 취소 문구 잘림을 제거하고 입력 제한과 초과 표시를 연결한다.
- **시작 조건:** `P12-T1` 완료.
- **완료 증거:** View 정적 검사와 ko/en/ja 및 499·500·501자 수동 검증 통과.
- **범위 밖:** 내비게이션 전체 높이, 보내기 capsule 높이, `/500` 색상, 작성·수정 API 흐름 변경.
- **TDD 예외 사유:** 현재 프로젝트에 UI test bundle target이 없으며 이번 범위에서 신규 test target을 추가하지 않는다.
- **대체 검증 방법:** View 정적 계약 검사와 Phase Gate의 시뮬레이터 수동 검증을 사용한다.
**Files:**
- Modify: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift`
**Interfaces:**
- Consumes: `P12-T1``writePlaceholder`, `maxContentCount`, 초과 상태, `canSend`.
- Produces: 500자로 제한된 `TextEditor` binding과 정상·초과 입력 UI 상태.
- [x] 취소 `Text``.frame(width: 64, height: 56, alignment: .leading)`만 제거한다.
- [x] 내비게이션 `.frame(height: 56)`과 보내기 capsule `.frame(height: 44)`는 유지한다.
- [x] content가 비어 있을 때 `writePlaceholder`를 표시하고 입력이 시작되면 숨긴다.
- [x] `TextEditor`가 전달한 새 content는 `String(newValue.prefix(viewModel.maxContentCount))`까지만 반영해 타이핑과 붙여넣기 모두 500자를 넘지 않게 한다.
- [x] ViewModel에 이미 500자를 초과한 content가 있으면 메시지 본문과 현재 글자 수를 `Color.red400`으로 표시한다.
- [x] 500자 초과 상태에서는 `canSend == false`에 따라 보내기 버튼을 비활성화하고 `/500`은 기존 `Color.gray500`을 유지한다.
- [x] 기존 focus, 키보드, 취소 모달, 작성·수정 submit 흐름은 변경하지 않는다.
- [x] `rg -n "writePlaceholder|prefix\(viewModel.maxContentCount\)|Color.red400|frame\(width: 64, height: 56|frame\(height: 56\)|frame\(height: 44\)" SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift`로 추가·유지·제거 계약을 확인한다.
#### Phase 12 Gate
**Goal 실행 `P12-GATE`:** Phase 12 요구사항과 기존 작성·수정 흐름의 회귀 여부를 최종 판정한다.
- **시작 조건:** `P12-T1`, `P12-T2` 완료.
- **완료 증거:** 아래 자동·수동 검증 통과와 실제 결과를 검증 기록에 누적.
- **범위 밖:** Gate 통과를 위한 기존 검증 완화나 관련 없는 소스 수정.
```bash
rg -n "응원 메시지를 입력해 주세요|Enter a message of support|応援メッセージを入力してください|writePlaceholder" SodaLive/Sources/I18n/I18n.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift
rg -n "maxContentCount|content.count|prefix\(viewModel.maxContentCount\)|Color.red400|canSend" SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift
git diff --check -- SodaLive/Sources/I18n/I18n.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteView.swift SodaLive/Sources/V2/CreatorChannel/FanTalk/Write/CreatorChannelFanTalkWriteViewModel.swift docs/20260704_크리에이터_채널_팬Talk_탭/prd.md docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md
xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build
```
**Expected:** 다국어 문구와 500자 경계 및 초과 상태 계약이 확인되고, 공백 오류 없이 `** BUILD SUCCEEDED **`가 출력된다.
수동 검증:
- [x] ko/en/ja에서 취소 문구가 잘리지 않고 placeholder가 정확히 표시된다.
- [x] 499자와 500자는 입력되며 유효 content일 때 보내기 버튼이 활성화된다.
- [x] 501번째 타이핑 문자와 500자를 초과하는 붙여넣기 문자는 반영되지 않고 글자 수가 500을 넘지 않는다.
- [x] 수정 초기값 등으로 500자를 초과한 예외 상태를 주입하면 메시지 본문과 현재 글자 수가 `Color.red400`으로 표시되고 보내기 버튼이 비활성화된다.
- [x] 공백·줄바꿈만 입력한 상태, 취소 모달, 작성·수정 submit 흐름이 기존과 동일하다.
- 2026-08-25: 사용자 인터뷰에서 500자 허용·501번째 이후 입력 차단, 500자 초과 예외 상태의 본문·현재 글자 수 붉은 표시와 보내기 비활성화, 취소 문구의 고정 크기만 제거하는 범위를 확정했다. PRD와 Phase 12 계획만 추가했으며 Swift 구현과 빌드 검증은 수행하지 않았다.
- 2026-08-25: Phase 12 구현을 완료했다. `writePlaceholder`, 500자 제한 binding, 500자 초과 상태와 전송 비활성화, 취소 문구의 고정 frame 제거를 적용했으며 기존 내비게이션·보내기 높이와 작성·수정 흐름은 유지했다.
- 2026-08-25: Phase 12의 두 `rg` 정적 계약 검사, Swift `String.count`/`prefix(_:)` 499·500·501자 경계 검사, `git diff --check`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행했고 `** BUILD SUCCEEDED **`를 확인했다.
- 2026-08-25: 사용자 요청에 따라 visual QA와 시뮬레이터 수동 검증은 수행하지 않았다. ko/en/ja 표시와 실제 입력 동작에 관한 수동 검증 체크박스 5개는 사용자 확인 전까지 미완료로 유지한다.

View File

@@ -27,6 +27,9 @@ Figma 참조:
- 팬레터 탭 empty 문구와 작성 버튼이 한국어로 하드코딩되어 앱 언어 설정에 맞게 표시되지 않는다.
- 내가 쓴 팬Talk의 `수정하기` 액션이 실제 수정 화면으로 연결되지 않는다.
- V2 화면에서 재사용할 수 있는 Figma 기준 액션 모달이 없어 입력 종료 안내 모달을 일관되게 표시하기 어렵다.
- 팬Talk 쓰기 화면의 취소 문구가 고정된 가로·세로 크기에 배치되어 언어에 따라 글자가 잘릴 수 있다.
- 팬Talk 쓰기 입력 영역에 placeholder가 없어 사용자가 입력 목적을 바로 알기 어렵다.
- 화면에 최대 글자 수 `500`이 표시되지만 실제 입력과 전송 가능 상태에는 상한이 적용되지 않는다.
## 3. Goals
- `CreatorChannelTab.fanTalk` 선택 시 팬Talk 탭 API를 호출하고 응답 데이터로 화면을 구성한다.
@@ -48,6 +51,10 @@ Figma 참조:
- 입력 종료 안내 모달은 V2 공용 액션 모달로 구현해 제목, 안내 문구, 버튼 1, 버튼 2의 label/action을 바꿔 재사용할 수 있게 한다.
- 팬Talk 작성과 수정은 이번에 작성하는 같은 쓰기 페이지에서 수행한다.
- 작성/수정 API는 기존 팬Talk API인 `ExplorerRepository.writeCheers``ExplorerRepository.modifyCheers`를 사용한다.
- 팬Talk 쓰기 화면의 취소 문구는 고정 `width: 64`, `height: 56`을 제거해 ko/en/ja에서 잘리지 않게 표시한다.
- 팬Talk 입력 영역은 ko/en/ja placeholder를 표시한다.
- 사용자는 500자까지 입력할 수 있고 501번째 이후 입력은 반영되지 않는다.
- 예외적으로 500자를 초과한 content가 주입되면 메시지 본문과 현재 글자 수를 `Color.red400`으로 표시하고 보내기 버튼을 비활성화한다.
## 4. Non-Goals
- 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다.
@@ -201,6 +208,19 @@ data class CreatorChannelFanTalkReplyResponse(
- 키보드가 내려가도 입력 값과 보내기 버튼 활성화 상태는 유지한다.
- 입력 값이 없는 상태에서 취소 버튼을 터치하면 이전 화면으로 닫는다.
- 입력 값이 있는 상태에서 취소 버튼을 터치하면 입력 종료 안내 모달을 표시한다.
- 상단 내비게이션의 취소 `Text`에 적용된 고정 `width: 64`, `height: 56`만 제거한다.
- 상단 내비게이션 전체 높이 `56`과 보내기 capsule 높이 `44`는 유지한다.
- content가 비어 있을 때 입력 영역에 아래 placeholder를 표시한다.
- 한국어: `응원 메시지를 입력해 주세요.`
- 영어: `Enter a message of support.`
- 일본어: `応援メッセージを入力してください。`
- placeholder는 `I18n.CreatorChannelFanTalk`에서 ko/en/ja 값으로 관리하고 작성·수정 모드의 빈 content에 동일하게 적용한다.
- 글자 수는 Swift `String.count` 기준이며 최대 500자까지 허용한다.
- 사용자가 타이핑하거나 붙여넣어 500자를 초과하면 첫 500자만 content에 반영해 501번째 이후 문자는 입력되지 않게 한다.
- 500자인 content는 유효하며, 공백·줄바꿈만 있는 기존 비활성화 조건은 유지한다.
- 수정 초기값이나 외부 상태에서 500자를 초과한 content가 주입될 가능성은 별도 예외 상태로 처리한다.
- 예외 상태에서는 보내기 버튼을 비활성화하고, 입력 메시지 본문과 현재 글자 수를 `Color.red400`으로 표시한다.
- 예외 상태에서도 `/500` 최대 글자 수 표시는 기존 `Color.gray500`을 유지한다.
### 5.11 Reusable V2 action modal
- 입력 종료 안내 모달은 Figma `308:19046` 기준으로 표시한다.
@@ -246,6 +266,11 @@ data class CreatorChannelFanTalkReplyResponse(
- 수정 모드 진입 시 수정 대상 `fanTalkId`와 기존 글 content가 함께 전달된다.
- 수정 모드는 선택한 fanTalk의 기존 content를 입력값으로 표시한다.
- 수정 성공 후 팬Talk 탭 첫 페이지가 다시 조회된다.
- ko/en/ja에서 취소 문구가 잘리지 않고, 내비게이션 높이 `56`과 보내기 capsule 높이 `44`는 유지된다.
- content가 비어 있으면 앱 언어에 맞는 지정 placeholder가 표시되고 입력을 시작하면 사라진다.
- 500자는 입력·전송할 수 있고, 501번째 이후 타이핑과 붙여넣기 문자는 content에 반영되지 않는다.
- 예외적으로 500자를 초과한 content에서는 보내기 버튼이 비활성화되고 메시지 본문과 현재 글자 수가 `Color.red400`으로 표시된다.
- 공백·줄바꿈만 있는 content는 글자 수와 관계없이 보내기 버튼이 비활성화된다.
## 7. Technical Constraints
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
@@ -260,6 +285,9 @@ data class CreatorChannelFanTalkReplyResponse(
- `CheersReportDialogView`는 신고 사유 선택 전용 UI이므로 이번 확인형 V2 popup dialog 전환 범위에서 제외한다.
- 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
- Empty 문구와 `응원남기기` 버튼은 View에 직접 작성하지 않고 기존 크리에이터 채널 I18n 구조의 `pick(ko:en:ja:)` 값으로 관리한다.
- 쓰기 placeholder도 View에 직접 작성하지 않고 `I18n.CreatorChannelFanTalk``pick(ko:en:ja:)` 값으로 관리한다.
- 글자 수 제한은 Swift 표준 `String.count``prefix(_:)`를 사용하며 신규 dependency나 공용 입력 컴포넌트를 추가하지 않는다.
- 팬Talk 작성·수정 route와 API 계약은 변경하지 않는다.
- 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다.
## 8. Assumptions
@@ -284,3 +312,16 @@ data class CreatorChannelFanTalkReplyResponse(
- 2026-07-07: 사용자 후속 요청에 따라 팬Talk row 더보기의 삭제 확인 dialog는 팬Talk 탭 내부가 아니라 `CreatorChannelView` 상위 wrapping `ZStack`에서 표시한다. 삭제 확인 UI는 이번에 추가된 V2 공용 모달 `SodaV2ActionModal`을 사용하며, 팬Talk 리스트와 상세보기의 anchored context popup은 동일한 공용 컴포넌트와 동일한 `I18n` 문구를 사용한다. 신고 사유 선택 UI인 `CheersReportDialogView`는 기존 신고 플로우 재사용 범위로 유지하고 이번 확인형 V2 popup dialog 전환 범위에서 제외한다.
- 2026-08-14: 사용자 인터뷰로 팬레터 탭 empty 문구와 `응원남기기` 버튼의 ko/en/ja 값을 확정했다. 기존 작성 route와 내 채널 버튼 숨김 규칙은 유지하며 이번 작업에서는 Swift 구현을 수행하지 않는다.
- 2026-08-14: 사용자 요청에 따라 팬레터 탭 empty 작성 버튼은 `Color.gray800` 배경과 white 아이콘·텍스트를 사용하도록 확정했다. 홈 후원·팬레터 empty 버튼은 white/black, 후원 탭 empty 버튼은 기존 `Color.soda400`/white를 유지한다.
- 2026-08-25: 사용자 인터뷰로 500자까지 허용하고 501번째 이후 사용자 입력을 차단하며, 예외적인 500자 초과 상태에서는 메시지 본문과 현재 글자 수를 모두 붉게 표시하고 보내기 버튼을 비활성화하기로 확정했다.
- 2026-08-25: 상단 내비게이션에서는 취소 문구의 고정 `width: 64`, `height: 56`만 제거하고 내비게이션 높이 `56`과 보내기 capsule 높이 `44`는 유지하기로 확정했다.
- 2026-08-25: 기존 `CreatorChannelFanTalkWriteView`, `CreatorChannelFanTalkWriteViewModel`, `I18n.CreatorChannelFanTalk`을 확인했으며, 이번 작업에서는 요구사항 문서만 갱신하고 Swift 구현은 수행하지 않았다.
- 2026-08-25: Phase 12를 구현해 ko/en/ja placeholder, 500자 입력 제한, 500자 초과 예외 표시·전송 비활성화, 취소 문구의 고정 frame 제거를 적용했다. `§5.10`과 성공 기준의 관련 항목을 코드 및 정적 계약과 대조해 누락이 없음을 확인했다.
- 2026-08-25: Swift 499·500·501자 경계 검사, `git diff --check`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행했고 `** BUILD SUCCEEDED **`를 확인했다. 사용자 요청에 따라 visual QA와 시뮬레이터 수동 검증은 수행하지 않았다.
## 11. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계획 |
|---|---|---|---|---|---|
| 2026-08-25 | `DEC-FANTALK-WRITE-001` | 확정 | 500자까지 허용하고 501번째 이후 사용자 입력은 반영하지 않는다. 이미 500자를 초과한 content는 별도 예외 상태로 유지한다. | 사용자 인터뷰 A안 선택 | `§5.10`, Phase 12 |
| 2026-08-25 | `DEC-FANTALK-WRITE-002` | 확정 | 500자 초과 예외 상태에서는 메시지 본문과 현재 글자 수를 `Color.red400`으로 표시하고 보내기 버튼을 비활성화한다. | 사용자 인터뷰 C안 선택 | `§5.10`, Phase 12 |
| 2026-08-25 | `DEC-FANTALK-WRITE-003` | 확정 | 취소 문구의 고정 `width: 64`, `height: 56`만 제거하고 내비게이션 높이 `56`과 보내기 capsule 높이 `44`는 유지한다. | 사용자 인터뷰 A안 선택 | `§5.10`, Phase 12 |