Skip to content

✨ [Feat] Watch The Ping 목록·읽기·수신 확인 화면 (#1208) - #1284

Open
JEONG-J wants to merge 5 commits into
developfrom
feat/1208
Open

✨ [Feat] Watch The Ping 목록·읽기·수신 확인 화면 (#1208)#1284
JEONG-J wants to merge 5 commits into
developfrom
feat/1208

Conversation

@JEONG-J

@JEONG-J JEONG-J commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

🔗 관련 이슈

Closes #1208

선행 PR 위에 쌓았습니다 — 아래가 먼저 머지돼야 합니다.

✨ PR 유형

새로운 기능 (워치 앱 화면 추가)

📷 스크린샷 or 영상(UI 변경 시)

시뮬레이터 캡처는 아직 첨부하지 못했습니다 — iPhone 컴패니언이 스냅샷을 밀어 주는 경로(#1210 의 iPhone 측 퍼블리셔)가 아직 붙지 않아, 워치 단독 실행 시 실제 공지 데이터가 들어오지 않습니다.
대신 #if DEBUG 프리뷰 6종으로 상태별 화면을 모두 확인할 수 있습니다.

프리뷰 확인 내용
PingListView — 미확인·긴급·확인함 안읽음 점 · 긴급 좌측 색바 · 필수 확인 배지가 한 행에서 분리돼 보이는지
PingListView — 빈 상태 "새 공지 없음"
PingListView — A11y 크기 최대 Dynamic Type 에서 행이 깨지지 않는지
PingDetailView — 미확인 긴급 하단 고정 확인 CTA
PingDetailView — 확인 완료 비활성 CTA + 「이미 확인한 공지입니다」
PingDetailView — A11y 크기 최대 Dynamic Type 에서 본문·CTA 배치

🛠️ 작업내용

표시 모델 — WatchPingItem

  • 스냅샷의 WatchNotice 와 워치에서 방금 누른 낙관적 읽음 오버레이를 합쳐 최신순 목록을 만듭니다.
  • 안읽음 · 긴급(isAlert) · 필수 확인(isMustRead) 세 신호를 서로 다른 축으로 유지합니다. 확인한 긴급 공지는 긴급 신호만 남습니다.
  • 색·위치로만 주는 신호를 글자로 푸는 VoiceOver 라벨(accessibilityLabel)을 모델이 직접 만듭니다.

상태 — PingInbox (@Observable, Loadable)

  • 앱 셸이 소유하는 앱 생명주기 관리자입니다(WatchRouter 와 같은 자리). 확인 버튼은 상세에서 눌리고 결과는 목록이 즉시 반영해야 해서, 화면 로컬 상태로는 이 왕복이 성립하지 않습니다.
  • WatchSessionCoordinator.receivedState(applicationContext 수신분)와 requestSync() 결과를 각각 들고, generatedAt 이 더 새로운 쪽을 화면에 그립니다. 동률이면 iPhone 이 퍼블리시한 컨텍스트를 택합니다.
  • 스냅샷이 하나라도 있으면 동기화 실패보다 캐시를 우선합니다. 워치는 iPhone 이 멀어지면 늘 도달 불가가 되는데, 그때마다 목록을 에러 화면으로 바꾸면 마지막 공지를 읽을 수 없습니다. 실패는 상단 캡션으로만 알립니다.
  • WatchConnectivityErrorAppError 매핑. 스키마 불일치는 페이로드 손상과 구분해 「iPhone 앱을 업데이트해 주세요」로 냅니다.

확인 전송 (양방향 동기화)

  • transferUserInfo 큐 채널(.noticeRead)로 보냅니다 — 계약상 이 종류가 허용하는 유일한 채널이고, 연결이 끊겨 있어도 접수되며 앱이 종료돼도 시스템이 전송을 이어 갑니다.
  • 전송 즉시 읽음을 오버레이해 목록·상세가 같은 상태를 봅니다. 큐 전달은 시스템이 보장하므로 되돌리지 않습니다.

화면

  • PingListViewList + Loadable 분기(로딩 / 빈 상태 / 목록 / 실패). 행 배경은 watchListRowBackground불투명 서피스만 씁니다(Glass 미적용, 스펙 §2.2).
  • PingDetailView — 본문 ScrollView + .safeAreaInset(edge: .bottom) 하단 고정 확인 CTA. 확인 완료 시 비활성 + 사유 표시.
  • WatchRootViewnavigationDestination.pingList · .pingDetail(noticeID:) 를 연결했습니다. WatchRoute 케이스는 추가하지 않았습니다(✨ Feature: Watch 홈 통합 글랜스 + 네비게이션 IA 골격 #1206 이 정의한 열거형을 그대로 씁니다).

디자인 시스템

  • watchListRowBackground(isSelected:leadingAccent:)leadingAccent 를 추가했습니다. 긴급 색바를 행 배경이 그려야 행 끝까지 닿습니다 — 콘텐츠에 그리면 셀 인셋만큼 떠서 시안과 어긋납니다.

의존성

  • UMCWatchAppUMCFoundation 을 추가했습니다. Loadable·AppError 를 워치 전용으로 다시 만들지 않기 위해서이며, 해당 모듈은 이미 destinations: [.iPhone, .appleWatch] 로 워치 슬라이스를 빌드합니다.

테스트 21건 추가WCSession 없이 검증되도록 정렬·오버레이·스냅샷 선택·에러 매핑을 순수 함수로 분리했습니다.

📋 추후 진행 상황

  • iPhone 측 퍼블리셔 — 실제 공지 스냅샷을 updateApplicationContext 로 밀어 주는 경로와, 워치가 보낸 .noticeRead 를 서버 수신 확인 API 로 중계하는 핸들러는 ♻️ Refactor: WatchConnectivity 도메인 페이로드 계약 정의 + 양방향 수신 경로 구현 #1210 iPhone 측 후속 작업입니다. 이 PR 은 워치 쪽 소비·전송만 담당합니다.
  • 실기기 캡처/영상 — 워치 실기기 페어링과 iPhone 컴패니언 실행이 필요해 사람 손이 필요합니다. 위 퍼블리셔가 붙은 뒤 실기기에서 확인 왕복(워치 확인 → iPhone 반영 → 새 스냅샷 수신)을 촬영해 첨부해야 합니다.
  • 디자인팀 확인 — 긴급 색바 색상으로 WatchColor.statusError(#FF453A) 를 썼습니다. 시안에 워치 전용 긴급 색이 따로 있다면 토큰을 교체해야 합니다.
  • 미확인 건수 → 복잡성(Complication) 노출은 별도 이슈 범위입니다.

📌 리뷰 포인트

  1. 두 스냅샷 출처의 우선순위PingInbox.latest(_:_:). requestSync()coordinator.receivedState 를 갱신하지 않아 출처가 둘로 갈립니다. generatedAt 비교 + 동률 시 컨텍스트 우선이 맞는 규칙인지 봐 주세요.
  2. 실패보다 캐시 우선 — 도달 불가 시 목록을 에러로 덮지 않고 캡션만 띄우는 판단. 반대로 오래된 공지를 최신처럼 보여 줄 위험도 있어 트레이드오프를 봐 주시면 좋겠습니다.
  3. 낙관적 읽음을 되돌리지 않는 것transferUserInfo 는 시스템이 전달을 보장하므로 롤백 경로를 두지 않았습니다.
  4. 신호 3종 분리 — 안읽음(오렌지 점) / 긴급(좌측 색바) / 필수 확인(배지)이 한 행에서 겹쳐도 구분되는지, VoiceOver 낭독 순서가 자연스러운지.
  5. Glass 절제List 행·화면 배경에 Glass 를 쓰지 않았습니다(워치 스펙 §2.2). 누락된 곳이 있는지 봐 주세요.

✅ Checklist

PR이 다음 요구 사항을 충족하는지 확인해주세요!!!

🧪 검증

대상 명령 결과
watchOS xcodebuild test -scheme UMCWatchApp -destination 'platform=watchOS Simulator,name=Apple Watch Series 11 (46mm)' ✅ 35 tests / 4 suites 통과
iOS cd UMCApp && make test ✅ 32 tests / 5 suites 통과

- WatchEnvelope 봉투 코덱 추가 — JSON 을 단일 키에 Data 로 실어 sendMessage ·
  updateApplicationContext · transferUserInfo 세 채널이 하나의 코덱을 공유
- WatchMessage(요청 5종) · WatchReply(응답 4종) · WatchConnectivityError(실패 11종)
  계약 정의, 수동 Codable 로 스키마 버전 상한 검증
- 페이로드 값 타입 추가 — WatchSessionState · WatchSchedule · WatchNotice ·
  WatchAttendanceRequest / Result. 서버 정수 식별자는 전 레이어 String (절대 규칙 #2)
- WatchSessionCoordinator 를 @mainactor @observable 로 재작성, WCSessionDelegate
  양방향 수신 경로 구현 (요청 핸들러 주입 · userInfo AsyncStream · 앱 컨텍스트 상태)
- replyHandler 없이 sendMessage 를 호출해 성공 시 continuation 이 매달리던 버그를
  구조적으로 제거 — 모든 전송이 replyHandler 를 넘긴다
- WatchMessenger 삭제 — 구현체 1개짜리 pass-through 프로토콜이었고 Sendable 준수가
  non-Sendable 코디네이터와 충돌
- CoreWatchConnectivity 에 테스트 타깃 활성화 (includesTests: true)
- 계약 테스트 22개 추가 (2 suites)
- 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") 제거
- "오늘 세션" 라벨 + "오늘 세션 없음" 값이 같은 말을 두 줄로 반복해 위계가 흐려지던 문제
- 빈 상태는 문구 한 줄만 카드에 남긴다. 세션이 있을 때의 라벨/값 위계는 그대로
- WatchPingItem: 스냅샷 공지 + 낙관적 읽음 오버레이를 합쳐 최신순 목록을 만드는 표시 모델.
  안읽음·긴급·필수 확인 세 신호를 색이 아닌 글자로도 푸는 VoiceOver 라벨 포함
- PingInbox: 앱 셸이 소유하는 수신함. applicationContext 수신분과 requestSync 결과 중
  더 새로운 스냅샷을 고르고, 실패해도 캐시를 버리지 않아 iPhone 이 멀어져도 마지막 공지를 읽을 수 있다
- 확인 전송은 transferUserInfo 큐 채널(.noticeRead) — 연결이 끊겨 있어도 접수되고 앱 종료 후에도 이어진다.
  전송 즉시 읽음을 오버레이해 목록·상세가 같은 상태를 본다
- WatchConnectivityError → AppError 매핑: 스키마 불일치는 손상과 구분해 업데이트 안내로 낸다
- PingListView / PingDetailView 를 WatchRoute 의 .pingList · .pingDetail 목적지에 연결
- watchListRowBackground 에 leadingAccent 추가 — 긴급 색바를 행 배경이 그려 행 끝까지 닿게 한다
- 테스트 21건 추가 (WCSession 없이 정렬·오버레이·스냅샷 선택·에러 매핑 검증)
@JEONG-J JEONG-J added the ✨ Feature 새로운 기능을 추가합니다. label Aug 30, 2026
@JEONG-J JEONG-J self-assigned this Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능을 추가합니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Feature: Watch The Ping 목록·읽기·수신확인 화면

1 participant