Conversation
- Core/WatchDesignSystem 모듈 신설 — iOS 카탈로그는 watchOS 에서 항상 dark 를 resolve 하므로(브랜드색이 #4869F0 대신 #4264F0 이 됨) 워치 전용 레이어로 분리 - 토큰 3종 추가: WatchColor(표면·브랜드·상태·텍스트 20종) · WatchTypography(WatchTextRole 5종) · WatchLayout(코너·보더·패딩·스페이싱) - 표면 API 추가: watchCard(_:leadingAccent:) · watchScreenBackground() · watchListRowBackground(isSelected:) - 컴포넌트 2종 추가: WatchActionButton(역할 3종) · WatchStatusBadge(상태 5종) - Glass 절제 규칙을 API 로 강제 — WatchCardStyle 을 닫힌 enum 으로 두어 카드에 Glass 를 넣을 경로 자체를 없애고, Glass API 는 WatchActionButton 한 파일로 한정 - 토큰 드리프트 가드 테스트 추가 — iOS Colors.xcassets 를 파싱해 브랜드색 불일치 시 실패 - docs/claude/watch-design-system.md 신규 · CLAUDE.md 레퍼런스 인덱스 갱신
- WatchRoute/WatchFallbackRoute — 출석·The Ping 두 축 진입점과 폴백 3종(권한 거부·연결 끊김·큐잉)을 경로로 확보 - WatchRouter — 앱 셸이 소유하는 @observable 단일 스택. NavigationPath 대신 [WatchRoute] 구체 배열로 두어 스택 내용을 테스트로 검증 - WatchRootView — navigationDestination 단일 등록. 목적지는 후속 이슈가 교체할 플레이스홀더 - HomeGlanceView/HomeGlanceViewModel — solid 카드 글랜스, 세션·공지 행을 tap-chip 으로 승격, 세션 없을 때 "오늘 세션 없음" 빈 상태 - 승인 대기는 WatchStatus.pending(중립+인디고 링). 앰버 #FFB340 은 개정 스펙상 지각 전용이라 쓰지 않음 - 카운트·식별자는 전 레이어 String, Int 변환은 비교 시점에만 (규칙 #2). 숫자 아닌 값은 0건 처리 - watchAppProject 헬퍼에 includesTests 추가 + WKApplication 키 보강 — 없으면 테스트 호스트 설치가 거부됨 - ContentView("Hello Watch") 제거
- "오늘 세션" 라벨 + "오늘 세션 없음" 값이 같은 말을 두 줄로 반복해 위계가 흐려지던 문제 - 빈 상태는 문구 한 줄만 카드에 남긴다. 세션이 있을 때의 라벨/값 위계는 그대로
- 실패 원인 4축(권한·연결·세션·서버)으로 나눈 WatchFallbackReason 9종 추가 (P0-7 오프라인 큐는 대기/만료가 문구·아이콘·CTA 가 달라 케이스를 분리) - 무음 실패 3중 잠금 — 연관값 없는 enum 의 CaseIterable 합성으로 신규 케이스 자동 테스트 편입, category·presentation 스위치 default 제거로 컴파일 강제, init(classifying:) 비Optional 로 미분류 에러도 화면 없이 사라지지 않게 함 - 동작 없는 CTA 금지 — WatchFallbackScene 핸들러를 옵셔널로 두어 핸들러를 넘기지 않으면 버튼 자체를 렌더하지 않음 - 표시 형태 3종 — 전체화면(WatchFallbackView) · 인라인 카드(WatchOfflineQueueCard) · 상단 고정 배너(WatchMandatoryNoticeBanner) - P0-8 배너를 NavigationStack 바깥 safeAreaInset 에 붙여 스와이프 dismiss 경로 제거 - 오프라인 큐 3시간 유효창 — 서버 판정 창(과거 180분)과 같은 값으로 워치가 보내기 전에 스스로 버리고 공결 사유 제출로 안내 - P0-3 진입 경로 연결 — WatchSessionCoordinator.isReachable 로 글랜스에 폴백 행 노출, 빈 세션 문구를 캐시 표기로 전환 - WatchStatus 에 Equatable 추가(WatchFallbackPresentation 합성용, 동작 변화 없음) - docs/claude/watch-design-system.md 에 폴백 계약 섹션(§9) 추가
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔗 관련 이슈
Closes #1209
✨ PR 유형
📷 스크린샷 or 영상(UI 변경 시)
폴백 9종 전부
#if DEBUG프리뷰로 확인할 수 있게 넣어 두었습니다. Xcode 캔버스에서 바로 보실 수 있습니다.WatchFallbackScene.swiftWatchFallbackView.swiftWatchOfflineQueue.swiftWatchMandatoryNotice.swiftWatchRootView.swift시뮬레이터 캡처는 실패 상태를 실제로 만들어 내는 출석 플로우(#1207)가 붙은 뒤에 첨부하겠습니다.
🛠️ 작업내용
스펙 §4 P0 갭 화면 8종을 실패 원인별로 구분되게 구현했습니다.
1. 실패 원인 4축 분류 —
WatchFallbackReason권한 · 연결 · 세션 · 서버 4축(
WatchFailureCategory)으로 나누고, 화면 8종을 9개 케이스로 표현했습니다.(P0-7 오프라인 큐는 "대기"와 "유효시간 만료"가 문구·아이콘·CTA 가 전부 다른 두 상태라 케이스를 갈랐습니다.)
locationPermissionDeniedlocationUnavailablephoneDisconnectedcheckInRequestFailedalreadyCheckedIncheckInWindowClosedofflineQueuedofflineQueueExpiredmandatoryNoticeUnread2. 무음 실패 금지 — 컴파일·테스트 3중 잠금
WatchFallbackReason에 연관값을 두지 않아CaseIterable이 합성됩니다 → 케이스를 추가하면 자동으로 기존 테스트 커버리지에 들어옵니다(문구 비어있음·제목 중복·4축 커버 검사).category·presentation스위치에default:를 두지 않았습니다 → 케이스 추가 시 컴파일 에러로 드러납니다.init(classifying error: Error)는 Optional 을 반환하지 않습니다 → 어떤 에러도 화면 없이 사라지지 않습니다. 분류되지 않은 에러는 재시도와 iPhone 대체 경로를 모두 가진checkInRequestFailed로 귀착시킵니다.3. 동작 없는 CTA 금지
WatchFallbackScene.onPrimaryAction/onSecondaryAction을 옵셔널로 두고, 핸들러가 없으면 버튼을 아예 그리지 않습니다.눌러도 아무 일이 없는 버튼이 곧 무음 실패라, 만들 수 없게 구조로 막았습니다.
(#1205
WatchActionButton(disabledReason:)의 "사유 없는 비활성 버튼 금지" 규약과 같은 계열입니다.)4. 표시 형태 3종
WatchFallbackView(WatchRoute.fallback(reason)목적지). "다시 시도" 는router.pop()으로 요청을 띄운 화면으로 되돌리고, "iPhone 에서 시도" 는router.popToRoot()로 워치 쪽 시도를 접습니다.WatchOfflineQueueCard. 만료 시.danger표면으로 바뀌고, 대기 중에는 남은 유효 시간을 힌트에 꽂습니다.WatchMandatoryNoticeBanner.NavigationStack바깥safeAreaInset(edge: .top)에 붙여 dismiss 제스처 자체가 존재하지 않습니다.confirm()외에 사라지는 경로가 없습니다.5. 오프라인 큐 3시간 유효창
서버가 수신 시각 기준 과거 180분 이내만 출석 판정에 쓰므로(스펙 §3.3), 워치가 보내기 전에 스스로 버리고 공결 사유 제출로 안내합니다.
WatchOfflineQueueWindow.state(measuredAt:now:)는Date.now를 읽지 않는 순수 함수라 경계값을 테스트로 고정했습니다(0분 · 179분 · 180분 · 181분 · 시계 역행).6. 진입 경로 연결
WatchSessionCoordinator.isReachable로 실제 신호가 붙어 있습니다 — 글랜스에 폴백 진입 행이 뜨고, 빈 세션 문구도 "(마지막 동기화 기준)" 캐시 표기로 바뀝니다.WatchRoute.fallback)로 감싸져 스택에 쌓이는 것까지 테스트로 검증했습니다. 실제 신호(위치 권한·GPS 타임아웃·출석 API 응답·공지 수신)는 그 흐름을 만드는 ✨ Feature: Watch 출석 플로우 화면 7종 (목록·정시·지각·지오펜스·결과 3종) #1207 · ♻️ Refactor: WatchConnectivity 도메인 페이로드 계약 정의 + 양방향 수신 경로 구현 #1210 이 붙입니다.WatchFallbackDebugMenu)로 메웠습니다. 홈 글랜스의 "폴백 하네스" 행에서 폴백 9종 전체화면 · 오프라인 큐 대기/만료 카드 · 필수 확인 배너를 실기기에서 바로 띄워 볼 수 있습니다. 파일 전체가#if DEBUG라 릴리스 빌드에는 들어가지 않습니다(절대 규칙 ✨ Feature: MainButton Component 제작 #5). 명함 Debug 하네스(♻️ Refactor: 근거리 교환 계약 정리 — 죽은 API·상수 필드·구현 캐스팅·버전 상한 #1238)와 같은 방식입니다.7. 동적 문구 주입 —
replacing(title:message:hint:)presentation은 순수 정적 값이라, 화면을 그리는 시점에만 알 수 있는 문구는 한 메서드로 꽂습니다. P0-5 출석 시각(· HH:MM) · P0-7 남은 유효 시간 · P0-8 공지 제목이 전부 이 경로를 씁니다. (초기 구현의replacingHint(_:)/replacingMessage(_:)두 개를 합쳐 P0-5 의 제목 주입 경로도 함께 열었습니다.)검증
📋 추후 진행 상황
WatchLocationMeasurement.timeout공유) · 출석 API 호출에서 실패를WatchFallbackReason(classifying:)으로 넘겨 P0-1 · P0-2 · P0-4 · P0-5 · P0-6 에 실제 신호를 붙입니다. "다시 시도" 가 실제 재요청을 수행하도록 핸들러도 이때 교체됩니다.WatchMandatoryNoticeCenter는 공지를 받아 두는 그릇까지만 구현돼 있습니다.replacing(title:))는 열어 뒀고, ♻️ Refactor: WatchConnectivity 도메인 페이로드 계약 정의 + 양방향 수신 경로 구현 #1210 이 출석 시각을 실어 주면 한 줄로 꽂힙니다. (배너 제목·큐 잔여 시간과 같은 방식)📌 리뷰 포인트
문구 — 9종의 한국어 카피(
WatchFallbackPresentation.swift)를 봐 주세요. 스펙 §5 원문을 따라-습니다체로 통일했고, 제목이 서로 겹치지 않는 것을 테스트로 강제하고 있습니다. 어색한 표현이 있으면 이 파일 한 곳만 고치면 전 화면에 반영됩니다.아이콘 대체 — 스펙의 "bt-slash" 에 대응하는 Bluetooth 글리프가 SF Symbols 에 없어
iphone.slash로 대체했습니다(문구가 "iPhone 과 연결이 끊겼습니다"라 오히려 더 정확하다고 판단). 대안이 있으면 알려 주세요.배너를
NavigationStack바깥에 붙인 선택 — 스택 안에 두면 좌측 엣지 스와이프로 pop 돼 "무시 불가" 계약이 깨집니다.WatchRootView.swift의safeAreaInset위치를 봐 주세요.WatchStatus에Equatable추가 —WatchFallbackPresentation의Equatable합성을 위해CoreWatchDesignSystem에 순수 추가만 했습니다(동작 변화 없음).미분류 위치 에러 처리 — 권한 거부를 뺀 나머지
CLError는 코드를 가리지 않고 전부locationUnavailable로 보냅니다. 원인이 뭐든 사용자가 할 일(자리 옮기고 재시도)이 같고, 위치 실패를 서버 실패 화면으로 흘리면 안내가 틀리기 때문입니다.폴백 화면이 캐시 데이터를 다시 보여줘야 하는지 — P0-3 "캐시 데이터만 표시"는 홈 글랜스 수준에서 충족했습니다(연결이 끊겨도 마지막 값이 남고 빈 세션 문구에 "(마지막 동기화 기준)"이 붙음). 폴백 화면 자체는 설명만 하고 데이터를 다시 그리지는 않는데, 디자인 의도가 어느 쪽인지 확인 부탁드립니다.
앰버(
.warning#FFB340)의 의미 범위 — 여기서는 P0-1·P0-2·P0-3·P0-8 에 썼습니다. 앰버를 출석 "지각" 전용으로 예약해 둔 것인지(2026-08-26 스펙 개정), 경고 일반으로 써도 되는지 ✨ Feature: Watch 출석 플로우 화면 7종 (목록·정시·지각·지오펜스·결과 3종) #1207 출석 목록과 나란히 놓이기 전에 확정이 필요합니다.✅ Checklist
PR이 다음 요구 사항을 충족하는지 확인해주세요!!!