Skip to content

✨ [Feat] Watch Complication 3종 — 다음 세션·출석 상태·미확인 공지 (#1215) - #1282

Open
JEONG-J wants to merge 6 commits into
developfrom
feat/1215
Open

✨ [Feat] Watch Complication 3종 — 다음 세션·출석 상태·미확인 공지 (#1215)#1282
JEONG-J wants to merge 6 commits into
developfrom
feat/1215

Conversation

@JEONG-J

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

Copy link
Copy Markdown
Contributor

🔗 관련 이슈

Closes #1215

의존 이슈(둘 다 아직 develop 미머지 → 이 브랜치에 선반영한 stacked PR):

두 PR 이 develop 에 머지되면 이 PR 의 diff 는 자동으로 Complication 코드만 남습니다.

실기기 동작 선행 조건: #1211WatchSessionCoordinator.publishSessionState(_:) 는 정의만
있고 iPhone 앱에서 부르는 곳이 아직 0건입니다(#1211 이 그 배선을 다룹니다). 즉 이 PR 은
"스냅샷이 도착하면 워치페이스가 무엇을 어떻게 그리는가"까지를 완결하고, 그 앞단(푸시 → iPhone →
updateApplicationContext)은 #1211 에서 연결됩니다. 그때까지 실기기 워치페이스는
「iPhone 로그인 필요」 폴백에 머무릅니다.

✨ PR 유형

  • 새로운 기능 추가 (Feature)
  • 버그 수정
  • 리팩터링
  • 디자인 변경
  • 문서 수정

watchOS 워치페이스 Complication 3종을 WidgetKit accessory family 로 신규 구현했습니다.

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

워치페이스에 Complication 을 물리적으로 배치해야 나오는 화면이라 시뮬레이터 캡처는
GUI 조작이 필요합니다. 리뷰어가 직접 확인할 수 있도록 재현 절차만 남깁니다.

cd UMCApp && make build SCHEME=UMCWatchApp \
  DESTINATION='platform=watchOS Simulator,name=Apple Watch Ultra 3 (49mm)'
# 워치 시뮬레이터 → 워치페이스 길게 누르기 → 편집 → Complication 슬롯 선택
# → "UMC" 아래 「다음 세션」 / 「출석 상태」 / 「미확인 공지」

각 위젯이 .accessoryCircular · .accessoryRectangular · .accessoryInline 3 패밀리를
모두 지원하므로, 슬롯 종류에 관계없이 배치됩니다.

🛠️ 작업내용

1. Complication 스냅샷 도출 로직 (CoreWatchConnectivity)

Complication 이 그릴 값은 뷰가 아니라 Core 에서 계산합니다. 위젯 익스텐션은 유닛 테스트
타깃이 붙지 않아, 로직이 익스텐션에 있으면 검증할 방법이 없기 때문입니다.

파일 역할
Sources/Complication/ComplicationSnapshot.swift WatchSessionState(#1210) → 워치페이스가 필요로 하는 최소 형태로 축약
Sources/Complication/ComplicationStore.swift App Group UserDefaults 읽기/쓰기 + WidgetCenter.reloadAllTimelines()
  • ComplicationSnapshot(state:) — 다음 세션(끝나지 않은 것 중 가장 이른 것, 동시간대는
    scheduleId 로 타이브레이크) · 출석 상태 · 미확인 공지 수를 한 번에 도출
  • ComplicationAttendanceState — 8상태(none/upcoming/awaiting/pending/present/
    late/excused/absent). 서버 확정 상태(PRESENT/LATE/EXCUSED/ABSENT)는 그대로 쓰고,
    미확정 구간은 출석 윈도우와 현재 시각으로 계산
  • ComplicationTimeline.entries(from:now:) — 세션 시작/종료·체크인 시작·정시 마감·지각 마감을
    경계 시각으로 삼아 최대 6개 엔트리 생성. 1분 폴링 대신 "값이 바뀌는 순간"에만 엔트리를 둡니다

2. 위젯 익스텐션 신설 (UMCApp/UMCWatchComplication/)

  • UMCWatchComplicationBundle — 3종 위젯 번들
  • ComplicationProvider — 3종이 공유하는 단일 TimelineProvider.
    경계 시각이 있으면 .atEnd, 없으면 1시간 뒤 .after(_:) 폴백
  • NextSessionComplication — 다음 세션까지 남은 시간
    (원형은 ProgressView(timerInterval:) 로 링·숫자를 시스템이 갱신 → 엔트리 소비 0)
  • AttendanceStatusComplication — 출석 상태 심볼 + 라벨, 승인 대기는 링으로 구분
  • PingCountComplication — 미확인 공지 수 (99 초과 시 99+, 사각형은 "N분 전 기준" 신선도 표기)
  • ComplicationStyle.fullColor 전용 틴트 매핑 + 로그아웃 공통 뷰

3. iPhone → 워치 → 워치페이스 동기화 경로

UMCWatchApp/Sources/ComplicationSyncModifier.swift 에서 WatchSessionCoordinator(#1210)의
receivedState 변화를 구독해 스냅샷을 저장합니다. 워치는 서버를 직접 폴링하지 않으므로
(설계 스펙 §7) 이 경로가 Complication 이 최신값을 얻는 유일한 통로입니다.
initial: true 를 준 이유는 콜드런치 시딩 — 활성화 시점에 이미 도착해 있던 컨텍스트에는
델리게이트 콜백이 다시 오지 않아 첫 값이 통째로 누락됩니다.

4. Tuist 구성

  • Project+WidgetExtension.swiftdestinations/deploymentTargets/displayName
    파라미터로 일반화 (기본값이 기존 iOS 설정이라 UMCAppWidget무변경)
  • App Group group.com.umc.product.watch 신규 — iPhone 과 워치는 App Group 컨테이너를
    공유하지 못해 iOS 위젯의 group.com.umc.product.widget 을 재사용할 수 없습니다
  • UMCWatchApp 에 익스텐션 의존 추가 → Embed Foundation Extensions.appex 자동 임베드

5. 테스트

CoreWatchConnectivity 에 21개 추가 (스냅샷 도출 15 · 스토어 6).
경계 시각 산출, 8상태 매핑, 미로그인/미동기화 폴백, 색맹 안전성(색 없이도 3채널로 구분되는지)을
잠급니다.

📋 추후 진행 상황

후속 이슈에서 이어지는 작업

  1. 🐛 Bug: iOS 앱에 WCSession 미배선 — CoreWatchConnectivity 링크·DI 등록·activate() 호출 없음 #1211 — iPhone 쪽 WC 배선. 위 "관련 이슈" 참고. 이 PR 의 수신부는 완성돼 있어
    publishSessionState(_:) 호출부만 생기면 별도 수정 없이 워치페이스까지 값이 흐릅니다.

사람만 할 수 있는 작업 (코드·스펙 쪽은 이 PR 에서 전부 끝냈습니다):

  1. App Group 등록 — Apple Developer 포털에서 group.com.umc.product.watch 를 생성하고
    워치 앱·익스텐션 App ID 에 capability 를 추가한 뒤 프로비저닝 프로파일 재발급.
    이 작업 전까지는 실기기에서 스냅샷 공유가 조용히 실패합니다(시뮬레이터는 동작).
  2. 디자인팀 확인 — tinted(accented) 모드 목업 확정 (설계 스펙 §9 미해결).
    코드 기본값은 이미 "색에 의존하지 않는 3채널(심볼·라벨·링) 인코딩"으로 잠가 뒀으므로
    목업이 나와도 로직 변경 없이 틴트 매핑만 조정하면 됩니다.
  3. 워치페이스 실배치 검증 — 3종을 실제 슬롯에 올려 원형/사각/인라인 렌더링 확인 (GUI 조작).
  4. 갤러리 문구 확정 — 현재 「다음 세션」/「출석 상태」/「미확인 공지」. 카피 최종본 반영.
  5. 🐛 [Fix] Watch 공유 Keychain Access Group entitlement 복구 (#1213) #1269 머지 후UMCApp/UMCWatchApp/UMCWatchApp.entitlements 에 Keychain Access Group
    키를 병합 (이 PR 에서 파일을 신규 생성했으므로 충돌 지점입니다).

📌 리뷰 포인트

  1. 타임라인 정책ComplicationTimeline.boundaries 가 고르는 경계 시각이 스펙과 맞는지.
    watchOS 는 Complication 리로드 예산이 빠듯해서, 1분 간격 엔트리 대신 "값이 바뀌는 순간"만
    엔트리로 두고 카운트다운은 시스템(ProgressView(timerInterval:))에 맡겼습니다.
  2. .pending.excused 가 같은 틴트 — 스펙상 둘 다 "확정되지 않았거나 예외" 축이라
    색으로 가르지 않고 심볼/링으로 구분했습니다. 의도한 해석이 맞는지 봐주세요.
  3. accented/vibrant 모드 처리complicationTint(_:mode:).fullColor 가 아닐 때
    .primary 를 주는 이유는, 커스텀 색을 넘기면 시스템 치환 대상이 하나로 뭉개져 강조 계층이
    통째로 사라지기 때문입니다.
  4. App Group 분리 — iPhone 위젯 그룹을 재사용하지 않고 워치 전용 그룹을 새로 판 판단.
  5. Project+WidgetExtension 일반화 — 기본 인자로 기존 iOS 동작을 보존했는지
    (UMCAppWidget 빌드로 확인했습니다).

✅ Checklist

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

검증 결과

항목 결과
make test (UMCApp, iPhone 17 Pro) ** TEST SUCCEEDED **
make test SCHEME=CoreWatchConnectivity 43 tests / 4 suites 통과 (기존 22 + 신규 21)
make build SCHEME=UMCWatchComplication (watchOS) BUILD SUCCEEDED (warning 0)
make build SCHEME=UMCWatchApp BUILD SUCCEEDED
make build SCHEME=UMCAppWidget (iOS 회귀) BUILD SUCCEEDED
절대 규칙 #9 (AppProduct/ 무변경) git diff --stat -- AppProduct/ 비어 있음
금지 API (glassEffect·@StateObject·@Published·NavigationView) 검출 0

- 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 레퍼런스 인덱스 갱신
- CoreWatchConnectivity 에 ComplicationSnapshot·ComplicationStore 추가 —
  WatchSessionState 를 워치페이스가 필요로 하는 최소 형태로 축약하고
  App Group UserDefaults 로 익스텐션에 전달
- ComplicationTimeline 이 세션 시작·종료·체크인/정시/지각 마감을 경계 시각으로
  삼아 최대 6개 엔트리 생성 — 1분 폴링 대신 값이 바뀌는 순간에만 엔트리를 둔다
- UMCWatchComplication 익스텐션 신설: 다음 세션·출석 상태·미확인 공지 3종이
  accessoryCircular·Rectangular·Inline 을 모두 지원
- 출석 8상태를 심볼·라벨·링 3채널로 구분 — accented/vibrant 에서 색이 치환돼도,
  색각 이상 사용자에게도 구분이 살아남는다
- WatchSessionCoordinator 수신 시 스냅샷을 저장하고 타임라인을 리로드 —
  워치는 서버를 직접 폴링하지 않으므로 이 경로가 유일한 갱신 통로
- Project+WidgetExtension 헬퍼를 destinations·deploymentTargets·displayName 로
  일반화 (기본값이 기존 iOS 설정이라 UMCAppWidget 은 무변경)
- CoreWatchConnectivity 테스트 21개 추가 (스냅샷 도출 15 · 스토어 6)
- docs/claude/watch-complications.md 신규 — 데이터 흐름(워치는 서버를 폴링하지
  않는다)·모듈 배치 근거·App Group 함정·출석 8상태 매핑 표·tinted 규칙·
  타임라인 정책·확장 체크리스트·테스트·트러블슈팅 6건
- CLAUDE.md 상세 레퍼런스 표에 인덱스 행 추가
@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 Complication 3종 (WidgetKit accessory family)

1 participant