Skip to content

✨ [Feat] Watch 출석 플로우 화면 7종 — 목록 · 세션(정시·지각·지오펜스 이탈) · 결과(출석·지각·공결) (#1207) - #1287

Merged
JEONG-J merged 7 commits into
developfrom
feat/1207
Aug 30, 2026
Merged

✨ [Feat] Watch 출석 플로우 화면 7종 — 목록 · 세션(정시·지각·지오펜스 이탈) · 결과(출석·지각·공결) (#1207)#1287
JEONG-J merged 7 commits into
developfrom
feat/1207

Conversation

@JEONG-J

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

Copy link
Copy Markdown
Contributor

🔗 관련 이슈

Closes #1207

✨ PR 유형

  • 새로운 기능 추가 (Feature)

워치 출석 플로우 화면을 구현했습니다. 이슈의 "화면 7종"은 3개 네비게이션 목적지(목록 · 세션 · 결과)가 데이터에서 파생하는 7가지 시각 상태입니다. WatchRoute 케이스를 새로 추가하지 않았습니다 — #1206 이 남긴 설계 의도("#1207 은 케이스를 추가하지 않고 WatchRootView 의 destination 만 실제 화면으로 바꿔 끼우면 된다", WatchRoute.swift)를 그대로 따랐습니다.

# 화면 상태 어디서 나오나
1 출석 목록 WatchAttendanceListView
2 세션 — 정시 출석 창 AttendanceTimeWindow.onTime
3 세션 — 지각 인정 시간 AttendanceTimeWindow.lateWindow
4 세션 — 지오펜스 이탈 측정 거리 > AttendancePolicy.geofenceRadius(50m)
5 결과 — 출석 확정 WatchAttendanceOutcome.present
6 결과 — 지각 확정 .late
7 결과 — 공결 인정 .excused
(+) 결과 — 결석 / 승인 대기 .absent / .pending

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

시뮬레이터 캡처는 아직 붙이지 않았습니다. 대신 각 화면에 상태별 #Preview 를 넣어 Xcode 캔버스에서 바로 확인할 수 있습니다 (정시 · 지각 · 지오펜스 이탈 · 결과 5종 · 빈 목록 · Dynamic Type .accessibility3).

🛠️ 작업내용

1. 출석 목록 (WatchAttendanceListView)

  • Loadable 4분기 렌더링: 로딩 · 실패 · 빈 목록(ContentUnavailableView) · 목록
  • 진행 중인 일정(정시·지각 창)만 watchListRowBackground(isSelected:) 로 끌어올림
  • 행 전체가 진입점 — 진행 중이면 세션, 끝났고 결과가 있으면 결과 화면으로 보냅니다
  • 실패 화면에 재시도 버튼을 두지 않았습니다. 워치는 일정을 직접 조회하지 않고 iPhone 이 밀어주는 쪽이라 여기서 누를 수 있는 재시도가 존재하지 않습니다

2. 출석 세션 (WatchAttendanceSessionView)

  • 정시 · 지각 · 지오펜스 이탈을 별도 화면이 아니라 한 화면의 파생 상태로 구현
  • 마감 카운트다운은 서버 정책(onTimeEndAt / lateEndAt)에서만 만듭니다. 정책이 없으면 타이머를 감춥니다 — 클라이언트 상수로 마감 시각을 지어내면 서버 판정과 다른 숫자를 카운트다운하게 됩니다
  • 지오펜스 이탈 시 거리를 대형 지표로 띄우고 Crown 회전으로 재측정합니다. Crown 포커스는 이탈 상태에서만 가져갑니다(그 외에는 기본 스크롤 유지)
  • CTA 비활성은 WatchActionButtondisabledReason 단일 경로만 사용
  • 지각 창이어도 CTA 는 인디고 .primary 입니다. 상태색(앰버)과 액션색(인디고)은 분리된 축이라 상태가 버튼 색을 바꾸지 않습니다

3. 출석 결과 (WatchAttendanceResultView)

  • 출석 확정 · 지각 · 공결 · 결석 4결과 + 그 앞의 승인 대기를 한 화면이 파생
  • 결과가 아직 안 온 상태(.loading)는 실패가 아니라 승인 대기입니다. 결과는 ATTENDANCE_STATUS_CHANGED 푸시로만 도착하고 승인까지 수십 분~며칠 걸립니다(설계 §3.1)
  • 누적 출석 횟수는 출석 확정일 때만 노출합니다. 지각·공결·결석 화면에 붙이면 방금 받은 결과가 출석으로 집계된 것처럼 읽힙니다

4. WatchAttendanceOutcome 신설 (표시 전용 enum)

  • HomeDomain.ScheduleAttendanceStatus 는 서버의 "EXCUSED".present 로 접습니다. 그래서는 설계 §3.2 의 "공결에 초록(출석 확정)을 주지 않는다"를 표현할 수 없습니다
  • 접는 것 자체는 iOS 도메인 의미론(출석률 집계에서 공결을 출석으로 본다)이라 바꾸면 iOS 화면 전반에 파급됩니다 → 워치 표시 전용 enum 을 따로 뒀습니다
  • 공결은 대기와 같은 중립색이되 실루엣(checkmark.seal)과 링 없음으로 구분합니다

5. 상태 소유권

Glass 절제 규칙 (설계 §2.2)

📋 추후 진행 상황

선행 PR 의존 — 이 브랜치는 아래 3개 위에 쌓여 있어 머지 전까지 diff 에 선행 커밋이 함께 보입니다. 선행 PR 이 머지되면 이 PR 의 diff 는 워치 출석 화면만 남습니다.

의존 PR 브랜치 이 PR 이 쓰는 것
#1275 feat/1205 WatchColor · Font.watch · WatchLayout · watchCard · WatchActionButton · WatchStatusBadge
#1280 feat/1206 WatchRoute · WatchRouter · WatchRootView
#1279 refac/1212 AttendanceTimeWindow · 멀티플랫폼 Domain 타깃

후속 이슈에서 붙일 것 — 이번 PR 은 화면과 표시 로직까지입니다. 실제 데이터 연결은 다음 이슈들 소관이라 주입 지점만 열어 뒀습니다.

사람이 확인해야 할 것

  • 시뮬레이터 캡처 첨부 (46mm / 42mm 실기기 확인 포함)
  • 아래 리뷰 포인트 2·3 은 설계 문서에 명시가 없어 제가 판단한 부분입니다. 디자인 확인이 필요합니다

📌 리뷰 포인트

  1. WatchAttendanceOutcome 분리가 맞는 선택인지ScheduleAttendanceStatus.excused 를 추가하는 쪽이 근본적이지만 iOS 출석률 집계·기존 화면 전반에 파급됩니다. 워치 표시 전용으로 막은 게 적절한 경계인지 봐주세요
  2. 비대면 일정(location == nil)의 CTA 처리 (설계 문서에 명시 없음) — 지오펜스를 요구하면 CTA 가 영원히 비활성으로 남아서, 장소가 없는 일정은 시간창만으로 판정하도록 했습니다 (WatchAttendanceSessionViewModel.requiresGeofence)
  3. 목록에서 마감(.expired) 일정의 배지 색 (설계 문서에 명시 없음).error(빨강)로 뒀습니다. 출석 자체가 불가능한 종료 상태라 .error 축에 올렸는데, 이미 출석 결과가 나온 지난 일정에 빨간 배지가 붙는 게 과한지 판단이 필요합니다
  4. 결과 화면이 WatchStatusBadge 를 안 쓰는 이유 — 그 컴포넌트는 심볼 폰트를 .cardLabel 로 고정해서 결과 화면의 대형 심볼(.metric) 크기를 낼 수 없습니다. 팔레트 렌더링 순서(점, 링)는 배지와 동일하게 맞췄습니다. 컴포넌트에 크기 파라미터를 여는 편이 나으면 ✨ Feature: Watch 디자인 토큰·공통 컴포넌트 구축 (Glass 절제 규칙) #1205 쪽에서 조정하겠습니다
  5. Project.swiftUMCFoundation 명시 링크Loadable · AppError 를 워치에서 직접 쓰므로 HomeDomain 경유 전파에 기대지 않고 직접 링크했습니다

✅ Checklist

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

- 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") 제거
- 일정 모델을 옮기지 않고 HomeDomain·NoticeDomain·CoreDomain 의 Domain 타겟을
  [.iPhone, .appleWatch] 로 개방 — #981 의 "Home* 단일 소유자" 경계 유지
- ActivityDomain 의 HomeDomain·CoreDomain 조건부 의존(.when([.ios])) 제거로
  watchOS 링크 실패(unable to resolve module dependency) 해소
- AttendanceTimeWindow 시간대 판정을 순수 이니셜라이저로 추출하고
  ChallengerAttendanceUseCase 가 위임 — 판정 규칙 단일 소스화(동작 불변)
- UMCWatchApp 이 ActivityDomain·HomeDomain 을 링크하고 출석 목록 화면에서
  AttendanceTimeWindow(schedule:now:) 를 직접 호출
- 판정 로직 테스트 15종 추가 · 경계/플랫폼 정책을 build-and-modules.md 에 반영
- "오늘 세션" 라벨 + "오늘 세션 없음" 값이 같은 말을 두 줄로 반복해 위계가 흐려지던 문제
- 빈 상태는 문구 한 줄만 카드에 남긴다. 세션이 있을 때의 라벨/값 위계는 그대로
# Conflicts:
#	UMCApp/UMCWatchApp/Project.swift
#	UMCApp/UMCWatchApp/Sources/ContentView.swift
- WatchAttendanceListView: Loadable 4분기(로딩·실패·빈 목록·목록) · 진행 중 행 solid 선택 표면 · 행 탭으로 세션/결과 진입
- WatchAttendanceSessionView: 정시 · 지각 · 지오펜스 이탈 3상태를 한 화면이 데이터에서 파생, 마감 카운트다운과 Crown 재측정
- WatchAttendanceResultView: 출석 확정 · 지각 · 공결 · 결석 4결과 + 승인 대기를 한 화면이 파생
- WatchAttendanceOutcome 추가: 서버 EXCUSED 를 present 로 접는 ScheduleAttendanceStatus 로는 "공결에 초록을 주지 않는다"는 표시 규칙을 낼 수 없어 워치 표시 전용 enum 을 분리
- WatchAttendanceViewModel 을 앱 셸 소유 environment 로 승격 — 목록·세션·결과가 같은 일정 집합과 결과 캐시를 본다
- 시간대 판정은 ActivityDomain 의 AttendanceTimeWindow 에 위임, 지오펜스 반경은 AttendancePolicy.geofenceRadius 재사용
- WatchRootView destination 을 실제 화면으로 교체 (일정 미도착 시 플레이스홀더 폴백)
- Project.swift 에 UMCFoundation 명시 링크 (Loadable · AppError 직접 사용)
- 테스트 24종 추가 (총 44 tests / 6 suites 통과)
# Conflicts:
#	UMCApp/Core/WatchDesignSystem/Sources/Components/WatchStatusBadge.swift
#	UMCApp/Tuist/ProjectDescriptionHelpers/Project+WatchApp.swift
#	UMCApp/UMCWatchApp/Project.swift
#	UMCApp/UMCWatchApp/Sources/Attendance/WatchAttendanceListView.swift
#	UMCApp/UMCWatchApp/Sources/Attendance/WatchAttendanceViewModel.swift
#	UMCApp/UMCWatchApp/Sources/Home/HomeGlanceView.swift
#	UMCApp/UMCWatchApp/Sources/Home/HomeGlanceViewModel.swift
#	UMCApp/UMCWatchApp/Sources/Routing/WatchRootView.swift
#	UMCApp/UMCWatchApp/Sources/Routing/WatchRoute.swift
#	UMCApp/UMCWatchApp/Sources/UMCWatchApp.swift
#	UMCApp/UMCWatchApp/Tests/HomeGlanceViewModelTests.swift
#	UMCApp/UMCWatchApp/Tests/WatchRouterTests.swift
#	docs/claude/watch-design-system.md
@JEONG-J
JEONG-J merged commit 3ebda29 into develop Aug 30, 2026
2 checks passed
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 출석 플로우 화면 7종 (목록·정시·지각·지오펜스·결과 3종)

1 participant