feat: 사전 투표창 + 홈 .pen 정밀 매칭 + Poll API · Skeleton 인프라#34
Conversation
- PreVoteBattle / PreVoteOption / PhilosopherAvatar Entity + .mock
- PreVote 폴더 (Reducer · View) — 2지선다 옵션 카드 + VS 뱃지 + Pretendard 24pt 타이틀 + 그라데이션 콘텐츠 영역, 탭바 hidden
- HomeCoordinator 라우터에 .preVote 푸시 / dismiss·submit → backAction 위임
- DesignSystem: 철학자 아바타 자산 (플라톤·사르트르·순자) + ImageAsset case 추가
- DesignSystem 공통 컴포넌트: PickeNavigationBar (좌측 back · 가운데 옵션 · 우측 ViewBuilder), 시스템 공유용 ShareSheet (UIActivityViewController wrapper), CustomButton height override (CTAButtonSize 기본값 + 명시적 height)
- AGENTS.md: Coordinator 의 `extension X { @Reducer public enum XScreen { ... } }` 구조 절대 건드리지 말 것 규칙 명시
- HomeFeature.View 에 heroTapped · hotBattleTapped · bestBattleTapped · newBattleTapped 추가 - DelegateAction.presentPreVote(battleId:) 시그니처로 통일하여 카드 종류 무관하게 battleId 만 코디네이터에 전달 - HomeView 의 카드들에 .contentShape(Rectangle()) + .onTapGesture 부착, Vote 카드는 Button 래핑 제거 - HeroCarouselView 에 onTap 클로저 매개변수 추가 - 홈 배경을 .pen `#fafaf9` 매칭을 위해 .beige50 → .beige200 으로 보정
- NewBattleCardView 재작성 — Admission 옵션 카드 (아바타 40×40 + 입장 + 철학자) · VS 뱃지 24×24 secondary200 · beige300/beige600 stroke - 아바타: PhilosopherAvatar 매핑된 일러스트 fallback · 추후 API 의 philosopherAImageUrl/BImageUrl 들어오면 KFImage 로 우선 표시 - NewBattle Entity 에 philosopherAImageURL · philosopherBImageURL 복원 (mock 은 picsum URL) - HomeHeaderView 하단에 1pt beige600 stroke overlay 추가 — .pen Header bottom stroke 매칭
- GNB 아이콘 PNG → SVG 로 교체 (preserves-vector-representation · template-rendering-intent: original) - inactive 4개 + active 4개 총 8 imageset 등록 (Icon-홈/탐색/빠른배틀/마이 × default·active) - MainTabCoordinator.Tab.iconAsset(isSelected:) 함수화하여 선택 상태에 따라 active 자산 반환 - MainTabView.tabIcon 에서 selectedTab 비교로 isSelected 분기 + .renderingMode(.original) 강제하여 SVG 원본 색 유지 (시스템 tint 적용 X)
- HomeService 등이 Entity 타입을 직접 참조할 수 있도록 .Domain(implements: .Entity) 의존성 추가
…ts 캐시 - OAuthLoginRequest 에 idToken: String? 추가 (CodingKey: identityToken) · redirectUri 옵셔널화 - AuthInterface / Default · Mock · AuthRepositoryImpl / AuthUseCaseImpl 의 login 시그니처에 idToken · redirectUri? 추가 - UnifiedOAuthUseCase.appleLogin: redirectUri = nil, idToken = payload.idToken 으로 호출하고 authCode · idToken 을 UserDefaults 에 동시 저장 - AuthLocalStorage 신규 — picke.auth.authCode / picke.auth.idToken 키로 단순 캐시 (Keychain 과 분리) - Google · Kakao 는 기존 redirectUri 유지하고 idToken = nil 명시
- SplashFeature 의 keychain 자격 분기를 임시 주석 처리 (이후 Apple 로그인 흐름 검증 끝나면 복원 예정) - AppReducer 가 splash.onAppear 시 곧바로 .view(.presentAuth) 를 dispatch 하도록 임시 변경
- pull_request (develop · main · master, opened/synchronize) 트리거 - Swift diff 만 추출하여 [LINE N] 어노테이션 후 모델에 전달 → 라인 단위 인라인 코멘트 게시 - 기존 Codex PR Review 워크플로우와 독립 실행되어 한쪽 실패가 다른 쪽에 영향 X - secrets: GEMINI_API_KEY 필요 (repo Settings → Secrets → Actions)
…..) 자동 Bearer 토큰 첨부 - App 모듈에 .SPM.kingfisher 의존성 추가 - KingfisherConfigurator: AnyModifier 로 매 요청마다 KeychainManaging.accessToken() 을 Authorization 헤더에 자동 첨부 - AppDelegate 의 DI bootstrap 직후 호출하여 모든 KFImage 가 인증 토큰을 갖고 동작
- VoteCardView: 선택 전 단순 옵션 4개 → 선택 후 빈칸 채움 + secondary100/500 percentage bar 4 row (.pen `Radar Wrap`), 0 → target width easeOut 0.6s 애니메이션 - QuizCardView: .pen 단일 상태로 복원 (Result variant 없음) - HomeSectionHeader: title 안의 라틴 단어(Best · Pické)와 한글 `배틀` 을 primary500 으로 자동 강조 (AttributedString) - HeroCarouselView: thumbnail 영역 정밀 매핑 (375 wide + GeometryReader + clipped), var → @ViewBuilder func 변환 - NewBattleCardView: thumbnail 95h 복원 + 아바타 베이지 원형 배경 + 16×28 일러스트 ( .pen `Avatar/Philosopher` Frame 195 매칭) - HotBattle mock 에 picsum thumbnail URL 추가하여 trendingBattles 빈 응답 시에도 이미지 표시 - HomeView · HomeSkeletonView 의 inner sub-view 들을 `@ViewBuilder` 명시
- 자식 ≥ 2개를 감싸거나 if/switch/ForEach 분기·반복이 있는 inner `private var`/`func` 에 `@ViewBuilder` 명시 (OnBoardingView · MainTabView · PreVoteView) - AGENTS.md `#### 🧱 @ViewBuilder 함수 vs var` 규칙 섹션 추가 — 자식 개수/분기 유무로 함수+@ViewBuilder vs var 결정 기준 명문화
- Apple 로그인 동작 확인을 위해 임시로 비활성화했던 keychain 자격 분기를 복원 - SplashFeature 가 hasStoredCredential 에 따라 .presentMainTab / .presentAuth 로 분기 - AppReducer 의 splash.onAppear 핸들러도 .none 으로 원복하여 SplashFeature 의 delegate 흐름이 정상 작동
- Mode 1.tokens.json 폐기, primitive/semantic/component/$metadata JSON 도입 - Tools/TokenGenerator.swift 를 새 포맷에 맞게 재작성: 3-파일 deep-merge, 참조 체인 재귀 해석, rgba() 색 지원 - 자동 생성물 (ShapeStyle+.swift / CGFloat+Spacing+ / CGFloat+Radius+ / CGFloat+Component+ / ComponentToken.swift) 갱신
- Poll 도메인 추가 — Entity (PollDetail · PollOption · PollStatus + percentage helper), DTO+Mapper (yyyy-MM-dd · displayOrder 정렬), PollInterface · DefaultPollRepositoryImpl, PollRepositoryImpl (MoyaProvider<PollService>.authorized 패턴) - API 도메인 PieckeDomain 에 case poll 추가 + AppDIManager 에 PollRepositoryImpl 등록 - PreVoteFeature: State 에 poll · isLoading · pollId 추가, Result + mapError 단일 Response 패턴으로 fetchPoll 비동기 액션 구성 - PreVoteView: onAppear 시 fetchPoll, 로딩 중에는 PreVoteSkeletonView 분기 표시 - PreVoteSkeletonView 신규 — .pen `사전 투표창 - Skeleton Loader` (nTffe) 의 10개 Rectangle 절대 좌표 1:1 매핑, 375pt 디자인 기준 GeometryReader 로 비례 scale
- DesignSystem 에 SkeletonView 신규 — beige600 base + 좌→우 shimmer 그라데이션 1.2s 무한 반복 (cornerRadius 옵션)
- HeroCarouselView · HotBattleCardView · NewBattleCardView 의 KFImage 에 .placeholder { SkeletonView() } 적용해 이미지 로딩 중 shimmer 표시
- Kingfisher 의 기본 캐싱(memory + disk) 정책은 그대로 사용
- Tokens Studio 단일 파일 (`Mode 1.tokens.json`) 적용에 맞춰 다중 분할 (component/primitive/semantic/$metadata) JSON 4개 정리 - ShapeStyle+ / ComponentToken / CGFloat 토큰 코드젠 결과 일괄 갱신 - LoginView: 정의되지 않은 `.gray200` / `.borderGray` → 동일 색의 alias `.neutral200` / `.borderGrayDefault` 로 교체
| import UIKit | ||
| import WeaveDI | ||
|
|
||
| import DomainInterface |
There was a problem hiding this comment.
🔵 [P4] Readability
모듈 임포트 순서를 UIKit이나 Foundation 계열 다음에 알파벳 순으로 정렬하는 것이 좋습니다. WeaveDI와 DomainInterface의 순서를 바꾸는 것을 고려해 보세요.
| import DomainInterface | |
| import WeaveDI | |
| import DomainInterface |
| import Kingfisher | ||
|
|
||
| import DomainInterface | ||
| import Foundations |
There was a problem hiding this comment.
🔵 [P4] Readability
Foundations 모듈이 DomainInterface보다 먼저 임포트되어야 할 특별한 이유가 없다면, 알파벳 순서로 정렬하는 것이 좋습니다.
| import Foundations | |
| import Foundations | |
| import DomainInterface |
|
|
||
| @preconcurrency import AsyncMoya | ||
|
|
||
| public final class PollRepositoryImpl: PollInterface, @unchecked Sendable { |
There was a problem hiding this comment.
🔵 [P4] Readability
@unchecked Sendable은 동시성 안전성을 명시적으로 보장할 수 없을 때 사용됩니다. PollRepositoryImpl의 현재 구현을 보면 MoyaProvider<PollService>는 스레드 안전하게 사용되므로, @unchecked를 제거하고 final class PollRepositoryImpl: PollInterface, Sendable로 선언하는 것이 더 명확하고 안전합니다. MoyaProvider는 내부적으로 Sendable을 따르고 있습니다.
| guard let data = dto.data else { | ||
| let message = dto.error?.message ?? "투표 데이터 응답이 비어 있습니다" | ||
| Log.error("[PollRepositoryImpl] empty poll payload: \(message)") | ||
| throw AuthError.backendError(message) |
There was a problem hiding this comment.
🟡 [P3] Minor
PollRepositoryImpl에서 AuthError를 던지는 대신, PollError와 같은 Poll 도메인에 특화된 에러나 더 일반적인 NetworkError를 사용하는 것이 모듈의 책임과 Clean Architecture 원칙에 더 부합합니다. AuthError는 Auth 모듈에 한정되어야 합니다.
| throw AuthError.backendError(message) | |
| throw NetworkError.backendError(message) |
| settings: .settings(), | ||
| dependencies: [ | ||
| .Data(implements: .API), | ||
| .Domain(implements: .Entity), |
There was a problem hiding this comment.
🟠 [P2] Major
Data/Service 모듈은 Domain/Entity 모듈에 의존해서는 안 됩니다. Clean Architecture의 계층별 의존성 규칙(Presentation → Domain ← Data)에 따르면, Data 계층은 Domain 계층의 내용을 알지 못해야 합니다. Entity는 Domain 계층의 일부이며, Data/Service에서 이를 직접 임포트하는 것은 의존성 방향을 위반합니다. Data/Model에서 Entity로 매핑하고, Data/Repository가 Entity를 반환하는 것은 허용됩니다. PollService 자체는 Entity를 임포트하지 않으므로, 이 의존성은 잘못 등록된 것으로 보입니다.
| .Domain(implements: .Entity), | |
| // .Domain(implements: .Entity), // Data/Service는 Domain/Entity에 직접 의존하지 않아야 합니다. |
| public struct PreVoteView: View { | ||
| @Bindable public var store: StoreOf<PreVoteFeature> | ||
|
|
||
| public init(store: StoreOf<PreVoteFeature>) { |
There was a problem hiding this comment.
🔵 [P4] Readability
SwiftUI View의 init은 대개 public으로 선언하는 것이 일반적이지만, PreVoteView는 HomeCoordinator에서 직접 생성되므로 internal 접근 제어자만으로도 충분합니다. 이는 API 노출을 최소화하고 캡슐화를 강화하는 데 도움이 됩니다.
| public init(store: StoreOf<PreVoteFeature>) { | |
| internal init(store: StoreOf<PreVoteFeature>) { |
|
|
||
| @ViewBuilder | ||
| private func avatarView(_ philosopher: PhilosopherAvatar) -> some View { | ||
| let asset: ImageAsset = switch philosopher { |
There was a problem hiding this comment.
🟡 [P3] Minor
PhilosopherAvatar enum에 imageAsset computed property를 추가하여 이미지 애셋을 반환하도록 만들면, 이 avatarView 함수에서 switch 문을 중복으로 작성할 필요가 없어 코드를 더 간결하게 만들고 재사용성을 높일 수 있습니다. 이는 NewBattleCardView의 private extension PhilosopherAvatar와 유사한 방식으로 구현할 수 있습니다.
Projects/Domain/Entity/Sources/Home/PreVoteBattle.swift 파일의 PhilosopherAvatar enum에 추가하면 됩니다.
| let asset: ImageAsset = switch philosopher { | |
| public extension PhilosopherAvatar { | |
| var imageAsset: ImageAsset { | |
| switch self { | |
| case .plato: .avatarPlato | |
| case .sartre: .avatarSartre | |
| case .sunja: .avatarSunja | |
| } | |
| } | |
| } |
그리고 avatarView에서는 다음과 같이 사용할 수 있습니다.
| let asset: ImageAsset = switch philosopher { | |
| return Image(asset: philosopher.imageAsset) |
| static var bedgeOutlineBackround: Color { .beige50 } | ||
| static var bedgeOutlineBorder: Color { .primary100 } | ||
| static var bedgeOutlineText: Color { .primary500 } | ||
| static var avatarBackround: Color { .beige600 } |
There was a problem hiding this comment.
🔵 [P4] Readability
avatarBackround에 오타가 있습니다. background가 올바른 철자입니다.
| static var avatarBackround: Color { .beige600 } | |
| static var avatarBackground: Color { .beige600 } |
| static var listItemAgreementBackgroundDefault: Color { .bgDefault } | ||
| static var listItemAgreementBorderDefault: Color { .gray50 } | ||
| static var listItemAgreementBorderError: Color { .init(hex: "C92D33", alpha: 0.4) } | ||
| static var listItemAgreementTextDefualt: Color { .gray800 } |
There was a problem hiding this comment.
🔵 [P4] Readability
listItemAgreementTextDefualt에 오타가 있습니다. Default가 올바른 철자입니다.
| static var listItemAgreementTextDefualt: Color { .gray800 } | |
| static var listItemAgreementTextDefault: Color { .gray800 } |
| public static var inverse: Color { .bedgeFilledBackgroundInverse } | ||
| } | ||
| public enum Avatar { | ||
| public static var backround: Color { .avatarBackround } |
There was a problem hiding this comment.
🔵 [P4] Readability
backround에 오타가 있습니다. background가 올바른 철자입니다.
| public static var backround: Color { .avatarBackround } | |
| public static var background: Color { .avatarBackground } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 016a1cb29d
ℹ️ 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".
| case .routeAction(_, action: .home(.delegate(.presentPreVote))): | ||
| state.routes.push(.preVote(.init())) |
There was a problem hiding this comment.
Pass the selected battle id into pre-vote
When any home card sends presentPreVote(battleId:), this route handler discards the associated id and pushes PreVoteFeature.State() with its default pollId = 1. As a result, tapping hero/hot/best/new cards for any battle other than 1 still fetches and shares /poll/1, so users see the wrong poll despite HomeFeature forwarding the tapped battleId.
Useful? React with 👍 / 👎.
| VoteCardView(question: vote) | ||
| .contentShape(Rectangle()) | ||
| .onTapGesture { } |
There was a problem hiding this comment.
Wire the vote card tap to its action
The Today Pické vote card now installs a tap gesture but leaves the handler empty, so tapping the card never sends voteTapped(vote) even though HomeFeature has the delegate path to open pre-vote. This makes the vote card the only home entry point that cannot navigate to the pre-vote screen.
Useful? React with 👍 / 👎.
| if let token = keychainManager.accessToken(), !token.isEmpty { | ||
| req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") |
There was a problem hiding this comment.
Restrict Bearer headers to protected image URLs
This global Kingfisher modifier attaches the access token to every KFImage request, not just Picke protected /api/v1/resources/... URLs. If any home image URL is external or user/API-controlled, the app will send the user's Bearer token to that host; the modifier should check the expected host/path before setting Authorization.
Useful? React with 👍 / 👎.
- protectedHostSuffixes 화이트리스트 (picke.store / dev.picke.store) 도입
- host suffix 정확 일치 + path.hasPrefix("/api/") + 토큰 존재 4-조건 만족 시에만 Authorization 헤더 첨부
- picsum.photos / 카카오 CDN 같은 외부 호스트로 keychain accessToken 이 새지 않도록 차단
| @@ -1,7 +1,7 @@ | |||
| import ProjectDescription | |||
| import DependencyPackagePlugin | |||
There was a problem hiding this comment.
🔵 [P4] Readability
이 import DependencyPackagePlugin 선언은 프로젝트 상단에 이미 존재하며 중복됩니다. 제거해 주세요.
| import DependencyPackagePlugin |
| import ProjectDescription | ||
| import DependencyPackagePlugin | ||
| import DependencyPlugin | ||
| import ProjectDescription |
There was a problem hiding this comment.
🔵 [P4] Readability
이 import ProjectDescription 선언은 프로젝트 상단에 이미 존재하며 중복됩니다. 제거해 주세요.
| import ProjectDescription |
| let position: Int | ||
| let total: Int | ||
|
|
||
| private let thumbnailHeight: CGFloat = 220 |
There was a problem hiding this comment.
🔵 [P4] Readability
HeroCarouselView의 Self.thumbnailHeight와 동일한 상수를 HeroCardView 내부에서 다시 선언하고 있습니다. 중복을 피하고 HeroCarouselView의 Self.thumbnailHeight를 사용하는 것이 좋습니다. 예를 들어 HeroCardView의 이니셜라이저를 통해 전달받거나, HeroCarouselView의 static 상수를 참조하도록 수정할 수 있습니다.
현재 HeroCardView는 HeroCarouselView의 Subview이므로, HeroCardView에서 직접 HeroCarouselView.thumbnailHeight에 접근하기는 어렵습니다. 따라서 thumbnailHeight를 HeroCardView의 프로퍼티로 선언하고 HeroCarouselView에서 전달해 주는 방식이 더 적절해 보입니다.
| if let vote = store.currentVote { | ||
| VoteCardView(question: vote) | ||
| .contentShape(Rectangle()) | ||
| .onTapGesture { } |
There was a problem hiding this comment.
🟡 [P3] Minor
VoteCardView에 빈 onTapGesture가 있습니다. 현재는 아무 동작도 하지 않지만, HomeFeature.Action.voteTapped(VoteQuestion) 액션을 보내서 PreVoteView로 이동하는 등의 의도가 있었다면 이를 구현해야 합니다. 의도치 않게 추가된 경우라면 제거해 주세요.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d1cc3ece6e
ℹ️ 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".
| state.isLoading = false | ||
| switch result { | ||
| case let .success(poll): | ||
| state.poll = poll |
There was a problem hiding this comment.
Use fetched poll data for pre-vote content
When fetchPoll succeeds, the reducer only assigns state.poll, but the loaded pre-vote screen renders its title, summary, tags, background, and option cards from store.battle, which is initialized to PreVoteBattle.mock and never updated from the response. For any real poll whose content differs from the mock, users will see/select the hard-coded mock battle while the feature shares/submits the fetched poll id, so the poll details shown in production are wrong.
Useful? React with 👍 / 👎.
- PhilosopherAvatar.imageAsset 매핑을 Home 모듈 내부 internal extension (PhilosopherAvatar+ImageAsset.swift) 으로 추출 - Entity 가 DesignSystem 에 의존하지 않도록 매핑은 Presentation 계층에 위치 - NewBattleCardView 의 private extension PhilosopherAvatar 제거 → 공용 extension 사용 - PreVoteView.avatarView 의 switch 문 제거 → Image(asset: philosopher.imageAsset) 한 줄 - HeroCardView 의 thumbnailHeight 중복 상수 제거 → init 매개변수로 받고 HeroCarouselView 가 Self.thumbnailHeight 주입 (단일 진실 공급원)
| import Kingfisher | ||
|
|
||
| import DomainInterface | ||
| import Foundations |
There was a problem hiding this comment.
🔵 [P4] Readability
Foundations 모듈이 이 파일에서 실제로 사용되는지 확인해주세요. URL과 Set은 Foundation에서 제공되며, KeychainManaging은 DomainInterface에서 가져옵니다. 불필요한 import는 제거하는 것이 좋습니다.
| import Foundations | |
| // import Foundations |
| guard let data = dto.data else { | ||
| let message = dto.error?.message ?? "투표 데이터 응답이 비어 있습니다" | ||
| Log.error("[PollRepositoryImpl] empty poll payload: \(message)") | ||
| throw AuthError.backendError(message) |
There was a problem hiding this comment.
🟠 [P2] Major
PollRepositoryImpl에서 AuthError.backendError를 던지는 것은 의미상 적절하지 않습니다. 인증 관련 오류가 아닌 투표 데이터 관련 오류이므로, PollError와 같이 도메인 특화된 오류를 정의하거나 더 일반적인 NetworkError 또는 AppError를 사용하는 것이 좋습니다.
| throw AuthError.backendError(message) | |
| throw PollError.dataMissing(message) // PollError를 정의하거나 AppError 사용 |
| import Foundation | ||
|
|
||
| import API | ||
| import Foundations |
There was a problem hiding this comment.
🔵 [P4] Readability
Foundations 모듈이 이 파일에서 실제로 사용되는지 확인해주세요. Moya 및 API는 이미 필요한 기능을 제공합니다. 불필요한 import는 제거하는 것이 좋습니다.
| import Foundations | |
| // import Foundations |
| /// 강조 규칙: | ||
| /// - 라틴 단어 (Best · Pické 등) 가 있으면 그 단어만 primary500, 나머지 한글은 neutral900 | ||
| /// - 라틴 단어가 없으면 한글 `배틀` 만 primary500, 나머지 한글은 neutral900 | ||
| private var attributedTitle: AttributedString { |
There was a problem hiding this comment.
🔵 [P4] Readability
View의 computed property에서 정규식을 사용한 복잡한 AttributedString 처리 로직이 구현되어 있습니다. 정적 텍스트의 경우 큰 문제는 없지만, 뷰 업데이트가 잦아지거나 텍스트가 동적으로 변경될 경우 성능 저하의 가능성이 있습니다. 이러한 텍스트 처리 로직은 Feature 또는 ViewModel 계층에서 미리 처리하여 AttributedString을 직접 전달하는 것을 고려해볼 수 있습니다.
| if let vote = store.currentVote { | ||
| VoteCardView(question: vote) | ||
| .contentShape(Rectangle()) | ||
| .onTapGesture { } |
There was a problem hiding this comment.
🟡 [P3] Minor
VoteCardView에 빈 onTapGesture 클로저가 있습니다. PR 설명의 테스트 플랜에 따르면, Vote 카드 탭 시 사전 투표창으로 진입해야 하므로, 여기에 send(.voteTapped(vote)) 액션을 호출하여 PreVoteView를 표시하는 로직을 추가해야 합니다.
| .onTapGesture { } | |
| .onTapGesture { send(.voteTapped(vote)) } |
| static var gray700: Color { .init(hex: "222120") } | ||
| static var gray800: Color { .init(hex: "1A1918") } | ||
| static var gray900: Color { .init(hex: "131212") } | ||
|
|
There was a problem hiding this comment.
🔴 [P1] Critical
ShapeStyle+.swift 파일에 SPM.mixpanelSessionReplay와 같은 SPM 의존성 선언이 포함되어 있습니다. 이는 Project.swift에서 실수로 복사된 것으로 보이며, 컴파일 오류를 유발할 수 있는 치명적인 오류입니다. 즉시 제거해야 합니다.
| // .SPM.mixpanelSessionReplay, // 이 줄을 제거해야 합니다. |
|
|
||
| @State private var phase: CGFloat = -1 | ||
|
|
||
| private let baseColor = Color(red: 239 / 255, green: 234 / 255, blue: 224 / 255) // beige600 #EFEAE0 |
There was a problem hiding this comment.
🔵 [P4] Readability
SkeletonView 내의 baseColor가 하드코딩된 RGB 값으로 정의되어 있습니다. Design System에 이미 beige600 토큰이 정의되어 있으므로, 이를 직접 사용하는 것이 일관성과 유지보수 측면에서 더 좋습니다.
| private let baseColor = Color(red: 239 / 255, green: 234 / 255, blue: 224 / 255) // beige600 #EFEAE0 | |
| private let baseColor = Color.beige600 |
| @State private var phase: CGFloat = -1 | ||
|
|
||
| private let baseColor = Color(red: 239 / 255, green: 234 / 255, blue: 224 / 255) // beige600 #EFEAE0 | ||
| private let shimmerColor = Color(red: 254 / 255, green: 254 / 255, blue: 253 / 255) // beige50 #FEFEFD |
There was a problem hiding this comment.
🔵 [P4] Readability
SkeletonView 내의 shimmerColor가 하드코딩된 RGB 값으로 정의되어 있습니다. Design System에 이미 beige50 토큰이 정의되어 있으므로, 이를 직접 사용하는 것이 일관성과 유지보수 측면에서 더 좋습니다.
| private let shimmerColor = Color(red: 254 / 255, green: 254 / 255, blue: 253 / 255) // beige50 #FEFEFD | |
| private let shimmerColor = Color.beige50 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f53ed2acd4
ℹ️ 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".
| .routeAction(_, action: .preVote(.delegate(.submit))): | ||
| return .send(.view(.backAction)) |
There was a problem hiding this comment.
Submit the pre-vote before popping the screen
When a user selects an option and taps primaryButtonTapped, PreVoteFeature sends .delegate(.submit(pollId:side:)), but this route handler treats that submit exactly like dismiss and immediately goes back. In this flow there is no repository call or parent action that records the selected side, so the CTA appears to complete a pre-vote while the vote is never persisted.
Useful? React with 👍 / 👎.
| git fetch origin "${{ github.event.pull_request.head.ref }}" | ||
| git diff "origin/${{ github.event.pull_request.base.ref }}"..."origin/${{ github.event.pull_request.head.ref }}" -- "*.swift" > diff.txt |
There was a problem hiding this comment.
Fetch fork PR heads from the PR ref
For pull requests opened from forks, origin points at the base repository, so fetching and diffing origin/${{ github.event.pull_request.head.ref }} only works when a branch with the same name exists in the base repo. In fork PRs this step fails before Gemini runs (or can diff the wrong branch if the name collides); fetch the PR ref/head SHA instead of the head branch name from origin.
Useful? React with 👍 / 👎.
- 기존: pollResponse success 시 state.poll 만 갱신, View 는 mock 으로 초기화된 state.battle 만 사용 → 실제 응답이 화면에 반영 안 됨 + share/submit 은 새 pollId 로 동작해 콘텐츠 불일치 - makeBattle(from:fallback:) 헬퍼로 PollDetail → PreVoteBattle 변환: battleId/titlePrefix/titleSuffix/options 매핑, summary/tags/backgroundImageURL 은 API 에 없으므로 fallback (mock) 유지 - 옵션은 displayOrder 정렬된 상태에서 앞 2개를 좌/우 카드로, 부족하면 fallback option 사용. philosopher 매핑은 인덱스 기반 (.plato/.sartre/.sunja)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9bf1ade835
ℹ️ 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".
| case .home: | ||
| return"api/v1/home" | ||
| case .poll: | ||
| return"api/v1/poll" |
There was a problem hiding this comment.
Use the documented plural poll route
The new poll DTO/entity comments describe the detail API as GET /api/v1/polls/{pollId}, but this domain is singular and PollAPI.detailPoll appends /<id>, so fetchPoll will request /api/v1/poll/{id}. In environments where the backend exposes the documented plural route, every pre-vote detail fetch returns 404 and the screen remains on fallback/mock data; please align the base path with the poll detail endpoint.
Useful? React with 👍 / 👎.
| AuthLocalStorage.authCode = authCode | ||
| AuthLocalStorage.idToken = payload.idToken |
There was a problem hiding this comment.
Keep Apple credentials out of UserDefaults
On every successful Apple sign-in this writes the authorization code and identity token to UserDefaults via AuthLocalStorage, while the only clear() I found is never called. These values are login credentials/PII and persist outside the keychain even though the real session tokens are saved securely just below, so a logged-in user's Apple token can remain in app preferences/backups; keep them in memory, store them in the keychain, or clear them immediately after the backend exchange.
Useful? React with 👍 / 👎.
- PhilosopherAvatar.imageAsset 매핑을 Home 모듈 내부 internal extension (PhilosopherAvatar+ImageAsset.swift) 으로 추출 - Entity 가 DesignSystem 에 의존하지 않도록 매핑은 Presentation 계층에 위치 - NewBattleCardView 의 private extension PhilosopherAvatar 제거 → 공용 extension 사용 - PreVoteView.avatarView 의 switch 문 제거 → Image(asset: philosopher.imageAsset) 한 줄 - HeroCardView 의 thumbnailHeight 중복 상수 제거 → init 매개변수로 받고 HeroCarouselView 가 Self.thumbnailHeight 주입 (단일 진실 공급원)
|
{ |
요약
U5WO41:1 매핑 + Property 1=Default ↔ Result 전환 + bar 채워지는 애니메이션.poll배틀강조 규칙, 카드 진입 라우팅 (Hero·Hot·Best·Vote·New 탭 → 사전 투표창), 탭바 active/inactive 아이콘 분리SkeletonView(shimmer) + KFImage placeholder, PreVoteSkeletonView (.pennTffe절대 좌표 1:1 매핑)requestModifier로 보호 이미지 (/api/v1/resources/...) 에 keychain accessToken Bearer 헤더 자동 첨부Refs (부분 완료 / 후속 PR 필요)
Refs #4— 사전 투표창 UI + Poll detail GET 만 완료, 사후 투표·재투표 / vote 제출(post pre/post) / vote-stats 미구현Refs #3— 홈 메인 + 카드 진입 라우팅 완료, 큐레이팅/알림 모달 미구현Refs #18— 사전 투표창 + 홈 카드 KFImage placeholder 만 적용, 다른 화면 후속Refs #19— 보호 이미지 인증 첨부 완료Refs #24— Tokens Studio 코드젠 정렬 적용Refs #31— Apple identityToken 본 PR 에 포함테스트 플랜
UIActivityViewController) 표시Authorizationbody 에identityToken포함 (Google·Kakao 는 null).neutral200,.borderGrayDefault) 정상 렌더링