Feat/#19 travel follow contents api#20
Conversation
Walkthrough저장소 기반 아키텍처에서 서비스 기반 아키텍처로 전환하며, 토큰 저장소와 제공자를 추가하고, 인증 및 네트워크 계층을 개선했습니다. AuthRepository와 FollowRepository를 제거하고 새로운 서비스 프로토콜과 팩토리를 도입했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant FollowVC as FollowDetailViewController
participant Interactor as FollowDetailInteractor
participant Calendar as CalendarView
participant TravelSvc as TravelService
participant Network as NetworkAPI
participant Storage as KeychainStorage
User->>FollowVC: 여행 추가 버튼 탭
FollowVC->>Interactor: didTapAddToTrip()
Interactor->>Calendar: routeToTripCalendar(templateTotalDays)
Calendar->>Calendar: setTemplateTotalDays(days)
User->>Calendar: 날짜 범위 선택
Calendar->>Interactor: tripCalendarDidSelectRange(startDate, endDate)
Interactor->>TravelSvc: createUserTravel(request)
TravelSvc->>Network: POST /api/v1/travels
Network-->>TravelSvc: CreateUserTravelResponse
TravelSvc-->>Interactor: Result<CreateTravelResponse, CreateTravelError>
Interactor->>Storage: (암묵적 토큰 사용)
Interactor-->>FollowVC: followDetailDidAddTrip(title, dates)
FollowVC->>User: 여행 생성 완료 알림
sequenceDiagram
participant App as AppComponent
participant Factory as TokenRepositoryFactory
participant Keychain as KeychainStorage
participant Repository as TokenRepository
participant Adapter as TokenProviderAdapter
participant Service as TravelService
App->>Factory: makeTokenProvider()
Factory->>Keychain: KeychainStorage()
Factory->>Repository: TokenRepository(keychainStorage)
Repository->>Keychain: save(token, forKey: .accessToken)
Keychain-->>Repository: success
Factory->>Adapter: TokenProviderAdapter(repository)
Factory-->>App: TokenProviding
Service->>Adapter: accessToken()
Adapter->>Repository: get(.accessToken)
Repository->>Keychain: load(forKey: .accessToken)
Keychain-->>Repository: token
Repository-->>Adapter: String?
Adapter-->>Service: String?
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 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: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Projects/Domain/Sources/Model/Travel/TravelPlace.swift (1)
57-83:⚠️ Potential issue | 🟡 Minor
googleMapsUri비옵션 계약을 확인해 주세요.서버 API에서 이 필드가 항상 존재하지 않으면,
PlaceResponse디코딩이 실패하고 런타임 오류가 발생할 수 있습니다. 현재 구조에서는 다음과 같은 문제가 있습니다:
PlaceResponse(DTO)의googleMapsUri: String이 비옵션으로 정의됨 (Decodable)PlaceInfo도googleMapsUri: String이 비옵션이며 기본값 없음- 변환(FollowTransform)에서 폴백이나 기본값 처리 없이 직접 매핑됨
서버가 이 필드를 항상 제공한다고 보장되지 않으면, optional(
String?) 또는 기본값 처리를 고려해 주세요.Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift (1)
91-111:⚠️ Potential issue | 🟠 Major에러 발생 시 사용자에게 피드백이 없습니다.
detailResult가 실패할 경우 로딩만 숨기고 반환합니다. 사용자는 왜 데이터가 로드되지 않았는지 알 수 없습니다. 에러 상태를 UI에 표시하는 것을 권장합니다.🐛 에러 처리 개선 제안
let detailResult = await followService.fetchTravelDetail(id: recommendationId) -guard case .success(let detail) = detailResult else { - await MainActor.run { - presenter.hideLoading() - } - return -} +switch detailResult { +case .success(let detail): + // 성공 처리는 아래에서 계속 + break +case .failure(let error): + await MainActor.run { + presenter.hideLoading() + // TODO: presenter.showError(error) 등 에러 UI 표시 + } + return +} + +guard case .success(let detail) = detailResult else { return }
🤖 Fix all issues with AI agents
In `@Projects/Data/Sources/Repository/Auth/TokenRepository.swift`:
- Around line 27-37: 현재 save(_:for:)와 delete(_:)가 keychainStorage.save(...)와
keychainStorage.delete(...)의 Bool 반환값을 무시하고 있으므로 실패를 호출자에게 전달하도록 변경하세요: update
the public func save(_ value: String, for type: TokenType) and public func
delete(_ type: TokenType) to either return Bool (propagate the result of
keychainStorage.save(...) / keychainStorage.delete(...)) or throw an error based
on the Bool, keeping public func get(_:) unchanged; reference
keychainStorage.save, keychainStorage.delete, save(_:for:) and delete(_:) when
making the change.
In `@Projects/Domain/Sources/Interface/Auth/TokenRepositoryProtocol.swift`:
- Around line 17-21: Add Sendable conformance to the related protocol types:
update KeychainStorageProtocol to declare it conforms to Sendable and update
TokenRepositoryProtocol to declare it conforms to Sendable so the dependency
chain matches TokenProviding’s Sendable requirement; then remove the use of
`@unchecked` Sendable from TokenProviderAdapter (or ensure TokenProviderAdapter is
safely Sendable) so the safety contract is explicit. Ensure you modify the
protocol declarations named KeychainStorageProtocol and TokenRepositoryProtocol
accordingly and re-run the compile to confirm no other types need Sendable
adjustments.
In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift`:
- Around line 126-127: 현재 let places = (try? result.get()) ?? []는 모든 에러를 무시하고 빈
배열로 대체하므로 네트워크 에러를 로깅하거나 사용자에세 알릴 수 있도록 수정하세요: call
followService.fetchPlaces(travelId:recommendationId, day:day), then unwrap
result with a do-catch around try result.get() (or switch on result) and in the
catch/log branch use your logger (or return a failure state) to record the error
before deciding to return [] or propagate the error; update handling around
followService.fetchPlaces and the places variable so errors are not silently
swallowed.
- Around line 210-221: handleCreateTravelError currently only prints errors to
the console (print calls), so users get no feedback; update this method to show
user-facing feedback instead—map each CreateTravelError case (.validationFailed,
.invalidDateOrder, .notFoundTemplate, .unknown) to an appropriate UI
notification (e.g., present an alert or invoke a toast helper), use a localized,
user-friendly message, and ensure the presentation runs on the main thread; if
this interactor cannot access UI directly, forward the error via a
delegate/callback (e.g., an onError / showError method on a presenter or
delegate) so the UI layer can display the alert/toast.
In `@Projects/Features/FollowFeature/Sources/Views/BudgetView.swift`:
- Around line 74-76: The configure(budget: Int) currently does integer division
(budget/10000) which drops fractions and triggers SwiftLint spacing errors;
change the calculation to use floating-point (e.g., let value = Double(budget) /
10000) and format the string according to desired behavior (either round to
whole 만원 with standard rounding or display one decimal place for precision)
before calling budgetLabel.setText, and ensure spaces around the / operator to
satisfy SwiftLint; update the configure(budget:) implementation and the
budgetLabel.setText invocation accordingly.
In `@Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift`:
- Around line 203-205: The sequence label is incorrectly displaying
place.sequence - 1 (making the first item 0); in configure(with:isLast:) of
PlaceCell, update the sequenceLabel.setText call to use the original 1-based
value (use place.sequence) so it matches TravelPlace and TravelMapView (replace
"\(place.sequence - 1)" with "\(place.sequence)").
In `@Projects/Modules/DSKit/Sources/Component/NDGLModalViewController.swift`:
- Around line 52-55: buttonStackView currently uses a hardcoded spacing and
several vertical/horizontal gaps use the wrong scaling; change buttonStackView
spacing to use scaled values (use adjustedW or adjusted for horizontal gaps)
instead of literal 8, and replace vertical spacing constraints that use adjusted
with adjustedH; also add left/right constraints for titleLabel and subtitleLabel
(they currently only have centerX) so long text can wrap—ensure their
leading/trailing are constrained to the container with appropriate
adjustedW/adjusted insets and allow multiline by setting numberOfLines = 0 where
needed (look for symbols: buttonStackView, titleLabel, subtitleLabel and any
constraint calls around lines ~128-141).
- Line 93: The function declaration in NDGLModalViewController violates
SwiftLint's modifier_order rule; reorder the modifiers on the viewDidLoad method
so that `override` comes before `public` (i.e., change the modifier order for
the viewDidLoad declaration to comply with modifier_order).
In `@Projects/Modules/Networks/Sources/DTO/Follow/FollowDTO.swift`:
- Around line 55-63: PlaceResponse currently declares googleMapsUri as
non-optional which will cause decoding to fail if the backend omits it; change
googleMapsUri to an optional String? on the PlaceResponse struct (like
regularOpeningHours) and update any conversion/usage logic that assumes a
non-nil value (e.g. initializers, mapping functions, or UI consumers that
reference PlaceResponse.googleMapsUri) to safely unwrap or provide a fallback;
verify all places that construct or decode PlaceResponse handle googleMapsUri
being nil.
In `@Projects/Modules/Networks/Sources/Plugin/AuthPlugin.swift`:
- Around line 21-26: In prepare(_ request: URLRequest, target: any TargetType)
replace the use of request.addValue("Bearer \(token)", forHTTPHeaderField:
"Authorization") with request.setValue("Bearer \(token)", forHTTPHeaderField:
"Authorization") so the Authorization header is overwritten rather than
appended; locate the logic in the prepare function that reads
tokenProvider.accessToken() and update the header modification call accordingly.
In `@Projects/Modules/Networks/Sources/Service/TravelService.swift`:
- Around line 13-22: TravelService currently stores a shared DateFormatter
(dateFormatter) which is not thread-safe and can race when async methods like
createUserTravel run concurrently; instead, remove reliance on the shared
dateFormatter and instantiate a new DateFormatter inside the call sites (e.g.,
inside createUserTravel) for each use, and when constructing that per-call
formatter set its locale to Locale(identifier: "en_US_POSIX") and timeZone to
TimeZone(secondsFromGMT: 0) (or use ISO8601DateFormatter if preferred) so date
string output is deterministic and thread-safe; update init to no longer create
a persistent dateFormatter and update any references to use the per-call
formatter.
🧹 Nitpick comments (12)
Projects/Features/FollowFeature/Sources/Views/PlaceDetailBottomSheetView.swift (1)
225-239: 체류시간이 없을 때 구분점(•)가 남습니다.durationText가 비어도 dotLabel이 그대로 보여서 “•”만 남는 UI 아티팩트가 생깁니다. durationText가 비었을 때 dotLabel/durationLabel을 숨기는 처리가 필요합니다.
✅ 제안 수정안
let durationText: String if let duration = place.estimatedDuration { let hours = duration / 60 let minutes = duration % 60 if hours > 0 && minutes > 0 { durationText = "\(hours)시간 \(minutes)분 체류 예상" } else if hours > 0 { durationText = "\(hours)시간 체류 예상" } else { durationText = "\(minutes)분 체류 예상" } } else { durationText = "" } durationLabel.setText(.bodySR, text: durationText, color: UIColor(hexCode: "#444444")) + let hasDuration = !durationText.isEmpty + dotLabel.isHidden = !hasDuration + durationLabel.isHidden = !hasDurationProjects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift (1)
93-93: SwiftLint: 단일 표현식 함수에서는 implicit return 사용 권장Swift 스타일 가이드에 따르면, 단일 표현식으로 구성된 함수나 클로저에서는
return키워드를 생략하는 것이 더 간결합니다.♻️ 제안하는 수정 사항
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return CGSize(width: collectionView.bounds.width, height: 129) + CGSize(width: collectionView.bounds.width, height: 129) }Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift (1)
233-236: 이동 시간 정보가 비어 있을 때 표시 방식 확인 요청.
formatTravelTime가 빈 문자열을 반환하면 라벨이 비어 있는 채로 행/체브론이 노출됩니다. 의도된 UX가 아니라면 숨김 또는 플레이스홀더를 고려해 주세요.🧩 비어있을 때 숨김 예시
- travelTimeContainerView.isHidden = false - let travelTimeText = formatTravelTime(place: place) - travelTimeLabel.setText(.bodySR, text: travelTimeText, color: UIColor(hexCode: "#757575")) + let travelTimeText = formatTravelTime(place: place) + travelTimeContainerView.isHidden = travelTimeText.isEmpty + travelTimeLabel.setText(.bodySR, text: travelTimeText, color: UIColor(hexCode: "#757575"))Projects/Domain/Sources/Model/Home/HomeError.swift (1)
11-15:LocalizedError준수를 고려해 보세요.사용자에게 에러 메시지를 표시할 때
localizedDescription을 커스텀하려면LocalizedError프로토콜 준수가 유용할 수 있습니다.♻️ 선택적 개선안
-public enum HomeError: Error, Sendable { +public enum HomeError: Error, Sendable, LocalizedError { case networkError(message: String) case serverError(message: String) case unknown(code: String, message: String) + + public var errorDescription: String? { + switch self { + case .networkError(let message), + .serverError(let message): + return message + case .unknown(_, let message): + return message + } + } }Projects/Modules/Networks/Sources/DTO/Travel/TravelDTO.swift (1)
27-29: 테스트 용이성을 위해public init추가를 고려하세요.
CreateUserTravelResponse는Decodable이므로 이니셜라이저가 자동 생성되지만, 유닛 테스트에서 mock 응답을 생성할 때public init이 없으면 인스턴스 생성이 불편할 수 있습니다.♻️ 테스트 용이성을 위한 이니셜라이저 추가
public struct CreateUserTravelResponse: Decodable, Sendable { public let userTravelId: Int + + public init(userTravelId: Int) { + self.userTravelId = userTravelId + } }Projects/Domain/Sources/Model/Travel/CreateTravelRequest.swift (1)
11-21: 날짜 유효성 검증 추가를 고려하세요.
startDate가endDate보다 이후인 경우 잘못된 요청이 생성될 수 있습니다. 도메인 모델 레벨에서 검증하거나, 최소한 문서화를 통해 호출자에게 책임을 명시하는 것이 좋습니다.♻️ 날짜 유효성 검증 추가 예시
public struct CreateTravelRequest: Sendable { public let templateId: Int public let startDate: Date public let endDate: Date - public init(templateId: Int, startDate: Date, endDate: Date) { + public init(templateId: Int, startDate: Date, endDate: Date) throws { + guard startDate <= endDate else { + throw CreateTravelRequestError.invalidDateRange + } self.templateId = templateId self.startDate = startDate self.endDate = endDate } } + +public enum CreateTravelRequestError: Error { + case invalidDateRange +}또는 throwing initializer 대신 factory method나 서비스 레이어에서 검증할 수도 있습니다.
Projects/Features/FollowFeature/Sources/Views/MediaInfoView.swift (1)
178-202: 이미지 로딩 실패 시 fallback 처리를 고려해보세요.이미지 로딩이 실패할 경우를 대비한 처리가 없습니다.
RecommendContentCell에서 사용하는 것처럼 실패 시 placeholder 배경색을 설정하는 것을 권장합니다.💡 이미지 로딩 실패 처리 추가 제안
if let profileURLString = detail.youtube.profileImage, let profileURL = URL(string: profileURLString) { profileImageView.kf.setImage( with: profileURL, placeholder: nil, options: [ .transition(.fade(0.2)), .cacheOriginalImage ] - ) + ) { [weak self] result in + if case .failure = result { + self?.profileImageView.backgroundColor = .systemGray5 + } + } }Projects/Features/FollowFeature/Sources/FollowDetailViewController.swift (1)
246-247: 셀 높이/간격 상수는 컬렉션뷰와 공유하면 유지보수에 유리합니다.
레이아웃 값이 변경될 때 이곳과 셀 레이아웃이 분리되어 있으면 불일치가 생길 수 있어, PlaceListCollectionView에 상수/계산 로직을 모으는 방식도 고려해 주세요.Projects/Features/HomeFeature/Sources/HomeInteractor.swift (1)
89-97: 실패 시 기존 데이터가 빈 값으로 덮일 수 있습니다.
모든 Result를try?로 무시하고 기본값으로 대체하면 네트워크 실패 시 기존 데이터가 사라지고 실패 상태도 노출되지 않습니다. 실패 항목은 기존 데이터를 유지하거나 에러 UI/로그를 노출하는 흐름을 고려해주세요.Projects/Features/HomeFeature/Sources/HomeBuilder.swift (1)
23-34: computed property가 매번 새 인스턴스를 생성합니다.
homeService,followService,travelService가 computed property로 구현되어 매번 접근 시 새 인스턴스가 생성됩니다. 이는 불필요한 객체 생성과 상태 불일치를 유발할 수 있습니다.
lazy var또는Component의shared패턴을 사용하여 인스턴스를 캐싱하는 것을 권장합니다.♻️ shared 패턴 적용 예시
- var homeService: HomeServiceProtocol { - // TODO: 실제 API 연동 시 실제 Service로 교체 - MockHomeService() - } + var homeService: HomeServiceProtocol { + // TODO: 실제 API 연동 시 실제 Service로 교체 + shared { MockHomeService() } + } - var followService: FollowServiceProtocol { - makeFollowService() - } + var followService: FollowServiceProtocol { + shared { makeFollowService() } + } - var travelService: TravelServiceProtocol { - makeTravelService(tokenProvider: dependency.tokenProvider) - } + var travelService: TravelServiceProtocol { + shared { makeTravelService(tokenProvider: dependency.tokenProvider) } + }Projects/Features/HomeFeature/Sources/Mock/MockHomeService.swift (1)
55-66: 에러가 무시되고 있습니다.
fetchAllPopularTrips()가 실패할 경우 에러를 전파하지 않고.success([])를 반환합니다. Mock 서비스라도 에러를 적절히 전파해야 실제 서비스와 동일한 동작을 테스트할 수 있습니다.♻️ 에러 전파 적용
func fetchPopularTrips(category: TripCategory) async -> Result<[PopularTrip], HomeError> { let allTripsResult = await fetchAllPopularTrips() - guard case .success(let allTrips) = allTripsResult else { - return .success([]) - } + switch allTripsResult { + case .success(let allTrips): + if category == .all { + return .success(allTrips.values.flatMap { $0 }) + } else { + return .success(allTrips[category] ?? []) + } + case .failure(let error): + return .failure(error) + } - - if category == .all { - return .success(allTrips.values.flatMap { $0 }) - } else { - return .success(allTrips[category] ?? []) - } }Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift (1)
197-206: 성공/실패 처리 시print()대신 로깅 프레임워크 사용을 권장합니다.
print()는 릴리즈 빌드에서 성능 영향이 있고, 로그 레벨 관리가 불가능합니다.os_log,OSLog, 또는 프로젝트에서 사용하는 로깅 유틸리티를 사용하는 것이 좋습니다.
| public func save(_ value: String, for type: TokenType) { | ||
| keychainStorage.save(value, forKey: type.rawValue) | ||
| } | ||
|
|
||
| public func get(_ type: TokenType) -> String? { | ||
| keychainStorage.load(forKey: type.rawValue) | ||
| } | ||
|
|
||
| public func delete(_ type: TokenType) { | ||
| keychainStorage.delete(forKey: type.rawValue) | ||
| } |
There was a problem hiding this comment.
save와 delete 메서드에서 반환값이 무시되고 있습니다.
keychainStorage.save()와 keychainStorage.delete()의 Bool 반환값이 무시되고 있습니다. 저장/삭제 실패 시 호출자에게 알려야 하는 경우를 고려해 보세요.
💡 반환값 전파 고려
현재 구현도 문제없이 동작하지만, 실패 처리가 필요한 경우를 위해 다음과 같이 변경할 수 있습니다:
- public func save(_ value: String, for type: TokenType) {
- keychainStorage.save(value, forKey: type.rawValue)
+ `@discardableResult`
+ public func save(_ value: String, for type: TokenType) -> Bool {
+ keychainStorage.save(value, forKey: type.rawValue)
}또는 현재 프로토콜 설계가 void 반환을 의도한 것이라면 무시해도 됩니다.
📝 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.
| public func save(_ value: String, for type: TokenType) { | |
| keychainStorage.save(value, forKey: type.rawValue) | |
| } | |
| public func get(_ type: TokenType) -> String? { | |
| keychainStorage.load(forKey: type.rawValue) | |
| } | |
| public func delete(_ type: TokenType) { | |
| keychainStorage.delete(forKey: type.rawValue) | |
| } | |
| `@discardableResult` | |
| public func save(_ value: String, for type: TokenType) -> Bool { | |
| keychainStorage.save(value, forKey: type.rawValue) | |
| } | |
| public func get(_ type: TokenType) -> String? { | |
| keychainStorage.load(forKey: type.rawValue) | |
| } | |
| public func delete(_ type: TokenType) { | |
| keychainStorage.delete(forKey: type.rawValue) | |
| } |
🤖 Prompt for AI Agents
In `@Projects/Data/Sources/Repository/Auth/TokenRepository.swift` around lines 27
- 37, 현재 save(_:for:)와 delete(_:)가 keychainStorage.save(...)와
keychainStorage.delete(...)의 Bool 반환값을 무시하고 있으므로 실패를 호출자에게 전달하도록 변경하세요: update
the public func save(_ value: String, for type: TokenType) and public func
delete(_ type: TokenType) to either return Bool (propagate the result of
keychainStorage.save(...) / keychainStorage.delete(...)) or throw an error based
on the Bool, keeping public func get(_:) unchanged; reference
keychainStorage.save, keychainStorage.delete, save(_:for:) and delete(_:) when
making the change.
| public protocol TokenRepositoryProtocol { | ||
| func save(_ value: String, for type: TokenType) | ||
| func get(_ type: TokenType) -> String? | ||
| func delete(_ type: TokenType) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.swift" | grep -i token | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 384
🏁 Script executed:
rg "class TokenRepository" --type swift -A 10Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1026
🏁 Script executed:
rg "TokenProviderAdapter" --type swift -A 5Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1766
🏁 Script executed:
rg "TokenRepositoryProtocol" --type swift -B 2 -A 10Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 6385
🏁 Script executed:
rg "Sendable" --type swift | grep -i "token\|repository\|provider"Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 885
🏁 Script executed:
rg "KeychainStorageProtocol" --type swift -B 2 -A 5Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 2293
🏁 Script executed:
cat -n Projects/Data/Sources/Repository/Auth/TokenRepository.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1194
🏁 Script executed:
rg "@unchecked Sendable" --type swift -B 2 -A 8Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 5221
TokenRepositoryProtocol과 KeychainStorageProtocol을 명시적으로 Sendable로 준수하세요.
TokenProviderAdapter가 @unchecked Sendable로 표시되어 있으나, TokenRepositoryProtocol이 Sendable을 준수하지 않습니다. TokenProviding이 Sendable을 선언하고 있으므로, 의존성도 명시적으로 Sendable을 준수해야 합니다. @unchecked Sendable을 제거하고 다음을 수정하세요:
KeychainStorageProtocol에: Sendable추가TokenRepositoryProtocol에: Sendable추가
이를 통해 안전성 계약을 명확히 하고 @unchecked 억제를 제거할 수 있습니다.
🤖 Prompt for AI Agents
In `@Projects/Domain/Sources/Interface/Auth/TokenRepositoryProtocol.swift` around
lines 17 - 21, Add Sendable conformance to the related protocol types: update
KeychainStorageProtocol to declare it conforms to Sendable and update
TokenRepositoryProtocol to declare it conforms to Sendable so the dependency
chain matches TokenProviding’s Sendable requirement; then remove the use of
`@unchecked` Sendable from TokenProviderAdapter (or ensure TokenProviderAdapter is
safely Sendable) so the safety contract is explicit. Ensure you modify the
protocol declarations named KeychainStorageProtocol and TokenRepositoryProtocol
accordingly and re-run the compile to confirm no other types need Sendable
adjustments.
| let result = await followService.fetchPlaces(travelId: recommendationId, day: day) | ||
| let places = (try? result.get()) ?? [] |
There was a problem hiding this comment.
에러가 무시되고 빈 배열로 대체됩니다.
try? result.get() ?? [] 패턴은 네트워크 에러를 포함한 모든 에러를 무시합니다. 최소한 에러를 로깅하거나 사용자에게 알리는 것을 권장합니다.
🛡️ 에러 로깅 추가 제안
let result = await followService.fetchPlaces(travelId: recommendationId, day: day)
-let places = (try? result.get()) ?? []
+let places: [TravelPlace]
+switch result {
+case .success(let data):
+ places = data
+case .failure(let error):
+ // TODO: 적절한 로깅 프레임워크 사용
+ print("장소 로드 실패: \(error)")
+ places = []
+}📝 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.
| let result = await followService.fetchPlaces(travelId: recommendationId, day: day) | |
| let places = (try? result.get()) ?? [] | |
| let result = await followService.fetchPlaces(travelId: recommendationId, day: day) | |
| var places: [TravelPlace] | |
| switch result { | |
| case .success(let data): | |
| places = data | |
| case .failure(let error): | |
| // TODO: 적절한 로깅 프레임워크 사용 | |
| print("장소 로드 실패: \(error)") | |
| places = [] | |
| } |
🤖 Prompt for AI Agents
In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift` around
lines 126 - 127, 현재 let places = (try? result.get()) ?? []는 모든 에러를 무시하고 빈 배열로
대체하므로 네트워크 에러를 로깅하거나 사용자에세 알릴 수 있도록 수정하세요: call
followService.fetchPlaces(travelId:recommendationId, day:day), then unwrap
result with a do-catch around try result.get() (or switch on result) and in the
catch/log branch use your logger (or return a failure state) to record the error
before deciding to return [] or propagate the error; update handling around
followService.fetchPlaces and the places variable so errors are not silently
swallowed.
| private func handleCreateTravelError(_ error: CreateTravelError) { | ||
| switch error { | ||
| case .validationFailed(let field, let message): | ||
| print("유효성 검증 실패 - \(field): \(message)") | ||
| case .invalidDateOrder(let message): | ||
| print("날짜 순서 오류: \(message)") | ||
| case .notFoundTemplate(let message): | ||
| print("템플릿 없음: \(message)") | ||
| case .unknown(let code, let message): | ||
| print("알 수 없는 오류 (\(code)): \(message)") | ||
| } | ||
| } |
There was a problem hiding this comment.
에러 발생 시 사용자에게 피드백이 없습니다.
handleCreateTravelError가 에러를 print()로만 출력하고 있어, 사용자는 여행 생성 실패 원인을 알 수 없습니다. Alert이나 토스트 메시지로 에러를 표시하는 것을 권장합니다.
🐛 사용자 피드백 추가 제안
private func handleCreateTravelError(_ error: CreateTravelError) {
+ let message: String
switch error {
case .validationFailed(let field, let message):
- print("유효성 검증 실패 - \(field): \(message)")
+ message = "입력값 오류: \(message)"
case .invalidDateOrder(let message):
- print("날짜 순서 오류: \(message)")
+ message = "날짜를 다시 확인해주세요"
case .notFoundTemplate(let message):
- print("템플릿 없음: \(message)")
+ message = "여행 템플릿을 찾을 수 없습니다"
case .unknown(let code, let message):
- print("알 수 없는 오류 (\(code)): \(message)")
+ message = "오류가 발생했습니다. 다시 시도해주세요"
}
+ // TODO: presenter.showErrorAlert(message: message)
}🤖 Prompt for AI Agents
In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift` around
lines 210 - 221, handleCreateTravelError currently only prints errors to the
console (print calls), so users get no feedback; update this method to show
user-facing feedback instead—map each CreateTravelError case (.validationFailed,
.invalidDateOrder, .notFoundTemplate, .unknown) to an appropriate UI
notification (e.g., present an alert or invoke a toast helper), use a localized,
user-friendly message, and ensure the presentation runs on the main thread; if
this interactor cannot access UI directly, forward the error via a
delegate/callback (e.g., an onError / showError method on a presenter or
delegate) so the UI layer can display the alert/toast.
| func configure(budget: Int) { | ||
| let formatter = NumberFormatter() | ||
| formatter.numberStyle = .decimal | ||
| let formattedNumber = formatter.string(from: NSNumber(value: budget)) ?? "\(budget)" | ||
| budgetLabel.setText(.bodyMSB, text: "\(formattedNumber)원", color: UIColor(hexCode: "#111111")) | ||
| budgetLabel.setText(.bodyMSB, text: "\(budget/10000)만원", color: UIColor(hexCode: "#111111")) | ||
| } |
There was a problem hiding this comment.
정수 나눗셈으로 인한 예산 표시 오류 가능성.
budget/10000 정수 나눗셈은 소수점 이하를 버립니다:
budget=15000→ "1만원" (실제: 1.5만원)budget=5000→ "0만원" (오해의 소지)budget=9999→ "0만원" (잘못된 표시)
만원 단위로만 표시하는 것이 의도된 동작인지 확인이 필요합니다. 만약 그렇다면 반올림 처리를 고려해 주세요.
또한, SwiftLint 경고에 따라 연산자 주위에 공백을 추가해야 합니다.
🔧 반올림 적용 및 연산자 공백 수정 제안
옵션 1: 반올림 적용 (만원 단위)
func configure(budget: Int) {
- budgetLabel.setText(.bodyMSB, text: "\(budget/10000)만원", color: UIColor(hexCode: "#111111"))
+ let rounded = (budget + 5000) / 10000
+ budgetLabel.setText(.bodyMSB, text: "\(rounded)만원", color: UIColor(hexCode: "#111111"))
}옵션 2: 소수점 표시 (더 정확한 표시)
func configure(budget: Int) {
- budgetLabel.setText(.bodyMSB, text: "\(budget/10000)만원", color: UIColor(hexCode: "#111111"))
+ let value = Double(budget) / 10000.0
+ let formatted = value.truncatingRemainder(dividingBy: 1) == 0
+ ? String(format: "%.0f", value)
+ : String(format: "%.1f", value)
+ budgetLabel.setText(.bodyMSB, text: "\(formatted)만원", color: UIColor(hexCode: "#111111"))
}최소 수정: 연산자 공백만 추가
func configure(budget: Int) {
- budgetLabel.setText(.bodyMSB, text: "\(budget/10000)만원", color: UIColor(hexCode: "#111111"))
+ budgetLabel.setText(.bodyMSB, text: "\(budget / 10000)만원", color: UIColor(hexCode: "#111111"))
}🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 75-75: Operators should be surrounded by a single whitespace when they are being used
(operator_usage_whitespace)
🤖 Prompt for AI Agents
In `@Projects/Features/FollowFeature/Sources/Views/BudgetView.swift` around lines
74 - 76, The configure(budget: Int) currently does integer division
(budget/10000) which drops fractions and triggers SwiftLint spacing errors;
change the calculation to use floating-point (e.g., let value = Double(budget) /
10000) and format the string according to desired behavior (either round to
whole 만원 with standard rounding or display one decimal place for precision)
before calling budgetLabel.setText, and ensure spaces around the / operator to
satisfy SwiftLint; update the configure(budget:) implementation and the
budgetLabel.setText invocation accordingly.
| private let buttonStackView = UIStackView().then { | ||
| $0.axis = .horizontal | ||
| $0.spacing = 8 | ||
| $0.distribution = .fillEqually |
There was a problem hiding this comment.
스케일링 규칙 위반 및 타이틀/서브타이틀 래핑 제약 추가 필요
Line 54의 스택뷰 간격이 하드코딩이고, Line 129/134/139의 세로 간격이 adjusted(가로 스케일)로 되어 있어 DSKit 스케일링 규칙과 어긋납니다. 또한 Line 128-136에서 타이틀/서브타이틀이 centerX만 있어 긴 텍스트가 줄바꿈되지 않고 클리핑될 수 있습니다.
다음처럼 세로 간격은 adjustedH, 가로 간격은 adjustedW/adjusted, 그리고 타이틀/서브타이틀에 좌우 제약을 추가해주세요.
🛠️ 제안 수정
private let buttonStackView = UIStackView().then {
$0.axis = .horizontal
- $0.spacing = 8
+ $0.spacing = 8.adjustedW
$0.distribution = .fillEqually
}
titleLabel.snp.makeConstraints {
- $0.top.equalToSuperview().offset(32.adjusted)
- $0.centerX.equalToSuperview()
+ $0.top.equalToSuperview().offset(32.adjustedH)
+ $0.leading.trailing.equalToSuperview().inset(28.adjusted)
}
subtitleLabel.snp.makeConstraints {
- $0.top.equalTo(titleLabel.snp.bottom).offset(16.adjusted)
- $0.centerX.equalToSuperview()
+ $0.top.equalTo(titleLabel.snp.bottom).offset(16.adjustedH)
+ $0.leading.trailing.equalToSuperview().inset(28.adjusted)
}
descriptionLabel.snp.makeConstraints {
- $0.top.equalTo(subtitleLabel.snp.bottom).offset(12.adjusted)
+ $0.top.equalTo(subtitleLabel.snp.bottom).offset(12.adjustedH)
$0.leading.trailing.equalToSuperview().inset(28.adjusted)
}Based on learnings: DSKit 컴포넌트는 세로는 adjustedH, 가로는 adjustedW/adjusted로 스케일링하고 하드코딩 간격을 제거해야 합니다.
Also applies to: 128-141
🤖 Prompt for AI Agents
In `@Projects/Modules/DSKit/Sources/Component/NDGLModalViewController.swift`
around lines 52 - 55, buttonStackView currently uses a hardcoded spacing and
several vertical/horizontal gaps use the wrong scaling; change buttonStackView
spacing to use scaled values (use adjustedW or adjusted for horizontal gaps)
instead of literal 8, and replace vertical spacing constraints that use adjusted
with adjustedH; also add left/right constraints for titleLabel and subtitleLabel
(they currently only have centerX) so long text can wrap—ensure their
leading/trailing are constrained to the container with appropriate
adjustedW/adjusted insets and allow multiline by setting numberOfLines = 0 where
needed (look for symbols: buttonStackView, titleLabel, subtitleLabel and any
constraint calls around lines ~128-141).
|
|
||
| // MARK: - Lifecycle | ||
|
|
||
| public override func viewDidLoad() { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -t f "NDGLModalViewController.swift"Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 145
🏁 Script executed:
head -100 Projects/Modules/DSKit/Sources/Component/NDGLModalViewController.swift | tail -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 574
🌐 Web query:
SwiftLint modifier_order rule public override func Swift conventions
💡 Result:
SwiftLint’s modifier_order opt-in rule enforces a consistent ordering of Swift modifiers. With the default configuration, override must come before the access level (public, internal, etc.), so:
override public func foo() { } // ✅ passes modifier_order
public override func foo() { } // ❌ triggers modifier_orderThis is because SwiftLint’s default preferred_modifier_order starts with override, then acl (access control level), then other categories (setter ACL, dynamic, mutating, lazy, final, etc.). [1]
If you want public override instead, you can customize the rule in .swiftlint.yml via preferred_modifier_order (reordering the categories to match your team’s convention). [1]
Rule reference: SwiftLint documents and examples also show override public ... as the expected order. [1][2]
Sources: [1] [2]
SwiftLint 수정자 순서 규칙 위반 수정
SwiftLint의 modifier_order 규칙에 따라 override는 public 보다 앞에 와야 합니다.
제안된 수정
-public override func viewDidLoad() {
+override public func viewDidLoad() {📝 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.
| public override func viewDidLoad() { | |
| override public func viewDidLoad() { |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 93-93: override modifier should come before public
(modifier_order)
🤖 Prompt for AI Agents
In `@Projects/Modules/DSKit/Sources/Component/NDGLModalViewController.swift` at
line 93, The function declaration in NDGLModalViewController violates
SwiftLint's modifier_order rule; reorder the modifiers on the viewDidLoad method
so that `override` comes before `public` (i.e., change the modifier order for
the viewDidLoad declaration to comply with modifier_order).
| public struct PlaceResponse: Decodable, Sendable { | ||
| public let googlePlaceId: String | ||
| public let thumbnail: String? | ||
| public let latitude: Double | ||
| public let longitude: Double | ||
| public let name: String | ||
| public let regularOpeningHours: String? | ||
| public let googleMapsUri: String | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "googleMapsUri" -C 3 --type=swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 9668
🏁 Script executed:
rg -n "PlaceResponse" -C 2 --type=swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 2788
🏁 Script executed:
fd -e "md" -e "json" -e "yaml" -e "yml" | xargs rg -l "googleMapsUri|PlaceResponse" 2>/dev/null || trueRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 57
백엔드 API 스펙 확인이 필요합니다. googleMapsUri가 정말 필수 필드인지 명확하지 않습니다. 현재 non-optional이므로 응답에서 해당 필드가 누락되면 디코딩이 실패하여 전체 요청이 실패합니다. regularOpeningHours처럼 이미 optional로 처리 중인 필드도 있으므로, 백엔드에서 항상 해당 필드를 보장하지 않는다면 optional로 전환하고 변환 로직도 대응하는 것이 안전합니다.
🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/DTO/Follow/FollowDTO.swift` around lines 55
- 63, PlaceResponse currently declares googleMapsUri as non-optional which will
cause decoding to fail if the backend omits it; change googleMapsUri to an
optional String? on the PlaceResponse struct (like regularOpeningHours) and
update any conversion/usage logic that assumes a non-nil value (e.g.
initializers, mapping functions, or UI consumers that reference
PlaceResponse.googleMapsUri) to safely unwrap or provide a fallback; verify all
places that construct or decode PlaceResponse handle googleMapsUri being nil.
| public func prepare(_ request: URLRequest, target: any TargetType) -> URLRequest { | ||
| var request = request | ||
|
|
||
| if let token = tokenProvider.accessToken() { | ||
| request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
URLRequest addValue vs setValue Authorization header Swift
💡 Result:
setValue(_:forHTTPHeaderField:)sets/replaces the header’s value (if the field already exists, it’s overwritten). [1]addValue(_:forHTTPHeaderField:)appends to an existing header value, inserting the HTTP delimiter,automatically (so repeated calls build a comma-separated list). [1]
For Authorization, you almost always want setValue, because Authorization is normally a single value (e.g., Bearer <token>). If you use addValue more than once, you can accidentally end up with something like Bearer token1,Bearer token2, which many servers will reject. [1][2]
var req = URLRequest(url: url)
req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")Sources:
[1] Swift Foundation URLRequest docs/comments (corelibs-foundation) (fuchsia.googlesource.com)
[2] Discussion of addValue vs setValue behavior (stackoverflow.com)
Citations:
- 1: https://fuchsia.googlesource.com/third_party/swift-corelibs-foundation/%2B/upstream/fuchsia_release/Foundation/URLRequest.swift?utm_source=openai
- 2: https://stackoverflow.com/questions/29815589/difference-between-setting-an-nsmutableurlrequest-header-and-adding-one?utm_source=openai
🏁 Script executed:
# Check if there's any relevant documentation or examples in the repository
find . -type f -name "*.md" -o -name "*.txt" | xargs grep -l "Authorization\|addValue\|setValue" 2>/dev/null | head -10Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 57
🏁 Script executed:
# Search for addValue and Authorization usage in the codebase
rg -t swift "addValue.*Authorization|Authorization.*addValue" --no-headingRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 217
🏁 Script executed:
# Also search for all addValue calls on request objects
rg -t swift "request\.addValue" -A 1 -B 1Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 396
Authorization 헤더는 setValue를 사용하세요.
addValue는 기존 헤더 값에 쉼표 구분자를 붙여 누적되므로, 여러 번 호출 시 Bearer token1,Bearer token2 같은 중복 값이 생길 수 있습니다. Authorization 헤더는 단일 값이어야 하므로 setValue로 덮어써야 합니다.
✅ 수정 제안
- if let token = tokenProvider.accessToken() {
- request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
- }
+ if let token = tokenProvider.accessToken() {
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ }🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Plugin/AuthPlugin.swift` around lines 21 -
26, In prepare(_ request: URLRequest, target: any TargetType) replace the use of
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") with
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") so the
Authorization header is overwritten rather than appended; locate the logic in
the prepare function that reads tokenProvider.accessToken() and update the
header modification call accordingly.
| public final class TravelService: TravelServiceProtocol, @unchecked Sendable { | ||
|
|
||
| private let provider: MoyaProvider<TravelAPI> | ||
| private let dateFormatter: DateFormatter | ||
|
|
||
| public init(provider: MoyaProvider<TravelAPI> = MoyaProvider<TravelAPI>()) { | ||
| self.provider = provider | ||
| self.dateFormatter = DateFormatter() | ||
| self.dateFormatter.dateFormat = "yyyy-MM-dd" | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n Projects/Modules/Networks/Sources/Service/TravelService.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1978
공유 DateFormatter의 스레드 안전성 문제 및 타임존/로케일 설정 필요.
TravelService가 @unchecked Sendable로 지정되었지만 DateFormatter는 스레드 안전성을 보장하지 않습니다. createUserTravel은 async 함수로 여러 작업에서 동시에 호출될 수 있어 공유 DateFormatter에 대한 데이터 레이스 위험이 있습니다. 또한 명시적 locale/timeZone 설정이 없으면 시스템 설정에 따라 날짜 문자열이 변할 수 있습니다. 호출마다 로컬 formatter를 생성하고 POSIX locale 및 UTC timeZone을 설정하세요.
🔧 제안 변경
- private let dateFormatter: DateFormatter
-
public init(provider: MoyaProvider<TravelAPI> = MoyaProvider<TravelAPI>()) {
self.provider = provider
- self.dateFormatter = DateFormatter()
- self.dateFormatter.dateFormat = "yyyy-MM-dd"
} public func createUserTravel(request: CreateTravelRequest) async -> Result<CreateTravelResponse, CreateTravelError> {
+ let dateFormatter = DateFormatter()
+ dateFormatter.locale = Locale(identifier: "en_US_POSIX")
+ dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
+ dateFormatter.dateFormat = "yyyy-MM-dd"
+
let dto = CreateUserTravelRequest(
templateId: request.templateId,
startDate: dateFormatter.string(from: request.startDate),
endDate: dateFormatter.string(from: request.endDate)
)🤖 Prompt for AI Agents
In `@Projects/Modules/Networks/Sources/Service/TravelService.swift` around lines
13 - 22, TravelService currently stores a shared DateFormatter (dateFormatter)
which is not thread-safe and can race when async methods like createUserTravel
run concurrently; instead, remove reliance on the shared dateFormatter and
instantiate a new DateFormatter inside the call sites (e.g., inside
createUserTravel) for each use, and when constructing that per-call formatter
set its locale to Locale(identifier: "en_US_POSIX") and timeZone to
TimeZone(secondsFromGMT: 0) (or use ISO8601DateFormatter if preferred) so date
string output is deterministic and thread-safe; update init to no longer create
a persistent dateFormatter and update any references to use the per-call
formatter.
🔗 연결된 이슈
작업 내용
주요 코드 설명
기타 더 이야기해볼 점