Skip to content

[Work 57] 약속 이동 라이브 액티비티의 UI와 단계별 진행 흐름을 구현했습니다. - #27

Merged
sangYuLv merged 23 commits into
developfrom
WORK-57
Oct 1, 2026
Merged

sangYuLv merged 23 commits into
developfrom
WORK-57

Conversation

@sangYuLv

@sangYuLv sangYuLv commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

JIRA

📝 작업 내용

📌 요약

  • 약속 장소로 이동하는 동안 잠금화면·다이나믹 아일랜드에 표시되는 라이브 액티비티 추가
  • 탑승 수단 기준 단계 버튼(출발 → 탑승역 → 하차역 → 도착)으로 이동 단계 진행, 도착 시 자동 종료
  • 라이브 액티비티 탭 시 약속 지도, 위치 공유 버튼 탭 시 채팅방 진입과 동시에 내 위치 전송
  • 마이페이지에 실시간 현황 허용 상태 표시
  • 서버 push-to-start 전까지 쓰는 DEBUG 전용 시작 메뉴

🔍 상세

[1] 타깃 구성

  • WhereAreYouWidget: 라이브 액티비티 UI를 담는 위젯 익스텐션 타깃
  • Shared 폴더: 앱·위젯이 함께 컴파일하는 코드
    AppointmentRouteActivityAttributes(액티비티 데이터), AdvanceRouteProgressIntent(단계 버튼), AppointmentDeepLink(딥링크 URL), RouteProgressService(단계 진행 서비스 프로토콜)
  • Info.plist: NSSupportsLiveActivities, URL 스킴 whereareyou 추가

[2] 도메인

  • RouteStep: 노선명·노선 색·정거장 수 필드 추가 (도보·자동차는 nil)
  • RouteProgressPlan / RouteProgressSegment / RouteCheckpoint / RouteProgressStatus: 이동 한 번의 고정 정보(구간·확인 지점)와 변하는 상태(도달 지점 수·도착 인원)
  • RouteLiveActivityRepository → ActivityKitRouteLiveActivityRepository: 진행 중인 액티비티 조회·갱신·종료
  • RouteProgressService → DefaultRouteProgressService: 다음 확인 지점으로 진행, 마지막 지점이면 종료

[3] 잠금화면·다이나믹 아일랜드 UI

  1. 약속 이름 / 도착 인원(n/총원 도착)
  2. 약속 시각 · 장소 / 약속까지 남은 시간: 약속 시각이 지나면 라벨이 "약속 시간", 시간이 빨간 "+경과 시간"으로 전환
  3. 진행 바: 구간별 칩 — 도보 회색, 자차 indigo, 지하철·버스는 노선 색(버스는 🚌 + 번호), 지나온 구간은 흐리게, 현재 구간은 테두리
  4. 현재 구간 1줄: "강남역까지 4정거장", "이수역까지 도보 6분" 등
  5. 위치 공유 버튼 / 단계 버튼("출발했나요?" 등): 도착하면 둘 다 숨기고 "도착했어요"만 표시
  6. 다이나믹 아일랜드 컴팩트: 현재 구간 아이콘 + 남은 시간, 확장: 진행 바·현재 구간·버튼

[4] 단계·종료 규칙

  • 확인 지점: 출발 → 첫 탑승역 → 각 하차역(환승 포함) → 도착. 환승 도보는 건너뛰고 다음 노선을 현재 구간으로 표시. 자차·도보만 있으면 출발 → 도착
  • 종료: 약속 전 도착 → 약속 시각까지 유지, 약속 후 도착 → 1분 뒤 내려감
  • 액티비티는 한 번에 하나 — 새로 시작하면 기존 액티비티 전부 종료

[5] 버튼 동작

  1. 단계 버튼: LiveActivityIntent로 앱을 띄우지 않고 앱 프로세스에서 처리 — @AppDependency로 주입받은 RouteProgressService 호출
  2. 위치 공유 버튼: 딥링크 → 약속 탭 → 채팅방 진입 → 메시지 조회가 끝난 뒤 확인 없이 현재 위치 전송. 위치를 못 가져오면 설정 이동 알림
  3. 액티비티 탭: 딥링크 → 약속 탭 → 약속 지도 표시

[6] 실시간 현황 권한

  • LiveActivityPermissionStatus / LiveActivityPermissionRepository / ObserveLiveActivityPermissionUseCase: 위치 권한과 같은 구조로 허용 상태 조회·구독
  • 마이페이지 위치 카드에 실시간 현황 설정 행 추가 — 탭 시 설정 앱으로 이동 (앱에서 요청 불가)
  • 꺼져 있으면 시작하지 않음

[7] DEBUG 전용 시작 메뉴

  • 채팅 + 메뉴 → 라이브 액티비티 시작 (DEBUG) → 경로 시나리오(지하철 환승 / 버스 + 지하철 / 자차) 선택, 약속 시간 지난 상태로 시작 하위 메뉴
  • 관련 코드는 DebugLiveActivity 폴더에 모으고 전부 #if DEBUG

동작 없는 상태

  • 단계 진행은 내 폰에서만 반영되고 채팅·지도·친구에게 전달되지 않음
  • 도착 인원은 내 도착만 +1
  • 도착 버튼을 누르지 않으면 자동 종료 없음 — iOS가 8시간 뒤 강제 종료할 때까지 유지
  • 정식 시작 경로 없음 — DEBUG 메뉴로만 시작 가능

💬 리뷰 노트

진행 단계는 저장하지 않습니다

단계 버튼으로 어디까지 왔는지는 앱이나 서버에 상태로 기록하지 않고, 라이브 액티비티 상태에만 둡니다.
2단계에서도 단계 도착과 위치 공유 모두 "채팅방에 일회성 메시지 + 약속 지도에 내 좌표 갱신"만 할 예정이라, 저장소를 두지 않았습니다.

단계 버튼 처리를 UseCase가 아닌 Service로 둔 이유

프로젝트에서 화면(ViewModel)이 부르는 동작은 UseCase로 ScreenFactory에서 만들고, 화면 밖에서 부르는 동작은 Service로 DI에 등록하고 있습니다.
단계 버튼은 시스템이 실행하는 LiveActivityIntent가 화면 없이 부르므로 RouteProgressService로 두었습니다.

인텐트는 시스템이 직접 생성해 생성자로 의존성을 넘길 수 없어서, App Intents의 AppDependencyManager / @AppDependency를 사용했습니다.

  • AppDelegate가 앱 시작 시 DI 컨테이너에서 서비스를 꺼내 AppDependencyManager에 등록
  • 인텐트는 @AppDependency로 RouteProgressService 프로토콜만 주입받음 — DI 컨테이너를 모름
  • 프로토콜은 위젯도 컴파일해야 해서 Shared에 두었습니다

같은 기준으로 기존 FCMTokenService, SessionValidationService도 SignInService처럼 프로토콜(XxxService) + 구현(DefaultXxxService)으로 나누고 프로토콜 키로 등록했습니다. 호출부는 바뀌지 않았습니다.

서버가 할 일을 임시로 앱이 하는 코드

push-to-start는 앱 코드를 거치지 않으므로, 3단계에서는 확인 지점·구간 목록을 서버가 만들어 보내야 합니다. 지금은 그 역할을 앱이 대신합니다.
나중에 한 번에 지울 수 있도록 DebugLiveActivity 폴더에 모았습니다.

  • RouteLiveActivityStarting + ActivityKitRouteLiveActivityRepository+Start: 시작을 정식 저장소 프로토콜에서 분리
  • StartRouteLiveActivityUseCase, RouteCheckpointPlanner(3단계 서버 구현의 원본)
  • ChatViewController+LiveActivityDebugMenu: DEBUG 메뉴

이 폴더의 DEBUG 메뉴는 ViewModel·ScreenFactory를 거치지 않고 DIContainer에서 저장소를 직접 꺼내 유스케이스를 만듭니다. 지울 코드라 ChatViewModel·ScreenFactory·DI 등록에 흔적을 남기지 않으려고 일부러 택한 방식이며, 파일 주석에도 적어 두었습니다.
폴더 밖 연결점은 ChatViewController 메뉴의 #if DEBUG 한 곳뿐이고, Release 빌드에는 포함되지 않습니다.

도메인 모델과 액티비티 모델을 분리한 이유

도메인 RouteProgressPlan 등과 액티비티 모델 AppointmentRouteActivityAttributes가 거의 1:1이라 변환 코드가 있습니다.
UserDTO처럼 외부 형식과 도메인을 분리하는 방식에 맞췄고, 액티비티 데이터는 4KB 제한이 있어 좌표·Place 없이 표시용 값만 담도록 따로 두었습니다.

잠금화면이 검게만 나오던 문제

"약속까지"와 남은 시간 사이 공백을 줄이려고 타이머 Text(timerInterval:)에 .fixedSize()를 줬더니 배너 전체가 그려지지 않았습니다.
같은 시기에 여백도 늘렸는데, 잠금화면 라이브 액티비티는 높이 160pt를 넘어도 같은 증상이 납니다.

👉 결론: 타이머는 고정 폭(44pt)으로 두고, 여백은 160pt 안에서 조정. 코드에 160pt 제한 주석 추가

시간이 지나면 단계 버튼이 반응하지 않던 문제

staleDate가 지나면 시스템이 액티비티 상태를 .active에서 .stale로 바꾸는데, 조회를 .active만 하고 있어 버튼을 눌러도 대상을 찾지 못했습니다.
지금은 약속 시각을 staleDate로 걸어 "+경과 시간" 표시에 쓰고 있어 계속 해당되는 조건입니다.

👉 결론: 진행 중 판정에 .stale 포함

기존 동작을 바꾼 것

  • 마이페이지 위치 권한 행: viewWillAppear에서 한 번 읽던 방식에서 구독 방식으로 바꿔, 설정 앱에서 권한을 바꾸고 돌아오면 바로 갱신됩니다. -> 실시간 권한을 추가하면서 통일했습니다.
  • MyPageViewModel: 쓰이지 않던 LocationPermissionAction, requestLocationPermission(), handlePermissionAction(), cancellables를 삭제했습니다. 같은 이름의 코드가 상세 화면 LocationPermissionViewModel에도 있는데, 그쪽은 사용 중이라 그대로 두었습니다.
  • Mock 약속 데이터: MockAppointmentDetailRepository의 내 경로가 기존 다중 스텝 경로에서 시나리오 경로로 바뀌었고, 약속 시각이 13분 뒤 → 25분 뒤로 바뀌었습니다. 약속 지도 화면에도 그대로 반영됩니다.

확인 부탁드립니다

  • 지하철·버스 구분을 탑승 장소 타입(Place.type == .subway)으로 추정하고 있습니다. 실제 길찾기 API를 붙일 때 탈것 정보를 받을 필드를 RouteStep에 따로 두도록 수정해야 합니다. (탑승 교통 정보를 시내로만 추정하고 있었어서, 일반 열차나 시외 버스도 고려하도록 이 부분은 수정이 필요합니다.)
  • 3단계에서 단계 규칙은 서버가 맡고, 앱의 RouteCheckpointPlanner는 DebugLiveActivity 폴더와 함께 삭제합니다. 그때 서버 구현은 이 파일을 원본으로 옮깁니다.

추후 작업

  • 2단계 (약속·채팅 Firestore 전환 후): 단계 도착·위치 공유 시 채팅 전용 메시지 + 약속 지도 내 좌표 갱신, 도착 인원 실데이터, 되돌리기와 정정 메시지
  • 3단계 (Functions·APNs 키 구축 후): 출발 15분 전 push-to-start, 미도착 시 약속 + 1시간 서버 종료 푸시, 친구 도착 시 도착 인원 원격 갱신, DebugLiveActivity 폴더 삭제

📸 영상 / 이미지

WORK57.live.MP4

녹화 상태로 안 보이는 컴팩트 오른쪽 시간 표시 이미지

image

github-actions[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as duplicate.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI 코드 리뷰 - 아키텍처 관점

약속 이동 라이브 액티비티 전체 플로우(위젯 익스텐션 추가, 딥링크 처리, 단계 진행 Intent, 마이페이지 권한 표시)를 한 PR에서 구현했다. Shared 폴더로 앱-위젯 공유 모델을 분리하고, DebugLiveActivity 폴더를 #if DEBUG로 격리해 임시 코드가 프로덕션에 유입되지 않도록 한 구조는 명확하다. 다만 LiveActivityPermissionStatus(도메인)와 LiveActivityPermissionState(프레젠테이션) 두 타입이 값 집합이 동일함에도 별도로 존재해 변환 계층이 중복되고, AdvanceRouteProgressIntent가 DIContainer를 직접 참조하는 부분이 아키텍처 경계를 침범한다. RouteProgressService가 도메인 서비스임에도 DI 컨테이너에 concrete 타입으로 직접 등록되어 있어 테스트 대체가 불가능한 구조다.

Comment thread WhereAreYou/Shared/AdvanceRouteProgressIntent.swift Outdated
Comment thread WhereAreYou/WhereAreYou/Core/DI/Implementation/AppDIContainer+Register.swift Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI 코드 리뷰 - 버그 관점

약속 이동 라이브 액티비티 2단계 구현 PR이다. 위젯 익스텐션 추가, 딥링크 처리, 라이브 액티비티 Intent, 단계 진행 서비스, 마이페이지 실시간 현황 권한 표시를 포함한다. 전반적으로 구조가 명확하나, LiveActivityPermissionState+.swift에서 존재하지 않는 타입을 참조하는 버그, RouteProgressService.advance의 경계 조건 오류, 그리고 스레드 안전성 문제가 확인된다.

Comment thread WhereAreYou/WhereAreYou/Presentation/Chat/ChatViewModel.swift
github-actions[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as duplicate.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI 코드 리뷰 - 성능/최적화 관점

약속 이동 라이브 액티비티 기능을 추가하는 PR이다. 위젯 익스텐션 신규 추가, 딥링크 처리, RouteProgressService를 통한 단계 진행 처리, 마이페이지 실시간 현황 권한 표시 등이 포함된다. 성능 관점에서 주목할 부분은 두 곳이다. ActivityKitRouteLiveActivityRepository의 update/end에서 동일 appointmentID 액티비티를 매번 전체 스캔하는 방식, 그리고 RouteProgressBar에서 SwiftUI 렌더링 시 매 body 평가마다 Array(segments.enumerated())를 새로 할당하는 방식이다. 나머지 코드는 구조적으로 타당하며 명백한 성능 문제는 없다.

Comment thread WhereAreYou/WhereAreYouWidget/RouteProgressBar.swift Outdated
@sangYuLv
sangYuLv marked this pull request as ready for review September 28, 2026 15:01
@sangYuLv
sangYuLv requested a review from snughnu September 28, 2026 15:01
@sangYuLv
sangYuLv added this pull request to stack #29 September 28, 2026 15:57

@snughnu snughnu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

특별하게 리뷰를 남겨야겠다는 부분은 딱히 없었습니다.

ai리뷰와 리뷰노트의 확인 사항 모두 확인했고,
추후 작업을 진행하면서 변경될 부분들(Mock, debug 등)도 확인해 뒀습니다!

UI 예쁘네요 수고하셨습니다. 🙊

@sangYuLv
sangYuLv merged commit 06d5630 into develop Oct 1, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants