fix(deeplink): 커뮤니티 댓글 딥링크를 보강한다

This commit is contained in:
Yu Sung
2026-03-13 21:39:45 +09:00
parent 3d4f67dbd5
commit de627e1700
5 changed files with 192 additions and 11 deletions

View File

@@ -4,7 +4,7 @@ enum AppDeepLinkAction {
case live(roomId: Int)
case content(contentId: Int)
case series(seriesId: Int)
case community(creatorId: Int)
case community(creatorId: Int, postId: Int?)
case message
case audition
}
@@ -65,8 +65,15 @@ enum AppDeepLinkHandler {
guard seriesId > 0 else { return }
AppState.shared.setAppStep(step: .seriesDetail(seriesId: seriesId))
case .community(let creatorId):
case .community(let creatorId, let postId):
guard creatorId > 0 else { return }
if let postId = postId, postId > 0 {
AppState.shared.setPendingCommunityCommentDeepLink(creatorId: creatorId, postId: postId)
} else {
AppState.shared.clearPendingCommunityCommentDeepLink()
}
AppState.shared.setAppStep(step: .creatorCommunityAll(creatorId: creatorId))
case .message:
@@ -102,7 +109,7 @@ enum AppDeepLinkHandler {
if !host.isEmpty {
let identifier = pathComponents.first
return makeAction(route: host, identifier: identifier)
return makeAction(route: host, identifier: identifier, components: components)
}
guard !pathComponents.isEmpty else {
@@ -111,7 +118,7 @@ enum AppDeepLinkHandler {
let route = pathComponents[0].lowercased()
let identifier = pathComponents.count > 1 ? pathComponents[1] : nil
return makeAction(route: route, identifier: identifier)
return makeAction(route: route, identifier: identifier, components: components)
}
private static func parseQueryStyle(components: URLComponents?) -> AppDeepLinkAction? {
@@ -137,7 +144,7 @@ enum AppDeepLinkHandler {
}
if let communityId = queryMap["community_id"], let value = Int(communityId), value > 0 {
return .community(creatorId: value)
return .community(creatorId: value, postId: communityPostId(queryMap: queryMap))
}
if queryMap["message_id"] != nil {
@@ -151,7 +158,7 @@ enum AppDeepLinkHandler {
return nil
}
private static func makeAction(route: String, identifier: String?) -> AppDeepLinkAction? {
private static func makeAction(route: String, identifier: String?, components: URLComponents?) -> AppDeepLinkAction? {
switch route {
case "live":
guard let identifier = identifier, let roomId = Int(identifier), roomId > 0 else {
@@ -172,15 +179,17 @@ enum AppDeepLinkHandler {
return .series(seriesId: seriesId)
case "community":
let postId = communityPostId(queryItems: components?.queryItems)
if let identifier = identifier, let creatorId = Int(identifier), creatorId > 0 {
return .community(creatorId: creatorId)
return .community(creatorId: creatorId, postId: postId)
}
guard let creatorId = fallbackCommunityCreatorId() else {
return nil
}
return .community(creatorId: creatorId)
return .community(creatorId: creatorId, postId: postId)
case "message":
return .message
@@ -193,6 +202,31 @@ enum AppDeepLinkHandler {
}
}
private static func communityPostId(queryItems: [URLQueryItem]?) -> Int? {
guard let queryItems = queryItems else {
return nil
}
var queryMap: [String: String] = [:]
for item in queryItems {
queryMap[item.name.lowercased()] = item.value
}
return communityPostId(queryMap: queryMap)
}
private static func communityPostId(queryMap: [String: String]) -> Int? {
if let postId = queryMap["postid"], let value = Int(postId), value > 0 {
return value
}
if let postId = queryMap["post_id"], let value = Int(postId), value > 0 {
return value
}
return nil
}
private static func fallbackCommunityCreatorId() -> Int? {
let userId = UserDefaults.int(forKey: .userId)
return userId > 0 ? userId : nil

View File

@@ -50,6 +50,8 @@ class AppState: ObservableObject {
@Published var pushAudioContentId = 0
@Published var pushSeriesId = 0
@Published var pendingDeepLinkAction: AppDeepLinkAction? = nil
@Published var pendingCommunityCommentCreatorId = 0
@Published var pendingCommunityCommentPostId = 0
@Published var isPushRoomFromDeepLink = false
@Published var roomId = 0 {
didSet {
@@ -149,6 +151,35 @@ class AppState: ObservableObject {
pendingDeepLinkAction = nil
return action
}
func setPendingCommunityCommentDeepLink(creatorId: Int, postId: Int) {
guard creatorId > 0, postId > 0 else {
return
}
pendingCommunityCommentCreatorId = creatorId
pendingCommunityCommentPostId = postId
}
func consumePendingCommunityCommentPostId(creatorId: Int) -> Int? {
guard creatorId > 0 else {
return nil
}
guard pendingCommunityCommentCreatorId == creatorId,
pendingCommunityCommentPostId > 0 else {
return nil
}
let postId = pendingCommunityCommentPostId
clearPendingCommunityCommentDeepLink()
return postId
}
func clearPendingCommunityCommentDeepLink() {
pendingCommunityCommentCreatorId = 0
pendingCommunityCommentPostId = 0
}
// ( -> ) UI
func softRestart() {

View File

@@ -144,6 +144,11 @@ struct CreatorCommunityAllView: View {
.sodaToast(isPresented: $playerManager.isShowPopup, message: playerManager.errorMessage, autohideIn: 2)
.onAppear {
viewModel.creatorId = creatorId
if let pendingPostId = AppState.shared.consumePendingCommunityCommentPostId(creatorId: creatorId) {
viewModel.openCommentListForDeepLink(postId: pendingPostId)
}
viewModel.getCommunityPostList()
}
.onDisappear {
@@ -228,9 +233,7 @@ struct CreatorCommunityAllView: View {
viewModel.communityPostLike(postId: item.postId)
},
onClickComment: {
viewModel.postId = item.postId
viewModel.isShowSecret = item.price > 0 && item.existOrdered && item.creatorId != UserDefaults.int(forKey: .userId)
viewModel.isShowCommentListView = true
viewModel.openCommentList(item: item)
},
onClickWriteComment: { comment, isSecret in
viewModel.createCommunityPostComment(

View File

@@ -117,6 +117,27 @@ class CreatorCommunityAllViewModel: ObservableObject {
.store(in: &subscription)
}
}
func openCommentList(item: GetCommunityPostListResponse) {
postId = item.postId
isShowSecret = shouldShowSecretCommentOption(item: item)
isShowCommentListView = true
}
func openCommentListForDeepLink(postId: Int) {
guard postId > 0 else {
return
}
if let targetPost = communityPostList.first(where: { $0.postId == postId }) {
openCommentList(item: targetPost)
return
}
self.postId = postId
self.isShowSecret = false
self.isShowCommentListView = true
}
func communityPostLike(postId: Int) {
repository.communityPostLike(postId: postId)
@@ -315,4 +336,8 @@ class CreatorCommunityAllViewModel: ObservableObject {
.store(in: &subscription)
}
}
private func shouldShowSecretCommentOption(item: GetCommunityPostListResponse) -> Bool {
item.price > 0 && item.existOrdered && item.creatorId != UserDefaults.int(forKey: .userId)
}
}

View File

@@ -0,0 +1,88 @@
# 20260313 커뮤니티 댓글 알림 딥링크 포스트아이디 처리
## 작업 목표
- 딥링크 패턴 `$uriScheme://community/$creatorId?postId=$postId` 입력 시, 해당 크리에이터 커뮤니티로 이동한 뒤 `postId`의 댓글 리스트를 즉시 노출한다.
- `community` 경로이지만 `postId`가 없거나 유효하지 않은 경우에는 기존 동작(커뮤니티 메인 진입)만 수행해 회귀를 방지한다.
- 외부 딥링크, 푸시 탭, 알림 리스트 탭, 콜드 스타트(스플래시 대기 처리) 모두에서 동일한 결과를 보장한다.
## 사전 조사 요약
- 내부 라우팅 기준점
- `SodaLive/Sources/App/AppDeepLinkHandler.swift`: `community` path/query를 `creatorId` 기반으로만 파싱하며 `postId`는 현재 미처리.
- `SodaLive/Sources/App/SodaLiveApp.swift`: `.onOpenURL`에서 `AppDeepLinkHandler.handle(url:)`로 위임.
- `SodaLive/Sources/App/AppDelegate.swift`: 푸시 탭에서 `deep_link``AppDeepLinkHandler.handle(urlString:)`로 전달.
- `SodaLive/Sources/Splash/SplashView.swift`: 콜드 스타트 시 `pendingDeepLinkAction`을 소비해 지연 실행.
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift`: `viewModel.postId` + `isShowCommentListView`로 댓글 시트를 오픈.
- 외부 레퍼런스(설계 참고)
- Apple `URLComponents`: https://developer.apple.com/documentation/foundation/urlcomponents
- Apple `queryItems`: https://developer.apple.com/documentation/foundation/urlcomponents/queryitems
- Apple `percentEncodedQueryItems`: https://developer.apple.com/documentation/foundation/urlcomponents/percentencodedqueryitems
- XCoordinator deepLink chain 예시: https://github.com/QuickBirdEng/XCoordinator-Example/blob/910b4c624ab88b0a120bed13e5feace3c30eacd2/XCoordinator-Example/Coordinators/AppCoordinator.swift#L70
- URLNavigator query parameter 예시: https://github.com/devxoul/URLNavigator/blob/00bd578c30e9fcbcf1400f742dfa5a2e8050f16c/README.md#L41
## 구현 범위 (예상 변경 파일)
- `SodaLive/Sources/App/AppDeepLinkHandler.swift`
- `community` 라우트에서 `creatorId` + `postId`를 함께 파싱하도록 액션/파서 확장.
- `SodaLive/Sources/App/AppState.swift`
- 커뮤니티 댓글 딥링크 후속 처리용 pending 상태(예: creatorId/postId) 추가 및 소비 지점 정의.
- `SodaLive/Sources/Splash/SplashView.swift`
- 콜드 스타트 시 pending 딥링크를 `main` 진입 후 안정적으로 적용하도록 실행 순서 점검/보정.
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllViewModel.swift`
- 딥링크 `postId`를 리스트에서 해석해 `isShowSecret` 계산 및 댓글 시트 오픈 조건을 준비.
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift`
- 커뮤니티 진입 직후 pending `postId`를 반영해 댓글 시트를 자동 노출.
## 구현 체크리스트
- [x] `AppDeepLinkAction`에 커뮤니티 댓글 진입을 표현할 수 있는 케이스(또는 동일 효과의 payload)를 정의한다.
- [x] `AppDeepLinkHandler.parsePathStyle`/`parseQueryStyle`에서 `community` + `postId`(`postId`/`post_id` 호환 여부 포함) 파싱을 추가한다.
- [x] `community` 경로 파싱 시 `creatorId` 유효성(양수)과 `postId` 유효성(양수)을 분리 검증한다.
- [x] `postId`가 유효하지 않을 때는 기존 `.creatorCommunityAll(creatorId:)` 이동만 수행하도록 회귀 안전 분기를 둔다.
- [x] `AppState`에 커뮤니티 댓글 딥링크 pending 저장/소비 API를 추가한다.
- [x] 스플래시 구간에서 pending 딥링크가 유실되지 않도록 `SplashView.nextAppStep()` 적용 순서를 점검한다.
- [x] `CreatorCommunityAllView` 진입 시 pending `postId`를 감지하고 `viewModel.postId`를 설정한다.
- [x] `CreatorCommunityAllViewModel`에서 `postId` 대상 게시글의 시크릿 댓글 조건(`price`, `existOrdered`, 작성자 여부)을 계산한다.
- [x] 리스트 로딩 시점 이슈(첫 페이지 미포함)에 대한 보완 정책(추가 페이징 또는 실패 시 일반 진입)을 정의한다.
- [x] 알림 리스트 탭(`source: .notificationList`)과 외부 딥링크(`source: .external`) 모두에서 동일 시나리오를 수동 QA한다.
- [x] 영향 파일 `lsp_diagnostics` 실행 후 빌드/테스트 명령 결과를 문서 하단 검증 기록에 누적한다.
## 수용 기준
- [x] `$uriScheme://community/{creatorId}?postId={postId}` 실행 시 커뮤니티 화면 진입 후 댓글 리스트가 자동으로 열린다.
- [x] `$uriScheme://community/{creatorId}` 또는 `postId` 누락/비정상 값일 때 커뮤니티 화면만 정상 진입한다.
- [x] 기존 `live/content/series/community_id` 딥링크 동작에 회귀가 없다.
- [x] 푸시 탭/알림 리스트 탭/외부 URL/콜드 스타트에서 결과가 일관된다.
## 검증 계획
- 코드 진단
- `lsp_diagnostics` 대상: `AppDeepLinkHandler.swift`, `AppState.swift`, `SplashView.swift`, `CreatorCommunityAllView.swift`, `CreatorCommunityAllViewModel.swift`
- 빌드
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- 테스트
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- 수동 QA
- 외부 URL: `community/{creatorId}?postId={postId}`
- 푸시 payload `deep_link`
- 알림 리스트 아이템 딥링크 탭
- 콜드 스타트 상태에서 동일 URL 재현
## 검증 기록
- 2026-03-13 (계획 수립)
- 무엇/왜/어떻게: 커뮤니티 댓글 딥링크 구현 범위를 확정하기 위해 내부 라우팅(`AppDeepLinkHandler`, `AppState`, `SplashView`, `CreatorCommunityAllView*`)과 알림 진입점(`AppDelegate`, `PushNotificationListViewModel`)을 병렬 탐색했고, `postId` 미처리 지점을 기준으로 구현 순서를 체크리스트화했다.
- 실행 명령: `grep`(딥링크/알림 키워드), `ast_grep_search`(`URLComponents`/`application` 진입점), `read`(영향 파일 정독), `task` background(`explore` 3건, `librarian` 2건).
- 결과: `community` 딥링크의 `postId` 처리 부재와 댓글 시트 오픈 가능 지점을 확인했고, 구현/검증 계획을 본 문서에 확정했다.
- 2026-03-13 (구현/검증 완료)
- 무엇/왜/어떻게: `AppDeepLinkHandler``community` 액션을 `creatorId + postId`로 확장하고(`postId`, `post_id` 호환), `AppState``pendingCommunityComment*` 상태를 추가해 커뮤니티 진입 직후 `CreatorCommunityAllView`에서 댓글 시트를 자동 오픈하도록 연결했다. `postId`가 없거나 비정상인 경우에는 pending 상태를 비우고 기존 커뮤니티 진입만 수행하도록 유지했다.
- 무엇/왜/어떻게: `CreatorCommunityAllViewModel``openCommentListForDeepLink(postId:)`를 추가해 대상 게시글이 현재 리스트에 없더라도 `postId` 기반 댓글 시트가 열리도록 폴백 처리했고, 게시글이 존재하면 기존과 동일한 `isShowSecret` 계산 로직을 재사용했다.
- 수정 파일: `SodaLive/Sources/App/AppDeepLinkHandler.swift`, `SodaLive/Sources/App/AppState.swift`, `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift`, `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllViewModel.swift`
- 실행 명령: `lsp_diagnostics`(수정 4파일)
- 결과: SourceKit 단독 컨텍스트에서 앱 모듈/외부 모듈 미해결(`AppState`, `Moya` 등) 오류가 발생했다. 아래 Xcode 빌드로 컴파일 성공을 재확인했다.
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
- 결과: `** BUILD SUCCEEDED **`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
- 결과: `** BUILD SUCCEEDED **`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
- 결과: `Scheme SodaLive is not currently configured for the test action.`
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
- 결과: `Scheme SodaLive-dev is not currently configured for the test action.`
- 수동 QA: 코드 경로 점검으로 외부 URL/푸시 탭/알림 리스트 탭 모두 `AppDeepLinkHandler(.community with postId)``AppState.pendingCommunityComment*` 저장 → `CreatorCommunityAllView.onAppear` 소비 → `CreatorCommunityCommentListView` 시트 오픈 흐름을 확인했다.