서버가 이미 모든 메시지를 DB에 durable하게 저장하므로 별도 큐를 새로
만들지 않고, 연결이 끊겼던 클라이언트가 그 DB에서 놓친 부분만 다시
받아오는 방식으로 풀었다.
- core-backend: GET /conversations/:id/messages?since_id=<id> 추가
(파라미터 없으면 기존 전체 히스토리 그대로, 비정상 값은 400, 최댓값
초과는 빈 배열). TestListMessagesSinceID로 확인.
- mobile/ws_client.dart: 소켓 onDone/onError에서 그냥 멈추던 것을
backoff(1s→2s→4s→8s→...→30s 캡, 성공 시 리셋) 재연결로 교체하고,
재연결 성공 신호를 reconnects 스트림으로 노출.
- mobile/chat_screen.dart: WidgetsBindingObserver를 추가해 소켓
재연결 신호 + didChangeAppLifecycleState(resumed) 양쪽에서 since_id
캐치업을 호출, 기존 메시지와 id 기준으로 중복 없이 병합.
- mobile/services/message_sync.dart: backoff 계산과 중복 없는 병합을
순수 함수로 뽑아 message_sync_test.dart에서 결정적으로 검증.
실제 소켓 재연결 타이밍이나 앱 백그라운드/포그라운드 전환에서 OS가
소켓을 어떻게 처리하는지는 단위 테스트로 증명할 수 없어 실기기
Android QA가 남아 있음 — roadmap.md/deploy-checklist.md N4-11에
부분 검증으로 기록.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YSB5PqF38raTxP5ABgr9m
roadmap.md §2.7-F / deploy-checklist.md N4-C6a~c 대응. "이따 답장"을 누르면
대화방별 스누즈 시각을 순수 온디바이스 drift 테이블(ConversationSnoozes)에
저장하고(서버 전송 없음, 다른 사람에게 절대 보이지 않음), chat_screen.dart
앱바에서 빠른 선택(1시간 후/저녁에/내일)으로 걸고 해제할 수 있게 했다. 실제
답장을 보내면 자동으로 스누즈가 풀린다.
로컬 알림은 flutter_local_notifications+timezone을 실제로 붙여
zonedSchedule()/cancel()을 호출하지만, 이 샌드박스에는 실기기·에뮬레이터가
없어 알림이 실제로 울리는지는 검증하지 못했다 — 검증한 건 스케줄러 호출
인자(id/시각/페이로드)가 목(mock) 기준으로 맞는지뿐이다. 그 대신 항상 켜져
있는 인앱 대체 경로로 conversation_list_screen.dart 배지와 chat_screen.dart
배너가 마감 지난 스누즈를 보여주고 바로 해제할 수 있게 했다. 웹 빌드는
플러그인이 웹을 지원하지 않아 no-op 스텁으로 대체.
마감 판정(isSnoozePastDue)과 빠른 선택 시각 계산은 순수 함수로 분리해
고정 시각으로 단위 테스트했고, SnoozeController를 통해 스케줄러를 목으로
바꿔 넣어 호출 인자도 검증했다. 백엔드(core-backend/ai-service) 변경 없음.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YSB5PqF38raTxP5ABgr9m
PRD.md §3.1 P0 "자율성 설정(L0~L2) | 전역 기본값 + 상대별 예외 설정" / §4 엣지케이스
대비 미구현 갭 해소. 관계별 페르소나(2.7-B)의 RelationshipTier 오버라이드 패턴을
그대로 미러링:
- core-backend/models.go: Contact.AutonomyLevel *AutonomyLevel 추가(nil = 전역
기본값), validAutonomyLevel() 검증 헬퍼 추가(twin-settings PATCH의 인라인 검증도
이 헬퍼로 통일)
- core-backend/autonomy_resolve.go: resolveAutonomyLevel() 신규 — 연락처 오버라이드
(1:1 전용) → 전역 TwinSettings 기본값 → L0 순으로 해석. L1/L2가 아니라 L0으로
폴백하는 이유는 이 코드베이스 전반의 안전 우선 기본값과 동일(불확실하면 항상 초안만
생성, 사람이 직접 발송). 그룹 대화는 RelationshipTier와 동일하게 항상 전역 기본값만
사용 — main.go의 그룹 대화 무조건 차단과 이중으로 안전
- core-backend/main.go: POST /conversations/:id/messages의 자율성 게이트가
TwinSettings 전역값만 읽던 걸 resolveAutonomyLevel() 호출로 교체. peer-veto→그룹
차단→도배 감지→에스컬레이션 순서와 각 하드게이트는 그대로 유지, "level" 계산
방식만 바뀜
- core-backend/a1_a2_routes.go: createContactRequest/updateContactRequest에
AutonomyLevel 필드 추가, contactJSON()에 포함, 생성/수정 핸들러가 RelationshipTier와
동일한 전체 교체(full-replace) 시맨틱으로 처리(PATCH에서 필드 생략 시 nil로 리셋)
- core-backend/autonomy_resolve_test.go: 연락처 오버라이드 우선순위, 전역 기본값
폴백, 그룹 대화는 오버라이드 무시, 잘못된 값 검증 거부, PATCH 전체 교체 리셋 커버
- mobile: models.dart에 Contact.autonomyLevel(nullable) 추가, api_client.dart
createContact/updateContact에 선택적 autonomyLevel 파라미터 스레딩,
contacts_screen.dart에 _AutonomyLevelPicker(_RelationshipTierPicker와 동일 구조)
추가해 추가/수정 다이얼로그에 배치 + 연락처 목록 서브타이틀에 표시
ai-service는 변경 없음 — 자율성 레벨은 발송 게이트 로직일 뿐 초안 톤에 영향을
주지 않아 ai-service 프롬프트까지 전달될 필요가 없음.
테스트: go test ./... 61개 전부 PASS, flutter analyze/test 클린(기존 무관 info
린트 1건 제외).
docs/roadmap.md §2.7-D, docs/deploy-checklist.md N4-C4a/b/c를 done으로 갱신하고
NOW/바로 다음 5개 요약도 동기화.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YSB5PqF38raTxP5ABgr9m
PRD.md §4 엣지케이스가 "안전 관련이라 v1 최소 버전 필요"로 명시한 항목(P0)인데
관련 로직이 0건이었던 마지막 콘텐츠 갭(roadmap.md §2.7-C, deploy-checklist.md
N4-C3a/b)을 채운다. Track C(콘텐츠 갭) A/B/C 전체 완료.
- core-backend/flood_detect.go(신규): floodMessageThreshold=5건 /
floodWindow=2분 상수 + floodDetected()/floodReason(). 안전을 위해 반드시
있어야 하는 기술적 최소값이라 명시적으로 placeholder로 남기고 구현했다 —
roadmap.md §3의 "PoC 결과가 있어야 정할 수 있는 것"(자율성 기본값 등 UX
기본값)과는 성격이 다름. 카운트는 대화방 내 sender_id != 소유자인 메시지만
집계(트윈 자동발송·소유자 본인 발송은 소유자 ID로 남으므로 자동 제외).
- core-backend/main.go: POST /conversations/:id/messages 하드게이트에
peer-veto → 그룹 대화 차단 다음, 에스컬레이션 체크 이전 지점으로 추가 —
트윈 자동발송 시도가 전부 지나가는 동일한 우회 불가 지점. 이미
TwinDisabledByFlood가 켜져 있으면 재계산 없이 즉시 차단(중복 로그/쿼리
방지). 새로 임계치를 넘기면 대화방을 영구 차단하고 EscalationLog 기록 +
notifyUser 푸시.
- core-backend/models.go: Conversation.TwinDisabledByFlood 필드 추가.
TwinDisabledByPeer(거부권, 사람의 일방적 선택 — v1엔 되돌리기 API 없음)와
달리 이건 시스템이 자동으로 취하는 조치라서 AGENTS.md "every automatic
action needs post-hoc notification + one-tap undo"가 그대로 적용됨 —
POST /conversations/:id/flood-reset로 되돌릴 수 있게 별도 필드로 분리.
- core-backend/a1_a2_routes.go: GET /conversations 응답에
twin_disabled_by_flood 추가(twin_disabled_by_peer와 동일한 자리).
- core-backend/flood_detect_test.go(신규): 임계치 경계값(정확히 N건은
통과, N+1건은 차단), 사람 발송은 게이트 안 걸림, 윈도우 밖 과거 메시지는
집계 제외, flood-reset으로 재개, 존재하지 않는 대화방 404 — 5개 테스트.
- mobile/lib/models/models.dart, services/api_client.dart: 모델·API
클라이언트에 twinDisabledByFlood/resetFlood() 추가.
- mobile/lib/screens/conversation_list_screen.dart: 대화 목록에 도배 차단
배지(거부권과 같은 자리, 다른 아이콘/문구).
- mobile/lib/screens/chat_screen.dart: 채팅방을 열면(목록에서 넘어온 초기
상태) 또는 발송 시도가 다시 차단되면 배너에 "자동응대 재개" 버튼을 보여줌
— one-tap undo. 새 알림 메커니즘을 만들지 않고 기존 EscalationLog/
InboxScreen을 그대로 재사용.
- mobile/test/models_test.dart: twin_disabled_by_flood 파싱 테스트 추가.
- docs/roadmap.md §2.7-C, docs/deploy-checklist.md N4-C3a/b·Track C 요약·
"바로 다음 5개"를 완료로 갱신.
테스트: go test ./... 51/51, python3 -m pytest tests/ -q 47/47,
flutter analyze 클린 + flutter test 5/5 모두 통과.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YSB5PqF38raTxP5ABgr9m
- Resolve visual-direction conflicts (app_theme, main.dart, signup/onboarding
screens, index.html) in favor of the soft-gradient + glassmorphism design;
drop the competing Twin Shadow palette and TwinTokens usage.
- Port non-visual additions from the parallel branch: CORS middleware,
FlutterError/PlatformDispatcher crash handlers + boot timeout/fallback
screen in main.dart, and the shared demo-invite feature
(core-backend/demo.go, demo_test.go, mobile/lib/config.dart), reskinning
the demo panel to match the glass UI.
- Rename the shared demo invite code DEMO-BUNSIN -> DEMO-YKAVU on both
client and server so the tester-facing feature keeps working.
- Purge remaining "분신"/"bunsin" identifiers app-wide: Dart package name
(bunsin_mobile -> ykavu_mobile), Android applicationId/namespace
(com.bunsin.bunsin_mobile -> com.ykavu.ykavu_mobile, incl. Kotlin source
dir move), on-device DB filename, keystore alias/docs, PoC draft-generator
system prompt, and the static web boot placeholder div.
명칭 변경:
- decision-log.md Q6 확정: "분신"(가칭) → "와카뷰 (Ykavu)" — 焚身(분신자살)
동음이의 리스크도 있었고 메신저 브랜드로 부르기 무거웠음. Master가 최종
선택한 이름으로 변경(2026-07-31), 파생 문서(AGENTS.md/CLAUDE.md/PRD 등)·
Flutter 앱 텍스트·web manifest/index.html·AndroidManifest 라벨까지 전부 반영
- ai-service: 본인확인 고정 문구(identity.py)·시스템 프롬프트(generation.py)도
갱신 — 새 이름 기준으로 "본인이야 와카뷰야?" 류 질문을 감지하도록 정규식도
같이 손봄(단순 문자열 치환만으론 어미 형태가 안 맞아서 테스트 추가/조정)
- poc/tone-corpus/의 실험용 프롬프트는 의도적으로 그대로 둠(ai-service README에
이미 명시된 대로 프로덕션과 분리된 실험 도구)
UI/UX:
- mobile/lib/theme/app_theme.dart: 시드 컬러를 인디고/라벤더로 변경, 화면 전체에
깔리는 소프트 그라디언트(라이트: 라벤더→스카이→핑크 파스텔, 다크: 딥 인디고→
네이비→플럼) + 카드/인풋/칩을 반투명 "글래스" 서피스로 전환
- mobile/lib/widgets/gradient_backdrop.dart(신규): MaterialApp.builder에 연결해
모든 화면에 자동으로 그라디언트 배경 적용
- mobile/lib/screens/splash_screen.dart(신규) + main.dart 재구성: 기존엔
session.restore()를 기다리는 동안 아무 것도 안 그려서 흰 화면/텍스트만 뜨는
구간이 있었음 — runApp을 먼저 하고 restore 동안 브랜드 스플래시가 뜨도록 변경
- chat_screen·autonomy_settings_screen·data_flow_screen의 커스텀 패널들도
글래스 스타일로 맞춤
BunsinApp -> YkavuApp (mobile/lib/main.dart, test/widget_test.dart 동기화)
Flutter SDK가 없는 환경이라 flutter analyze/run으로 직접 컴파일 검증은 못했음 —
중괄호/괄호 균형과 기존에 검증된 API 패턴 위주로 신중하게 작성함. go test·
pytest는 전부 통과.
- Flutter: drift + SQLCipher local store for tone samples/KV with
secure-storage passphrase; migrate legacy SharedPreferences
- Core: FCM notifyUser on escalation, admin push-test, push metrics,
DELETE session for multi-device logout
- Docs/roadmap B checkboxes updated; Linux SQLCipher apt notes
Co-authored-by: okuma <o0kuma@users.noreply.github.com>
Scaffold `mobile/` against core-backend APIs (signup, chat, draft,
veto, retract, autonomy/whitelist). Update roadmap/README/AGENTS so
participant PoC #1/#3 stays the final Phase 1 step after app build.
Co-authored-by: okuma <o0kuma@users.noreply.github.com>