feat: 알림받기/미읽음 뱃지 + Mixpanel 트래킹 + 무료충전 리워드 광고 + recap 잠금#39
Conversation
- GET /me/mypage, /me/credits/history 연동 (Clean Architecture 전 레이어) - 마이페이지 메인: 프로필 카드/포인트 충전/철학자 유형/메뉴 + 스켈레톤 - 포인트 내역: offset 페이지네이션 + 스켈레톤, 히스토리 아이콘 → 주제 제안 팝업 - 설정: 로그아웃/탈퇴(AuthUseCase + Keychain clear → 로그인 복귀), 약관/개인정보 웹뷰 - CustomAlert .logout/.withdraw/.suggestTopic 스타일 + 공통 팝업 헬퍼 추출 - PickeNavigationBar centerTitle 지원, 푸시 화면 toolbar(.hidden) nav/tab - Date/금액 포맷 Utill·Entity 분리, 색상 .foregroundStyle(.token) 단축형 정리 - MainTab myPage → ProfileCoordinator, 세션 종료 → App presentAuth #9 #10 #13 #15 #16
- GET /me/battle-records 연동 (Clean Architecture 전 레이어, offset 무한 스크롤 + 스켈레톤) - 마이페이지 '내 배틀 기록' 메뉴 → 화면 push - PickeEmptyStateView 공통 컴포넌트(noDataLogo) — 포인트내역/배틀기록 빈 상태 적용 - Entity 멀티-타입 파일 분리(SOLID, 1타입 1파일) + Home 기능별 세부 폴더링 (Battle/Detail·PreVote·VoteStats·Recommend, Comment/Item·Perspective·PerspectiveComment, Scenario/Chat, Section/Battle·Vote) - AGENTS.md: Entity 1타입 1파일 규칙 추가 #9 #12
- GET /me/content-activities 연동 (Clean Architecture 전 레이어) - 내 콘텐츠 활동: 내 댓글/좋아요 탭 전환 + 무한 스크롤 + 스켈레톤, 빈 상태 - 공지사항·이벤트: 탭(공지사항/이벤트), API 미구현으로 빈 콘텐츠 - SkeletonBlock(DesignSystem) 공통 컴포넌트로 추출 — 3개 스켈레톤 중복 제거 - Utill: Date.relativeKoreanString(상대시간) 추가 - 마이페이지 '내 콘텐츠 활동' / '공지방·이벤트' 메뉴 → 화면 push #11 #14
- GET/PATCH /me/notification-settings 연동 (Clean Architecture 전 레이어) - 알림 설정: 3섹션(기능별/소셜/마케팅) 6토글, 토글 시 낙관적 갱신 + PATCH, 커스텀 토글(32×18) + 스켈레톤 - 설정 '알림설정' 메뉴 → 알림 설정 화면 push - 내 콘텐츠 활동/공지·이벤트: 좌우 DragGesture 로 탭 전환(CommentView UX) - Profile 전체 ScrollView .scrollBounceBehavior(.basedOnSize) 오버스크롤 방지 - Entity 1타입 1파일(NotificationSettings/Key/Section) #10 #11
- 상단 종(알림) 아이콘 openNotification → 알림 설정 화면 push (미연결 상태였음) - 알림 설정 섹션 헤더↔행 간격을 picke.pen 기준 4 로 정합 #10
마이페이지에서 배틀 주제 제안 플로우를 열고, 제안 API 전 계층과 입력 화면을 연결하며 배경 탭 시 키보드가 내려가도록 포커스 해제를 적용한다.
마이페이지 루트의 종 아이콘 이벤트는 유지하되 알림 설정 화면으로 바로 push 하던 라우팅만 제거한다.
- GET /me/recap 연동 (Clean Architecture, Entity 1타입 1파일: PhilosopherRecap/RecapCard/RecapScores/RecapScoreAxis/PreferenceReport/FavoriteTopic) - 컴포넌트 분리(잠금 화면 재사용 대비): RecapPhilosopherCard / RecapRadarChart / RecapScoreBar / RecapMatchCard - 성향 분석: 육각 레이더 차트(중심→값 펼침 애니메이션) + 6축 점수 바(채움 애니메이션) - 내 취향 리포트(통계 3칸 + 선호 주제 랭킹), 궁합 BEST/WORST 2카드, 공유하기 - 마이페이지 '나의 철학자 유형' 카드 탭 → 리캡 화면 push #13
- 공유하기 → 애플 시스템 공유 시트(ShareItem + ShareSheet, 유형명/설명/태그/이미지) - RecapSkeletonView 추가 — 로딩 시 ProgressView 대신 섹션 스켈레톤 - 성향 점수 바: 값 숫자 잘림 수정(fixedSize) + picke.pen Radar Wrap 스펙(바 48, 패딩 [8,12]) 정합 #13
- RecapScores.gridAxes 추가 — 바 2열 그리드 순서를 디자인대로(레이더 각도 순서와 분리) #13
- 레이더 축: 원칙(위)·이성·개인·변화·내면·직관 (디자인 라벨/순서) - 데이터 꼭짓점에 점(dot) 표시, 상단 '원칙' SemiBold + 라벨 12pt - 레이더는 axes(직관), 점수 바 그리드는 gridAxes(이상)로 각 디자인 분리 #13
- 정규 육각형 → 디자인 육각형(폭/높이 0.846, 측면 꼭짓점 y±0.559) - 데이터 폴리곤 채움 primary500 8%(#89382514), 꼭짓점 점 6px, 4겹 그리드+스포크 - 라벨 10pt (원칙: SemiBold/neutral900, 나머지: Medium/gray400) - 중심→값 펼침 애니메이션(easeOut 0.9s) 유지 #13
- body에서 Path 직접 그리던 방식은 withAnimation이 모양을 보간 못 해 즉시 완성됨 - RadarPolygon: Shape 로 분리하고 progress 를 animatableData 로 노출 → 중심에서 값까지 부드럽게 펼쳐짐 - 꼭짓점 점은 .position(progress) 로 함께 보간 #13
- recap: totalParticipation < 5 → RecapLockedView(??형 카드 + 블러 성향 + '배틀 5개' 안내), 아니면 성향 표시 - 마이페이지 철학자 카드: 미확정 시 '??형'+그레이 placeholder, 확정 시 '유형명 · 라벨'+철학자 이미지 - ProfileFeature: philosopherLabel/imageURL/isPhilosopherLocked 추가, mypage 매핑 - 닉네임 옆 자물쇠 제거(picke.pen 노드에 없음), 미사용 isLocked 제거 - RecapLockedView 블러 4.375 등 잠금 화면 패딩 picke.pen 정합 #13
- /me/mypage 의 philosopher 가 미확정 시 null 로 내려와 디코딩 에러(DataError) 발생 - MyPageDataDTO 의 profile/philosopher/tier 를 옵셔널로 + 매퍼 nil→.empty 폴백 - MyProfile/MyPhilosopher/MyTier 에 .empty 정적 헬퍼 추가 - philosopher null → ??형 잠금 정상 표시 #9
- isPhilosopherLocked 시 brain placeholder 대신 Image(asset: .lock) 노출 #9
- 서버가 data:nil(200)로 응답 시 에러 던지지 않고 PhilosopherRecap.empty 반환 - totalParticipation 0 → isLocked → RecapLockedView 노출 (스켈레톤 멈춤 버그 수정) - empty 헬퍼를 Model private → Entity public static 으로 중앙화
fastlane 실행 전에 match keychain을 env 기반으로 unlock해 macOS 키체인 비밀번호 팝업을 줄인다.
- ??형 색상 gray500→gray800, 아바타 원 beige600→gray50(#EBEBEB) - 카드 내부 콘텐츠 패딩 20 추가 (pen Content 패딩 정합) - 성향 분석 잠금 레이더 라벨/형태 pen 정합(원칙·논리·일관성·공감·실용·직관, 거의 꽉 찬 육각형)
- 데이터 계층: NotificationAPI/Service/DTO/Mapper/Entity/Interface/UseCase/Repository + DI 등록
- GET /notifications(목록·카테고리·페이징), GET /{id}(상세, 화면 연결 보류), POST /{id}/read, POST /read-all
- PieckeDomain.notification 추가
- Notification 모듈: NotificationCoordinator + NotificationFeature/View (picke.pen 알림받기 정합)
- 카테고리 탭(전체·콘텐츠·공지사항·이벤트), 무한 스크롤, 탭 시 읽음/모두 읽음
- 스켈레톤(SkeletonBlock)·빈화면(PickeEmptyStateView) Profile 패턴 적용
- Home·Profile·Hifi 종 아이콘 → NotificationCoordinator 진입 연결
- UseCase 모듈에 AnalyticsUseCase 추가 (auth/battle/session 이벤트 + 공통 유저 프로퍼티) - 로그인 성공/실패·회원가입 성공 시 Mixpanel 트래킹 + identify - UseCase Project 에 Mixpanel/SessionReplay 의존성 추가 - 인프라(SPM·App init·SessionReplay·토큰)는 기존 구성 활용
- HasUnreadNotification @shared(inMemory) 전역 플래그 도입 - 알림 목록 로드(전체 탭) 시 미읽음 여부 반영, 모두 읽음/단건 읽음 시 갱신 - 홈 헤더·프로필 종 아이콘에 errorDefault 빨간점 오버레이
- 충전 바 좌측(보유 포인트) 탭 → 포인트 내역 상세 진입 - 무료 충전 배지 탭 → GoogleMobileAds 리워드 동영상 광고(네이티브) - RewardedAdClient 의존성 추가, 보상 획득 시 마이페이지 재조회로 포인트 갱신 - 광고 클릭 랜딩(웹뷰)은 SDK 인앱 브라우저로 자동 처리 - Profile 모듈에 googleMobileAds 의존성 추가
- SettingsFeature: 로그아웃 완료 시 logout_succeeded 트래킹 - PreVoteFeature: 배틀 상세 진입(detailOpened), 사전/사후 투표 제출(prevote/postvoteSubmitted) 트래킹
|
{ |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9c92602c1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .Shared(implements: .Shared), | ||
| .SPM.composableArchitecture, | ||
| .SPM.tcaFlow, | ||
| .SPM.googleMobileAds, |
There was a problem hiding this comment.
Add Profile's missing external packages
The new Profile target imports and uses Kingfisher (ProfileView, ContentActivityView, and recap cards) and imports LogMacro in several reducers, but this dependency list only links TCA/TCAFlow/GoogleMobileAds. When Tuist generates the target, Swift will compile Profile without those products in its target dependencies, so the app build fails with missing Kingfisher/LogMacro modules as soon as the new Profile sources are compiled.
Useful? React with 👍 / 👎.
| .Shared(implements: .Shared), | ||
| .SPM.composableArchitecture, | ||
| .SPM.tcaFlow, | ||
| ], |
There was a problem hiding this comment.
Link LogMacro for Notification
The new Notification target's reducer imports LogMacro (NotificationFeature.swift), but this project only declares UseCase/Shared/TCA/TCAFlow dependencies. Since LogMacro is an external product that other targets add explicitly when they use it, the Notification target will fail to compile with no such module 'LogMacro' until .SPM.logMarco is added here.
Useful? React with 👍 / 👎.
… UseCase 이전 - xcconfig(REWARD_AD_UNIT) → Info.plist → Bundle 주입, 미설정 시 테스트 ID 폴백 - RewardedAdClient 를 Profile → UseCase 모듈로 이동(AnalyticsUseCase 와 동일하게 side-effect 클라이언트 일원화) - Profile googleMobileAds 의존성 제거, UseCase 로 이관
There was a problem hiding this comment.
전반적으로 잘 구성된 PR입니다. 알림, 프로필, 리캡, 배틀 제안 등 여러 핵심 기능이 TCA와 Clean Architecture 원칙에 따라 깔끔하게 구현되었습니다. DTO-Entity 매핑, 비동기 로직, UI/UX 디테일(스켈레톤, 빈 화면, 스와이프 제스처, 레이더 차트 애니메이션 등)에 대한 고민과 노력이 돋보입니다. 특히 Mixpanel 트래킹과 GoogleMobileAds 리워드 광고 연동, 전역 Shared 상태를 활용한 미읽음 뱃지 구현도 훌륭합니다.
주요 개선점으로는 AppReducer 내 미사용 파라미터와 BattleView의 오타가 발견되었습니다. 또한, Entity 모듈의 대규모 파일 이동 및 이름 변경이 있었는데, 이는 기존 코드베이스와의 호환성과 추후 유지보수 시 혼란을 줄이기 위해 한 번에 처리하는 것이 좋았을 것으로 보입니다. 다만, 이는 이미 변경된 사항이며, 이번 PR의 주요 구현 내용 자체에는 문제가 없습니다.
| Reduce { state, action in | ||
| switch action { | ||
| case .view(let viewAction): | ||
| case let .view(viewAction): |
There was a problem hiding this comment.
🔵 [P4] Readability
Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .view(let viewAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.
| case .inner(let innerAction): | ||
|
|
||
| case let .inner(innerAction): |
There was a problem hiding this comment.
🔵 [P4] Readability
Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .inner(let innerAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.
| case .async(let asyncAction): | ||
|
|
||
| case let .async(asyncAction): |
There was a problem hiding this comment.
🔵 [P4] Readability
Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .async(let asyncAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.
| case .navigation(let navigationAction): | ||
|
|
||
| case let .navigation(navigationAction): |
There was a problem hiding this comment.
🔵 [P4] Readability
Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .navigation(let navigationAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.
| case .scope(let scopeAction): | ||
|
|
||
| case let .scope(scopeAction): |
There was a problem hiding this comment.
🔵 [P4] Readability
Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .scope(let scopeAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.
|
|
||
| private func handleViewAction( | ||
| state: inout State, | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
handleViewAction 메서드에서 state 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효하지만, state는 inout으로 선언되어 상태를 변경할 수 있는 역할을 합니다. 이 메서드에서 state를 변경하는 액션(예: state = ...)이 없는지 다시 확인하는 것이 좋습니다. 만약 미래에 state를 사용할 가능성이 있다면 _를 제거하는 것이 더 나을 수 있습니다.
| state _: inout State, | |
| state: inout State, |
|
|
||
| private func handleAsyncAction( | ||
| state: inout State, | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
handleAsyncAction 메서드에서 state 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효하지만, state는 inout으로 선언되어 상태를 변경할 수 있는 역할을 합니다. 이 메서드에서 state를 변경하는 액션(예: state = ...)이 없는지 다시 확인하는 것이 좋습니다. 만약 미래에 state를 사용할 가능성이 있다면 _를 제거하는 것이 더 나을 수 있습니다.
| state _: inout State, | |
| state: inout State, |
| private func handleNavigationAction( | ||
| state: inout State, | ||
| action: NavigationAction | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
handleNavigationAction 메서드에서 state 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효하지만, state는 inout으로 선언되어 상태를 변경할 수 있는 역할을 합니다. 이 메서드에서 state를 변경하는 액션(예: state = ...)이 없는지 다시 확인하는 것이 좋습니다. 만약 미래에 state를 사용할 가능성이 있다면 _를 제거하는 것이 더 나을 수 있습니다.
| state _: inout State, | |
| state: inout State, |
| state: inout State, | ||
| action: NavigationAction | ||
| state _: inout State, | ||
| action _: NavigationAction |
There was a problem hiding this comment.
🔵 [P4] Readability
handleNavigationAction 메서드에서 action 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효합니다. 이 메서드는 현재 return .none만 포함하므로 action이 사용되지 않는 것이 맞습니다. 다만, 미래에 NavigationAction 케이스를 추가하고 해당 action을 사용할 경우 _를 제거해야 합니다. 현재 상태에서는 문제가 없습니다.
| .frame(width: 135, height: 90) | ||
|
|
||
| Text("오늘의 배틀이 없습니다") | ||
| Text("아직 빠른 배틀이 선정되지않았어요\n 조금만 기다려주세요!") |
There was a problem hiding this comment.
🔵 [P4] Readability
띄어쓰기 오류가 있습니다. "선정되지않았어요"는 "선정되지 않았어요"로 수정해야 합니다.
| Text("아직 빠른 배틀이 선정되지않았어요\n 조금만 기다려주세요!") | |
| Text("아직 빠른 배틀이 선정되지 않았어요\n 조금만 기다려주세요!") |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e5812b3ddb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| public var body: some View { | ||
| VStack(spacing: 0) { | ||
| HomeHeaderView { /* TODO: 알림 화면 */ } // sticky — 스크롤 영향 없음 | ||
| HomeHeaderView(hasUnread: store.hasUnreadNotification) { send(.notificationTapped) } // sticky — 스크롤 영향 없음 |
There was a problem hiding this comment.
Seed unread badge from home response
When a user lands on Home with unread notifications, the header now renders from hasUnreadNotification, but the Home fetch still only stores the server's home.newNotice in the unused newNotice field and never copies it into the shared badge state. As a result the red dot stays hidden until the user opens the notification screen and fetches the list; update the shared flag from home.newNotice on the Home response so the badge reflects the initial server state.
Useful? React with 👍 / 👎.
| case let .recordTapped(record): | ||
| return .send(.delegate(.openRecord(battleId: record.battleId))) |
There was a problem hiding this comment.
Handle battle-record card navigation
When a user taps a battle-history card, this emits openRecord, but rg openRecord shows no ProfileCoordinator route case handles that delegate, so the action falls through to the default branch and the Button does nothing. Add a coordinator route for battleRecord(.delegate(.openRecord(...))) (or remove the tap affordance until navigation is supported) so history cards actually open the battle.
Useful? React with 👍 / 👎.
- 이벤트 통합(분리 금지): sign_up / battle_step(step_name) / report_action / community_action / ad_revenue - identify(userTag) 로그인 성공 직후 연결, sign_up(method)은 신규 가입 시 - battle_step: pre_vote/post_vote(PreVote), audio_end(ChatRoom) - report_action: recap 조회(view)/공유(share) + top_indicator - community_action: 댓글 등록 성공 시 content_id/comment_length - ad_revenue: 리워드 광고 시청 완료(placement: 충전소) - 명세 외 이벤트 제거(login_failed/logout 등 — 로그아웃은 BE 담당) fix: BattleView 빈 배틀 안내 오타(선정되지않았어요), ProfileTests import 오타(rofile→Profile)
There was a problem hiding this comment.
이번 PR은 알림, 마이페이지(프로필, 리캡, 포인트 내역, 콘텐츠 활동, 설정) 등 여러 핵심 기능을 추가하고 기존 엔티티를 모듈화하여 재구성하는 대규모 변경을 포함합니다. Clean Architecture, TCA, SwiftUI 컨벤션을 일관되게 적용하여 잘 구조화된 코드를 보입니다. 특히 Mixpanel 트래킹 및 무료 충전 리워드 광고와 같은 외부 SDK 연동을 TCA DependencyKey 패턴을 사용하여 깔끔하게 구현한 점이 돋보입니다. @Shared 프로퍼티를 활용한 전역 알림 뱃지 관리, 스켈레톤 및 빈 화면 처리, 사용자 경험 개선을 위한 디테일한 SwiftUI 구현도 훌륭합니다. 전반적으로 코드 품질, 아키텍처 준수, 기능 구현 모두 높은 수준을 유지하고 있습니다.
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🔵 [P4] Readability
파라미터 state가 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다. 다른 handle 함수에서도 동일하게 적용되었습니다.
|
|
||
| private func handleAsyncAction( | ||
| state: inout State, | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
파라미터 state가 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다. 다른 handle 함수에서도 동일하게 적용되었습니다.
| private func handleNavigationAction( | ||
| state: inout State, | ||
| action: NavigationAction | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
파라미터 state가 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다. 다른 handle 함수에서도 동일하게 적용되었습니다.
| state: inout State, | ||
| action: NavigationAction | ||
| state _: inout State, | ||
| action _: NavigationAction |
There was a problem hiding this comment.
🔵 [P4] Readability
파라미터 action이 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다.
| } | ||
| } | ||
| .frame(maxWidth: .infinity) | ||
| // 전체 탭을 가로지르는 연속 베이스 라인 (셀별로 끊겨 보이던 문제 해결). |
There was a problem hiding this comment.
🔵 [P4] Readability
전체 탭을 가로지르는 연속적인 베이스 라인을 추가하여 UI 일관성을 개선하고 시각적 끊김을 해결했습니다. 이는 사용자 경험 측면에서 좋은 디테일입니다.
|
|
||
| extension RewardedAdClient: DependencyKey { | ||
| public static let liveValue = RewardedAdClient( | ||
| showRewardedAd: { await RewardedAdPresenter().present() } |
There was a problem hiding this comment.
🟠 [P2] Major
RewardedAdClient의 liveValue에서 RewardedAdPresenter를 사용하여 GoogleMobileAds와 같은 UIKit 의존적인 비동기 작업을 캡슐화한 것은 좋은 패턴입니다. RewardedAdPresenter가 NSObject와 FullScreenContentDelegate를 채택하고 withCheckedContinuation을 통해 TCA의 Effect와 비동기 콜백 기반 API를 연결하는 방식은 깔끔하고 효율적입니다. 이는 TCA의 '외부 효과(side effect)' 처리 원칙을 잘 준수하며, 테스트 가능성을 높이고 메인 리듀서에서 UI 관련 복잡성을 분리합니다.
|
|
||
| // MARK: - 이벤트 정의 (명세서) | ||
|
|
||
| public enum AnalyticsEvent: Sendable { |
There was a problem hiding this comment.
🟠 [P2] Major
AnalyticsEvent enum을 사용하여 Mixpanel 트래킹 이벤트를 타입 안전하게 정의한 것은 매우 훌륭합니다. 이벤트 이름과 속성을 enum과 관련 struct로 명세함으로써, 휴먼 에러를 줄이고 코드의 가독성 및 유지보수성을 크게 향상시킵니다. 이는 track 및 identify 메서드의 일관된 사용을 보장합니다.
| state.isSubmitting = false | ||
| switch result { | ||
| case .success: | ||
| case let .success(perspective): |
There was a problem hiding this comment.
🟡 [P3] Minor
댓글 작성 후 서버 응답의 실제 진영(perspective.option)을 기반으로 selectedFilter를 업데이트하는 로직은 사용자 경험을 크게 개선합니다. 사용자가 방금 작성한 댓글이 올바른 탭에 즉시 표시되도록 하여 UI 반응성을 높입니다.
| } | ||
|
|
||
| /// picke.pen 토글 (32×18, ON=primary500 / OFF=neutral200, knob 14). | ||
| private struct NotificationToggle: View { |
There was a problem hiding this comment.
🔵 [P4] Readability
NotificationToggle을 private struct로 분리하여 토글 컴포넌트의 책임을 명확히 하고 재사용성을 높였습니다. Capsule과 Circle을 활용한 애니메이션 구현도 간결하며 디자인 가이드를 잘 따르고 있습니다.
| import DesignSystem | ||
| import Entity | ||
|
|
||
| public struct RecapRadarChart: View { |
There was a problem hiding this comment.
🟠 [P2] Major
RecapRadarChart는 레이더 차트를 커스텀 Shape와 애니메이션(animatableData)을 활용하여 동적으로 구현한 훌륭한 예시입니다. 복잡한 UI 요소를 선언적이고 성능 효율적으로 처리하는 SwiftUI의 강점을 잘 활용했습니다. 특히 progress 상태를 이용한 중심에서 값으로 펼쳐지는 애니메이션은 사용자에게 시각적으로 매력적인 경험을 제공합니다.
There was a problem hiding this comment.
전체적으로 새로운 기능들을 Clean Architecture, TCA, SwiftUI 컨벤션에 맞춰 매우 잘 구현했습니다. 특히 Notification 및 Profile 모듈이 독립적으로 잘 구축되었고, @Shared TCA State를 활용한 전역 상태 관리, Mixpanel 및 AdMob 연동 방식, UI/UX 디테일(스켈레톤, 빈 화면, 스와이프 제스처, 커스텀 알림) 등이 훌륭합니다. Domain/Entity 계층의 리팩토링도 모듈성 향상에 기여했습니다.
Notification 모듈에 불필요한 Base.swift 파일이 포함되어 있으니 제거하는 것이 좋습니다.
|
|
||
| private func handleViewAction( | ||
| state: inout State, | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
state 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.
|
|
||
| private func handleAsyncAction( | ||
| state: inout State, | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
state 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.
| private func handleNavigationAction( | ||
| state: inout State, | ||
| action: NavigationAction | ||
| state _: inout State, |
There was a problem hiding this comment.
🔵 [P4] Readability
state 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.
| state: inout State, | ||
| action: NavigationAction | ||
| state _: inout State, | ||
| action _: NavigationAction |
There was a problem hiding this comment.
🔵 [P4] Readability
action 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.
| } | ||
|
|
||
| /// 노드 종료 시간(초) = 노드 시작 + audioDuration. | ||
| /// 노드 종료 시간(초). |
There was a problem hiding this comment.
🟡 [P3] Minor
노드 종료 시간 계산 로직이 audioDuration 합산 대신 nextNodeIds의 시작 시간을 활용하도록 변경되었습니다. 이는 오디오 재생과 UI 동기화 문제를 해결하기 위한 중요한 비즈니스 로직 변경으로 보입니다.
| @@ -1,18 +1,21 @@ | |||
| import DependencyPackagePlugin | |||
There was a problem hiding this comment.
🔵 [P4] Readability
DependencyPackagePlugin과 DependencyPlugin은 Project.swift 파일 상단에서만 사용되며, 그 외 일반 Swift 소스 코드에서는 사용되지 않습니다. 해당 파일의 변경은 일반적으로 Tuist 프로젝트 구성의 일부이며, 실제 앱 코드와는 무관하므로 큰 문제는 아닙니다. 하지만 정리 차원에서 이와 같은 플러그인 의존성은 일반적으로 ProjectDescriptionHelpers 내부에 두거나, import 순서 규칙이 있는 경우 이에 맞춰 재배치하는 것이 좋습니다. 여기서는 단순히 import 문의 위치 변경이므로 P4로 분류합니다.
| @@ -1,18 +1,21 @@ | |||
| import DependencyPackagePlugin | |||
| import DependencyPlugin | |||
There was a problem hiding this comment.
🔵 [P4] Readability
DependencyPackagePlugin과 DependencyPlugin은 Project.swift 파일 상단에서만 사용되며, 그 외 일반 Swift 소스 코드에서는 사용되지 않습니다. 해당 파일의 변경은 일반적으로 Tuist 프로젝트 구성의 일부이며, 실제 앱 코드와는 무관하므로 큰 문제는 아닙니다. 하지만 정리 차원에서 이와 같은 플러그인 의존성은 일반적으로 ProjectDescriptionHelpers 내부에 두거나, import 순서 규칙이 있는 경우 이에 맞춰 재배치하는 것이 좋습니다. 여기서는 단순히 import 문의 위치 변경이므로 P4로 분류합니다.
| @@ -0,0 +1,21 @@ | |||
| // | |||
There was a problem hiding this comment.
🔵 [P4] Readability
Notification 모듈에 Base.swift 파일이 새롭게 추가되었으나, 내부 내용은 DDDAttendance라는 프로젝트에서 복사된 것으로 보이는 일반적인 SwiftUI BaseView 스텁입니다. 이 파일은 실제 Notification 모듈에서 사용되지 않으므로, 프로젝트 클린업을 위해 제거하는 것이 좋습니다.
| // | |
| // 이 파일이 사용되지 않는 경우 제거하세요. | |
| // 이 파일이 다른 모듈에서 복사된 임시 파일인 경우, 관련 없는 내용을 삭제하고 필요하다면 실제 Base.swift 역할을 하는 파일로 교체하세요. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b7e22a9ce1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /public static func appBuildVersion\(buildVersion: String = "\d+"\) -> String \{/, | ||
| "public static func appBuildVersion(buildVersion: String = \"#{new_build_number}\") -> String {" |
There was a problem hiding this comment.
Match the build-number regex to the helper signature
In the QA lane this gsub no longer matches the file it is meant to update: this commit changed Extension+String.swift to public extension String { static func appBuildVersion(...) ... }, but the regex still requires public static func. As a result the lane logs a new build number but writes the file unchanged, so tuist generate keeps using the old CFBundleVersion and repeated TestFlight uploads will be rejected for reusing the same build number.
Useful? React with 👍 / 👎.
개요
마이페이지/프로필·알림·리캡·배틀 제안 등 핵심 기능과 Mixpanel 트래킹, 무료충전 리워드 광고를 한 번에 반영했습니다.
주요 변경
🔔 알림받기 (Notification)
NotificationCoordinator+NotificationFeature/View신규 — picke.pen알림받기정합HasUnreadNotification전역 공유 → 모두 읽음 시 제거🙍 프로필/마이페이지
🧩 recap(나의 철학자 유형)
📊 Mixpanel (PICKé 핵심 이벤트 명세서)
sign_up/battle_step(pre_vote·audio_end·post_vote) /report_action/community_action/ad_revenue+ identify💰 무료충전
관련 이슈
테스트
Picke-Stage빌드 성공 (error 0)