Skip to content

Comments

Style/#61 챌린지 플로우 뷰 구현#67

Merged
sum130 merged 10 commits intodevelopfrom
style/#61-challengemissionview
Jan 18, 2026
Merged

Style/#61 챌린지 플로우 뷰 구현#67
sum130 merged 10 commits intodevelopfrom
style/#61-challengemissionview

Conversation

@sum130
Copy link
Contributor

@sum130 sum130 commented Jan 15, 2026

🔗 연결된 이슈

📄 작업 내용

  • challengemissionview 구현
  • challengemissionprogressview 구현
구현 내용 IPhone 16 pro IPhone 13 mini
GIF

💻 주요 코드 설명

코드 설명할 파일 이름 (ex: ChallengeProgressView)

  • ChallengeProgressView에서 CherryLevel을 enum으로 설정해서 단계별로 이미지, 텍스트를 변하게 설정했습니다.
enum CherryLevel: Int {
    case mong = 0
    case bbo
    case chok
    case pang
    case ggu
    
    var levelNumber: Int { rawValue }
    
    static func from(progressRate: Double) -> CherryLevel {
        switch progressRate {
        case 0.0..<25.0:
            return .mong
        case 25.0..<50.0:
            return .bbo
        case 50.0..<75.0:
            return .chok
        case 75.0..<100.0:
            return .pang
        case 100.0...:
            return .ggu
        default:
            return .mong
        }
    }
    
  • 서버에서 주는 미션 진행률에 따라 0~4단계로 나눴습니다.
  • 이에 따라 캐릭터 이미지, 이름, 진행바가 달라집니다.

👀 기타 더 이야기해볼 점

제발 완만히 어푸 받을 수 있길..!!! 뷰 마감 하고시퍼요 엉엉슨..

@sum130 sum130 requested a review from a team January 15, 2026 17:24
@sum130 sum130 self-assigned this Jan 15, 2026
@sum130 sum130 requested review from soseoyo12, wotjs020708 and y-eonee and removed request for a team January 15, 2026 17:24
@sum130 sum130 linked an issue Jan 15, 2026 that may be closed by this pull request
2 tasks
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

챌린지 기능의 전체 흐름을 구현하기 위해 여러 뷰와 에셋을 추가합니다. 루틴 선택, 미션 선택, 진행 상황 추적을 포함한 챌린지 진행 화면을 구성하고, 네비게이션 및 탭 바 상태를 관리하는 코디네이터를 통합합니다.

Changes

Cohort / File(s) 변경 사항
Challenge UI Assets
Assets.xcassets/ChallengeImage/challengeImage.imageset/Contents.json, challenge_gaugebar_*.imageset/Contents.json, cherry*.imageset/Contents.json, illustration_challenge_start.imageset/Contents.json
챌린지 UI에 필요한 SVG 이미지 에셋 11개 추가 (게이지바, 체리 이모지, 일러스트레이션)
Challenge View - Core Features
Presentation/Feature/ChallengeView/ChallengeProgressView.swift
CherryLevel 열거형과 진행 상황 표시 UI 추가 (스크롤 기반 레이아웃, 게이지바, TO-DO 리스트)
Challenge View - Navigation & Selection
SelectMissionView.swift, SelectRoutineView.swift, StartChallengeView.swift, LoadingView.swift
루틴/미션 선택 UI 개선, 뷰 모델 패턴 도입, 로딩 네비게이션 로직 추가
Challenge Coordinator
Coordinator/ViewModel/SelectRoutineViewModel.swift
뷰 모델 기반 루틴 선택 상태 관리 클래스 추가 (RoutineType 열거형 포함)
Global UI Components
Presentation/Global/Components/MissionCard.swift, SelectionChip.swift, CherrishNavigationBar.swift, Extension/View+.swift
라디오 버튼 기반 미션 카드 재구성, 선택 칩 배경 업데이트, 하이라이트 텍스트 확장 메서드 추가

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant StartChallengeView as StartChallenge<br/>View
    participant SelectRoutineView as SelectRoutine<br/>View
    participant SelectMissionView as SelectMission<br/>View
    participant ChallengeProgressView as ChallengeProgress<br/>View
    participant LoadingView
    participant ChallengeCoordinator
    participant TabBarCoordinator

    User->>StartChallengeView: 챌린지 시작 버튼 클릭
    StartChallengeView->>TabBarCoordinator: 탭 바 숨김 설정
    StartChallengeView->>ChallengeCoordinator: selectRoutine 네비게이션
    
    ChallengeCoordinator->>SelectRoutineView: 루틴 선택 화면 표시
    User->>SelectRoutineView: 루틴 선택 (ViewModel)
    SelectRoutineView->>SelectRoutineView: nextButtonState 활성화
    User->>SelectRoutineView: 다음 버튼
    SelectRoutineView->>ChallengeCoordinator: selectMission 네비게이션
    
    ChallengeCoordinator->>SelectMissionView: 미션 선택 화면 표시
    User->>SelectMissionView: 미션 선택 (다중 선택)
    SelectMissionView->>SelectMissionView: selectedStates 업데이트
    User->>SelectMissionView: 다음 버튼
    SelectMissionView->>ChallengeCoordinator: challengeProgress 네비게이션
    
    ChallengeCoordinator->>LoadingView: 로딩 화면 표시
    LoadingView->>LoadingView: 3초 대기 (moveNextAfterDelay)
    LoadingView->>ChallengeCoordinator: selectMission 네비게이션
    
    ChallengeCoordinator->>ChallengeProgressView: 진행 상황 추적 화면 표시
    ChallengeProgressView->>ChallengeProgressView: CherryLevel 계산 및 UI 렌더링
    User->>ChallengeProgressView: TO-DO 항목 체크 및 완료
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Feat/#40 startchallengeview #46: 챌린지 시작 흐름과 관련하여 StartChallengeView, SelectRoutineView 라우팅, ChallengeImage 에셋을 동일하게 수정하는 겹치는 변경사항이 있습니다.
  • feat: #27 chipComponent 구현 #38: MissionCard와 SelectionChip 컴포넌트 렌더링 및 선택 동작을 직접적으로 변경하여 관련성이 높습니다.
  • Style/#48 selectroutineview 구현 #54: SelectRoutineView에서 RoutineType 열거형과 선택 상태를 처리하며, 코디네이터/탭 바 처리 및 확장 메서드를 함께 수정합니다.

Suggested reviewers

  • soseoyo12
  • wotjs020708
  • y-eonee
🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 일부 변경사항이 #61 요구사항의 직접 범위를 벗어났습니다. SelectRoutineView 리팩토링(뷰 모델 추가, public enum 제거), StartChallengeView 수정(탭바 히든 로직), LoadingView 변경(navigation task 추가) 등은 mission selection 뷰 구현과 무관한 변경입니다. 범위 외 변경사항 분리: SelectRoutineView 리팩토링, StartChallengeView 탭바 수정, LoadingView 변경사항을 별도 PR로 분리하여 mission view 구현에 집중하시기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 '챌린지 플로우 뷰 구현'으로 변경사항의 핵심(ChallengeMissionView 및 ChallengeProgressView 구현)을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 구현한 두 개의 뷰(ChallengeMissionView, ChallengeMissionProgressView), CherryLevel enum 구현 및 진행률 단계 매핑에 대해 구체적으로 설명하고 있습니다.
Linked Issues check ✅ Passed PR은 #61에서 요구한 두 가지 뷰(챌린지미션선택뷰, 챌린지미션진행뷰)를 구현했으며, 변경사항은 SelectMissionView, ChallengeProgressView 및 관련 컴포넌트들의 구현과 일치합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch style/#61-challengemissionview

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift (1)

24-28: 배경이 둥근 모서리에 클리핑되지 않을 수 있습니다.

.background 수정자가 직접 적용되어 있지만, overlay의 RoundedRectangle(cornerRadius: 10)과 달리 배경색이 모서리에서 잘리지 않습니다. 이로 인해 배경색이 둥근 테두리 바깥으로 보일 수 있습니다.

🔧 배경에 cornerRadius 적용 제안
-        .background(isSelected ? .red100 : .gray0)
+        .background(
+            RoundedRectangle(cornerRadius: 10)
+                .fill(isSelected ? .red100 : .gray0)
+        )

또는 clipShape를 추가하는 방법도 있습니다:

         .background(isSelected ? .red100 : .gray0)
+        .clipShape(RoundedRectangle(cornerRadius: 10))
🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`:
- Around line 144-146: The CherrishButton instance with title "오늘 미션 종료하기" has
an empty action closure; either implement the intended behavior inside that
closure (e.g., call the view model method like viewModel.completeTodayMission(),
dispatch navigation or state updates, and update buttonState) or, if it’s
intentionally left for later, add a clear TODO comment inside the closure (e.g.,
// TODO: implement finish-today-mission action — track with issue #<id>) so
future reviewers know it’s pending; locate the empty closure on the
CherrishButton usage and replace it with the action implementation or the TODO
comment as appropriate.
- Around line 63-72: selectedStates is hardcoded to a fixed length and can go
out of sync with missions; replace the separate arrays by integrating selection
into a single model (e.g., a Mission struct with id/title/isSelected) or
initialize selectedStates from missions.count and update it whenever missions
changes; update ChallengeProgressView (and SelectMissionView analogously) to use
the new model/mission.isSelected property (or the dynamic selectedStates) so
indices remain consistent and avoid range errors.
- Line 108: The displayed progressRate is a Double and may show decimal
fractions; update the string interpolation in the TypographyText call that
currently uses "챌린지 달성률 \(progressRate)%" to format it as an integer (e.g., use
Int(progressRate) or String(format: "%.0f", progressRate) or
Int(progressRate.rounded()) ) so the view shows a whole percent value like "챌린지
달성률 76%".

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift`:
- Around line 33-45: The background Task started in moveNextAfterDelay() can
survive when the view is dismissed, causing unwanted navigation; store the Task
in a cancellable reference (e.g., an `@State` or `@StateObject` property like
navigationTask: Task<Void, Never>?), assign the Task when creating it in
moveNextAfterDelay(), and cancel it in onDisappear (navigationTask?.cancel());
also handle cancellation inside the Task (await Task.sleep can throw
CancellationError) and only call challengeCoordinator.push(.selectMission) when
the Task has not been cancelled.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift`:
- Around line 39-41: Remove the useless .padding(.horizontal, 33.adjustedW)
applied to the Spacer() instances in SelectMissionView (the Spacer() calls
around the mission content); Spacer only expands and ignores padding, so delete
the .padding on those Spacer()s and, if horizontal inset is intended, move the
.padding(.horizontal, 33.adjustedW) to the containing view (e.g., the
HStack/VStack or the mission card view) so the layout reflects the intended
horizontal spacing.
- Around line 14-23: selectedStates is initialized with a hardcoded count (6)
which can go out of sync with missions; change the initialization to derive its
size from missions (e.g., Array(repeating: false, count: missions.count) or
initialize selectedStates lazily from missions) or refactor by introducing a
Mission struct (struct Mission with id, text, isSelected) and replace the
separate missions and selectedStates with a single `@State` array of Mission so
selection state travels with each mission (update usages of missions and
selectedStates accordingly, e.g., map or binding to mission.isSelected).

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/CherrishNavigationBar.swift`:
- Line 60: Replace the vertical-based scaler with the width-based one for
horizontal padding: in CherrishNavigationBar locate the .padding(.horizontal,
8.adjustedH) expression and change adjustedH to adjustedW so horizontal padding
uses the width-adjusted value (i.e., .padding(.horizontal, 8.adjustedW)).

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift`:
- Around line 15-23: The outer VStack(alignment: .leading) in MissionCard.swift
is redundant because it only wraps a single HStack; remove the VStack wrapper
and its corresponding closing brace, leaving the HStack as the top-level view
(or apply alignment/padding directly on the HStack if needed) so Image,
TypographyText and Spacer remain unchanged; if the VStack was intentionally kept
for future expansion, add a brief comment above it indicating that intent
instead of leaving an unused wrapper.

In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View`+.swift:
- Around line 10-21: The extension method
highlight(_:highlightColor:normalText:) on View ignores self and returns a brand
new HStack; replace it with a standalone SwiftUI View struct (e.g., struct
HighlightText: View) that has stored properties highlightText: String,
normalText: String, highlightColor: Color = .red700 and implements var body ->
some View returning the HStack with TypographyText(...) so callers use
HighlightText(highlightText:..., normalText:...) instead of
someView.highlight(...); remove or deprecate the View extension to avoid
misleading semantics and update call sites accordingly.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b13a523 and feb6691.

⛔ Files ignored due to path filters (11)
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challengeImage.imageset/challengeImage.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_0.imageset/challenge_gaugebar_0.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_1.imageset/challenge_gaugebar_1.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_2.imageset/challenge_gaugebar_2.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_3.imageset/challenge_gaugebar_3.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_4.imageset/challenge_gaugebar_4.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry0.imageset/cherry0.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry1.imageset/cherry1.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry2.imageset/cherry2.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry3.imageset/cherry3.svg is excluded by !**/*.svg
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry4.imageset/cherry4.svg is excluded by !**/*.svg
📒 Files selected for processing (20)
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challengeImage.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_0.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_1.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_2.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_3.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_4.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry0.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry1.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry2.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry3.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry4.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/CherrishNavigationBar.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/SelectionChip.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+.swift
🧰 Additional context used
🧬 Code graph analysis (6)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/SelectionChip.swift (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
  • red200 (69-71)
  • gray0 (9-11)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+.swift (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
  • red700 (89-91)
  • gray800 (41-43)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Coordinator/AppCoordinatorProtocol.swift (2)
  • pop (22-24)
  • popToRoot (26-28)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (2)
  • body (9-17)
  • cherrishShadow (21-23)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (7)
  • gray1000 (49-51)
  • gray700 (37-39)
  • gray900 (45-47)
  • gray800 (41-43)
  • gray300 (21-23)
  • red200 (69-71)
  • gray0 (9-11)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift (4)
Cherrish-iOS/Cherrish-iOS/Presentation/Coordinator/AppCoordinatorProtocol.swift (1)
  • pop (22-24)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+.swift (1)
  • highlight (11-20)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
  • gray800 (41-43)
  • gray600 (33-35)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift (1)
  • push (24-26)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (4)
  • gray800 (41-43)
  • gray700 (37-39)
  • red100 (65-67)
  • gray0 (9-11)
🔇 Additional comments (15)
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry1.imageset/Contents.json (1)

1-12: LGTM!

Xcode 에셋 카탈로그 표준 형식을 따르는 올바른 Contents.json 파일입니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_1.imageset/Contents.json (1)

1-12: LGTM!

게이지바 에셋 시리즈의 일부로, 표준 Xcode 에셋 카탈로그 형식을 따릅니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_0.imageset/Contents.json (1)

1-12: LGTM!

게이지바 에셋 시리즈의 첫 번째 항목으로, 표준 형식을 따릅니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_4.imageset/Contents.json (1)

1-12: LGTM!

게이지바 에셋 시리즈(0-4)의 마지막 항목으로, 100% 진행률에 해당하는 에셋입니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry3.imageset/Contents.json (1)

1-12: LGTM!

체리 캐릭터 이미지 시리즈의 일부로, CherryLevel enum의 "pang" 레벨(75-100% 진행률)에 해당하는 에셋입니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challengeImage.imageset/Contents.json (1)

1-12: LGTM!

표준 Xcode 에셋 카탈로그 JSON 형식입니다. challengeImage.svg를 universal idiom으로 올바르게 선언하고 있습니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry4.imageset/Contents.json (1)

1-12: LGTM!

cherry4.svg 에셋이 올바른 형식으로 선언되었습니다. PR 목표에 명시된 CherryLevel enum의 5단계(mong, bbo, chok, pang, ggu) 중 최고 레벨인 'ggu'에 해당하는 이미지로 보입니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry2.imageset/Contents.json (1)

1-12: LGTM!

cherry2.svg 에셋이 올바른 형식으로 선언되었습니다. CherryLevel의 'chok' 단계(50.0..<75.0)에 해당하는 이미지입니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_3.imageset/Contents.json (1)

1-12: LGTM!

challenge_gaugebar_3.svg 에셋이 올바른 형식으로 선언되었습니다. ChallengeProgressView의 진행률 표시에 사용되는 게이지바 이미지 시리즈의 일부입니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry0.imageset/Contents.json (1)

1-12: LGTM!

표준 Xcode 에셋 카탈로그 구조입니다. cherry0.svg를 universal idiom으로 올바르게 참조하고 있습니다.

Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/challenge_gaugebar_2.imageset/Contents.json (1)

1-12: LGTM!

게이지바 에셋이 올바른 형식으로 추가되었습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/SelectionChip.swift (1)

21-24: LGTM!

RoundedRectangle 배경으로 변경하여 overlay의 stroke와 일관된 cornerRadius(10)를 사용하고 있습니다. 스타일링이 적절합니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift (1)

12-12: LGTM!

탭바 숨김 처리가 적절하게 구현되었습니다. 챌린지 시작 시 탭바를 숨기고 SelectRoutineView에서 뒤로 가거나 닫을 때 다시 보여주는 일관된 패턴이 잘 적용되었습니다.

Also applies to: 40-43

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift (1)

33-33: LGTM!

탭바 표시 상태 관리가 적절하게 구현되었습니다. 뒤로가기와 닫기 동작 시 탭바를 다시 표시하는 로직이 일관성 있게 적용되었습니다.

Also applies to: 45-52

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift (1)

10-52: CherryLevel 열거형이 잘 구현되었습니다.

PR 설명에 명시된 대로 진행률을 5단계 레벨로 매핑하는 로직이 명확하고 확장 가능하게 구현되었습니다. from(progressRate:) 정적 메서드를 통한 팩토리 패턴 사용도 적절합니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +63 to +72
@State private var missions: [String] = [
"진정 토너 + 세럼",
"진정 토너 + 세럼",
"선크림 3번 바르기",
"선크림 3번 바르기",
"선크림 3번 바르기",
"선크림 3번 바르기"
]

@State private var selectedStates: [Bool] = Array(repeating: false, count: 6)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

selectedStates 배열이 missions와 동기화되지 않았습니다.

SelectMissionView와 동일한 문제입니다. 배열 크기가 하드코딩되어 있어 missions 배열 변경 시 인덱스 범위 초과 오류가 발생할 수 있습니다. 미션과 선택 상태를 하나의 모델로 통합하는 것을 권장합니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`
around lines 63 - 72, selectedStates is hardcoded to a fixed length and can go
out of sync with missions; replace the separate arrays by integrating selection
into a single model (e.g., a Mission struct with id/title/isSelected) or
initialize selectedStates from missions.count and update it whenever missions
changes; update ChallengeProgressView (and SelectMissionView analogously) to use
the new model/mission.isSelected property (or the dynamic selectedStates) so
indices remain consistent and avoid range errors.

.padding(.vertical, 14.adjustedH)
VStack {
HStack {
TypographyText("챌린지 달성률 \(progressRate)%", style: .body1_m_14, color: .gray900)
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Double 타입의 진행률이 소수점으로 표시될 수 있습니다.

progressRateDouble 타입이므로 "챌린지 달성률 75.5%"처럼 소수점이 표시될 수 있습니다. 정수로 표시하려면 포맷팅이 필요합니다.

💡 정수 포맷팅 제안
-TypographyText("챌린지 달성률 \(progressRate)%", style: .body1_m_14, color: .gray900)
+TypographyText("챌린지 달성률 \(Int(progressRate))%", style: .body1_m_14, color: .gray900)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TypographyText("챌린지 달성률 \(progressRate)%", style: .body1_m_14, color: .gray900)
TypographyText("챌린지 달성률 \(Int(progressRate))%", style: .body1_m_14, color: .gray900)
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`
at line 108, The displayed progressRate is a Double and may show decimal
fractions; update the string interpolation in the TypographyText call that
currently uses "챌린지 달성률 \(progressRate)%" to format it as an integer (e.g., use
Int(progressRate) or String(format: "%.0f", progressRate) or
Int(progressRate.rounded()) ) so the view shows a whole percent value like "챌린지
달성률 76%".

Comment on lines 144 to 146
CherrishButton(title: "오늘 미션 종료하기", type: .next, state: .constant(buttonState)){

}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

버튼 액션이 비어있습니다.

"오늘 미션 종료하기" 버튼의 액션이 구현되지 않았습니다. 의도된 플레이스홀더라면 TODO 주석을 추가하여 추후 구현이 필요함을 명시해주세요.

향후 구현이 필요한 경우, 이 작업을 추적하기 위한 이슈를 생성해 드릴까요?

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`
around lines 144 - 146, The CherrishButton instance with title "오늘 미션 종료하기" has
an empty action closure; either implement the intended behavior inside that
closure (e.g., call the view model method like viewModel.completeTodayMission(),
dispatch navigation or state updates, and update buttonState) or, if it’s
intentionally left for later, add a clear TODO comment inside the closure (e.g.,
// TODO: implement finish-today-mission action — track with issue #<id>) so
future reviewers know it’s pending; locate the empty closure on the
CherrishButton usage and replace it with the action implementation or the TODO
comment as appropriate.

Comment on lines 15 to 23
VStack(alignment: .leading) {

HStack {
Image(isSelected ? .radiobtnSelected : .radiobtnDefault)
.padding(.leading, 14.adjustedW)
.padding(.vertical, 14.adjustedH)
TypographyText(missionText, style: .body1_r_14, color: isSelected ? .gray800 : .gray700)
Spacer()
Image(isSelected ? "radiobtn_selected" : "radiobtn_default")
}

Spacer()

Text(missionText)
.typography(.body1_m_14)
.foregroundStyle(isSelected ? .gray800 : .gray700)
.padding(.leading, 8.adjustedW)
.padding(.bottom, 6.adjustedH)
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

불필요한 VStack 래퍼 고려

VStack(alignment: .leading)이 단일 HStack만 감싸고 있습니다. 레이아웃 단순화를 위해 VStack 제거를 고려해볼 수 있습니다. 다만, 향후 확장성을 위해 의도적으로 유지한 것이라면 현재 상태도 괜찮습니다.

🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift`
around lines 15 - 23, The outer VStack(alignment: .leading) in MissionCard.swift
is redundant because it only wraps a single HStack; remove the VStack wrapper
and its corresponding closing brace, leaving the HStack as the top-level view
(or apply alignment/padding directly on the HStack if needed) so Image,
TypographyText and Spacer remain unchanged; if the VStack was intentionally kept
for future expansion, add a brief comment above it indicating that intent
instead of leaving an unused wrapper.

Comment on lines +10 to +21
extension View {
func highlight(
highlightText: String,
highlightColor: Color = .red700,
normalText: String
) -> some View {
HStack {
TypographyText(highlightText, style: .title1_sb_18, color: highlightColor)
TypographyText(normalText, style: .title1_sb_18, color: .gray800)
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

View extension이 self를 사용하지 않습니다.

이 함수는 View extension의 메서드로 정의되어 있지만, self를 전혀 사용하지 않고 완전히 새로운 HStack을 반환합니다. 이는 extension 메서드의 의미론적 목적에 맞지 않습니다.

someView.highlight(...)를 호출하면 someView는 무시되고 새로운 뷰가 반환됩니다.

♻️ 독립적인 View struct로 변경 제안
-extension View {
-    func highlight(
-        highlightText: String,
-        highlightColor: Color = .red700,
-        normalText: String
-    ) -> some View {
-        HStack {
-            TypographyText(highlightText, style: .title1_sb_18, color: highlightColor)
-            TypographyText(normalText, style: .title1_sb_18, color: .gray800)
-        }
-    }
-}
+struct HighlightText: View {
+    let highlightText: String
+    let highlightColor: Color
+    let normalText: String
+    
+    init(
+        highlightText: String,
+        highlightColor: Color = .red700,
+        normalText: String
+    ) {
+        self.highlightText = highlightText
+        self.highlightColor = highlightColor
+        self.normalText = normalText
+    }
+    
+    var body: some View {
+        HStack {
+            TypographyText(highlightText, style: .title1_sb_18, color: highlightColor)
+            TypographyText(normalText, style: .title1_sb_18, color: .gray800)
+        }
+    }
+}

사용 예시:

// Before: Text("").highlight(highlightText: "강조", normalText: "일반")
// After: HighlightText(highlightText: "강조", normalText: "일반")
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View`+.swift around
lines 10 - 21, The extension method highlight(_:highlightColor:normalText:) on
View ignores self and returns a brand new HStack; replace it with a standalone
SwiftUI View struct (e.g., struct HighlightText: View) that has stored
properties highlightText: String, normalText: String, highlightColor: Color =
.red700 and implements var body -> some View returning the HStack with
TypographyText(...) so callers use HighlightText(highlightText:...,
normalText:...) instead of someView.highlight(...); remove or deprecate the View
extension to avoid misleading semantics and update call sites accordingly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`:
- Around line 10-52: CherryLevel uses rawValue in cherryImage but levelNumber in
progressImage; make them consistent by using the same identifier across both
computed properties (either rawValue or levelNumber). Update either cherryImage
or progressImage to reference the other symbol (prefer using the explicit
levelNumber computed property for clarity) so both Image initializers use the
same value source; adjust Image("cherry\(...)") and
Image("challenge_gaugebar_\(...)") accordingly in the CherryLevel enum's
cherryImage and progressImage.
- Line 97: Replace the hardcoded image name construction with the enum's
existing computed property: where Image("cherry\\(cherryLevel.levelNumber)") is
used, use the CherryLevel.cherryImage property (i.e.,
Image(cherryLevel.cherryImage)) so the view relies on the CherryLevel enum's
cherryImage computed property for consistency and maintainability.
- Around line 56-61: The hardcoded view state (progressRate, remainMissions,
isChecked) must be moved out of the view and supplied by a ViewModel or
initializer: create a ChallengeProgressViewModel (ObservableObject) exposing
`@Published` properties progressRate: Double, remainMissions: Int, isChecked: Bool
(or expose Bindings if parent needs two‑way control), inject it into
ChallengeProgressView as `@ObservedObject` var viewModel (or add init parameters
for let progressRate: Double, let remainMissions: Int and Binding<Bool>
isChecked) and update the computed cherryLevel to call
CherryLevel.from(progressRate: viewModel.progressRate) (or from the init
property). Ensure all usages of progressRate, remainMissions, and isChecked
within ChallengeProgressView reference the viewModel properties or initializer
parameters so the UI reacts to external data changes.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift`:
- Line 39: Remove the redundant explicit nil initializer for the optional
published property: in SelectRoutineViewModel change the declaration of
selectedRoutine (type RoutineType?) to omit "= nil" so it reads just "@Published
var selectedRoutine: RoutineType?" to satisfy SwiftLint's
redundant_optional_initialization rule.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift`:
- Around line 42-52: The nested Task wrapper in moveNextAfterDelay prevents
navigationTask from being set immediately, breaking cancellation; remove the
outer Task and assign navigationTask directly (navigationTask = Task { ... }) so
cancellation from onDisappear (navigationTask?.cancel()) takes effect, keep the
sleep, guard !Task.isCancelled check, and await MainActor.run {
challengeCoordinator.push(.selectMission) } inside that single Task.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift`:
- Around line 82-84: Preview crashes because SelectRoutineView expects
EnvironmentObjects challengeCoordinator and tabBarCoordinator that aren’t
provided; update the Preview provider (the `#Preview` / Preview struct for
SelectRoutineView) to inject mock or default instances via
.environmentObject(...) for both challengeCoordinator and tabBarCoordinator so
the view can render in Xcode previews (reference SelectRoutineView,
challengeCoordinator, tabBarCoordinator, and the `#Preview` block).
♻️ Duplicate comments (5)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift (3)

63-72: selectedStates 배열이 missions와 동기화되지 않았습니다.

이전 리뷰에서 지적된 것처럼, selectedStates의 크기가 6으로 하드코딩되어 있어 missions 배열 변경 시 인덱스 범위 초과 오류가 발생할 수 있습니다.


109-109: Double 타입의 진행률이 소수점으로 표시될 수 있습니다.

이전 리뷰에서 지적된 것처럼, progressRateDouble 타입이므로 소수점이 표시될 수 있어 정수 포맷팅이 필요합니다.


145-147: 버튼 액션이 비어있습니다.

이전 리뷰에서 지적된 것처럼, "오늘 미션 종료하기" 버튼의 액션이 구현되지 않았습니다. TODO 주석을 추가하거나 실제 동작을 구현해주세요.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift (2)

14-27: selectedStates 크기를 missions와 동기화하세요.
Line 14-27에서 selectedStates가 6으로 하드코딩되어 있고, Line 62-67에서 인덱스 바인딩을 사용해 미션 수가 바뀌면 인덱스 범위 초과로 크래시가 날 수 있습니다. missions.count에 맞춰 동기화하세요.

🔧 동기화 예시
-@State private var selectedStates: [Bool] = Array(repeating: false, count: 6)
+@State private var selectedStates: [Bool] = []
...
-            .padding(.horizontal, 34.adjustedW)
+            .padding(.horizontal, 34.adjustedW)
+            .onAppear {
+                if selectedStates.count != missions.count {
+                    selectedStates = Array(repeating: false, count: missions.count)
+                }
+            }

Also applies to: 62-67


71-73: Spacer()에 대한 .padding(.horizontal)은 효과가 없습니다.
Line 71-73의 가로 패딩은 레이아웃에 영향을 주지 않으므로 제거하세요.

♻️ 정리 제안
 Spacer()
     .frame(height: 48.adjustedH)
-    .padding(.horizontal, 33.adjustedW)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between feb6691 and 18e3122.

📒 Files selected for processing (6)
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/CherrishNavigationBar.swift
🧰 Additional context used
🧬 Code graph analysis (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (1)
  • body (9-17)
Cherrish-iOS/Cherrish-iOS/Presentation/Coordinator/AppCoordinatorProtocol.swift (2)
  • pop (22-24)
  • popToRoot (26-28)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
  • gray1000 (49-51)
  • gray700 (37-39)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+.swift (1)
  • highlight (11-20)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
  • gray800 (41-43)
  • gray600 (33-35)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift (1)
  • push (24-26)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Coordinator/AppCoordinatorProtocol.swift (2)
  • pop (22-24)
  • popToRoot (26-28)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift (1)
  • select (45-47)
🪛 SwiftLint (0.57.0)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift

[Warning] 39-39: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (8)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/CherrishNavigationBar.swift (1)

59-60: LGTM!

네비게이션 바에 수평 패딩을 적용하고, adjustedW(width 기반 조정값)를 사용한 것이 적절합니다. 코드베이스 전반의 패딩 컨벤션과 일치합니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift (1)

14-40: UI 레이아웃 및 라이프사이클 처리가 적절합니다.

highlight() View extension 활용, onAppear/onDisappear 라이프사이클 핸들러 연결, 그리고 전체적인 레이아웃 구조가 잘 구성되어 있습니다. moveNextAfterDelay() 함수의 중첩 Task 문제만 수정되면 의도한 대로 동작할 것입니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift (1)

11-31: RoutineType 정의 및 타이틀 매핑 좋습니다.

케이스 구성과 타이틀 매핑이 UI 요구사항을 충족하며, 식별자도 명확합니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift (4)

12-20: 뷰모델 기반 상태 분리와 그리드 구성 깔끔합니다.

상태 관리가 단순해지고 UI 바인딩이 명확해졌습니다.


27-34: 탭바 가시성 복구 처리 좋습니다.

뒤로가기/루트 이동 시 탭바를 복구하는 흐름이 일관적입니다.


49-78: 루틴 그리드 및 선택 바인딩 구현 좋습니다.

선택 상태가 뷰모델에 일관되게 연결되어 유지보수성이 높습니다.


60-64: 버튼 상태 바인딩 적절합니다.

선택 유무에 따른 상태 반영이 명확합니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift (1)

75-77: 버튼 상태 계산과 네비게이션 흐름이 명확합니다.
Line 75-77의 버튼 액션과 상태 연결은 의도대로 잘 구성되어 있습니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 10 to 52
enum CherryLevel: Int {
case mong = 0
case bbo
case chok
case pang
case ggu

var levelNumber: Int { rawValue }

static func from(progressRate: Double) -> CherryLevel {
switch progressRate {
case 0.0..<25.0:
return .mong
case 25.0..<50.0:
return .bbo
case 50.0..<75.0:
return .chok
case 75.0..<100.0:
return .pang
case 100.0...:
return .ggu
default:
return .mong
}
}

var name: String {
switch self {
case .mong: return "몽롱체리"
case .bbo: return "뽀득체리"
case .chok: return "촉촉체리"
case .pang: return "팡팡체리"
case .ggu: return "꾸꾸체리"
}
}

var cherryImage: Image {
Image("cherry\(rawValue)")
}
var progressImage: Image {
Image("challenge_gaugebar_\(levelNumber)")
}
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

CherryLevel enum 구현이 잘 되어 있습니다.

진행률 기반 레벨 매핑 로직이 명확하고, 연관된 이미지와 이름을 computed property로 제공하는 구조가 좋습니다.

다만 작은 일관성 개선 사항이 있습니다:

  • Line 47: cherryImagerawValue를 사용
  • Line 50: progressImagelevelNumber를 사용

둘 다 동일한 값이지만, 일관성을 위해 통일하는 것이 좋습니다.

♻️ 일관성 개선 제안
     var cherryImage: Image {
-        Image("cherry\(rawValue)")
+        Image("cherry\(levelNumber)")
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enum CherryLevel: Int {
case mong = 0
case bbo
case chok
case pang
case ggu
var levelNumber: Int { rawValue }
static func from(progressRate: Double) -> CherryLevel {
switch progressRate {
case 0.0..<25.0:
return .mong
case 25.0..<50.0:
return .bbo
case 50.0..<75.0:
return .chok
case 75.0..<100.0:
return .pang
case 100.0...:
return .ggu
default:
return .mong
}
}
var name: String {
switch self {
case .mong: return "몽롱체리"
case .bbo: return "뽀득체리"
case .chok: return "촉촉체리"
case .pang: return "팡팡체리"
case .ggu: return "꾸꾸체리"
}
}
var cherryImage: Image {
Image("cherry\(rawValue)")
}
var progressImage: Image {
Image("challenge_gaugebar_\(levelNumber)")
}
}
enum CherryLevel: Int {
case mong = 0
case bbo
case chok
case pang
case ggu
var levelNumber: Int { rawValue }
static func from(progressRate: Double) -> CherryLevel {
switch progressRate {
case 0.0..<25.0:
return .mong
case 25.0..<50.0:
return .bbo
case 50.0..<75.0:
return .chok
case 75.0..<100.0:
return .pang
case 100.0...:
return .ggu
default:
return .mong
}
}
var name: String {
switch self {
case .mong: return "몽롱체리"
case .bbo: return "뽀득체리"
case .chok: return "촉촉체리"
case .pang: return "팡팡체리"
case .ggu: return "꾸꾸체리"
}
}
var cherryImage: Image {
Image("cherry\(levelNumber)")
}
var progressImage: Image {
Image("challenge_gaugebar_\(levelNumber)")
}
}
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`
around lines 10 - 52, CherryLevel uses rawValue in cherryImage but levelNumber
in progressImage; make them consistent by using the same identifier across both
computed properties (either rawValue or levelNumber). Update either cherryImage
or progressImage to reference the other symbol (prefer using the explicit
levelNumber computed property for clarity) so both Image initializers use the
same value source; adjust Image("cherry\(...)") and
Image("challenge_gaugebar_\(...)") accordingly in the CherryLevel enum's
cherryImage and progressImage.

Comment on lines +56 to +61
@State private var isChecked = false
private var progressRate: Double = 100
private var cherryLevel: CherryLevel {
CherryLevel.from(progressRate: progressRate)
}
private var remainMissions: Int = 3
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

하드코딩된 상태 값들을 ViewModel 또는 파라미터로 전환이 필요합니다.

progressRate, remainMissions 등이 현재 private var로 하드코딩되어 있어 동적으로 변경할 수 없습니다. 실제 서버 데이터 연동 시 ViewModel에서 주입받거나 View 초기화 파라미터로 받도록 변경이 필요합니다.

♻️ 권장 구조 예시
 struct ChallengeProgressView: View {
-    
-    `@State` private var isChecked = false
-    private var progressRate: Double = 100
-    private var cherryLevel: CherryLevel {
-        CherryLevel.from(progressRate: progressRate)
-    }
-    private var remainMissions: Int = 3
+    `@ObservedObject` var viewModel: ChallengeProgressViewModel
+    
+    private var cherryLevel: CherryLevel {
+        CherryLevel.from(progressRate: viewModel.progressRate)
+    }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`
around lines 56 - 61, The hardcoded view state (progressRate, remainMissions,
isChecked) must be moved out of the view and supplied by a ViewModel or
initializer: create a ChallengeProgressViewModel (ObservableObject) exposing
`@Published` properties progressRate: Double, remainMissions: Int, isChecked: Bool
(or expose Bindings if parent needs two‑way control), inject it into
ChallengeProgressView as `@ObservedObject` var viewModel (or add init parameters
for let progressRate: Double, let remainMissions: Int and Binding<Bool>
isChecked) and update the computed cherryLevel to call
CherryLevel.from(progressRate: viewModel.progressRate) (or from the init
property). Ensure all usages of progressRate, remainMissions, and isChecked
within ChallengeProgressView reference the viewModel properties or initializer
parameters so the UI reacts to external data changes.

// @Published var routines: [RoutineType] = []
@Published var routines: [RoutineType] = RoutineType.allCases

@Published var selectedRoutine: RoutineType? = nil
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

불필요한 optional nil 초기화 제거 권장.

SwiftLint 경고(redundant_optional_initialization)로 보고되고, 의미상 불필요합니다.

🧹 제안 수정
-    `@Published` var selectedRoutine: RoutineType? = nil
+    `@Published` var selectedRoutine: RoutineType?
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 39-39: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift`
at line 39, Remove the redundant explicit nil initializer for the optional
published property: in SelectRoutineViewModel change the declaration of
selectedRoutine (type RoutineType?) to omit "= nil" so it reads just "@Published
var selectedRoutine: RoutineType?" to satisfy SwiftLint's
redundant_optional_initialization rule.

Comment on lines +42 to 52
private func moveNextAfterDelay() {
Task {
navigationTask = Task {
try? await Task.sleep(nanoseconds: 3_000_000_000)
guard !Task.isCancelled else { return }
await MainActor.run {
challengeCoordinator.push(.selectMission)
}
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

중첩된 Task 구조로 인해 취소 로직이 제대로 동작하지 않습니다.

navigationTask가 외부 Task 블록 내부에서 비동기적으로 할당되기 때문에, onDisappearonAppear 직후에 호출되면 navigationTask가 아직 nil인 상태일 수 있습니다. 이 경우 navigationTask?.cancel()이 아무런 효과가 없어 원치 않는 네비게이션이 발생할 수 있습니다.

외부 Task 래퍼를 제거하고 navigationTask를 직접 할당해야 합니다.

🐛 수정 제안
 private func moveNextAfterDelay() {
-    Task {
-        navigationTask = Task {
-            try? await Task.sleep(nanoseconds: 3_000_000_000)
-            guard !Task.isCancelled else { return }
-            await MainActor.run {
-                challengeCoordinator.push(.selectMission)
-            }
+    navigationTask = Task {
+        try? await Task.sleep(nanoseconds: 3_000_000_000)
+        guard !Task.isCancelled else { return }
+        await MainActor.run {
+            challengeCoordinator.push(.selectMission)
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func moveNextAfterDelay() {
Task {
navigationTask = Task {
try? await Task.sleep(nanoseconds: 3_000_000_000)
guard !Task.isCancelled else { return }
await MainActor.run {
challengeCoordinator.push(.selectMission)
}
}
}
}
private func moveNextAfterDelay() {
navigationTask = Task {
try? await Task.sleep(nanoseconds: 3_000_000_000)
guard !Task.isCancelled else { return }
await MainActor.run {
challengeCoordinator.push(.selectMission)
}
}
}
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift`
around lines 42 - 52, The nested Task wrapper in moveNextAfterDelay prevents
navigationTask from being set immediately, breaking cancellation; remove the
outer Task and assign navigationTask directly (navigationTask = Task { ... }) so
cancellation from onDisappear (navigationTask?.cancel()) takes effect, keep the
sleep, guard !Task.isCancelled check, and await MainActor.run {
challengeCoordinator.push(.selectMission) } inside that single Task.

Comment on lines 82 to 84
#Preview {
SelectRoutineView()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

프리뷰에서 EnvironmentObject 미주입으로 크래시 가능.

프리뷰 실행 시 challengeCoordinator/tabBarCoordinator가 없어서 실패합니다.

🧩 제안 수정
 `#Preview` {
-    SelectRoutineView()
+    SelectRoutineView()
+        .environmentObject(ChallengeCoordinator())
+        .environmentObject(TabBarCoordinator())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#Preview {
SelectRoutineView()
}
`#Preview` {
SelectRoutineView()
.environmentObject(ChallengeCoordinator())
.environmentObject(TabBarCoordinator())
}
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift`
around lines 82 - 84, Preview crashes because SelectRoutineView expects
EnvironmentObjects challengeCoordinator and tabBarCoordinator that aren’t
provided; update the Preview provider (the `#Preview` / Preview struct for
SelectRoutineView) to inject mock or default instances via
.environmentObject(...) for both challengeCoordinator and tabBarCoordinator so
the view can render in Xcode previews (reference SelectRoutineView,
challengeCoordinator, tabBarCoordinator, and the `#Preview` block).

Copy link
Contributor

@wotjs020708 wotjs020708 left a comment

Choose a reason for hiding this comment

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

고생하셨어요 어푸푸푸

Copy link
Contributor

@y-eonee y-eonee left a comment

Choose a reason for hiding this comment

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

고생하셨습니당 ㅎ ㅎ 리뷰 확인해주세요!!

struct ChallengeProgressView: View {

@State private var isChecked = false
private var progressRate: Double = 100
Copy link
Contributor

Choose a reason for hiding this comment

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

여기 나중에 서버에서 받는걸로 쓰는거죠?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네네 맞아요! 확인용으로 일단 100으로 해놨습니당

Comment on lines +63 to +72
@State private var missions: [String] = [
"진정 토너 + 세럼",
"진정 토너 + 세럼",
"선크림 3번 바르기",
"선크림 3번 바르기",
"선크림 3번 바르기",
"선크림 3번 바르기"
]

@State private var selectedStates: [Bool] = Array(repeating: false, count: 6)
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분들은 나중에 다 뷰모델로 옮겨야할것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵! 뷰모델 작성할 때 반영하겠습니당

Copy link
Contributor

Choose a reason for hiding this comment

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

뷰 구현 할때 var bod에 모두 다 쓰지 말고 extension으로 밑에 빼서 가독성을 올려보면 좋을 것 같아요

struct view: some View {
var body: some View {
 예시뷰1
예시 뷰2
}

extension  {
    private var 예시뷰: some view {
}

요런식으로요!!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

반영했습니당!

Comment on lines 43 to 54
VStack(alignment: .leading, spacing: 4.adjustedH) {
TypographyText("챌린지 기간 동안",
style: .title1_sb_18,
color: .gray1000
)
TypographyText("진행할 미션을 선택해주세요.",
style: .title1_sb_18,
color: .gray1000
)
TypographyText("복수 선택이 가능해요.",
style: .body1_r_14,
color: .gray700
Copy link
Contributor

Choose a reason for hiding this comment

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

피그마 확인해보니까 챌린지기간동안 미션 선택해주세요 이 부분은 spacing 4가 들어가지는 않는 것 같아요!
\n으로 개행해서 프레임값 주고, 복수선택이 가능해요 이것만 spacing 주면 될 것 같아용

Comment on lines 82 to 84
#Preview {
SelectRoutineView()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

이거 한번만 빼주세요!

Image(isSelected ? .radiobtnSelected : .radiobtnDefault)
.padding(.leading, 14.adjustedW)
.padding(.vertical, 14.adjustedH)
TypographyText(missionText, style: .body1_r_14, color: isSelected ? .gray800 : .gray700)
Copy link
Contributor

Choose a reason for hiding this comment

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

이미지랑 텍스트 사이에 패딩값 있어요!!

@Binding var isSelected: Bool

var body: some View {
VStack(alignment: .leading) {
Copy link
Contributor

Choose a reason for hiding this comment

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

요거는 확인해봤는데 빼도 될것 같아요!!

@y-eonee y-eonee changed the title Style/#61 challengemissionview & challengemissionprogressview Style/#61 챌린지 플로우 뷰 구현 Jan 16, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift`:
- Around line 79-81: The Spacer() call in SelectMissionView.swift has a trailing
.padding(.bottom, 72.adjustedH) which has no effect; remove the unnecessary
.padding from the Spacer() and either (a) apply the padding to the surrounding
container (e.g., the parent VStack) or (b) replace with a fixed-height spacer
using .frame(height:) if you intended a concrete gap; update the Spacer() usage
accordingly so layout behaves as expected.
♻️ Duplicate comments (5)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift (3)

63-72: selectedStates 배열이 missions와 동기화되지 않았습니다.

SelectMissionView에서는 onAppearonChange를 통해 selectedStatesmissions.count에 맞게 동기화하도록 수정되었지만, 이 뷰에서는 여전히 하드코딩된 count: 6을 사용하고 있습니다. 동일한 패턴을 적용해주세요.

♻️ SelectMissionView와 동일한 동기화 패턴 적용
 `@State` private var selectedStates: [Bool] = Array(repeating: false, count: 6)

body 끝에 추가:

.onAppear {
    selectedStates = Array(repeating: false, count: missions.count)
}
.onChange(of: missions) { newMissions in
    selectedStates = Array(repeating: false, count: newMissions.count)
}

109-109: 진행률 표시 형식 개선이 필요합니다.

progressRateDouble 타입이므로 "75.5%"처럼 소수점이 표시될 수 있습니다.

-TypographyText("챌린지 달성률 \(progressRate)%", style: .body1_m_14, color: .gray900)
+TypographyText("챌린지 달성률 \(Int(progressRate))%", style: .body1_m_14, color: .gray900)

145-147: 버튼 액션이 비어있습니다.

"오늘 미션 종료하기" 버튼의 액션이 구현되지 않았습니다. 추후 구현이 필요하다면 TODO 주석을 추가해주세요.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift (1)

43-52: 중첩된 Task 구조가 여전히 존재합니다.

이전 리뷰에서 지적된 중첩 Task 문제가 해결되지 않았습니다. navigationTask가 외부 Task 블록 내부에서 비동기적으로 할당되므로, onDisappearonAppear 직후에 호출되면 navigationTask가 아직 nil인 상태일 수 있습니다. 이 경우 취소 로직이 동작하지 않아 원치 않는 네비게이션이 발생합니다.

🐛 외부 Task 래퍼 제거 제안
 private func moveNextAfterDelay() {
-    Task {
-        navigationTask = Task {
-            try? await Task.sleep(nanoseconds: 3_000_000_000)
-            guard !Task.isCancelled else { return }
-            await MainActor.run {
-                challengeCoordinator.push(.selectMission)
-            }
+    navigationTask = Task {
+        try? await Task.sleep(nanoseconds: 3_000_000_000)
+        guard !Task.isCancelled else { return }
+        await MainActor.run {
+            challengeCoordinator.push(.selectMission)
         }
     }
 }
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift (1)

71-73: Spacer()에 적용된 .padding()은 효과가 없습니다.

Spacer()는 빈 공간을 채우는 역할만 하므로 .padding(.horizontal)을 적용해도 레이아웃에 영향을 주지 않습니다.

♻️ 불필요한 padding 제거
 Spacer()
     .frame(height: 48.adjustedH)
-.padding(.horizontal, 33.adjustedW)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18e3122 and 1c52d38.

⛔ Files ignored due to path filters (1)
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/illustration_challenge_start.imageset/illustration_challenge_start.svg is excluded by !**/*.svg
📒 Files selected for processing (7)
  • Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/illustration_challenge_start.imageset/Contents.json
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift
  • Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift
🧰 Additional context used
🧬 Code graph analysis (6)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (1)
  • body (9-17)
Cherrish-iOS/Cherrish-iOS/Presentation/Coordinator/AppCoordinatorProtocol.swift (2)
  • pop (22-24)
  • popToRoot (26-28)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
  • gray1000 (49-51)
  • gray700 (37-39)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift (4)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (1)
  • body (9-17)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+.swift (1)
  • highlight (11-20)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (2)
  • gray800 (41-43)
  • gray600 (33-35)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift (1)
  • push (24-26)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift (1)
  • push (24-26)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift (1)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (4)
  • gray800 (41-43)
  • gray700 (37-39)
  • red100 (65-67)
  • gray0 (9-11)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (2)
  • body (9-17)
  • cherrishShadow (21-23)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Color.swift (7)
  • gray1000 (49-51)
  • gray700 (37-39)
  • gray900 (45-47)
  • gray800 (41-43)
  • gray300 (21-23)
  • red200 (69-71)
  • gray0 (9-11)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift (4)
Cherrish-iOS/Cherrish-iOS/Presentation/Global/Extension/View+Shadow.swift (1)
  • body (9-17)
Cherrish-iOS/Cherrish-iOS/Presentation/Coordinator/AppCoordinatorProtocol.swift (2)
  • pop (22-24)
  • popToRoot (26-28)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinator.swift (1)
  • push (24-26)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ViewModel/SelectRoutineViewModel.swift (1)
  • select (45-47)
🔇 Additional comments (15)
Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/illustration_challenge_start.imageset/Contents.json (1)

1-12: LGTM!

표준 Xcode 에셋 카탈로그 JSON 구조입니다. SVG 이미지 참조가 올바르게 설정되어 있습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Global/Components/MissionCard.swift (1)

14-33: LGTM!

MissionCard 컴포넌트가 잘 구현되었습니다. 이미지와 텍스트 사이의 패딩(Line 21)이 추가되어 이전 리뷰 피드백이 반영되었습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/LoadingView.swift (1)

14-41: UI 레이아웃 구현이 잘 되어 있습니다.

로딩 화면의 레이아웃과 컴포넌트 배치가 적절합니다. onDisappear에서 Task 취소 처리를 추가한 점이 좋습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift (2)

10-52: CherryLevel enum 구현이 적절합니다.

진행률 기반 레벨 매핑 로직이 명확하고, PR 목표에 명시된 범위(0-25-50-75-100)가 정확하게 구현되었습니다.


76-163: UI 레이아웃 구현이 잘 되어 있습니다.

ScrollView 기반의 레이아웃 구조와 프로젝트의 디자인 시스템 컴포넌트(cherrishShadow(), TypographyText, color extensions) 활용이 적절합니다. cherryLevel.cherryImage를 사용하여 이전 리뷰 피드백이 반영되었습니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift (3)

84-89: selectedStates 동기화 로직이 잘 추가되었습니다.

onAppearonChange를 통해 missions 배열 크기 변경 시 selectedStates를 자동으로 동기화하도록 수정되어 이전 리뷰 피드백이 반영되었습니다.


28-78: 전체적인 뷰 구현이 잘 되어 있습니다.

미션 선택 화면의 레이아웃, ForEach를 통한 MissionCard 바인딩, nextButtonState 계산 로직이 적절하게 구현되었습니다. Coordinator 패턴을 통한 네비게이션 처리도 프로젝트 패턴과 일관성 있습니다.


87-89: onChange API는 현재 배포 타겟에 적합합니다.

프로젝트의 최소 배포 타겟이 iOS 16.6이므로 현재의 onChange(of:) 클로저 시그니처는 올바릅니다. 이 구문은 iOS 16.6에서 표준이며 작동합니다. iOS 17 이상으로 최소 배포 타겟을 올린다면 새로운 API({ oldValue, newValue in } 시그니처)로 마이그레이션할 것을 고려하세요.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/StartChallengeView.swift (2)

11-13: 탭바 연동 추가 반영 좋습니다.
시작 액션에서 탭바 숨김까지 처리해 흐름이 명확합니다.

Also applies to: 41-42


27-31: 헤더 레이아웃/일러스트 교체 반영 OK.
패딩과 일러스트 변경이 의도대로 반영된 것으로 보입니다.

Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectRoutineView.swift (5)

13-20: 뷰모델 도입과 그리드 컬럼 구성이 깔끔합니다.
상태 분리와 레이아웃 정의가 명확해졌어요.


27-34: 탭바 복구 경로 확인 부탁드립니다.
현재 탭바 표시 복구가 버튼 액션에만 묶여 있으니, 스와이프 뒤로가기/프로그램matic pop 등 다른 경로가 없는지 확인해주세요.


49-55: LazyVGrid 기반 루틴 렌더링 LGTM.
반복 렌더링과 간격 설정이 자연스럽습니다.


60-66: 다음 버튼 상태를 뷰모델에서 가져오는 방식 좋습니다.
안정적으로 상태 흐름이 정리된 점이 좋아요.


71-79: 선택 칩 바인딩 로직 명확합니다.
선택 상태와 뷰모델 연동이 간단하고 읽기 쉬워졌습니다.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift`:
- Around line 86-88: The onChange handler on missions is redundant because
missions is a static array and selectedStates is already initialized in
onAppear; remove the .onChange(of: missions) { ... } block to eliminate
dead/duplicate code, or if you plan to update missions dynamically later, keep
it but update the closure to the iOS17 signature (use { oldValue, newValue in }
onChange) and ensure it resets selectedStates = Array(repeating: false, count:
newValue.count); refer to the onChange(of: missions), missions, selectedStates,
and onAppear symbols when applying the change.
- Around line 62-67: The ForEach(missions.indices...) + selectedStates[index]
access is unsafe if selectedStates is empty; either ensure selectedStates is
created with the same count as missions before the view renders (e.g., populate
selectedStates in onAppear or init so selectedStates.count == missions.count) or
refactor to pair mission text and selection into a single model (e.g., struct
Mission { id, text, isSelected } and use ForEach(missions) with
MissionCard(missionText: mission.text, isSelected:
$missions[index].isSelected)). Alternatively, use safe access inside the ForEach
(e.g., iterate Array(missions.enumerated()) and bind isSelected via a Binding
that checks selectedStates.indices.contains(index) before get/set) so you never
index out of range when rendering MissionCard.
- Around line 75-77: 선택된 미션 데이터(selectedStates in SelectMissionView)가 다음 화면으로
전달되지 않으니, SelectMissionView에서 challengeCoordinator.push(.challengeProgress) 호출 시
선택값을 전달하도록 수정하세요: 방법 A) ChallengeRoute.enum의 challengeProgress 케이스에 연관값(예:
[SelectedState] 또는 [Mission])을 추가하고 push 메서드 시그니처를 해당 연관값을 받도록 변경한 뒤
SelectMissionView의 CherrishButton 클릭에서
challengeCoordinator.push(.challengeProgress(selectedStates))로 호출하도록 업데이트하세요; 또는
방법 B) ChallengeCoordinator 또는 공유 ViewModel(singleton 혹은 `@EnvironmentObject`)으로
selectedStates를 저장하는 프로퍼티를 추가하고 SelectMissionView에서 그 프로퍼티를 설정한 다음 기존
challengeCoordinator.push(.challengeProgress)로 화면을 전환해 다음 화면에서
coordinator/ViewModel의 해당 프로퍼티를 읽어 사용하도록 구현하세요.
♻️ Duplicate comments (2)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift (2)

71-73: Spacer()에 적용된 .padding()은 효과가 없습니다.

이전 리뷰에서 지적된 내용입니다. Spacer().padding(.horizontal, 33.adjustedW)를 적용해도 레이아웃에 영향을 주지 않습니다.

♻️ 불필요한 padding 제거
 Spacer()
     .frame(height: 48.adjustedH)
-.padding(.horizontal, 33.adjustedW)

22-22: selectedStates 초기화 방식 개선을 권장합니다.

onAppear에서 재초기화하는 방식으로 해결했지만, 초기값을 빈 배열 []로 설정하면 더 안전합니다. 현재 하드코딩된 count: 6missions 배열과 불일치할 가능성이 있습니다.

♻️ 빈 배열로 초기화 제안
-@State private var selectedStates: [Bool] = Array(repeating: false, count: 6)
+@State private var selectedStates: [Bool] = []
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c52d38 and 97fe3e3.

📒 Files selected for processing (1)
  • Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +62 to +67
ForEach(missions.indices, id: \.self) { index in
MissionCard(
missionText: missions[index],
isSelected: $selectedStates[index]
)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

배열 인덱스 접근 시 안전성을 고려해주세요.

selectedStates를 빈 배열로 초기화하는 경우, onAppear 실행 전에 ForEach가 렌더링되면 인덱스 범위 초과로 크래시가 발생할 수 있습니다.

이를 방지하려면 ForEach 내에서 인덱스 유효성을 검사하거나, 미션과 선택 상태를 하나의 구조체로 통합하는 것이 좋습니다.

♻️ 안전한 접근 방식 제안
 VStack(spacing: 10.adjustedH) {
-    ForEach(missions.indices, id: \.self) { index in
-        MissionCard(
-            missionText: missions[index],
-            isSelected: $selectedStates[index]
-        )
+    ForEach(Array(zip(missions.indices, missions)), id: \.0) { index, mission in
+        if index < selectedStates.count {
+            MissionCard(
+                missionText: mission,
+                isSelected: $selectedStates[index]
+            )
+        }
     }
 }

또는 더 나은 접근법:

struct Mission: Identifiable {
    let id = UUID()
    let text: String
    var isSelected: Bool = false
}

`@State` private var missions: [Mission] = [
    Mission(text: "진정 토너 + 세럼"),
    // ...
]
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift`
around lines 62 - 67, The ForEach(missions.indices...) + selectedStates[index]
access is unsafe if selectedStates is empty; either ensure selectedStates is
created with the same count as missions before the view renders (e.g., populate
selectedStates in onAppear or init so selectedStates.count == missions.count) or
refactor to pair mission text and selection into a single model (e.g., struct
Mission { id, text, isSelected } and use ForEach(missions) with
MissionCard(missionText: mission.text, isSelected:
$missions[index].isSelected)). Alternatively, use safe access inside the ForEach
(e.g., iterate Array(missions.enumerated()) and bind isSelected via a Binding
that checks selectedStates.indices.contains(index) before get/set) so you never
index out of range when rendering MissionCard.

Comment on lines +75 to 77
CherrishButton(title: "플래너에 추가하기", type: .next, state: .constant(nextButtonState)){
challengeCoordinator.push(.challengeProgress)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# ChallengeCoordinator 열거형과 push 메서드 확인
find . -type f -name "*.swift" | xargs grep -l "ChallengeCoordinator" | head -5

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 532


🏁 Script executed:

# SelectMissionView 파일에서 선택된 미션과 상태 확인
cat -n Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift | head -100

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 3740


🏁 Script executed:

# ChallengeCoordinator 정의 찾기
rg -n "enum ChallengeCoordinator|struct ChallengeCoordinator|class ChallengeCoordinator" -A 20 --type swift

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 4606


🏁 Script executed:

# ChallengeRoute 열거형 정의 찾기
rg -n "enum ChallengeRoute" -A 20 --type swift

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 2344


🏁 Script executed:

# ChallengeCoordinatorView에서 challengeProgress 케이스 처리 확인
cat -n Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/Coordinator/ChallengeCoordinatorView.swift | head -80

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 2439


🏁 Script executed:

# SelectMissionView에서 ViewModel 관련 부분 확인
rg -n "@StateObject|@ObservedObject|ViewModel" Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift --type swift

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 52


선택된 미션 데이터가 전달되지 않고 있습니다.

현재 SelectMissionView에서 선택한 미션 정보(selectedStates)가 로컬 @State 변수로만 관리되고 있으며, challengeCoordinator.push(.challengeProgress)를 통해 다음 화면으로 전달되지 않습니다. ChallengeRoutechallengeProgress 케이스에는 연관값이 없고, push 메서드도 단순히 경로만 전달합니다.

선택된 미션 목록을 challengeProgress 화면에서 사용해야 한다면:

  • ChallengeRoute.challengeProgress에 선택된 미션을 연관값으로 추가하거나
  • ChallengeCoordinator 또는 공유 ViewModel에 선택 상태를 저장한 후 다음 화면에서 접근하도록 수정해주세요.
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift`
around lines 75 - 77, 선택된 미션 데이터(selectedStates in SelectMissionView)가 다음 화면으로
전달되지 않으니, SelectMissionView에서 challengeCoordinator.push(.challengeProgress) 호출 시
선택값을 전달하도록 수정하세요: 방법 A) ChallengeRoute.enum의 challengeProgress 케이스에 연관값(예:
[SelectedState] 또는 [Mission])을 추가하고 push 메서드 시그니처를 해당 연관값을 받도록 변경한 뒤
SelectMissionView의 CherrishButton 클릭에서
challengeCoordinator.push(.challengeProgress(selectedStates))로 호출하도록 업데이트하세요; 또는
방법 B) ChallengeCoordinator 또는 공유 ViewModel(singleton 혹은 `@EnvironmentObject`)으로
selectedStates를 저장하는 프로퍼티를 추가하고 SelectMissionView에서 그 프로퍼티를 설정한 다음 기존
challengeCoordinator.push(.challengeProgress)로 화면을 전환해 다음 화면에서
coordinator/ViewModel의 해당 프로퍼티를 읽어 사용하도록 구현하세요.

Comment on lines +86 to 88
.onChange(of: missions) { newMissions in
selectedStates = Array(repeating: false, count: newMissions.count)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check iOS deployment target from project files
find . -name "*.pbxproj" -o -name "project.yml" -o -name "*.xcconfig" | head -10

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 123


🏁 Script executed:

# Check the actual iOS deployment target
fd -type f -name "*.pbxproj" | head -1 | xargs cat | grep -i "IPHONEOS_DEPLOYMENT_TARGET" | head -10

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 240


🏁 Script executed:

# Examine the SelectMissionView.swift file and lines around 86-88
fd -type f -name "SelectMissionView.swift" | xargs cat

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 240


🏁 Script executed:

# Extract iOS deployment target from pbxproj
cat ./Cherrish-iOS/Cherrish-iOS.xcodeproj/project.pbxproj | grep -i "IPHONEOS_DEPLOYMENT_TARGET" | head -5

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 225


🏁 Script executed:

# Find and read SelectMissionView.swift
find . -name "SelectMissionView.swift" -exec cat {} \;

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 3103


onChange 핸들러는 현재 실행되지 않는 불필요한 코드입니다.

missions는 정적인 배열로 초기화되어 이후 수정되지 않으므로, onChange 블록이 실행될 수 없습니다. 추가로 onAppear에서 이미 동일한 selectedStates 초기화를 수행하고 있어 코드가 중복됩니다.

향후 서버에서 미션 데이터를 받아와 missions를 동적으로 업데이트할 계획이 있다면 유지하고, 아니라면 제거를 권장합니다. 만약 유지한다면, iOS 17 이상 타겟 시 새로운 API 문법({ oldValue, newValue in })으로 업데이트해야 합니다.

🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/SelectMissionView.swift`
around lines 86 - 88, The onChange handler on missions is redundant because
missions is a static array and selectedStates is already initialized in
onAppear; remove the .onChange(of: missions) { ... } block to eliminate
dead/duplicate code, or if you plan to update missions dynamically later, keep
it but update the closure to the iOS17 signature (use { oldValue, newValue in }
onChange) and ensure it resets selectedStates = Array(repeating: false, count:
newValue.count); refer to the onChange(of: missions), missions, selectedStates,
and onAppear symbols when applying the change.

Copy link
Contributor

@soseoyo12 soseoyo12 left a comment

Choose a reason for hiding this comment

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

수고하셨습니다~~

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry2.imageset/Contents.json`:
- Around line 1-12: The Contents.json in cherry2.imageset references cherry1.svg
which mismatches the folder name; rename the referenced asset to match the set
(either rename the actual file cherry1.svg → cherry2.svg and update the
"filename" field in cherry2.imageset/Contents.json to "cherry2.svg"), or if the
folder should be cherry1.imageset instead, rename the folder to cherry1.imageset
and ensure its Contents.json still points to "cherry1.svg"; also scan other sets
(e.g., cherry1.imageset that references cherry0.svg) for similar mismatches and
apply the same consistent renaming or document the intended naming convention.

In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`:
- Around line 10-39: The CherryLevel enum currently omits the chok case and
mis-maps progress ranges; update the enum to include case chok with appropriate
raw ordering (e.g., mong=1, bbo=2, chok=3, pang=4, ggu=5), then change
CherryLevel.from(progressRate:) to map 0.0..<25.0 -> .mong, 25.0..<50.0 -> .bbo,
50.0..<75.0 -> .chok, 75.0..<100.0 -> .pang, and progressRate >= 100.0 -> .ggu
(ensure >=100 goes to .ggu), and add the display string for .chok in the name
computed property; keep a sensible default (e.g., .mong) for unexpected values.
♻️ Duplicate comments (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift (3)

59-68: missionsselectedStates 동기화 이슈 재확인 필요합니다.
이미 이전 커밋에서 지적된 항목과 동일합니다. 배열 길이 불일치 시 인덱스 오류가 납니다.


117-117: 달성률 표시가 소수로 노출될 수 있습니다.
이전 리뷰에서 언급된 사항과 동일합니다. 정수 포맷팅을 적용해 주세요.


158-160: 버튼 액션이 비어 있습니다.
이전 리뷰에서 지적된 항목과 동일합니다. 구현 또는 TODO 주석이 필요합니다.

Comment on lines +1 to +12
{
"images" : [
{
"filename" : "cherry1.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check actual SVG files in cherry2.imageset folder
echo "=== Files in cherry2.imageset ==="
ls -la Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry2.imageset/

echo ""
echo "=== SVG files in cherry2.imageset ==="
find Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry2.imageset -type f -name "*.svg"

echo ""
echo "=== All SVG files in ChallengeImage folder ==="
find Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage -type f -name "*.svg"

echo ""
echo "=== cherry1.imageset structure for comparison ==="
ls -la Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry1.imageset/

Repository: TEAM-Cherrish/Cherrish-iOS

Length of output: 2147


cherry2.imageset 폴더명과 참조 파일명 불일치

cherry2.imageset 폴더에서 cherry1.svg 파일을 참조하고 있습니다. 폴더명과 파일명이 일치하지 않으며, cherry1.imageset에서도 동일한 패턴(cherry1 → cherry0.svg)이 있습니다.

일관성을 위해 다음 중 하나를 선택해 주세요:

  • cherry1.svg 파일을 cherry2.svg로 이름 변경하고 Contents.json 업데이트
  • 또는 의도된 네이밍 구조라면 문서화
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Assets.xcassets/ChallengeImage/cherry2.imageset/Contents.json`
around lines 1 - 12, The Contents.json in cherry2.imageset references
cherry1.svg which mismatches the folder name; rename the referenced asset to
match the set (either rename the actual file cherry1.svg → cherry2.svg and
update the "filename" field in cherry2.imageset/Contents.json to "cherry2.svg"),
or if the folder should be cherry1.imageset instead, rename the folder to
cherry1.imageset and ensure its Contents.json still points to "cherry1.svg";
also scan other sets (e.g., cherry1.imageset that references cherry0.svg) for
similar mismatches and apply the same consistent renaming or document the
intended naming convention.

Comment on lines +10 to +39
enum CherryLevel: Int {
case mong = 1
case bbo
case pang
case ggu

var levelNumber: Int { rawValue }

static func from(progressRate: Double) -> CherryLevel {
switch progressRate {
case 0.0..<25.0:
return .mong
case 25.0..<50.0:
return .bbo
case 50.0..<75.0:
return .pang
case 75.0...100.0:
return .ggu
default:
return .mong
}
}

var name: String {
switch self {
case .mong: return "몽롱체리"
case .bbo: return "뽀득체리"
case .pang: return "팡팡체리"
case .ggu: return "꾸꾸체리"
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

PR 요구사항(5단계)과 레벨 매핑이 불일치합니다.
현재 chok 단계가 누락되어 있고, 75–100 구간이 ggu로 매핑되며 100 초과가 mong로 떨어집니다. 요구사항(0–25 mong, 25–50 bbo, 50–75 chok, 75–100 pang, 100+ ggu)에 맞게 수정이 필요합니다.

🛠️ 제안 수정안
 enum CherryLevel: Int {
     case mong = 1
     case bbo
+    case chok
     case pang
     case ggu
@@
     static func from(progressRate: Double) -> CherryLevel {
         switch progressRate {
         case 0.0..<25.0:
             return .mong
         case 25.0..<50.0:
             return .bbo
         case 50.0..<75.0:
-            return .pang
-        case 75.0...100.0:
-            return .ggu
+            return .chok
+        case 75.0..<100.0:
+            return .pang
         default:
-            return .mong
+            return .ggu
         }
     }
@@
     var name: String {
         switch self {
         case .mong: return "몽롱체리"
         case .bbo: return "뽀득체리"
+        case .chok: return "촉촉체리"
         case .pang: return "팡팡체리"
         case .ggu: return "꾸꾸체리"
         }
     }
 }
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/ChallengeView/ChallengeProgressView.swift`
around lines 10 - 39, The CherryLevel enum currently omits the chok case and
mis-maps progress ranges; update the enum to include case chok with appropriate
raw ordering (e.g., mong=1, bbo=2, chok=3, pang=4, ggu=5), then change
CherryLevel.from(progressRate:) to map 0.0..<25.0 -> .mong, 25.0..<50.0 -> .bbo,
50.0..<75.0 -> .chok, 75.0..<100.0 -> .pang, and progressRate >= 100.0 -> .ggu
(ensure >=100 goes to .ggu), and add the display string for .chok in the name
computed property; keep a sensible default (e.g., .mong) for unexpected values.

@sum130 sum130 merged commit af053c2 into develop Jan 18, 2026
1 check passed
@sum130 sum130 deleted the style/#61-challengemissionview branch January 18, 2026 14:09
This was referenced Jan 19, 2026
Kimgyuilli pushed a commit that referenced this pull request Jan 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Style] 챌린지미션뷰 구현

4 participants