Skip to content

feat: 알림받기/미읽음 뱃지 + Mixpanel 트래킹 + 무료충전 리워드 광고 + recap 잠금#39

Merged
Roy-wonji merged 39 commits into
developfrom
feature/profile
Jun 10, 2026
Merged

feat: 알림받기/미읽음 뱃지 + Mixpanel 트래킹 + 무료충전 리워드 광고 + recap 잠금#39
Roy-wonji merged 39 commits into
developfrom
feature/profile

Conversation

@Roy-wonji

@Roy-wonji Roy-wonji commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

개요

마이페이지/프로필·알림·리캡·배틀 제안 등 핵심 기능과 Mixpanel 트래킹, 무료충전 리워드 광고를 한 번에 반영했습니다.

주요 변경

🔔 알림받기 (Notification)

  • NotificationCoordinator + NotificationFeature/View 신규 — picke.pen 알림받기 정합
  • Clean Architecture 풀스택(API/Service/DTO/Mapper/Entity/Interface/UseCase/Repository + DI)
    • 목록(카테고리·페이징)/읽음/모두 읽음 연동, 상세는 데이터 계층까지(화면 보류)
  • 카테고리 탭, 무한 스크롤, 스켈레톤·빈화면, Home·Profile·Hifi 종 아이콘 진입
  • 미읽음 빨간점: HasUnreadNotification 전역 공유 → 모두 읽음 시 제거

🙍 프로필/마이페이지

  • 마이페이지 메인, 포인트 내역, 설정·탈퇴·알림설정, 배틀 주제 제안, 내 배틀 기록, 내 콘텐츠 활동, 공지사항·이벤트, 나의 철학자 유형(recap) — picke.pen 정합 + API 연동
  • 충전 바 분리: 좌측 → 포인트 내역 상세 / 무료 충전 → 리워드 광고

🧩 recap(나의 철학자 유형)

  • 빈 응답(배틀 5개 미만) → 잠금 화면, 잠금 카드/그래프 picke.pen 정합

📊 Mixpanel (PICKé 핵심 이벤트 명세서)

  • sign_up / battle_step(pre_vote·audio_end·post_vote) / report_action / community_action / ad_revenue + identify

💰 무료충전

  • GoogleMobileAds 리워드 광고(유닛 ID는 REWARD_AD_UNIT config 주입), 보상 시 포인트 갱신

관련 이슈

테스트

  • Picke-Stage 빌드 성공 (error 0)

참고: 리워드 광고 유닛 ID는 현재 Google 테스트 ID(REWARD_AD_UNIT 미설정 시 폴백) — 실제 발급 ID로 교체 필요.

Roy-wonji added 30 commits June 6, 2026 23:52
- 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
- 프로필/콘텐츠활동 아바타: 항상 beige600 원 배경 위에 캐릭터 이미지/기본 아이콘
- 포인트내역 카드 패딩/간격을 배틀기록·콘텐츠활동과 12/12 로 통일
- .background(Color.xxx) → .background(.xxx, in:) 단축형 정리
- 공지/이벤트 빈 문구: 새로운 공지사항/이벤트가 없습니다

#9 #15
- 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 하던 라우팅만 제거한다.
- POST /battles/proposals 연동 (Clean Architecture, 파라미터를 BattleProposalDraft 로 묶음)
- 배틀 만들기 폼: 카테고리 칩 + 주제 + 양측 입장(A/B) + 부가 설명(200자) + 제안하기(-30P)
- 제안 성공 시 완료 팝업(CustomAlert) → 확인 시 화면 닫힘
- 포인트 내역 주제 제안 팝업 '제안하기' → 배틀 만들기 화면 push
- PickeEmptyStateView 문구 색상 beige300 → gray300 (라이트 배경 가독성)

#8 #16
- 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
- 포인트 내역: 카드 padding 12→16, 리스트 간격 12→16 (pen yqKel gap16 / Alram Box padding16)
- 포인트 내역 스켈레톤: padding 16 + scrollIndicators(.hidden)
- 설정 메뉴: 상단 padding 12 추가 (pen y7ta5 [12,16])
- 배틀기록/콘텐츠활동/공지/알림설정/철학자/잠금은 net 패딩 이미 pen 일치 확인

#9 #10 #15
- /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) 트래킹
@Roy-wonji Roy-wonji added ✨ 기능추가 새로운 기능 추가 🎨 디자인 UI 디자인 작업 labels Jun 10, 2026
@github-actions

Copy link
Copy Markdown

{
"summary": "전반적으로 NotificationProfile 모듈 신규 기능(알림받기, 리워드 광고, 리캡)에 대한 Clean ArchitectureTCA 컨벤션을 잘 따르고 있습니다. DTO에서 Entity로 매핑 시 적절한 기본값을 사용하며, Presentation 계층에서는 DomainInterface를 통해 UseCase를 주입받는 의존성 방향을 잘 지키고 있습니다.\n\n새로 추가된 모듈 및 기능에 대한 모듈 의존성, DI 설정, TCA Reducer 구조는 적절합니다. 특히 Mixpanel 트래킹을 위한 AnalyticsUseCase 도입과 타입 안전한 이벤트 정의는 좋은 패턴입니다.\n\n개선이 필요한 주요 사항:\n\n1. Swift 스타일 가이드: guard let else { ... } 후 빈 줄 추가, enum 정의 후 빈 줄 추가 등 스타일 가이드를 일관성 있게 적용하는 것이 좋습니다.\n2. 문구 수정: 일부 사용자에게 노출되는 문구에 오타가 있거나 어색한 부분이 있습니다.\n3. ViewModel-like Action 명칭: AppReducerView Action 중 presentView, presentRoot, presentAuth는 의도된 효과를 나타내므로, 발생한 이벤트를 명확히 설명하는 이름으로 변경하는 것을 권장합니다.\n4. AppReducer.NavigationAction: 현재 비어 있는 NavigationAction enum에 대해, 앞으로 사용 계획이 없다면 제거하거나 never case로 명시적으로 나타내는 것이 좋습니다.",
"comments": [
{
"path": "Projects/App/Sources/Reducer/AppReducer.swift",
"line": 70,
"code_snippet": " public enum NavigationAction: Equatable {}",
"body": "🔵 [P4] Readability\n\n현재 비어 있는 NavigationAction enum은 불필요합니다. 만약 앞으로도 사용 계획이 없다면 제거하거나, 의도적으로 비워둔 것이라면 never 케이스를 추가하여 명확히 하는 것이 좋습니다."
},
{
"path": "Projects/App/Sources/Reducer/AppReducer.swift",
"line": 152,
"code_snippet": " state _: inout State,",
"body": "🔵 [P4] Readability\n\nstate 매개변수가 사용되지 않으므로, 명시적으로 _를 사용하여 미사용을 나타낸 것은 좋으나, handleViewAction에서 state를 실제로 변경하지 않기 때문에 inout 키워드도 제거하는 것이 더 정확한 의도를 전달합니다.\n\nsuggestion\n state: inout State,\n"
},
{
"path": "Projects/App/Sources/Reducer/AppReducer.swift",
"line": 170,
"code_snippet": " state _: inout State,",
"body": "🔵 [P4] Readability\n\nstate 매개변수가 사용되지 않으므로, 명시적으로 _를 사용하여 미사용을 나타낸 것은 좋으나, handleAsyncAction에서 state를 실제로 변경하지 않기 때문에 inout 키워드도 제거하는 것이 더 정확한 의도를 전달합니다.\n\nsuggestion\n state: inout State,\n"
},
{
"path": "Projects/App/Sources/Reducer/AppReducer.swift",
"line": 199,
"code_snippet": " state _: inout State,",
"body": "🔵 [P4] Readability\n\nstate 매개변수가 사용되지 않으므로, 명시적으로 _를 사용하여 미사용을 나타낸 것은 좋으나, handleNavigationAction에서 state를 실제로 변경하지 않기 때문에 inout 키워드도 제거하는 것이 더 정확한 의도를 전달합니다.\n\nsuggestion\n state: inout State,\n"
},
{
"path": "Projects/App/Sources/Reducer/AppReducer.swift",
"line": 200,
"code_snippet": " action _: NavigationAction",
"body": "🔵 [P4] Readability\n\naction 매개변수가 사용되지 않으므로 _를 사용하여 미사용을 나타낸 것은 좋습니다."
},
{
"path": "Projects/Data/Model/Sources/Battle/Mapper/BattleProposalDataDTO+.swift",
"line": 12,
"code_snippet": " id: id ?? 0,",
"body": "🟠 [P2] Major\n\nBattleProposalDataDTOid 필드가 Int?로 정의되어 있어 0으로 기본값을 제공하고 있습니다. id는 엔티티의 고유 식별자로서 nil이 될 수 없으며 0으로 기본값을 설정하는 것은 비즈니스 로직에서 예상치 못한 충돌이나 혼란을 야기할 수 있습니다. id 필드는 서버 응답에서 항상 존재해야 하므로, idnil일 경우 NetworkError.noData와 같은 적절한 에러를 던지도록 수정하는 것을 권장합니다."
},
{
"path": "Projects/Data/Model/Sources/Battle/Mapper/BattleProposalDataDTO+.swift",
"line": 13,
"code_snippet": " userId: userId ?? 0,",
"body": "🟠 [P2] Major\n\nBattleProposalDataDTOuserId 필드가 Int?로 정의되어 있어 0으로 기본값을 제공하고 있습니다. userId는 엔티티의 중요한 식별자로서 nil이 될 수 없으며 0으로 기본값을 설정하는 것은 비즈니스 로직에서 예상치 못한 충돌이나 혼란을 야기할 수 있습니다. userId 필드는 서버 응답에서 항상 존재해야 하므로, userIdnil일 경우 NetworkError.noData와 같은 적절한 에러를 던지도록 수정하는 것을 권장합니다."
},
{
"path": "Projects/Data/Repository/Sources/Battle/BattleRepositoryImpl.swift",
"line": 212,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Notification/NotificationRepositoryImpl.swift",
"line": 42,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Notification/NotificationRepositoryImpl.swift",
"line": 56,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Profile/ProfileRepositoryImpl.swift",
"line": 30,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Profile/ProfileRepositoryImpl.swift",
"line": 44,
"code_snippet": " // 에러로 던지지 않고 빈 recap(totalParticipation 0 → isLocked)으로 반환.\n guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Profile/ProfileRepositoryImpl.swift",
"line": 60,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Profile/ProfileRepositoryImpl.swift",
"line": 84,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Profile/ProfileRepositoryImpl.swift",
"line": 108,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Profile/ProfileRepositoryImpl.swift",
"line": 120,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Data/Repository/Sources/Profile/ProfileRepositoryImpl.swift",
"line": 143,
"code_snippet": " guard let data = dto.data else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let data = dto.data else {\n \n"
},
{
"path": "Projects/Domain/UseCase/Sources/Analytics/AnalyticsUseCase.swift",
"line": 169,
"code_snippet": " guard !properties.isEmpty else { return }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard !properties.isEmpty else {\n \n"
},
{
"path": "Projects/Domain/UseCase/Sources/Analytics/AnalyticsUseCase.swift",
"line": 239,
"code_snippet": " var nilIfEmpty: String? { isEmpty ? nil : self }",
"body": "🔵 [P4] Readability\n\n삼항 연산자를 사용한 줄 바꿈은 일반적으로 ? 앞에서 이루어지는 것이 Swift 스타일 가이드에 더 부합합니다.\n\nsuggestion\n var nilIfEmpty: String? {\n isEmpty\n ? nil\n : self\n }\n"
},
{
"path": "Projects/Domain/UseCase/Sources/Battle/BattleUseCase.swift",
"line": 86,
"code_snippet": " }",
"body": "🔵 [P4] Readability\n\n함수 정의의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Presentation/Battle/Sources/Main/View/BattleView.swift",
"line": 342,
"code_snippet": " Text("아직 빠른 배틀이 선정되지않았어요\n 조금만 기다려주세요!")",
"body": "🟡 [P3] Minor\n\n선정되지않았어요선정되지 않았어요로 띄어쓰기가 필요합니다. 사용자에게 노출되는 문구이므로 자연스러운 표현으로 수정하는 것이 좋습니다."
},
{
"path": "Projects/Presentation/Chat/Sources/ChatRoom/Reducer/ChatRoomFeature.swift",
"line": 527,
"code_snippet": " guard time >= nodeEndTime else { return nil }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard time >= nodeEndTime else {\n \n"
},
{
"path": "Projects/Presentation/Chat/Sources/Comment/View/CommentView.swift",
"line": 264,
"code_snippet": " // 전체 탭을 가로지르는 연속 베이스 라인 (셀별로 끊겨 보이던 문제 해결).",
"body": "🔵 [P4] Readability\n\n주석은 일반적으로 코드 구현에 대한 설명보다는 '왜 이렇게 구현했는지'에 대한 의도를 설명하는 용도로 사용하는 것이 좋습니다. 이 주석은 구현 의도를 잘 설명하고 있습니다."
},
{
"path": "Projects/Presentation/Chat/Sources/Comment/View/CommentView.swift",
"line": 304,
"code_snippet": " .fill(isSelected ? Color.primary500 : Color.clear)",
"body": "🔵 [P4] Readability\n\n삼항 연산자를 사용한 줄 바꿈은 일반적으로 ? 앞에서 이루어지는 것이 Swift 스타일 가이드에 더 부합합니다.\n\nsuggestion\n .fill(\n isSelected\n ? Color.primary500\n : Color.clear\n )\n"
},
{
"path": "Projects/Presentation/MainTab/Sources/Reducer/MainTabCoordinator.swift",
"line": 80,
"code_snippet": " }",
"body": "🔵 [P4] Readability\n\nenum 정의의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Presentation/Notification/Sources/Coordinator/Reducer/NotificationCoordinator.swift",
"line": 110,
"code_snippet": "extension NotificationCoordinator {",
"body": "🔵 [P4] Readability\n\nextension NotificationCoordinator 블록 앞에 빈 줄을 추가하여 코드를 시각적으로 분리하는 것이 가독성에 좋습니다."
},
{
"path": "Projects/Presentation/Notification/Sources/Coordinator/Reducer/NotificationCoordinator.swift",
"line": 119,
"code_snippet": "extension NotificationCoordinator.NotificationScreen.State: Equatable {}",
"body": "🔵 [P4] Readability\n\nextension 선언의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Presentation/Notification/Sources/Main/Reducer/NotificationFeature.swift",
"line": 132,
"code_snippet": " state.$hasUnreadNotification.withLock { $0 = false }",
"body": "🟠 [P2] Major\n\nwithLock { $0 = false }withLock(\\.self) { $0 = false }와 같이 \.self를 명시적으로 사용하는 것이 좋습니다. withLockReferenceWritableKeyPath를 인자로 받는데, hasUnreadNotification@Shared 프로퍼티 래퍼로 선언되어 있어 암시적인 KeyPath가 제공될 수 있지만 명시적으로 \.self를 전달하는 것이 더 명확하고 TCA 1.25 컨벤션에 부합합니다."
},
{
"path": "Projects/Presentation/Notification/Sources/Main/Reducer/NotificationFeature.swift",
"line": 214,
"code_snippet": " state.$hasUnreadNotification.withLock { $0 = hasUnread }",
"body": "🟠 [P2] Major\n\nwithLock { $0 = hasUnread }withLock(\\.self) { $0 = hasUnread }와 같이 \.self를 명시적으로 사용하는 것이 좋습니다. withLockReferenceWritableKeyPath를 인자로 받는데, hasUnreadNotification@Shared 프로퍼티 래퍼로 선언되어 있어 암시적인 KeyPath가 제공될 수 있지만 명시적으로 \.self를 전달하는 것이 더 명확하고 TCA 1.25 컨벤션에 부합합니다."
},
{
"path": "Projects/Presentation/Profile/Sources/Ad/RewardedAdClient.swift",
"line": 60,
"code_snippet": " guard let rootViewController = Self.topViewController() else {",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let rootViewController = Self.topViewController() else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/BattleProposal/Reducer/BattleProposalFeature.swift",
"line": 132,
"code_snippet": " guard state.isSubmitEnabled else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.isSubmitEnabled else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/BattleRecord/Reducer/BattleRecordFeature.swift",
"line": 99,
"code_snippet": " guard state.items.isEmpty else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.items.isEmpty else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/BattleRecord/Reducer/BattleRecordFeature.swift",
"line": 106,
"code_snippet": " guard state.hasNext, !state.isLoadingMore, !state.isLoading else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.hasNext, !state.isLoadingMore, !state.isLoading else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/ContentActivity/Reducer/ContentActivityFeature.swift",
"line": 98,
"code_snippet": " guard state.items.isEmpty else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.items.isEmpty else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/ContentActivity/Reducer/ContentActivityFeature.swift",
"line": 105,
"code_snippet": " guard tab != state.selectedTab else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard tab != state.selectedTab else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/ContentActivity/Reducer/ContentActivityFeature.swift",
"line": 113,
"code_snippet": " guard state.hasNext, !state.isLoadingMore, !state.isLoading else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.hasNext, !state.isLoadingMore, !state.isLoading else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/Main/Reducer/ProfileFeature.swift",
"line": 150,
"code_snippet": " return .send(.async(.fetchProfile))",
"body": "🟡 [P3] Minor\n\nonAppear에서 비동기 작업을 시작하는 것은 일반적이지만, HomeFeatureNotificationFeatureonAppear에서는 guard state.items.isEmpty else { return .none }와 같이 items가 비어있을 때만 데이터를 fetch하도록 처리하고 있습니다. ProfileFeature도 동일한 패턴을 적용하여 불필요한 API 호출을 방지하는 것이 좋습니다.\n\nsuggestion\n case .onAppear:\n guard nickname.isEmpty else { return .none }\n return .send(.async(.fetchProfile))\n"
},
{
"path": "Projects/Presentation/Profile/Sources/Main/View/ProfileView.swift",
"line": 194,
"code_snippet": " // 배틀 5개 미만(미확정) → 잠금 이미지",
"body": "🔵 [P4] Readability\n\n주석은 일반적으로 코드 구현에 대한 설명보다는 '왜 이렇게 구현했는지'에 대한 의도를 설명하는 용도로 사용하는 것이 좋습니다. 이 주석은 구현 의도를 잘 설명하고 있습니다."
},
{
"path": "Projects/Presentation/Profile/Sources/Notice/Reducer/NoticeFeature.swift",
"line": 81,
"code_snippet": "}",
"body": "🔵 [P4] Readability\n\nextension 선언의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Presentation/Profile/Sources/PointHistory/Reducer/PointHistoryFeature.swift",
"line": 113,
"code_snippet": " guard state.items.isEmpty else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.items.isEmpty else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/PointHistory/Reducer/PointHistoryFeature.swift",
"line": 120,
"code_snippet": " guard state.hasNext, !state.isLoadingMore, !state.isLoading else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.hasNext, !state.isLoadingMore, !state.isLoading else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/Recap/Reducer/RecapFeature.swift",
"line": 103,
"code_snippet": " guard state.recap == nil else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard state.recap == nil else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/Recap/Reducer/RecapFeature.swift",
"line": 110,
"code_snippet": " guard let recap = state.recap else { return .none }",
"body": "🔵 [P4] Readability\n\nguard let else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard let recap = state.recap else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/Recap/View/Components/RecapRadarChart.swift",
"line": 18,
"code_snippet": " /// picke.pen 육각형 단위 꼭짓점 (수직 반지름=1, 수평 0.846, 측면 y=±0.559).",
"body": "🔵 [P4] Readability\n\n주석은 일반적으로 코드 구현에 대한 설명보다는 '왜 이렇게 구현했는지'에 대한 의도를 설명하는 용도로 사용하는 것이 좋습니다. 이 주석은 구현 의도를 잘 설명하고 있습니다."
},
{
"path": "Projects/Presentation/Profile/Sources/Recap/View/Components/RecapRadarChart.swift",
"line": 19,
"code_snippet": " /// 순서: 원칙↑ · 이성 · 개인 · 변화↓ · 내면 · 직관 (axes 순서와 동일).",
"body": "🔵 [P4] Readability\n\n주석은 일반적으로 코드 구현에 대한 설명보다는 '왜 이렇게 구현했는지'에 대한 의도를 설명하는 용도로 사용하는 것이 좋습니다. 이 주석은 구현 의도를 잘 설명하고 있습니다."
},
{
"path": "Projects/Presentation/Profile/Sources/Settings/Reducer/SettingsFeature.swift",
"line": 159,
"code_snippet": " guard !state.isProcessing else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard !state.isProcessing else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Sources/Settings/Reducer/SettingsFeature.swift",
"line": 175,
"code_snippet": " guard !state.isProcessing else { return .none }",
"body": "🔵 [P4] Readability\n\nguard else { ... } 문 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다.\n\nsuggestion\n guard !state.isProcessing else {\n \n"
},
{
"path": "Projects/Presentation/Profile/Tests/Sources/ProfileTests.swift",
"line": 9,
"code_snippet": "@testable import rofile",
"body": "🔴 [P1] Critical\n\n@testable import rofile에 오타가 있습니다. rofile 대신 Profile로 수정해야 합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/Image/ImageAsset.swift",
"line": 56,
"code_snippet": " case lock",
"body": "🔵 [P4] Readability\n\nImageAsset enum 케이스(lock) 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift",
"line": 43,
"code_snippet": " case suggestTopic",
"body": "🔵 [P4] Readability\n\nenum의 마지막 케이스 뒤에는 가독성을 위해 빈 줄을 추가하는 Swift 스타일 가이드를 따르는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift",
"line": 137,
"code_snippet": " )",
"body": "🔵 [P4] Readability\n\n함수 정의의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift",
"line": 147,
"code_snippet": " )",
"body": "🔵 [P4] Readability\n\n함수 정의의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomAlertState.swift",
"line": 159,
"code_snippet": " )",
"body": "🔵 [P4] Readability\n\n함수 정의의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomConfirmationPopupView.swift",
"line": 85,
"code_snippet": " }",
"body": "🔵 [P4] Readability\n\nswitch 문의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomConfirmationPopupView.swift",
"line": 100,
"code_snippet": " }",
"body": "🔵 [P4] Readability\n\n클로저의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomConfirmationPopupView.swift",
"line": 122,
"code_snippet": " }",
"body": "🔵 [P4] Readability\n\n클로저의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomConfirmationPopupView.swift",
"line": 143,
"code_snippet": " }",
"body": "🔵 [P4] Readability\n\n클로저의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomConfirmationPopupView.swift",
"line": 154,
"code_snippet": " }",
"body": "🔵 [P4] Readability\n\n클로저의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
},
{
"path": "Projects/Shared/DesignSystem/Sources/UI/Alert/CustomPopup/CustomConfirmationPopupView.swift",
"line": 180,
"code_snippet": " .buttonStyle(.plain)",
"body": "🔵 [P4] Readability\n\n클로저의 닫는 중괄호 } 앞에 빈 줄을 추가하여 가독성을 높이는 것을 권장합니다."
}
]
}

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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,
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 로 이관

@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.

전반적으로 잘 구성된 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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .view(let viewAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.

case .inner(let innerAction):

case let .inner(innerAction):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .inner(let innerAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.

case .async(let asyncAction):

case let .async(asyncAction):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .async(let asyncAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.

case .navigation(let navigationAction):

case let .navigation(navigationAction):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .navigation(let navigationAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.

case .scope(let scopeAction):

case let .scope(scopeAction):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

Swift 6의 case let 패턴은 가독성을 높이지만, 기존 case .scope(let scopeAction): 패턴을 사용하는 파일들과의 일관성을 고려하는 것이 좋습니다. 현재 프로젝트에 명시된 특정 패턴이 없다면, 팀 컨벤션을 따르는 것이 좋습니다.


private func handleViewAction(
state: inout State,
state _: inout State,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

handleViewAction 메서드에서 state 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효하지만, stateinout으로 선언되어 상태를 변경할 수 있는 역할을 합니다. 이 메서드에서 state를 변경하는 액션(예: state = ...)이 없는지 다시 확인하는 것이 좋습니다. 만약 미래에 state를 사용할 가능성이 있다면 _를 제거하는 것이 더 나을 수 있습니다.

Suggested change
state _: inout State,
state: inout State,


private func handleAsyncAction(
state: inout State,
state _: inout State,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

handleAsyncAction 메서드에서 state 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효하지만, stateinout으로 선언되어 상태를 변경할 수 있는 역할을 합니다. 이 메서드에서 state를 변경하는 액션(예: state = ...)이 없는지 다시 확인하는 것이 좋습니다. 만약 미래에 state를 사용할 가능성이 있다면 _를 제거하는 것이 더 나을 수 있습니다.

Suggested change
state _: inout State,
state: inout State,

private func handleNavigationAction(
state: inout State,
action: NavigationAction
state _: inout State,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

handleNavigationAction 메서드에서 state 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효하지만, stateinout으로 선언되어 상태를 변경할 수 있는 역할을 합니다. 이 메서드에서 state를 변경하는 액션(예: state = ...)이 없는지 다시 확인하는 것이 좋습니다. 만약 미래에 state를 사용할 가능성이 있다면 _를 제거하는 것이 더 나을 수 있습니다.

Suggested change
state _: inout State,
state: inout State,

state: inout State,
action: NavigationAction
state _: inout State,
action _: NavigationAction

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

handleNavigationAction 메서드에서 action 파라미터가 사용되지 않는 경우 _를 사용하여 명시적으로 무시하는 것은 유효합니다. 이 메서드는 현재 return .none만 포함하므로 action이 사용되지 않는 것이 맞습니다. 다만, 미래에 NavigationAction 케이스를 추가하고 해당 action을 사용할 경우 _를 제거해야 합니다. 현재 상태에서는 문제가 없습니다.

.frame(width: 135, height: 90)

Text("오늘의 배틀이 없습니다")
Text("아직 빠른 배틀이 선정되지않았어요\n 조금만 기다려주세요!")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

띄어쓰기 오류가 있습니다. "선정되지않았어요"는 "선정되지 않았어요"로 수정해야 합니다.

Suggested change
Text("아직 빠른 배틀이 선정되지않았어요\n 조금만 기다려주세요!")
Text("아직 빠른 배틀이 선정되지 않았어요\n 조금만 기다려주세요!")

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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 — 스크롤 영향 없음

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +109 to +110
case let .recordTapped(record):
return .send(.delegate(.openRecord(battleId: record.battleId)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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)

@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.

이번 PR은 알림, 마이페이지(프로필, 리캡, 포인트 내역, 콘텐츠 활동, 설정) 등 여러 핵심 기능을 추가하고 기존 엔티티를 모듈화하여 재구성하는 대규모 변경을 포함합니다. Clean Architecture, TCA, SwiftUI 컨벤션을 일관되게 적용하여 잘 구조화된 코드를 보입니다. 특히 Mixpanel 트래킹 및 무료 충전 리워드 광고와 같은 외부 SDK 연동을 TCA DependencyKey 패턴을 사용하여 깔끔하게 구현한 점이 돋보입니다. @Shared 프로퍼티를 활용한 전역 알림 뱃지 관리, 스켈레톤 및 빈 화면 처리, 사용자 경험 개선을 위한 디테일한 SwiftUI 구현도 훌륭합니다. 전반적으로 코드 품질, 아키텍처 준수, 기능 구현 모두 높은 수준을 유지하고 있습니다.

}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

파라미터 state가 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다. 다른 handle 함수에서도 동일하게 적용되었습니다.


private func handleAsyncAction(
state: inout State,
state _: inout State,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

파라미터 state가 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다. 다른 handle 함수에서도 동일하게 적용되었습니다.

private func handleNavigationAction(
state: inout State,
action: NavigationAction
state _: inout State,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

파라미터 state가 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다. 다른 handle 함수에서도 동일하게 적용되었습니다.

state: inout State,
action: NavigationAction
state _: inout State,
action _: NavigationAction

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

파라미터 action이 클로저 내에서 사용되지 않을 때 _를 사용하여 명시적으로 무시하는 것은 좋은 가독성 및 의도 표현 방식입니다.

}
}
.frame(maxWidth: .infinity)
// 전체 탭을 가로지르는 연속 베이스 라인 (셀별로 끊겨 보이던 문제 해결).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

전체 탭을 가로지르는 연속적인 베이스 라인을 추가하여 UI 일관성을 개선하고 시각적 끊김을 해결했습니다. 이는 사용자 경험 측면에서 좋은 디테일입니다.


extension RewardedAdClient: DependencyKey {
public static let liveValue = RewardedAdClient(
showRewardedAd: { await RewardedAdPresenter().present() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 [P2] Major

RewardedAdClientliveValue에서 RewardedAdPresenter를 사용하여 GoogleMobileAds와 같은 UIKit 의존적인 비동기 작업을 캡슐화한 것은 좋은 패턴입니다. RewardedAdPresenterNSObjectFullScreenContentDelegate를 채택하고 withCheckedContinuation을 통해 TCA의 Effect와 비동기 콜백 기반 API를 연결하는 방식은 깔끔하고 효율적입니다. 이는 TCA의 '외부 효과(side effect)' 처리 원칙을 잘 준수하며, 테스트 가능성을 높이고 메인 리듀서에서 UI 관련 복잡성을 분리합니다.


// MARK: - 이벤트 정의 (명세서)

public enum AnalyticsEvent: Sendable {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 [P2] Major

AnalyticsEvent enum을 사용하여 Mixpanel 트래킹 이벤트를 타입 안전하게 정의한 것은 매우 훌륭합니다. 이벤트 이름과 속성을 enum과 관련 struct로 명세함으로써, 휴먼 에러를 줄이고 코드의 가독성 및 유지보수성을 크게 향상시킵니다. 이는 trackidentify 메서드의 일관된 사용을 보장합니다.

state.isSubmitting = false
switch result {
case .success:
case let .success(perspective):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [P3] Minor

댓글 작성 후 서버 응답의 실제 진영(perspective.option)을 기반으로 selectedFilter를 업데이트하는 로직은 사용자 경험을 크게 개선합니다. 사용자가 방금 작성한 댓글이 올바른 탭에 즉시 표시되도록 하여 UI 반응성을 높입니다.

}

/// picke.pen 토글 (32×18, ON=primary500 / OFF=neutral200, knob 14).
private struct NotificationToggle: View {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

NotificationToggle을 private struct로 분리하여 토글 컴포넌트의 책임을 명확히 하고 재사용성을 높였습니다. CapsuleCircle을 활용한 애니메이션 구현도 간결하며 디자인 가이드를 잘 따르고 있습니다.

import DesignSystem
import Entity

public struct RecapRadarChart: View {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 [P2] Major

RecapRadarChart는 레이더 차트를 커스텀 Shape와 애니메이션(animatableData)을 활용하여 동적으로 구현한 훌륭한 예시입니다. 복잡한 UI 요소를 선언적이고 성능 효율적으로 처리하는 SwiftUI의 강점을 잘 활용했습니다. 특히 progress 상태를 이용한 중심에서 값으로 펼쳐지는 애니메이션은 사용자에게 시각적으로 매력적인 경험을 제공합니다.

@Roy-wonji Roy-wonji self-assigned this Jun 10, 2026
@Roy-wonji Roy-wonji merged commit ecf98bf into develop Jun 10, 2026
3 checks passed
@Roy-wonji Roy-wonji deleted the feature/profile branch June 10, 2026 14:54

@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.

전체적으로 새로운 기능들을 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

state 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.


private func handleAsyncAction(
state: inout State,
state _: inout State,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

state 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.

private func handleNavigationAction(
state: inout State,
action: NavigationAction
state _: inout State,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

state 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.

state: inout State,
action: NavigationAction
state _: inout State,
action _: NavigationAction

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

action 파라미터가 함수 내에서 사용되지 않는 경우 _로 표시하여 명시적으로 사용하지 않음을 나타내는 좋은 방법입니다.

}

/// 노드 종료 시간(초) = 노드 시작 + audioDuration.
/// 노드 종료 시간(초).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 [P3] Minor

노드 종료 시간 계산 로직이 audioDuration 합산 대신 nextNodeIds의 시작 시간을 활용하도록 변경되었습니다. 이는 오디오 재생과 UI 동기화 문제를 해결하기 위한 중요한 비즈니스 로직 변경으로 보입니다.

@@ -1,18 +1,21 @@
import DependencyPackagePlugin

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

DependencyPackagePluginDependencyPluginProject.swift 파일 상단에서만 사용되며, 그 외 일반 Swift 소스 코드에서는 사용되지 않습니다. 해당 파일의 변경은 일반적으로 Tuist 프로젝트 구성의 일부이며, 실제 앱 코드와는 무관하므로 큰 문제는 아닙니다. 하지만 정리 차원에서 이와 같은 플러그인 의존성은 일반적으로 ProjectDescriptionHelpers 내부에 두거나, import 순서 규칙이 있는 경우 이에 맞춰 재배치하는 것이 좋습니다. 여기서는 단순히 import 문의 위치 변경이므로 P4로 분류합니다.

@@ -1,18 +1,21 @@
import DependencyPackagePlugin
import DependencyPlugin

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

DependencyPackagePluginDependencyPluginProject.swift 파일 상단에서만 사용되며, 그 외 일반 Swift 소스 코드에서는 사용되지 않습니다. 해당 파일의 변경은 일반적으로 Tuist 프로젝트 구성의 일부이며, 실제 앱 코드와는 무관하므로 큰 문제는 아닙니다. 하지만 정리 차원에서 이와 같은 플러그인 의존성은 일반적으로 ProjectDescriptionHelpers 내부에 두거나, import 순서 규칙이 있는 경우 이에 맞춰 재배치하는 것이 좋습니다. 여기서는 단순히 import 문의 위치 변경이므로 P4로 분류합니다.

@@ -0,0 +1,21 @@
//

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 [P4] Readability

Notification 모듈에 Base.swift 파일이 새롭게 추가되었으나, 내부 내용은 DDDAttendance라는 프로젝트에서 복사된 것으로 보이는 일반적인 SwiftUI BaseView 스텁입니다. 이 파일은 실제 Notification 모듈에서 사용되지 않으므로, 프로젝트 클린업을 위해 제거하는 것이 좋습니다.

Suggested change
//
// 이 파일이 사용되지 않는 경우 제거하세요.
// 이 파일이 다른 모듈에서 복사된 임시 파일인 경우, 관련 없는 내용을 삭제하고 필요하다면 실제 Base.swift 역할을 하는 파일로 교체하세요.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread fastlane/Fastfile
Comment on lines +90 to +91
/public static func appBuildVersion\(buildVersion: String = "\d+"\) -> String \{/,
"public static func appBuildVersion(buildVersion: String = \"#{new_build_number}\") -> String {"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ 기능추가 새로운 기능 추가 🎨 디자인 UI 디자인 작업

Projects

None yet

1 participant