Conversation
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthrough프로필 생성 기능을 추가하기 위해 도메인, 데이터, 프레젠테이션 계층에 걸쳐 온보딩 흐름을 확장합니다. 새로운 저장소, 유스케이스, API 엔드포인트 및 관련 DTO를 도입하고, 뷰모델과 뷰를 업데이트하여 네트워크 기반 프로필 생성을 지원합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant InformationView as InformationView
participant OnboardingVM as OnboardingViewModel
participant CreateProfileUC as CreateProfileUseCase
participant OnboardingRepo as DefaultOnboardingRepository
participant NetworkService as NetworkService
participant AppCoordinator as AppCoordinator
User->>InformationView: "Next" 버튼 클릭
InformationView->>OnboardingVM: createProfile(name, age)
activate OnboardingVM
OnboardingVM->>OnboardingVM: isLoading = true
OnboardingVM->>CreateProfileUC: execute(name, age)
deactivate OnboardingVM
activate CreateProfileUC
CreateProfileUC->>OnboardingRepo: createProfile(name, age)
deactivate CreateProfileUC
activate OnboardingRepo
OnboardingRepo->>NetworkService: POST /api/onboarding/profiles
deactivate OnboardingRepo
activate NetworkService
Note over NetworkService: 네트워크 요청 실행
NetworkService-->>OnboardingRepo: ProfileEntity (성공)
deactivate NetworkService
OnboardingRepo-->>CreateProfileUC: ProfileEntity
CreateProfileUC-->>OnboardingVM: ProfileEntity
activate OnboardingVM
OnboardingVM->>OnboardingVM: isLoading = false
OnboardingVM->>OnboardingVM: isOnboardingCompleted = true
OnboardingVM-->>InformationView: 상태 업데이트 (isOnboardingCompleted)
deactivate OnboardingVM
InformationView->>AppCoordinator: navigationToTabbar()
AppCoordinator->>User: 탭 바 화면으로 이동
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Model/OnboardingDTO.swift`:
- Around line 24-29: CreateProfileResponseDTO.toEntity() currently uses an
ISO8601DateFormatter with .withFractionalSeconds which fails when the server
returns timestamps without fractional seconds, causing createdDate to
incorrectly fall back to Date(); update to attempt parsing with multiple
formats: first try ISO8601DateFormatter with [.withInternetDateTime,
.withFractionalSeconds], and if that returns nil try a second
ISO8601DateFormatter without .withFractionalSeconds (or a DateFormatter with
"yyyy-MM-dd'T'HH:mm:ssZ") before falling back to Date(), so createdDate reflects
the actual server time when possible.
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/InformationView.swift`:
- Around line 73-79: The button action must re-validate input and UI state
before calling the network: inside the CherrishButton closure, add guards to
ensure the local state is correct (e.g., buttonState equals the actionable state
used when creating the profile), the viewModel is not loading
(viewModel.isLoading is false), and age can be parsed to Int (let ageValue =
Int(age)); only then call await viewModel.createProfile(name: name, age:
ageValue). This prevents accidental requests if CherrishButton does not block
actions.
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/OnboardingViewModel.swift`:
- Around line 27-39: In createProfile(name:age:) avoid showing the generic error
message when the task was cancelled by handling cancellation explicitly: inside
the async function (or in the do/catch), detect CancellationError (or check
Task.isCancelled) and return early without setting errorMessage; keep the
existing catch let error as CherrishError branch for domain errors, and ensure
isLoading is cleared in a finally-like path (or after the do/catch) so loading
state is always reset even on cancellation. Reference: createProfile(name:age:),
createProfileUseCase.execute(...), errorMessage, isLoading.
- Around line 18-24: The init currently force-unwraps DIContainer.shared.resolve
for createProfileUseCase which can crash when DI is missing; fix by making
createProfileUseCase a required non-optional parameter (change
init(createProfileUseCase: CreateProfileUseCase, userDefaultService:
UserDefaultService = DefaultUserDefaultService())) so callers/tests must inject
it, or if you need a fallback keep the default but replace the force-unwrap with
a safe check (guard let resolved = DIContainer.shared.resolve(type:
CreateProfileUseCase.self) else { preconditionFailure("CreateProfileUseCase not
registered in DIContainer") }) and assign resolved to self.createProfileUseCase;
update references to the init and tests accordingly.
| extension CreateProfileResponseDTO { | ||
| func toEntity() -> ProfileEntity { | ||
| let dateFormatter = ISO8601DateFormatter() | ||
| dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] | ||
| let createdDate = dateFormatter.date(from: date) ?? Date() | ||
| return ProfileEntity(id: id, name: name, createdDate: createdDate) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the file in question
find . -name "OnboardingDTO.swift" -type f 2>/dev/null | head -20Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 128
🏁 Script executed:
#!/bin/bash
# List the structure to understand the repository layout
git ls-files | head -30Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 2440
🏁 Script executed:
#!/bin/bash
# Check if the file exists and look at the relevant lines
if [ -f "Cherrish-iOS/Cherrish-iOS/Data/Model/OnboardingDTO.swift" ]; then
echo "=== File found ==="
wc -l "Cherrish-iOS/Cherrish-iOS/Data/Model/OnboardingDTO.swift"
echo ""
echo "=== Content around lines 24-29 ==="
sed -n '20,35p' "Cherrish-iOS/Cherrish-iOS/Data/Model/OnboardingDTO.swift"
else
echo "File not found at expected path"
fiRepository: TEAM-Cherrish/Cherrish-iOS
Length of output: 572
🏁 Script executed:
#!/bin/bash
# Look for test files related to OnboardingDTO or CreateProfileResponseDTO
find . -name "*Test*" -o -name "*test*" -o -name "*Mock*" | grep -E "\.(swift|json)$" | head -20Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 133
🏁 Script executed:
#!/bin/bash
# Search for CreateProfileResponseDTO usage or tests
rg "CreateProfileResponseDTO" --type swift -A 5 -B 5Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 2674
🏁 Script executed:
#!/bin/bash
# Look for date parsing tests or examples
rg "ISO8601DateFormatter\|date.*format" --type swift -B 2 -A 2 | head -50Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check if there are any fixtures or mock data with date examples
rg "20[0-9]{2}-[0-9]{2}-[0-9]{2}" --type json | head -20Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check TestAPI.swift for mock responses
cat -n "Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/TestAPI.swift"Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 381
🏁 Script executed:
#!/bin/bash
# Look for any JSON or mock data files that might show server response format
find . -type f \( -name "*.json" -o -name "*mock*" -o -name "*Mock*" \) | head -20Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 1882
🏁 Script executed:
#!/bin/bash
# Search for any endpoint definitions that might show the API response format
rg "OnboardingEndPoint\|createProfile" --type swift -A 10 -B 2Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Search for endpoint definitions more broadly
rg "EndPoint" --type swift | grep -i "onboard\|profile" | head -20Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 413
🏁 Script executed:
#!/bin/bash
# Look for the endpoint file itself
find . -name "*EndPoint*" -o -name "*Endpoint*" | grep swiftRepository: TEAM-Cherrish/Cherrish-iOS
Length of output: 209
🏁 Script executed:
#!/bin/bash
# Check the entire OnboardingDTO file
cat -n "Cherrish-iOS/Cherrish-iOS/Data/Model/OnboardingDTO.swift"Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 944
🏁 Script executed:
#!/bin/bash
# Check the OnboardingEndPoint.swift file
cat -n "Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/OnboardingEndPoint.swift"Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 1538
🏁 Script executed:
#!/bin/bash
# Search for any backend specifications or API documentation
find . -type f \( -name "*.md" -o -name "README*" -o -name "API*" -o -name "SPEC*" \) | head -20Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 159
🏁 Script executed:
#!/bin/bash
# Check the README for any API specification
cat "README.md"Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 2764
🏁 Script executed:
#!/bin/bash
# Search for any test files or assertions that show expected date format
find . -name "*Test*" -o -name "*Tests*" | xargs grep -l "date\|CreateProfile" 2>/dev/null | head -10Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 52
🌐 Web query:
ISO8601DateFormatter withFractionalSeconds parsing without fractional seconds
💡 Result:
Short answer: ISO8601DateFormatter is (or historically was) strict — enabling .withFractionalSeconds makes the parser expect fractional seconds, and disabling it rejects strings that include them. The usual practical fixes are either try two formatters (one with .withFractionalSeconds and one without) or normalize the input string (strip or add fractional seconds) before parsing. (forums.swift.org)
Minimal example (fallback approach):
- Try parsing with [.withInternetDateTime, .withFractionalSeconds]; if that returns nil, try [.withInternetDateTime] (or the reverse order).
Alternative approaches:
- Preprocess the string to add/remove fractional seconds so it matches a single formatter.
- Use a third‑party ISO8601 parser or a custom Date decoding strategy that attempts multiple formats.
- Note: newer Swift/Foundation proposals aim to make ISO8601 parsing accept optional fractional seconds regardless of the formatting flag (so future APIs may remove this problem). (forums.swift.org)
If you want, I can paste a short Swift code snippet showing the two‑formatter fallback.
Citations:
- 1: https://forums.swift.org/t/iso8601dateformatter-fails-to-parse-a-valid-iso-8601-date/22999?utm_source=openai
- 2: https://forums.swift.org/t/iso8601dateformatter-fails-to-parse-a-valid-iso-8601-date/22999/21?utm_source=openai
🌐 Web query:
iOS Swift ISO8601DateFormatter formatOptions strict parsing behavior
💡 Result:
Short answer: ISO8601DateFormatter’s formatOptions are strict for both formatting and parsing — the parser expects the date string to match the exact components you included in formatOptions. For example, if you set [.withInternetDateTime, .withFractionalSeconds] the formatter will accept strings with fractional seconds but will refuse the same timestamp without them; conversely, using .withInternetDateTime alone won’t accept fractional seconds unless you add .withFractionalSeconds. The same strictness applies to separators/timezone options (colon separators, full time, etc.), so you often need either multiple formatters (fallbacks) or to normalize incoming strings before parsing. [1][2]
References
- StackOverflow explanation and iOS-version notes about withFractionalSeconds (parsing added iOS 11 / macOS 10.13). [1]
- Swift forums discussion showing the strict/mandatory behavior and common workarounds (try without/with fractional seconds as fallback or normalize). [2]
Sources
[1] Stack Overflow: “ISO8601DateFormatter doesn't parse ISO date string”.
[2] Swift Forums: “ISO8601DateFormatter fails to parse a valid ISO-8601 date”.
ISO8601 파싱 실패 시 createdDate가 현재 시각으로 덮입니다.
ISO8601DateFormatter는 formatOptions에서 설정한 형식을 엄격하게 검증합니다. .withFractionalSeconds를 지정하면 파서는 입력값에 반드시 소수점 초가 포함되기를 기대합니다. 서버가 소수점 없이 응답하면 파싱이 실패하고 현재 시간으로 폴백되어 데이터 무결성 문제가 발생합니다.
🛠️ 제안 수정
- let dateFormatter = ISO8601DateFormatter()
- dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
- let createdDate = dateFormatter.date(from: date) ?? Date()
+ let formatter = ISO8601DateFormatter()
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+ let fallback = ISO8601DateFormatter()
+ fallback.formatOptions = [.withInternetDateTime]
+ let createdDate = formatter.date(from: date)
+ ?? fallback.date(from: date)
+ ?? Date()🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Model/OnboardingDTO.swift` around lines 24 -
29, CreateProfileResponseDTO.toEntity() currently uses an ISO8601DateFormatter
with .withFractionalSeconds which fails when the server returns timestamps
without fractional seconds, causing createdDate to incorrectly fall back to
Date(); update to attempt parsing with multiple formats: first try
ISO8601DateFormatter with [.withInternetDateTime, .withFractionalSeconds], and
if that returns nil try a second ISO8601DateFormatter without
.withFractionalSeconds (or a DateFormatter with "yyyy-MM-dd'T'HH:mm:ssZ") before
falling back to Date(), so createdDate reflects the actual server time when
possible.
| CherrishButton(title: "다음", type: .next, state: $buttonState) { | ||
| Task { | ||
| guard let ageValue = Int(age) else { return } | ||
| await viewModel.createProfile(name: name, age: ageValue) | ||
| } | ||
| } | ||
| .disabled(viewModel.isLoading) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
유효성 검증을 액션 내부에서도 보강해주세요.
CherrishButton이 .normal 상태에서 액션을 차단하지 않는다면, 잘못된 입력으로 네트워크 요청이 나갈 수 있습니다. 상태와 무관하게 한 번 더 가드하는 편이 안전합니다.
✅ 제안 수정
CherrishButton(title: "다음", type: .next, state: $buttonState) {
Task {
+ let isNameValid = !name.isEmpty && !isNameOverLimit
+ let isAgeValid = !age.isEmpty && !isAgeOverLimit
+ guard isNameValid, isAgeValid else { return }
guard let ageValue = Int(age) else { return }
await viewModel.createProfile(name: name, age: ageValue)
}
}🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/InformationView.swift`
around lines 73 - 79, The button action must re-validate input and UI state
before calling the network: inside the CherrishButton closure, add guards to
ensure the local state is correct (e.g., buttonState equals the actionable state
used when creating the profile), the viewModel is not loading
(viewModel.isLoading is false), and age can be parsed to Int (let ageValue =
Int(age)); only then call await viewModel.createProfile(name: name, age:
ageValue). This prevents accidental requests if CherrishButton does not block
actions.
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/OnboardingViewModel.swift
Outdated
Show resolved
Hide resolved
| @MainActor | ||
| func createProfile(name: String, age: Int) async { | ||
| isLoading = true | ||
| errorMessage = nil | ||
|
|
||
| do { | ||
| let profile = try await createProfileUseCase.execute(name: name, age: age) | ||
| _ = userDefaultService.save(profile.id, key: .userID) | ||
| isOnboardingCompleted = true | ||
| } catch let error as CherrishError { | ||
| errorMessage = error.localizedDescription | ||
| } catch { | ||
| errorMessage = "알 수 없는 오류가 발생했습니다." |
There was a problem hiding this comment.
취소된 작업에 오류 메시지가 노출되지 않도록 분기하세요.
사용자가 화면을 떠나며 Task가 취소될 때도 “알 수 없는 오류”가 표시될 수 있습니다.
🔧 제안 수정안
- } catch let error as CherrishError {
+ } catch is CancellationError {
+ // 취소는 사용자에게 오류로 노출하지 않음
+ } catch let error as CherrishError {
errorMessage = error.localizedDescription
} catch {
errorMessage = "알 수 없는 오류가 발생했습니다."
}📝 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.
| @MainActor | |
| func createProfile(name: String, age: Int) async { | |
| isLoading = true | |
| errorMessage = nil | |
| do { | |
| let profile = try await createProfileUseCase.execute(name: name, age: age) | |
| _ = userDefaultService.save(profile.id, key: .userID) | |
| isOnboardingCompleted = true | |
| } catch let error as CherrishError { | |
| errorMessage = error.localizedDescription | |
| } catch { | |
| errorMessage = "알 수 없는 오류가 발생했습니다." | |
| `@MainActor` | |
| func createProfile(name: String, age: Int) async { | |
| isLoading = true | |
| errorMessage = nil | |
| do { | |
| let profile = try await createProfileUseCase.execute(name: name, age: age) | |
| _ = userDefaultService.save(profile.id, key: .userID) | |
| isOnboardingCompleted = true | |
| } catch is CancellationError { | |
| // 취소는 사용자에게 오류로 노출하지 않음 | |
| } catch let error as CherrishError { | |
| errorMessage = error.localizedDescription | |
| } catch { | |
| errorMessage = "알 수 없는 오류가 발생했습니다." | |
| } | |
| } |
🤖 Prompt for AI Agents
In
`@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/OnboardingViewModel.swift`
around lines 27 - 39, In createProfile(name:age:) avoid showing the generic
error message when the task was cancelled by handling cancellation explicitly:
inside the async function (or in the do/catch), detect CancellationError (or
check Task.isCancelled) and return early without setting errorMessage; keep the
existing catch let error as CherrishError branch for domain errors, and ensure
isLoading is cleared in a finally-like path (or after the do/catch) so loading
state is always reset even on cancellation. Reference: createProfile(name:age:),
createProfileUseCase.execute(...), errorMessage, isLoading.
y-eonee
left a comment
There was a problem hiding this comment.
온보딩하는 과정을 찍은 gif를 찍어주세요 ~ ~ 수고하셨습니다
리뷰 확인해주세요 !!
| let dateFormatter = ISO8601DateFormatter() | ||
| dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] | ||
| let createdDate = dateFormatter.date(from: date) ?? Date() | ||
| return ProfileEntity(id: id, name: name, createdDate: createdDate) |
There was a problem hiding this comment.
date가 유저엔티티에 필요하지는 않을 것 같아서 이 부분은 삭제하는게 좋을 것 같아요
| struct CreateProfileRequestDTO: Encodable { | ||
| let name: String | ||
| let age: Int | ||
| } | ||
|
|
||
|
|
||
| struct CreateProfileResponseDTO: Decodable { | ||
| let id: Int | ||
| let name: String | ||
| let date: String | ||
| } |
There was a problem hiding this comment.
리퀘스트랑 리스폰스는 파일을 분리하면 좋을 것 같아요
|
|
||
|
|
|
|
||
| import Alamofire | ||
|
|
||
| enum OnboardingEndPoint: EndPoint { |
There was a problem hiding this comment.
OnboardingEndPoint -> OnboardingAPI 로 바꾸면 좋을 것 같아염
| struct ProfileEntity { | ||
| let id: Int | ||
| let name: String | ||
| let createdDate: Date |
| #Preview { | ||
| OnboardingContainerView(viewModel: OnboardingViewModel()) | ||
| .environmentObject(OnboardingCoordinator()) | ||
| } |
| private let userDefaultService: UserDefaultService | ||
|
|
||
| init( | ||
| createProfileUseCase: CreateProfileUseCase? = nil, | ||
| userDefaultService: UserDefaultService = DefaultUserDefaultService() |
There was a problem hiding this comment.
유저디폴트가 프레젠테이션 레이어에서 필요할까요 ~~ ?? ,, 레포지토리 단으로 옮기면 좋을 것 같아요
| self.createProfileUseCase = createProfileUseCase | ||
| ?? DIContainer.shared.resolve(type: CreateProfileUseCase.self)! |
| _ = userDefaultService.save(profile.id, key: .userID) | ||
| isOnboardingCompleted = true |
There was a problem hiding this comment.
Actionable comments posted: 4
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/PresentationDependencyAssembler.swift (1)
43-45: 에러 로깅 일관성을 맞춰주세요.
FetchDashboardData해결 실패 시 다른 guard 문(라인 21-22, 32)과 달리CherrishLogger.error를 호출하지 않습니다. 일관성을 위해 에러 로깅을 추가하는 것이 좋습니다.♻️ 제안 수정
guard let fetchDashboardData = DIContainer.shared.resolve(type: FetchDashboardData.self) else { + CherrishLogger.error(CherrishError.DIFailedError) return }
🤖 Fix all issues with AI agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/OnboardingAPI.swift`:
- Around line 48-56: The current bodyParameters implementation in OnboardingAPI
manually builds a dictionary for the .createProfile case; instead, return the
CreateProfileRequestDTO (which conforms to Encodable) so the NetworkService can
encode it automatically: update the bodyParameters accessor (or add a new
bodyEncodable property) to return the request DTO for .createProfile rather than
a [String:Any] literal, and ensure NetworkService sends Encodable bodies (or use
JSONEncoder in the endpoint implementation to produce Data from the DTO).
Reference: OnboardingAPI, bodyParameters, and CreateProfileRequestDTO.
- Around line 12-57: The OnboardingAPI and OnboardingEndPoint types are
duplicated; consolidate them by keeping a single endpoint definition (pick
either OnboardingAPI or OnboardingEndPoint) and remove the other. Update all
references/imports across the codebase to point to the retained type (e.g.,
OnboardingAPI or OnboardingEndPoint), ensure the retained enum includes the
CreateProfile(request: CreateProfileRequestDTO) case and the same
implementations for basePath, path, method, headers, parameterEncoding,
queryParameters, and bodyParameters, and delete the duplicate file to avoid
conflicts and duplicated symbols.
In `@Cherrish-iOS/Cherrish-iOS/Data/Repository/OnboardingRepository.swift`:
- Around line 10-27: DefaultOnboardingRepository.createProfile currently ignores
the result of userDefaultService.save, which can hide persistence failures;
after receiving response (CreateProfileResponseDTO) call
userDefaultService.save(response.id, key: .userID) and check its return/throw
result, and if it indicates failure throw or propagate a Storage/Repository
error from createProfile so callers don't assume success; update createProfile
to either convert a false/save-failure into a thrown error (or adjust signature
if save is async/throws) and include the unique symbols
DefaultOnboardingRepository.createProfile, CreateProfileResponseDTO, and
userDefaultService.save in your change.
In `@Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift`:
- Around line 44-50: There is a duplicate block that re-declares
onboardingRepository and re-registers CreateProfileUseCase; remove the redundant
guard/register block so onboardingRepository is only resolved once and
CreateProfileUseCase is registered a single time (keep the original guard let
onboardingRepository = DIContainer.shared.resolve(type:
OnboardingInterface.self) and the DIContainer.shared.register(type:
CreateProfileUseCase.self) { DefaultCreateProfileUseCase(repository:
onboardingRepository) } registration, and delete the repeated copy).
♻️ Duplicate comments (3)
Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/InformationView.swift (1)
80-83: 액션 내부에서 입력 유효성 검증을 보강해주세요.현재
Int(age)변환만 검증하고 있습니다.CherrishButton이.normal상태에서 액션을 차단하지 않는다면, 빈 이름이나 제한 초과 입력으로 네트워크 요청이 발생할 수 있습니다.✅ 제안 수정
Task { + guard !name.isEmpty, !isNameOverLimit else { return } + guard !age.isEmpty, !isAgeOverLimit else { return } guard let ageValue = Int(age) else { return } await viewModel.createProfile(name: name, age: ageValue) }Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/OnboardingViewModel.swift (2)
21-36:CancellationError처리를 추가해 주세요.사용자가 화면을 떠나 Task가 취소될 때 "알 수 없는 오류가 발생했습니다" 메시지가 표시될 수 있습니다.
🔧 CancellationError 처리 추가
do { _ = try await createProfileUseCase.execute(name: name, age: age) isOnboardingCompleted = true + } catch is CancellationError { + // 취소는 사용자에게 오류로 노출하지 않음 } catch let error as CherrishError { errorMessage = error.localizedDescription } catch { errorMessage = "알 수 없는 오류가 발생했습니다." }
23-35:defer를 사용하여 로딩 상태 정리를 보장하세요.현재 구조에서는 예외 발생 시에도
isLoading = false가 실행되지만,defer를 사용하면 코드 흐름이 더 명확해집니다.♻️ defer 사용 제안
func createProfile(name: String, age: Int) async { isLoading = true errorMessage = nil + defer { isLoading = false } do { _ = try await createProfileUseCase.execute(name: name, age: age) isOnboardingCompleted = true + } catch is CancellationError { + // 취소는 오류로 표시하지 않음 } catch let error as CherrishError { errorMessage = error.localizedDescription } catch { errorMessage = "알 수 없는 오류가 발생했습니다." } - - isLoading = false }
| enum OnboardingAPI: EndPoint { | ||
| case createProfile(request: CreateProfileRequestDTO) | ||
|
|
||
| var basePath: String { | ||
| return "/api/onboarding" | ||
| } | ||
|
|
||
| var path: String { | ||
| switch self { | ||
| case .createProfile: | ||
| return "/profiles" | ||
| } | ||
| } | ||
|
|
||
| var method: HTTPMethod { | ||
| switch self { | ||
| case .createProfile: | ||
| return .post | ||
| } | ||
| } | ||
|
|
||
| var headers: HeaderType { | ||
| switch self { | ||
| case .createProfile: | ||
| return .basic | ||
| } | ||
| } | ||
|
|
||
| var parameterEncoding: ParameterEncoding { | ||
| return JSONEncoding.default | ||
| } | ||
|
|
||
| var queryParameters: [String: String]? { | ||
| return nil | ||
| } | ||
|
|
||
| var bodyParameters: Parameters? { | ||
| switch self { | ||
| case .createProfile(let request): | ||
| return [ | ||
| "name": request.name, | ||
| "age": request.age | ||
| ] | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find OnboardingEndPoint.swift file
fd -t f "OnboardingEndPoint.swift" --exec cat {}Repository: TEAM-Cherrish/Cherrish-iOS
Length of output: 1139
OnboardingAPI.swift와 OnboardingEndPoint.swift는 동일한 기능을 하므로 중복을 제거해야 합니다.
두 파일이 같은 역할을 수행합니다. OnboardingAPI와 OnboardingEndPoint 모두 동일한 케이스, 동일한 basePath, path, method, headers, parameterEncoding, queryParameters, bodyParameters를 가지고 있습니다. 하나의 파일로 통합하고 다른 하나는 삭제하며, 이를 참조하는 모든 곳에서 임포트를 업데이트하세요.
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/OnboardingAPI.swift` around
lines 12 - 57, The OnboardingAPI and OnboardingEndPoint types are duplicated;
consolidate them by keeping a single endpoint definition (pick either
OnboardingAPI or OnboardingEndPoint) and remove the other. Update all
references/imports across the codebase to point to the retained type (e.g.,
OnboardingAPI or OnboardingEndPoint), ensure the retained enum includes the
CreateProfile(request: CreateProfileRequestDTO) case and the same
implementations for basePath, path, method, headers, parameterEncoding,
queryParameters, and bodyParameters, and delete the duplicate file to avoid
conflicts and duplicated symbols.
| var bodyParameters: Parameters? { | ||
| switch self { | ||
| case .createProfile(let request): | ||
| return [ | ||
| "name": request.name, | ||
| "age": request.age | ||
| ] | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
DTO의 Encodable 활용을 고려해 주세요.
CreateProfileRequestDTO가 이미 name과 age를 가지고 있으므로, 수동으로 딕셔너리를 생성하는 대신 Encodable을 활용하면 유지보수성이 향상됩니다.
♻️ Encodable 활용 제안
만약 NetworkService가 Encodable body를 지원한다면:
var bodyParameters: Parameters? {
switch self {
case .createProfile(let request):
- return [
- "name": request.name,
- "age": request.age
- ]
+ return request.asDictionary() // 또는 Encodable extension 활용
}
}DTO에 필드가 추가될 때 이 파일을 수정할 필요가 없어집니다.
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Network/EndPoint/OnboardingAPI.swift` around
lines 48 - 56, The current bodyParameters implementation in OnboardingAPI
manually builds a dictionary for the .createProfile case; instead, return the
CreateProfileRequestDTO (which conforms to Encodable) so the NetworkService can
encode it automatically: update the bodyParameters accessor (or add a new
bodyEncodable property) to return the request DTO for .createProfile rather than
a [String:Any] literal, and ensure NetworkService sends Encodable bodies (or use
JSONEncoder in the endpoint implementation to produce Data from the DTO).
Reference: OnboardingAPI, bodyParameters, and CreateProfileRequestDTO.
| struct DefaultOnboardingRepository: OnboardingInterface { | ||
| private let networkService: NetworkService | ||
| private let userDefaultService: UserDefaultService | ||
|
|
||
| init(networkService: NetworkService, userDefaultService: UserDefaultService) { | ||
| self.networkService = networkService | ||
| self.userDefaultService = userDefaultService | ||
| } | ||
|
|
||
| func createProfile(name: String, age: Int) async throws -> Int { | ||
| let request = CreateProfileRequestDTO(name: name, age: age) | ||
| let response = try await networkService.request( | ||
| OnboardingAPI.createProfile(request: request), | ||
| decodingType: CreateProfileResponseDTO.self | ||
| ) | ||
| _ = userDefaultService.save(response.id, key: .userID) | ||
| return response.id | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
저장소 구현이 잘 되어 있습니다.
클린 아키텍처 패턴에 따라 네트워크 요청과 로컬 저장을 적절히 분리했습니다.
다만, userDefaultService.save() 결과 처리를 고려해 주세요.
Line 25에서 저장 결과를 무시하고 있습니다. 저장 실패 시에도 성공으로 반환되어 데이터 불일치가 발생할 수 있습니다.
💡 저장 실패 처리 제안
- _ = userDefaultService.save(response.id, key: .userID)
+ let saved = userDefaultService.save(response.id, key: .userID)
+ if !saved {
+ // 로깅 또는 에러 처리 고려
+ }📝 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.
| struct DefaultOnboardingRepository: OnboardingInterface { | |
| private let networkService: NetworkService | |
| private let userDefaultService: UserDefaultService | |
| init(networkService: NetworkService, userDefaultService: UserDefaultService) { | |
| self.networkService = networkService | |
| self.userDefaultService = userDefaultService | |
| } | |
| func createProfile(name: String, age: Int) async throws -> Int { | |
| let request = CreateProfileRequestDTO(name: name, age: age) | |
| let response = try await networkService.request( | |
| OnboardingAPI.createProfile(request: request), | |
| decodingType: CreateProfileResponseDTO.self | |
| ) | |
| _ = userDefaultService.save(response.id, key: .userID) | |
| return response.id | |
| } | |
| struct DefaultOnboardingRepository: OnboardingInterface { | |
| private let networkService: NetworkService | |
| private let userDefaultService: UserDefaultService | |
| init(networkService: NetworkService, userDefaultService: UserDefaultService) { | |
| self.networkService = networkService | |
| self.userDefaultService = userDefaultService | |
| } | |
| func createProfile(name: String, age: Int) async throws -> Int { | |
| let request = CreateProfileRequestDTO(name: name, age: age) | |
| let response = try await networkService.request( | |
| OnboardingAPI.createProfile(request: request), | |
| decodingType: CreateProfileResponseDTO.self | |
| ) | |
| let saved = userDefaultService.save(response.id, key: .userID) | |
| if !saved { | |
| // 로깅 또는 에러 처리 고려 | |
| } | |
| return response.id | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Data/Repository/OnboardingRepository.swift` around
lines 10 - 27, DefaultOnboardingRepository.createProfile currently ignores the
result of userDefaultService.save, which can hide persistence failures; after
receiving response (CreateProfileResponseDTO) call
userDefaultService.save(response.id, key: .userID) and check its return/throw
result, and if it indicates failure throw or propagate a Storage/Repository
error from createProfile so callers don't assume success; update createProfile
to either convert a false/save-failure into a thrown error (or adjust signature
if save is async/throws) and include the unique symbols
DefaultOnboardingRepository.createProfile, CreateProfileResponseDTO, and
userDefaultService.save in your change.
Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift
Outdated
Show resolved
Hide resolved
wotjs020708
left a comment
There was a problem hiding this comment.
고생하셨습니다 코드 참고 좀 하겠습니다!
Cherrish-iOS/Cherrish-iOS/Domain/DomainDependencyAssembler.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/SplashView.swift`:
- Around line 28-31: The delayed navigation in the SplashView's onAppear can be
scheduled as a cancellable DispatchWorkItem to prevent duplicate triggers; add a
`@State` optional DispatchWorkItem property (e.g., splashNavigationWorkItem) to
the SplashView, before creating a new DispatchWorkItem for calling
navigateBasedOnUserStatus() cancel any existing splashNavigationWorkItem, assign
the new work item and schedule it with DispatchQueue.main.asyncAfter, and ensure
you cancel the work item in onDisappear (or before scheduling) to avoid
duplicate/lingering navigation tasks.
| .onAppear { | ||
| DispatchQueue.main.asyncAfter(deadline: .now() + 3) { | ||
| appCoordinator.navigationToOnboarding() | ||
| navigateBasedOnUserStatus() | ||
| } |
There was a problem hiding this comment.
지연 네비게이션 중복 트리거 방지 필요.
onAppear가 여러 번 호출되면 3초 지연 작업이 중복 실행되어 네비게이션이 반복될 수 있습니다. 취소 가능한 작업으로 바꿔 주세요.
🛠️ 수정 제안 (취소 가능한 작업)
struct SplashView: View {
`@EnvironmentObject` private var appCoordinator: AppCoordinator
+ `@State` private var navigationWorkItem: DispatchWorkItem?
private let userDefaultService: UserDefaultService? = DIContainer.shared.resolve(type: UserDefaultService.self)
var body: some View {
ZStack(alignment: .center) {
LinearGradient(
@@
.onAppear {
- DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
- navigateBasedOnUserStatus()
- }
+ navigationWorkItem?.cancel()
+ let workItem = DispatchWorkItem { navigateBasedOnUserStatus() }
+ navigationWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: workItem)
}
+ .onDisappear {
+ navigationWorkItem?.cancel()
+ navigationWorkItem = nil
+ }
.ignoresSafeArea()
}🤖 Prompt for AI Agents
In `@Cherrish-iOS/Cherrish-iOS/Presentation/Feature/Onboarding/SplashView.swift`
around lines 28 - 31, The delayed navigation in the SplashView's onAppear can be
scheduled as a cancellable DispatchWorkItem to prevent duplicate triggers; add a
`@State` optional DispatchWorkItem property (e.g., splashNavigationWorkItem) to
the SplashView, before creating a new DispatchWorkItem for calling
navigateBasedOnUserStatus() cancel any existing splashNavigationWorkItem, assign
the new work item and schedule it with DispatchQueue.main.asyncAfter, and ensure
you cancel the work item in onDisappear (or before scheduling) to avoid
duplicate/lingering navigation tasks.

🔗 연결된 이슈
📄 작업 내용
이건 get이 아니라서 사진이 부실해여,,,
잘 포스트가 됩니다~