Feat/#34 travel tool#35
Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Walkthrough새로운 TravelTool 기능을 도입하며 날씨 데이터 통합, 도메인 모델 확장(위도/경도 및 위치 정보 추가), DI 구조 업데이트, 기존 기능들의 네비게이션 및 UI 리팩토링을 수행합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant TravelToolVC as TravelToolViewController
participant TravelToolInt as TravelToolInteractor
participant HomeUC as HomeUsecase
participant WeatherRepo as WeatherRepository
participant WeatherSvc as WeatherService
participant API as Google Weather API
User->>TravelToolVC: viewWillAppear()
TravelToolVC->>TravelToolInt: viewWillAppear()
TravelToolInt->>HomeUC: fetchTripSummary()
HomeUC-->>TravelToolInt: MyTripSummary
alt Trip exists
TravelToolInt->>WeatherRepo: fetchForecast(lat, lon, days)
WeatherRepo->>WeatherSvc: getForecast(lat, lon, days)
WeatherSvc->>API: GET /v1/forecast/days:lookup?key=...&location.latitude=...&location.longitude=...&days=...
API-->>WeatherSvc: ForecastResponse
WeatherSvc-->>WeatherRepo: ForecastResponse
WeatherRepo->>WeatherRepo: map to DailyWeatherInfo[]
WeatherRepo-->>TravelToolInt: [DailyWeatherInfo]
TravelToolInt->>TravelToolVC: updateTripCard(TravelToolTripState)
TravelToolInt->>TravelToolVC: updateWeather(TravelToolWeatherState.hasWeather)
else No trip
TravelToolInt->>TravelToolVC: updateWeather(TravelToolWeatherState.noTrip)
end
TravelToolVC-->>User: Display trip card & weather
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Projects/Features/TravelFeature/Sources/TravelInteractor.swift (1)
53-56:⚠️ Potential issue | 🟡 Minor
loadTrips()이중 호출 — 최초 화면 표시 시 데이터 중복 로드
didBecomeActive(Line 55, 기존 코드)와 새로 추가된viewWillAppear()(Line 117) 모두loadTrips()를 호출합니다. RIB이 최초 활성화되면didBecomeActive→loadTrips()직후viewWillAppear→loadTrips()가 연속 실행되어 동일 데이터를 두 번 로드합니다.현재는 목 데이터라 영향이 없지만, 실제 API로 교체되면 초기 진입 시 불필요한 네트워크 요청이 두 번 발생합니다.
viewWillAppear()가 데이터 로딩을 담당하는 것이 의도라면,didBecomeActive의loadTrips()호출을 제거하는 것이 좋습니다.🛠 수정 제안
override func didBecomeActive() { super.didBecomeActive() - loadTrips() }Also applies to: 116-118
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelFeature/Sources/TravelInteractor.swift` around lines 53 - 56, The interactor currently calls loadTrips() from both didBecomeActive() and viewWillAppear(), causing duplicate data loads on first activation; remove the redundant call by deleting or commenting out the loadTrips() invocation inside didBecomeActive() (keeping the viewWillAppear() call if you want view lifecycle to control loading), ensuring only one of didBecomeActive() or viewWillAppear() invokes loadTrips() to prevent double requests.
🧹 Nitpick comments (25)
Projects/Modules/DSKit/Resources/Assets.xcassets/ic_weather_sun_clouds01.imageset/Contents.json (1)
1-15: [전체 날씨 아이콘 에셋 공통] 다크 모드 변형 추가 권장10개의 날씨 아이콘 에셋 모두
appearances항목이 없어 다크 모드 전용 이미지가 정의되어 있지 않습니다. 디자인 스펙상 다크 모드 대응이 필요한 경우 아래와 같이any/darkappearance 변형을 추가하는 것을 고려해 주세요.프로젝트의 iOS 배포 타겟이 17.0이므로
preserves-vector-representation: true를 사용할 수 있습니다.🌙 다크 모드 변형 추가 예시 (예: ic_weather_sunny)
{ "images" : [ { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], "filename" : "ic_weather_sunny.svg", "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ic_weather_sunny_dark.svg", + "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/DSKit/Resources/Assets.xcassets/ic_weather_sun_clouds01.imageset/Contents.json` around lines 1 - 15, The Contents.json for the ic_weather_sun_clouds01.imageset lacks an appearances variant for dark mode; add an "appearances" entry under the appropriate image object to define "any" and "dark" variants (e.g., reference the existing ic_weather_sun_clouds01.svg for "any" and a dark-mode file like ic_weather_sun_clouds01_dark.svg for "dark"), keep "idiom":"universal" and preserve the existing "preserves-vector-representation": true in "properties", and repeat the same appearances pattern for the other 9 weather icon imagesets so each has explicit any/dark variants.Projects/Features/SearchFeature/Sources/SearchViewController.swift (4)
182-195:UICollectionView.CellRegistration/SupplementaryRegistration사용 시 수동register호출이 불필요합니다.
setDataSource()에서CellRegistration과SupplementaryRegistrationAPI를 사용하고 있으므로, 셀과 헤더 등록이 자동으로 처리됩니다.setCollectionView()의 수동register호출은 중복이므로 제거해도 됩니다.♻️ 중복 register 제거 제안
func setCollectionView() { - collectionView.do { - $0.register( - PopularInfoCell.self, - forCellWithReuseIdentifier: PopularInfoCell.cellIdentifier - ) - - $0.register( - SearchResultHeaderView.self, - forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, - withReuseIdentifier: SearchResultHeaderView.reusableViewIdentifier - ) - } + // CellRegistration과 SupplementaryRegistration이 자동 등록을 처리합니다. }메서드 자체가 비게 되면
setCollectionView()호출도 함께 제거할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/SearchFeature/Sources/SearchViewController.swift` around lines 182 - 195, The setCollectionView() method currently calls collectionView.register for PopularInfoCell and SearchResultHeaderView but setDataSource() uses UICollectionView.CellRegistration and SupplementaryRegistration which handle registration automatically; remove the manual collectionView.register calls (the body of setCollectionView()) to avoid duplicate registration, and if setCollectionView() becomes empty also remove its invocation wherever called; refer to setCollectionView(), setDataSource(), PopularInfoCell, SearchResultHeaderView, and the CellRegistration/SupplementaryRegistration usage to locate the code to change.
396-421: SwiftLint 경고: 사용하지 않는 클로저 파라미터와 implicit return.정적 분석에서 몇 가지 경고가 감지되었습니다:
- Line 398:
indexPath파라미터가 사용되지 않음 →_로 대체- Line 414:
elementKind,indexPath파라미터가 사용되지 않음 →_로 대체- Line 319, 398, 412:
return키워드를 implicit return으로 변경 가능♻️ 미사용 파라미터 수정 제안
- return UICollectionView.CellRegistration { cell, indexPath, item in - cell.configure( + UICollectionView.CellRegistration { cell, _, item in + cell.configure(- ) { [weak dataSource] headerView, elementKind, indexPath in + ) { [weak dataSource] headerView, _, _ in🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/SearchFeature/Sources/SearchViewController.swift` around lines 396 - 421, In createResultTripCellRegistration and createHeaderRegistration update unused closure parameters to use _ (replace indexPath in the CellRegistration closure and replace elementKind and indexPath in the SupplementaryRegistration closure) and remove the explicit return keywords where the closures or functions can rely on implicit returns; specifically modify createResultTripCellRegistration, the UICollectionView.CellRegistration closure for PopularInfoCell/SearchResultPresentationModel.ResultTrip, and createHeaderRegistration's SupplementaryRegistration for SearchResultHeaderView to eliminate the unused params and convert explicit "return" to implicit returns.
278-295: 알 수 없는 섹션에 대해 dequeue하지 않은UICollectionReusableView()를 반환하면 런타임 문제가 발생할 수 있습니다.Line 283에서
UICollectionReusableView()를 직접 생성하여 반환하고 있습니다. UIKit은 일반적으로 dequeue된 뷰를 기대합니다. 현재 섹션이 하나뿐이라 이 경로에 도달할 가능성은 낮지만, 안전하게nil을 반환하는 것이 좋습니다.♻️ nil 반환으로 변경
- guard SearchResultSectionKind(rawValue: indexPath.section) != nil else { - return UICollectionReusableView() - } + guard SearchResultSectionKind(rawValue: indexPath.section) != nil else { + return nil + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/SearchFeature/Sources/SearchViewController.swift` around lines 278 - 295, In configureSupplementaryView(), the closure assigned to dataSource.supplementaryViewProvider currently returns a newly created UICollectionReusableView() for unknown sections which can cause runtime issues; update the closure so that when SearchResultSectionKind(rawValue: indexPath.section) is nil it returns nil instead of instantiating UICollectionReusableView(), keeping the existing header dequeue path (using headerRegistration) intact; this touches the configureSupplementaryView method and the dataSource.supplementaryViewProvider closure.
252-259:dataSource접근 방식이 비일관적입니다.Line 37에서
dataSource는UICollectionViewDiffableDataSource<...>!(IUO)로 선언되어 있지만, Line 258에서는dataSource?로 옵셔널 체이닝을 사용하고 있습니다. 동작에 문제는 없지만,setDataSource()가viewDidLoad에서 호출되므로 항상 초기화되어 있다는 전제하에 일관되게 사용하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/SearchFeature/Sources/SearchViewController.swift` around lines 252 - 259, The code inconsistently accesses the IUO dataSource: since dataSource is declared as UICollectionViewDiffableDataSource...! and initialized in setDataSource() called from viewDidLoad, change optional chaining in applySnapShot to use the non-optional access (e.g., call dataSource.apply(snapshot, animatingDifferences: true) instead of dataSource?) so usage is consistent with the declaration; verify applySnapShot, setDataSource, viewDidLoad and the dataSource declaration to ensure initialization order remains correct.Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift (1)
226-237:handleCreateTravelError데드 코드이 함수는 현재 어디에서도 호출되지 않습니다.
catch블록은 에러 타입에 무관하게 단순print(error)만 실행하며 이 핸들러를 사용하지 않습니다. 제거하거나 실제 에러 분기 처리에 활용하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift` around lines 226 - 237, The handleCreateTravelError(_:) function is dead code; either remove it or wire it into the error handling path: replace the generic print(error) in the catch blocks that handle CreateTravelError with a call to handleCreateTravelError(error as? CreateTravelError ?? .unknown(-1, "\(error)")), and if you choose removal, delete handleCreateTravelError(_:) and any unused imports to keep the code clean. Ensure you update any catch clauses where CreateTravelError may be thrown (e.g., in createTravel or related do/catch) so the compiler no longer flags the function as unused or leaves generic prints.Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift (1)
31-37: 테스트 환경을 위한 번들 폴백 선택적 개선 제안
baseURL과 달리weatherApiKey(및 기존apiKey)는Bundle.main만 사용합니다. 유닛 테스트에서 WeatherService를 실제로 호출하는 경우.xctest번들에서 키를 찾지 못해fatalError가 발생할 수 있습니다.baseURL의 패턴과 통일하면 테스트 환경 안정성이 높아집니다.♻️ 제안 수정
public static var weatherApiKey: String { - let bundle = Bundle.main + let bundle = Bundle.allBundles.first { $0.bundlePath.hasSuffix(".xctest") } ?? Bundle.main guard let key = bundle.infoDictionary?["GOOGLE_WEATHER_API_KEY"] as? String else { fatalError("GOOGLE_WEATHER_API_KEY not found in Info.plist") } return key }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift` around lines 31 - 37, The static var weatherApiKey (and likewise apiKey) currently only reads from Bundle.main and fatals in tests; update these accessors to follow the same bundle-fallback pattern used by baseURL: first attempt Bundle.main.infoDictionary, then attempt the test/XCTest bundle (e.g., Bundle(for: NetworkConfiguration.self) or the bundle that contains the test resources), and only call fatalError if neither bundle contains the key. Modify the static properties weatherApiKey and apiKey to try the secondary bundle before failing so unit tests using the .xctest bundle won't crash.Projects/Features/HomeFeature/Sources/HomeInteractor.swift (2)
182-184:viewWillAppear에서 매번 전체 데이터를 재요청합니다.탭 전환이나 네비게이션 복귀 시마다
fetchHomeData()가 호출되어 로딩 상태 표시와 함께 모든 API를 다시 호출합니다. 이로 인해 화면 깜빡임과 불필요한 네트워크 요청이 발생할 수 있습니다. 마지막 fetch 시간을 기록하여 일정 간격 이내에는 재요청을 건너뛰는 등의 방어 로직을 고려해보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/HomeFeature/Sources/HomeInteractor.swift` around lines 182 - 184, The viewWillAppear() currently calls fetchHomeData() unconditionally causing redundant network requests and UI flicker; modify HomeInteractor to track the last successful fetch timestamp (e.g., lastFetchDate property) and in viewWillAppear() check the elapsed time against a defined freshness interval (e.g., 1-5 minutes) and only call fetchHomeData() if stale or if a forced refresh flag is set; ensure pull-to-refresh and explicit refresh paths clear/override lastFetchDate so they always trigger fetchHomeData(), and keep access to lastFetchDate thread-safe if used from multiple queues.
112-128:usecase참조 방식이 일관적이지 않습니다.Line 112에서
let usecase = self.usecase로 로컬 변수를 만들었지만, Line 127-128에서는 다시self.usecase를 직접 사용하고 있습니다. 동작에는 문제가 없지만, 하나의 방식으로 통일하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/HomeFeature/Sources/HomeInteractor.swift` around lines 112 - 128, 현재 블록에서 let usecase = self.usecase로 로컬 참조를 만들었으니 이후 호출도 일관되게 사용하도록 수정하세요: self.usecase.fetchCategoryList() 및 self.usecase.fetchPopularTripList(id:) 대신 로컬 usecase.fetchCategoryList() 및 usecase.fetchPopularTripList(id:)로 바꿔서 fetchCategoryList()와 fetchPopularTripList(id:) 호출이 모두 동일한 usecase 변수(및 toHomeModel()/toPopularHomeModel() 매핑)로 이루어지게 하세요.Projects/Data/Sources/Transform/UserTravelTransform.swift (1)
44-50:DateFormatter가 호출 시마다 새로 생성됩니다.
toDate()메서드가 호출될 때마다DateFormatter를 새로 할당합니다.DateFormatter는 생성 비용이 높으므로static으로 캐싱하는 것이 좋습니다. 이 부분은 기존 코드이지만, 이번 변경과 관련된 파일이라 참고로 남깁니다.♻️ 리팩터링 제안
extension String { + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd" + return formatter + }() + func toDate() -> Date? { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.dateFormat = "yyyy-MM-dd" - return formatter.date(from: self) + return String.dateFormatter.date(from: self) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Data/Sources/Transform/UserTravelTransform.swift` around lines 44 - 50, The toDate() extension on String currently allocates a new DateFormatter on every call; change it to use a cached static DateFormatter (e.g., a private static property on String like private static let cachedDateFormatter) configured with locale "en_US_POSIX" and dateFormat "yyyy-MM-dd", and have func toDate() call that cached formatter instead of creating a new one; reference the extension String and func toDate() to locate where to replace the per-call DateFormatter with the static cached instance.Projects/Data/Sources/Repository/Weather/WeatherRepository.swift (2)
31-31:compactMap실패 항목 조용히 제거 — 디버깅 어려움
toDomain()이 nil을 반환하는 항목(잘못된displayDate등)은 조용히 제거됩니다. 예상보다 날씨 카드 수가 적어도 원인을 파악하기 어렵습니다. 디버그 빌드에서 로그를 남기면 추적이 쉬워집니다.♻️ 디버그 로그 추가 제안
-return response.forecastDays.compactMap { $0.toDomain() } +let mapped = response.forecastDays.map { $0.toDomain() } +#if DEBUG +let dropped = mapped.filter { $0 == nil }.count +if dropped > 0 { + print("[WeatherRepository] toDomain() failed for \(dropped) day(s)") +} +#endif +return mapped.compactMap { $0 }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Data/Sources/Repository/Weather/WeatherRepository.swift` at line 31, 현재 response.forecastDays.compactMap { $0.toDomain() }가 toDomain()가 nil을 반환하는 항목을 조용히 제거해 원인 파악이 어렵습니다; response.forecastDays를 순회하면서 let domain = $0.toDomain()로 변환 실패 시 (예: 잘못된 displayDate) 디버그 빌드에서 logger 또는 print로 해당 항목의 식별자(예: $0.displayDate 또는 $0.id)와 오류 컨텍스트를 남기고, 성공한 항목만 수집하도록 변경해 주세요 (참조 심볼: toDomain(), response.forecastDays).
9-11:import정렬 순서 수정 (SwiftLint 경고)
Domain(D)이Foundation(F)보다 먼저 와야 합니다.♻️ import 정렬 수정
+import Domain import Foundation -import Domain import Networks🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Data/Sources/Repository/Weather/WeatherRepository.swift` around lines 9 - 11, Reorder the import statements to satisfy SwiftLint import ordering by placing import Domain before import Foundation; specifically update the top of WeatherRepository.swift so the imports read "import Domain", then "import Foundation", then "import Networks" (refer to the existing import Foundation, import Domain, import Networks lines to locate the change).Projects/Features/TravelToolFeature/Sources/Views/TravelToolWeatherView.swift (2)
68-82:.noTrip,.preparing케이스의 타이틀 문자열 중복두 케이스 모두
"여행 중 날씨"로 동일한 텍스트를 설정합니다. 추후 문자열 변경 시 한 곳만 수정하는 실수를 방지하기 위해 공통 처리로 추출하면 좋습니다.♻️ 공통 타이틀 설정 추출 제안
func configure(_ state: TravelToolWeatherState) { noTripView.isHidden = true preparingView.isHidden = true collectionView.isHidden = true + titleLabel.setText(.subTitleMSB, text: "여행 중 날씨", color: DSKitAsset.Colors.black700.color) switch state { case .noTrip: - titleLabel.setText(.subTitleMSB, text: "여행 중 날씨", color: DSKitAsset.Colors.black700.color) noTripView.isHidden = false case .preparing: - titleLabel.setText(.subTitleMSB, text: "여행 중 날씨", color: DSKitAsset.Colors.black700.color) preparingView.isHidden = false case .hasWeather(let title, let forecasts): titleLabel.setText(.subTitleMSB, text: "\(title) 여행 중 날씨", color: DSKitAsset.Colors.black700.color)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/Views/TravelToolWeatherView.swift` around lines 68 - 82, The .noTrip and .preparing branches duplicate the same title string "여행 중 날씨"; extract the common title-setting into shared logic before the switch (or at the top of the switch) so both cases reuse it instead of calling titleLabel.setText separately. Locate the switch on state in TravelToolWeatherView (the case handlers .noTrip, .preparing, and .hasWeather and the titleLabel.setText calls) and change it so the common title is set once (e.g., compute a defaultTitle or call titleLabel.setText(.subTitleMSB, text: defaultTitle, color: DSKitAsset.Colors.black700.color)) and keep the .hasWeather branch setting its dynamic "\(title) 여행 중 날씨" as before.
35-45:cv변수명 SwiftLint 경고SwiftLint가
cv식별자명이 너무 짧다고 경고합니다(identifier_name 규칙).♻️ 변수명 수정
-let cv = UICollectionView(frame: .zero, collectionViewLayout: layout) -cv.showsHorizontalScrollIndicator = false -cv.backgroundColor = .clear -cv.register(WeatherDayCell.self, forCellWithReuseIdentifier: WeatherDayCell.identifier) -return cv +let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) +collectionView.showsHorizontalScrollIndicator = false +collectionView.backgroundColor = .clear +collectionView.register(WeatherDayCell.self, forCellWithReuseIdentifier: WeatherDayCell.identifier) +return collectionView🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/Views/TravelToolWeatherView.swift` around lines 35 - 45, Rename the short local variable cv inside the collectionView property closure to a longer descriptive name (e.g., collectionViewInstance) to satisfy SwiftLint's identifier_name rule; update all references in that closure (the showsHorizontalScrollIndicator, backgroundColor, register(WeatherDayCell.self, forCellWithReuseIdentifier: WeatherDayCell.identifier), and the return statement) so they use collectionViewInstance instead of cv.Projects/Data/Sources/Transform/WeatherTransform.swift (1)
9-11:import정렬 순서 수정 (SwiftLint 경고)알파벳 순서상
Domain(D)이Foundation(F)보다 앞에 와야 합니다.♻️ import 정렬 수정
+import Domain import Foundation -import Domain import Networks🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Data/Sources/Transform/WeatherTransform.swift` around lines 9 - 11, 현재 import 정렬이 알파벳 순서를 따르지 않아 SwiftLint 경고가 발생합니다; 파일의 import 블록(현재 선언된 Foundation, Domain, Networks)을 알파벳 순서로 재정렬하여 Domain, Foundation, Networks 순서로 정렬하고 불필요한 공백이나 중복 import가 없는지 확인해 주세요.Projects/Data/Sources/DI/WeatherServiceFactory.swift (1)
9-11:import정렬 순서 수정 (SwiftLint 경고)
Networks와Moya의 순서가 알파벳 순서(Moya→Networks)와 반대입니다.♻️ import 정렬 수정
import Foundation -import Networks import Moya +import Networks🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Data/Sources/DI/WeatherServiceFactory.swift` around lines 9 - 11, Reorder the import statements so they follow alphabetical order to satisfy SwiftLint: change the import block (currently listing Foundation, Networks, Moya) to list Foundation, Moya, Networks; update the import declarations at the top of WeatherServiceFactory.swift so Moya appears before Networks.Projects/Domain/Sources/Model/Weather/WeatherInfo.swift (1)
15-15:weatherType를 타입 안전한 enum으로 교체 권장
weatherType: String은WeatherIconMapper에서 문자열 리터럴과 직접 비교됩니다. API 응답에 알 수 없는 값이 오거나 오타가 생겨도 컴파일 타임에 감지할 수 없고, 항상 default 케이스로 조용히 fallback됩니다.♻️ WeatherType enum 도입 제안
WeatherInfo.swift에 enum 추가:+public enum WeatherType: String { + case clear = "CLEAR" + case mostlyClear = "MOSTLY_CLEAR" + case partlyCloudy = "PARTLY_CLOUDY" + case mostlyCloudy = "MOSTLY_CLOUDY" + case cloudy = "CLOUDY" + case foggy = "FOGGY" + case lightRain = "LIGHT_RAIN" + case scatteredShowers = "SCATTERED_SHOWERS" + case rain = "RAIN" + case heavyRain = "HEAVY_RAIN" + case showers = "SHOWERS" + case lightSnow = "LIGHT_SNOW" + case snow = "SNOW" + case heavySnow = "HEAVY_SNOW" + case blizzard = "BLIZZARD" + case flurries = "FLURRIES" + case thunderstorm = "THUNDERSTORM" + case thunderstorms = "THUNDERSTORMS" +} public struct DailyWeatherInfo { public let date: Date public let maxTemperature: Double public let minTemperature: Double - public let weatherType: String + public let weatherType: WeatherType이후
WeatherTransform.swift와WeatherIconMapper도 enum 기반으로 수정.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Domain/Sources/Model/Weather/WeatherInfo.swift` at line 15, Replace the loose String-typed weatherType with a type-safe WeatherType enum in WeatherInfo (add cases for known API values plus an .unknown(rawValue:) or .unknown case with RawRepresentable/Codable support), update WeatherTransform to map API responses into WeatherInfo using WeatherType (initializing from the raw string and falling back to .unknown), and change WeatherIconMapper to switch on WeatherType rather than comparing string literals; ensure WeatherType implements Codable/RawRepresentable (or a failable init(rawValue:)) so decoding handles unknown API values gracefully and existing fallback logic remains intact.Projects/Features/TravelToolFeature/Sources/TravelToolViewController.swift (2)
57-59: 배경색에 하드코딩된.white사용 — 디자인 시스템 색상 토큰 권장다른 뷰들이
DSKitAsset.Colors를 사용하는 것과 달리.white가 직접 사용되었습니다. 다크 모드 또는 향후 테마 변경 시 일관성이 깨질 수 있습니다.♻️ 개선 예시
func setStyle() { - view.backgroundColor = .white + view.backgroundColor = DSKitAsset.Colors.white.color }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/TravelToolViewController.swift` around lines 57 - 59, The background color is hardcoded to .white in TravelToolViewController.setStyle(), which bypasses the design system and breaks theming; change the assignment to use the design system color token (for example DSKitAsset.Colors.primaryBackground or the appropriate DSKitAsset.Colors member used elsewhere) so the view uses DS colors and supports dark mode/themes, updating only the line inside the setStyle() method.
38-41:viewWillAppear에서 매 탭 선택 시마다 데이터 재요청 발생탭을 전환할 때마다
viewWillAppear가 호출되어 여행 정보 + 날씨 API가 반복 호출됩니다. 현재는 의도된 동작이나, 빠른 탭 전환 시 중복 요청이 발생할 수 있으므로 인터랙터 레벨에서 로딩 중 중복 요청 방지나 기본 캐싱 전략 도입을 고려해 보세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/TravelToolViewController.swift` around lines 38 - 41, 현재 TravelToolViewController의 viewWillAppear에서 listener?.viewWillAppear()가 매 탭 전환마다 호출되어 인터랙터 레벨에서 여행 정보/날씨 API가 중복 요청되는 문제가 있습니다; 수정 방법은 인터랙터(또는 Interactor 프로토콜을 구현한 클래스)에 요청 중복 방지 로직을 추가하는 것입니다—예: TravelToolInteractor에 Bool 프로퍼티(isLoading) 또는 요청 키 기반 캐시를 도입하고 public 메서드(fetchTravelInfo, fetchWeather 등)가 호출될 때 이미 요청 중이면 새 요청을 무시하거나 캐시된 응답을 반환하도록 하며, 요청 완료 시 isLoading을 false로 설정(또는 캐시 만료 정책 적용)하고 필요하면 이전 네트워크 작업을 취소할 수 있는 취소 토큰을 추가하세요; 이 변경으로 TravelToolViewController의 viewWillAppear나 listener?.viewWillAppear() 호출은 그대로 두어도 되지만 인터랙터 내부에서 중복 호출을 방지합니다.Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift (2)
93-95: 빈TravelToolListener구현 — 향후 이벤트 처리 예정 여부 확인현재
TravelToolListener에 대한 빈 구현만 있습니다. TravelTool에서 탭바로 전달해야 할 이벤트(예: 여행 추가 후 홈 탭 전환 등)가 있다면 여기에 추가해야 합니다.새 이슈를 열어
TravelToolListener의 필요 이벤트를 구현하도록 추적할까요?🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift` around lines 93 - 95, The empty TravelToolListener extension on TabBarInteractor should either be populated with the concrete event handlers or tracked via an issue: add implementations for the expected callbacks (e.g., methods named like travelDidAdd(_:), travelDidUpdate(_:), or travelShouldSwitchToHome()) inside the extension so they call TabBarInteractor routines to switch tabs or update state, or if you don’t have the event contract yet, create a tracking issue referencing TravelToolListener and TabBarInteractor and add a TODO comment in the extension pointing to that issue ID so future work will implement events such as "travel added -> switch to Home" and related handling.
86-88: 탭 인덱스 매직 넘버 및 미사용 파라미터
presenter.switchToTab(at: 2)의2가 하드코딩되어 있어 탭 순서 변경 시 조용히 오작동합니다. 또한title,startDate,endDate파라미터가 현재 구현에서 전혀 사용되지 않습니다 — 이전 구현에서 이 값들을 활용하던 로직이 제거된 것이라면 의도한 것인지 확인이 필요합니다.♻️ 개선 예시 (MainInteractor와 함께 공유 상수 사용)
func homeDidAddTrip(title: String, startDate: Date, endDate: Date) { - presenter.switchToTab(at: 2) + presenter.switchToTab(at: TabIndex.myTrip) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift` around lines 86 - 88, homeDidAddTrip(title:startDate:endDate:) currently ignores its parameters and uses a magic number in presenter.switchToTab(at: 2); replace the hardcoded 2 with a shared, named tab index (e.g., Tabs.trips or TabIndex.trips) used across MainInteractor/Presenter and update presenter.switchToTab(at:) call to use that constant; also either use the incoming title/startDate/endDate (forward them to presenter or storage) or remove them from the signature (or prefix with _ / add a TODO comment) so they are not misleading — update the function homeDidAddTrip(...) accordingly and ensure callers are adjusted to match.Projects/Features/MainFeature/Sources/MainInteractor.swift (1)
56-59: 탭 인덱스 매직 넘버 → 명명된 상수 추출 권장
switchToTab(at: 1)의1은 홈 탭을 가리키지만, 탭 순서가 변경될 경우 이 값이 조용히 잘못된 탭을 가리킬 수 있습니다.TabBarInteractor.homeDidAddTrip에서 사용하는2와 함께, 탭 인덱스를 공유 열거형이나 네임스페이스 상수로 추출하는 것을 권장합니다.♻️ 개선 예시
+// TabBarFeature 또는 공유 모듈에 정의 +enum TabIndex { + static let travelTool = 0 + static let home = 1 + static let myTrip = 2 +} func followDetailDidViewTrip() { router?.detachFollow() - router?.switchToTab(at: 1) + router?.switchToTab(at: TabIndex.home) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MainFeature/Sources/MainInteractor.swift` around lines 56 - 59, Replace the magic numeric tab indices with a shared named constant or enum and update all call sites; specifically, create a common TabIndex (or TabBar.Tab) enum/struct that defines cases/properties like home (and others) and replace the literal in MainInteractor.followDetailDidViewTrip's router?.switchToTab(at: 1) and the literal used in TabBarInteractor.homeDidAddTrip (currently 2) to use TabIndex.home (or the equivalent named constant) so tab ordering changes remain safe and consistent.Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift (3)
204-211:DateFormatter를 호출 시마다 새로 생성하고 있습니다.
DateFormatter는 생성 비용이 높은 객체입니다.convertToState(line 169)에서 매번 두 번씩 호출되며,viewWillAppear마다 실행됩니다.static프로퍼티로 캐싱하면 불필요한 할당을 줄일 수 있습니다.♻️ 수정 제안
extension Date { + private static let travelToolKoreanFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + formatter.dateFormat = "M월 d일" + return formatter + }() + func toTravelToolKoreanMMdd() -> String { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "ko_KR") - formatter.dateFormat = "M월 d일" - return formatter.string(from: self) + return Date.travelToolKoreanFormatter.string(from: self) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift` around lines 204 - 211, The Date extension's toTravelToolKoreanMMdd() creates a new DateFormatter on every call causing unnecessary allocations (it's called twice per convertToState and on every viewWillAppear); fix this by adding a cached static DateFormatter (e.g., private static let koreanMMddFormatter) inside the Date extension and have toTravelToolKoreanMMdd() use that shared formatter; ensure the formatter is configured once with locale "ko_KR" and dateFormat "M월 d일" so callers like convertToState and viewWillAppear stop creating new formatters.
58-62:didBecomeActive와viewWillAppear에서fetchTripInfo()를 중복 호출합니다.RIBs에서 인터랙터가 최초 attach될 때
didBecomeActive가 호출되고, 뷰가 나타날 때viewWillAppear가 호출됩니다. 최초 로드 시 두 메서드가 연속으로 실행되어fetchTripInfo()가 두 번 호출됩니다. 두 번째 호출이 첫 번째를 취소(line 72)하지만, 불필요한 네트워크 요청과 UI 깜빡임이 발생할 수 있습니다.
viewWillAppear가 탭 전환 시마다 갱신을 담당하므로,didBecomeActive에서의 호출은 제거해도 됩니다.🔧 수정 제안
override func didBecomeActive() { super.didBecomeActive() - - fetchTripInfo() }Also applies to: 196-200
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift` around lines 58 - 62, didBecomeActive()에서 호출한 fetchTripInfo()가 viewWillAppear()에서도 호출되어 최초 어태치 시 중복 네트워크 요청과 UI 깜빡임을 유발하니, Interactor의 didBecomeActive()에서 fetchTripInfo() 호출을 제거하고 viewWillAppear()에만 남겨 두어 탭 전환 시 갱신 동작은 유지하세요; 동일한 중복 호출이 파일 내 다른 위치(라인 범위 196-200)에도 존재하므로 해당 위치에서도 didBecomeActive/유사 초기화 경로에서의 중복 fetchTripInfo() 호출을 삭제하여 단일 진입점(viewWillAppear 또는 탭 갱신 담당 메서드)만이 네트워크 요청을 트리거하도록 정리하세요.
74-97: Task 내부에서guard let self이후MainActor.run클로저들이self를 강하게 캡처합니다.Line 75의
guard let self이후self가 로컬 강한 참조로 바인딩되어,MainActor.run클로저들(lines 95, 101, 116, 150)에서 암묵적으로 강한 캡처됩니다.willResignActive에서 Task를 취소하므로 실질적인 retain cycle 위험은 낮지만, 취소 체크 사이의 구간에서 인터랙터가 예상보다 오래 유지될 수 있습니다.현재 구조에서는
willResignActive의 취소 처리가 이를 방어하고 있어 실질적 문제는 아니지만, 인지해 두시면 좋겠습니다.Also applies to: 100-103, 116-118, 150-157
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift` around lines 74 - 97, The Task binds self strongly with "guard let self" then later MainActor.run closures implicitly capture that strong reference; change to avoid retaining self by not creating a strong local for the whole Task scope—remove "guard let self" and instead capture self weakly in each async closure (use [weak self, tripState] in MainActor.run) or use a weakSelf/guard pattern inside each MainActor.run closure before calling presenter.updateTripCard, convertToState, or other methods (e.g., presenter.updateTripCard, convertToState, usecase.fetchMyTripInfo) so the interactor can be released if the Task is cancelled (also apply same weak-capture fix to other MainActor.run occurrences and closures mentioned and referenced by willResignActive).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Projects/Data/Sources/Transform/WeatherTransform.swift`:
- Around line 24-25: The mapping in WeatherTransform currently uses
maxTemperature?.degrees ?? 0 and minTemperature?.degrees ?? 0 which collapses
missing data into a valid 0°C value; change the transform so it preserves nil
when the source is missing (e.g., map to Optional Double by using
maxTemperature?.degrees and minTemperature?.degrees without the ?? 0) and then
update the domain model DailyWeatherInfo temperature fields to be Double? (or
use Double.nan consistently as a sentinel) and ensure the presentation layer
renders nil (or Double.nan) as a placeholder (e.g., "–") rather than "0°C".
- Around line 15-20: The code in WeatherTransform uses
Calendar.current.date(from: components) without an explicit time zone, and
elsewhere DateFormatter is used without setting timezone, which makes the code
implicitly depend on the device locale and risks incorrect date calculations;
update WeatherTransform to set components.timeZone (e.g.,
TimeZone(secondsFromGMT: 0) or the intended TZ) before calling
Calendar.current.date(from:), and ensure any DateFormatter instances (e.g., in
UserTravelTransform) explicitly set formatter.timeZone to the same intended zone
so the intent is clear and consistent across WeatherTransform and related
formatting code.
In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift`:
- Line 214: Remove the debug print statement that logs "여행 생성 성공 - userTravelId:
\(response.userTravelId)" in FollowDetailInteractor (the response handling code
where response.userTravelId is used) and either delete it or replace it with a
structured logging call (e.g., use the app's logger/info method) so production
logging is consistent and not using print; ensure the replacement uses the same
context (userTravelId) and log level appropriate for success messages.
- Around line 200-213: NDGLModalViewController currently allows system
swipe-to-dismiss so onLater/onViewTrip closures (triggered in
FollowDetailInteractor via presenter.showTripCreatedModal) are not always
called; fix by either setting isModalInPresentation = true in
NDGLModalViewController (around init/config where modalPresentationStyle =
.overFullScreen is set) or implement UIAdaptivePresentationControllerDelegate
(add presentationControllerDidDismiss(_:)) to invoke the same dismissal handling
that dimViewTapped/onCancelTapped/onActionTapped would (call the
onLater/onViewTrip handlers or directly call router?.detachTripCalendar() and
listener?.detachFollowDetail()/listener?.followDetailDidViewTrip() as
appropriate). Also remove the debug print("여행 생성 성공 - userTravelId:
\(response.userTravelId)") from the trip-creation success path so no stray logs
remain.
In `@Projects/Features/HomeFeature/Sources/HomeInteractor.swift`:
- Around line 156-177: The Task created in fetchPopularTrips is not stored so
concurrent tasks can race and overwrite homeDataRelay; add a private var
fetchPopularTask: Task<Void, Never>? to the class, then in fetchPopularTrips
cancel any existing fetchPopularTask before assigning a new Task that performs
the current fetch (use usecase.fetchPopularTripList(...)), keep the [weak self]
capture, check Task.isCancelled as already done, and on completion or in the
catch/finally clear fetchPopularTask = nil so subsequent calls behave correctly
(similar to how fetchDataTask is managed).
In `@Projects/Features/MainFeature/Sources/MainBuilder.swift`:
- Line 25: The class declaration for MainComponent lists dependency protocols
without a space after a comma between PopularTravelDependency and
SearchDependency, violating SwiftLint's comma rule; edit the MainComponent
declaration (the line containing "final class MainComponent:
Component<MainDependency>, FollowDetailDependency,
PopularTravelDependency,SearchDependency, SettingDependency, TabBarDependency")
and insert a single space after the comma so it reads
"...PopularTravelDependency, SearchDependency..." to satisfy SwiftLint.
In `@Projects/Features/SearchFeature/Sources/SearchInteractor.swift`:
- Around line 76-97: In fetchData (and specifically inside the Task assigned to
fetchDataTask), update the catch block to treat CancellationError differently:
detect if the caught error is a CancellationError (or Task.isCancelled) and in
that case do nothing/return without calling presenter.showErrorView(true) (but
ensure presenter.setLoading(false) is not incorrectly shown for cancelled tasks
if you want to keep loading state unchanged), otherwise handle non-cancellation
errors as before (call presenter.showErrorView(true) and setLoading(false));
reference fetchDataTask, usecase.searchTemplate(keyword:), and
presenter.showErrorView to locate where to change the error handling.
In `@Projects/Features/SearchFeature/Sources/SearchViewController.swift`:
- Around line 374-390: In emptyLayout(), replace the direct
NSCollectionLayoutGroup(layoutSize:) initializer with a factory method and
include the item as a subitem so the group is not empty; specifically, create
the group using NSCollectionLayoutGroup.horizontal(layoutSize:subitems:) or
.vertical(layoutSize:subitems:) (whichever matches intended layout) and pass the
previously created item in the subitems array; keep the same groupSize and
itemSize definitions and return the section built from that group.
In `@Projects/Features/TravelToolFeature/Project.swift`:
- Around line 8-10: 현재 파일의 import 문들이 알파벳 순으로 정렬되어 있지 않아 SwiftLint
`sorted_imports` 규칙을 위반하고 있습니다; Project.swift 상단의 import 목록(import
ProjectDescription, import ProjectDescriptionHelpers, import DependencyPlugin)을
알파벳 순으로 정렬하여 import DependencyPlugin, import ProjectDescription, import
ProjectDescriptionHelpers 순서로 정리하고, 변경 후 SwiftLint를 실행하거나 자동 정렬 툴로 확인하여 규칙을
통과하도록 수정하세요.
In `@Projects/Features/TravelToolFeature/Sources/TravelToolBuilder.swift`:
- Around line 41-42: The init modifier order in TravelToolBuilder's initializer
is reversed; update the initializer declaration in the TravelToolBuilder class
(public override init(dependency: TravelToolDependency)) to place override
before the access modifier (override public init(dependency:
TravelToolDependency)) so it complies with SwiftLint's modifier_order rule;
ensure any other initializers in TravelToolBuilder follow the same ordering.
In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift`:
- Around line 109-120: The daysFromToday check in fetchMyTripInfo (where you
compute startOfToday/startOfEnd and guard daysFromToday > 0) can produce a
.preparing weather state while convertToState returns .empty for past trips,
causing UI inconsistency; update fetchMyTripInfo to reuse the same logic as
convertToState (or call convertToState(summary) and use its result) instead of
unilaterally setting .preparing when daysFromToday <= 0, and ensure
presenter.updateWeather(...) and the trip card state are set from that single
source of truth; also verify (or add a comment/assert) whether fetchMyTripInfo’s
API (/api/v1/travels/upcoming) guarantees only future trips and handle the
past-trip case defensively by mapping it to .empty.
In `@Projects/Features/TravelToolFeature/Sources/TravelToolViewController.swift`:
- Around line 65-75: setLayout is missing a bottom constraint for weatherView so
its content can be obscured by the floating custom tab bar (68pt) or produce
ambiguous heights; fix by either adding a bottom constraint on weatherView to
view.safeAreaLayoutGuide.bottom (or to superview with inset equal to the tab bar
height) to guarantee content stays above the tab bar, or wrap
tripCardView+weatherView in a UIScrollView and constrain the scroll view edges
to the safeAreaLayoutGuide and its content layout guide so content can scroll
under the 68pt floating tab bar; update the constraints in setLayout (reference:
weatherView, tripCardView, setLayout) accordingly.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolOnGoingView.swift`:
- Around line 45-49: In configure (or wherever imageUrl is handled) clear any
previously set image when the URL is invalid by setting imageView.image = nil in
the else branch that currently only sets imageView.backgroundColor; also make
the fallback background color consistent with the initial backgroundColor (use
the same .systemGray6 value instead of .systemGray5) so
imageView.kf.setImage(with:) doesn't leave the old image visible and the
background color matches across states.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolUpComingView.swift`:
- Around line 37-41: The image-loading block in TravelToolUpComingView.swift
only handles URL(string:) parsing and misses Kingfisher download failures;
update the imageView.kf.setImage call to include a completion handler (or use
the result closure) and on error/failure set a fallback (e.g.,
imageView.backgroundColor = .systemGray5 or a placeholder image) so the view
never stays blank; keep the existing else branch for invalid URLs and ensure the
completion handler checks for .failure and applies the same fallback.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolWeatherView.swift`:
- Around line 333-334: The switch handling weather condition icons in
TravelToolWeatherView.swift currently maps LIGHT_SNOW, SNOW, HEAVY_SNOW,
BLIZZARD, and FLURRIES to DSKitAsset.Assets.icWeatherCloud.image, which
misrepresents snow/blizzard states; update the mapping so snow-related cases use
a dedicated snow icon (e.g., DSKitAsset.Assets.icWeatherSnow.image) and if no
snow asset exists, add one to DSKit and use it for at least HEAVY_SNOW and
BLIZZARD to avoid showing a cloud for severe snow—modify the switch case block
that currently returns DSKitAsset.Assets.icWeatherCloud.image for those
condition strings to return the new snow asset instead.
- Around line 191-208: The configure(with:) method creates a new DateFormatter
per cell causing performance issues; replace the local dateFormatter with one or
two static DateFormatter instances (e.g., static private let shortDateFormatter
and static private let weekdayFormatter) configured once with Locale(identifier:
"ko_KR") and the respective dateFormat ("MM.dd" and "EEEE"), then use those
static formatters when setting dateLabel and dayOfWeekLabel; update references
in configure(with:) to use the static formatters and leave other UI updates
(tempLabel, iconImageView) unchanged.
In `@Projects/Modules/Networks/Sources/TargetType/WeatherAPI.swift`:
- Around line 17-19: The WeatherAPI's baseURL property currently force-unwraps a
literal URL string; replace this with the same safe pattern used by other
TargetType implementations by either returning NetworkConfiguration.baseURL or
by safely initializing the URL (e.g., use optional URL(string:) with a fallback
to NetworkConfiguration.baseURL or guard/throw if initialization fails). Update
the WeatherAPI.baseURL implementation to avoid "!" and ensure it returns a
non-optional URL without force-unwrapping, referencing WeatherAPI.baseURL and
NetworkConfiguration.baseURL to locate where to change.
---
Outside diff comments:
In `@Projects/Features/TravelFeature/Sources/TravelInteractor.swift`:
- Around line 53-56: The interactor currently calls loadTrips() from both
didBecomeActive() and viewWillAppear(), causing duplicate data loads on first
activation; remove the redundant call by deleting or commenting out the
loadTrips() invocation inside didBecomeActive() (keeping the viewWillAppear()
call if you want view lifecycle to control loading), ensuring only one of
didBecomeActive() or viewWillAppear() invokes loadTrips() to prevent double
requests.
---
Nitpick comments:
In `@Projects/Data/Sources/DI/WeatherServiceFactory.swift`:
- Around line 9-11: Reorder the import statements so they follow alphabetical
order to satisfy SwiftLint: change the import block (currently listing
Foundation, Networks, Moya) to list Foundation, Moya, Networks; update the
import declarations at the top of WeatherServiceFactory.swift so Moya appears
before Networks.
In `@Projects/Data/Sources/Repository/Weather/WeatherRepository.swift`:
- Line 31: 현재 response.forecastDays.compactMap { $0.toDomain() }가 toDomain()가
nil을 반환하는 항목을 조용히 제거해 원인 파악이 어렵습니다; response.forecastDays를 순회하면서 let domain =
$0.toDomain()로 변환 실패 시 (예: 잘못된 displayDate) 디버그 빌드에서 logger 또는 print로 해당 항목의
식별자(예: $0.displayDate 또는 $0.id)와 오류 컨텍스트를 남기고, 성공한 항목만 수집하도록 변경해 주세요 (참조 심볼:
toDomain(), response.forecastDays).
- Around line 9-11: Reorder the import statements to satisfy SwiftLint import
ordering by placing import Domain before import Foundation; specifically update
the top of WeatherRepository.swift so the imports read "import Domain", then
"import Foundation", then "import Networks" (refer to the existing import
Foundation, import Domain, import Networks lines to locate the change).
In `@Projects/Data/Sources/Transform/UserTravelTransform.swift`:
- Around line 44-50: The toDate() extension on String currently allocates a new
DateFormatter on every call; change it to use a cached static DateFormatter
(e.g., a private static property on String like private static let
cachedDateFormatter) configured with locale "en_US_POSIX" and dateFormat
"yyyy-MM-dd", and have func toDate() call that cached formatter instead of
creating a new one; reference the extension String and func toDate() to locate
where to replace the per-call DateFormatter with the static cached instance.
In `@Projects/Data/Sources/Transform/WeatherTransform.swift`:
- Around line 9-11: 현재 import 정렬이 알파벳 순서를 따르지 않아 SwiftLint 경고가 발생합니다; 파일의 import
블록(현재 선언된 Foundation, Domain, Networks)을 알파벳 순서로 재정렬하여 Domain, Foundation,
Networks 순서로 정렬하고 불필요한 공백이나 중복 import가 없는지 확인해 주세요.
In `@Projects/Domain/Sources/Model/Weather/WeatherInfo.swift`:
- Line 15: Replace the loose String-typed weatherType with a type-safe
WeatherType enum in WeatherInfo (add cases for known API values plus an
.unknown(rawValue:) or .unknown case with RawRepresentable/Codable support),
update WeatherTransform to map API responses into WeatherInfo using WeatherType
(initializing from the raw string and falling back to .unknown), and change
WeatherIconMapper to switch on WeatherType rather than comparing string
literals; ensure WeatherType implements Codable/RawRepresentable (or a failable
init(rawValue:)) so decoding handles unknown API values gracefully and existing
fallback logic remains intact.
In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift`:
- Around line 226-237: The handleCreateTravelError(_:) function is dead code;
either remove it or wire it into the error handling path: replace the generic
print(error) in the catch blocks that handle CreateTravelError with a call to
handleCreateTravelError(error as? CreateTravelError ?? .unknown(-1,
"\(error)")), and if you choose removal, delete handleCreateTravelError(_:) and
any unused imports to keep the code clean. Ensure you update any catch clauses
where CreateTravelError may be thrown (e.g., in createTravel or related
do/catch) so the compiler no longer flags the function as unused or leaves
generic prints.
In `@Projects/Features/HomeFeature/Sources/HomeInteractor.swift`:
- Around line 182-184: The viewWillAppear() currently calls fetchHomeData()
unconditionally causing redundant network requests and UI flicker; modify
HomeInteractor to track the last successful fetch timestamp (e.g., lastFetchDate
property) and in viewWillAppear() check the elapsed time against a defined
freshness interval (e.g., 1-5 minutes) and only call fetchHomeData() if stale or
if a forced refresh flag is set; ensure pull-to-refresh and explicit refresh
paths clear/override lastFetchDate so they always trigger fetchHomeData(), and
keep access to lastFetchDate thread-safe if used from multiple queues.
- Around line 112-128: 현재 블록에서 let usecase = self.usecase로 로컬 참조를 만들었으니 이후 호출도
일관되게 사용하도록 수정하세요: self.usecase.fetchCategoryList() 및
self.usecase.fetchPopularTripList(id:) 대신 로컬 usecase.fetchCategoryList() 및
usecase.fetchPopularTripList(id:)로 바꿔서 fetchCategoryList()와
fetchPopularTripList(id:) 호출이 모두 동일한 usecase 변수(및
toHomeModel()/toPopularHomeModel() 매핑)로 이루어지게 하세요.
In `@Projects/Features/MainFeature/Sources/MainInteractor.swift`:
- Around line 56-59: Replace the magic numeric tab indices with a shared named
constant or enum and update all call sites; specifically, create a common
TabIndex (or TabBar.Tab) enum/struct that defines cases/properties like home
(and others) and replace the literal in MainInteractor.followDetailDidViewTrip's
router?.switchToTab(at: 1) and the literal used in
TabBarInteractor.homeDidAddTrip (currently 2) to use TabIndex.home (or the
equivalent named constant) so tab ordering changes remain safe and consistent.
In `@Projects/Features/SearchFeature/Sources/SearchViewController.swift`:
- Around line 182-195: The setCollectionView() method currently calls
collectionView.register for PopularInfoCell and SearchResultHeaderView but
setDataSource() uses UICollectionView.CellRegistration and
SupplementaryRegistration which handle registration automatically; remove the
manual collectionView.register calls (the body of setCollectionView()) to avoid
duplicate registration, and if setCollectionView() becomes empty also remove its
invocation wherever called; refer to setCollectionView(), setDataSource(),
PopularInfoCell, SearchResultHeaderView, and the
CellRegistration/SupplementaryRegistration usage to locate the code to change.
- Around line 396-421: In createResultTripCellRegistration and
createHeaderRegistration update unused closure parameters to use _ (replace
indexPath in the CellRegistration closure and replace elementKind and indexPath
in the SupplementaryRegistration closure) and remove the explicit return
keywords where the closures or functions can rely on implicit returns;
specifically modify createResultTripCellRegistration, the
UICollectionView.CellRegistration closure for
PopularInfoCell/SearchResultPresentationModel.ResultTrip, and
createHeaderRegistration's SupplementaryRegistration for SearchResultHeaderView
to eliminate the unused params and convert explicit "return" to implicit
returns.
- Around line 278-295: In configureSupplementaryView(), the closure assigned to
dataSource.supplementaryViewProvider currently returns a newly created
UICollectionReusableView() for unknown sections which can cause runtime issues;
update the closure so that when SearchResultSectionKind(rawValue:
indexPath.section) is nil it returns nil instead of instantiating
UICollectionReusableView(), keeping the existing header dequeue path (using
headerRegistration) intact; this touches the configureSupplementaryView method
and the dataSource.supplementaryViewProvider closure.
- Around line 252-259: The code inconsistently accesses the IUO dataSource:
since dataSource is declared as UICollectionViewDiffableDataSource...! and
initialized in setDataSource() called from viewDidLoad, change optional chaining
in applySnapShot to use the non-optional access (e.g., call
dataSource.apply(snapshot, animatingDifferences: true) instead of dataSource?)
so usage is consistent with the declaration; verify applySnapShot,
setDataSource, viewDidLoad and the dataSource declaration to ensure
initialization order remains correct.
In `@Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift`:
- Around line 93-95: The empty TravelToolListener extension on TabBarInteractor
should either be populated with the concrete event handlers or tracked via an
issue: add implementations for the expected callbacks (e.g., methods named like
travelDidAdd(_:), travelDidUpdate(_:), or travelShouldSwitchToHome()) inside the
extension so they call TabBarInteractor routines to switch tabs or update state,
or if you don’t have the event contract yet, create a tracking issue referencing
TravelToolListener and TabBarInteractor and add a TODO comment in the extension
pointing to that issue ID so future work will implement events such as "travel
added -> switch to Home" and related handling.
- Around line 86-88: homeDidAddTrip(title:startDate:endDate:) currently ignores
its parameters and uses a magic number in presenter.switchToTab(at: 2); replace
the hardcoded 2 with a shared, named tab index (e.g., Tabs.trips or
TabIndex.trips) used across MainInteractor/Presenter and update
presenter.switchToTab(at:) call to use that constant; also either use the
incoming title/startDate/endDate (forward them to presenter or storage) or
remove them from the signature (or prefix with _ / add a TODO comment) so they
are not misleading — update the function homeDidAddTrip(...) accordingly and
ensure callers are adjusted to match.
In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift`:
- Around line 204-211: The Date extension's toTravelToolKoreanMMdd() creates a
new DateFormatter on every call causing unnecessary allocations (it's called
twice per convertToState and on every viewWillAppear); fix this by adding a
cached static DateFormatter (e.g., private static let koreanMMddFormatter)
inside the Date extension and have toTravelToolKoreanMMdd() use that shared
formatter; ensure the formatter is configured once with locale "ko_KR" and
dateFormat "M월 d일" so callers like convertToState and viewWillAppear stop
creating new formatters.
- Around line 58-62: didBecomeActive()에서 호출한 fetchTripInfo()가
viewWillAppear()에서도 호출되어 최초 어태치 시 중복 네트워크 요청과 UI 깜빡임을 유발하니, Interactor의
didBecomeActive()에서 fetchTripInfo() 호출을 제거하고 viewWillAppear()에만 남겨 두어 탭 전환 시 갱신
동작은 유지하세요; 동일한 중복 호출이 파일 내 다른 위치(라인 범위 196-200)에도 존재하므로 해당 위치에서도
didBecomeActive/유사 초기화 경로에서의 중복 fetchTripInfo() 호출을 삭제하여 단일 진입점(viewWillAppear
또는 탭 갱신 담당 메서드)만이 네트워크 요청을 트리거하도록 정리하세요.
- Around line 74-97: The Task binds self strongly with "guard let self" then
later MainActor.run closures implicitly capture that strong reference; change to
avoid retaining self by not creating a strong local for the whole Task
scope—remove "guard let self" and instead capture self weakly in each async
closure (use [weak self, tripState] in MainActor.run) or use a weakSelf/guard
pattern inside each MainActor.run closure before calling
presenter.updateTripCard, convertToState, or other methods (e.g.,
presenter.updateTripCard, convertToState, usecase.fetchMyTripInfo) so the
interactor can be released if the Task is cancelled (also apply same
weak-capture fix to other MainActor.run occurrences and closures mentioned and
referenced by willResignActive).
In `@Projects/Features/TravelToolFeature/Sources/TravelToolViewController.swift`:
- Around line 57-59: The background color is hardcoded to .white in
TravelToolViewController.setStyle(), which bypasses the design system and breaks
theming; change the assignment to use the design system color token (for example
DSKitAsset.Colors.primaryBackground or the appropriate DSKitAsset.Colors member
used elsewhere) so the view uses DS colors and supports dark mode/themes,
updating only the line inside the setStyle() method.
- Around line 38-41: 현재 TravelToolViewController의 viewWillAppear에서
listener?.viewWillAppear()가 매 탭 전환마다 호출되어 인터랙터 레벨에서 여행 정보/날씨 API가 중복 요청되는 문제가
있습니다; 수정 방법은 인터랙터(또는 Interactor 프로토콜을 구현한 클래스)에 요청 중복 방지 로직을 추가하는 것입니다—예:
TravelToolInteractor에 Bool 프로퍼티(isLoading) 또는 요청 키 기반 캐시를 도입하고 public
메서드(fetchTravelInfo, fetchWeather 등)가 호출될 때 이미 요청 중이면 새 요청을 무시하거나 캐시된 응답을 반환하도록
하며, 요청 완료 시 isLoading을 false로 설정(또는 캐시 만료 정책 적용)하고 필요하면 이전 네트워크 작업을 취소할 수 있는 취소
토큰을 추가하세요; 이 변경으로 TravelToolViewController의 viewWillAppear나
listener?.viewWillAppear() 호출은 그대로 두어도 되지만 인터랙터 내부에서 중복 호출을 방지합니다.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolWeatherView.swift`:
- Around line 68-82: The .noTrip and .preparing branches duplicate the same
title string "여행 중 날씨"; extract the common title-setting into shared logic
before the switch (or at the top of the switch) so both cases reuse it instead
of calling titleLabel.setText separately. Locate the switch on state in
TravelToolWeatherView (the case handlers .noTrip, .preparing, and .hasWeather
and the titleLabel.setText calls) and change it so the common title is set once
(e.g., compute a defaultTitle or call titleLabel.setText(.subTitleMSB, text:
defaultTitle, color: DSKitAsset.Colors.black700.color)) and keep the .hasWeather
branch setting its dynamic "\(title) 여행 중 날씨" as before.
- Around line 35-45: Rename the short local variable cv inside the
collectionView property closure to a longer descriptive name (e.g.,
collectionViewInstance) to satisfy SwiftLint's identifier_name rule; update all
references in that closure (the showsHorizontalScrollIndicator, backgroundColor,
register(WeatherDayCell.self, forCellWithReuseIdentifier:
WeatherDayCell.identifier), and the return statement) so they use
collectionViewInstance instead of cv.
In
`@Projects/Modules/DSKit/Resources/Assets.xcassets/ic_weather_sun_clouds01.imageset/Contents.json`:
- Around line 1-15: The Contents.json for the ic_weather_sun_clouds01.imageset
lacks an appearances variant for dark mode; add an "appearances" entry under the
appropriate image object to define "any" and "dark" variants (e.g., reference
the existing ic_weather_sun_clouds01.svg for "any" and a dark-mode file like
ic_weather_sun_clouds01_dark.svg for "dark"), keep "idiom":"universal" and
preserve the existing "preserves-vector-representation": true in "properties",
and repeat the same appearances pattern for the other 9 weather icon imagesets
so each has explicit any/dark variants.
In `@Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift`:
- Around line 31-37: The static var weatherApiKey (and likewise apiKey)
currently only reads from Bundle.main and fatals in tests; update these
accessors to follow the same bundle-fallback pattern used by baseURL: first
attempt Bundle.main.infoDictionary, then attempt the test/XCTest bundle (e.g.,
Bundle(for: NetworkConfiguration.self) or the bundle that contains the test
resources), and only call fatalError if neither bundle contains the key. Modify
the static properties weatherApiKey and apiKey to try the secondary bundle
before failing so unit tests using the .xctest bundle won't crash.
| var components = DateComponents() | ||
| components.year = displayDate.year | ||
| components.month = displayDate.month | ||
| components.day = displayDate.day | ||
|
|
||
| guard let date = Calendar.current.date(from: components) else { return nil } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.swift" | grep -E "(Weather|Travel)" | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1449
🏁 Script executed:
fd -e swift | grep -i "weather"Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 649
🏁 Script executed:
fd -e swift | grep -i "travel"Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 3939
🏁 Script executed:
cat -n Projects/Data/Sources/Transform/WeatherTransform.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1063
🏁 Script executed:
cat -n Projects/Data/Sources/Transform/UserTravelTransform.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1935
🏁 Script executed:
rg -n "displayDate" Projects/Data/Sources/Transform/WeatherTransform.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 212
🏁 Script executed:
rg -n "ForecastDayResponse" Projects/Modules/Networks/Sources/DTO/Weather/ -A 15Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1956
🏁 Script executed:
rg -n "timezone\|TimeZone\|GMT" Projects/Data/Sources/Transform/ Projects/Modules/Networks/Sources/DTO/Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 57
암묵적 타임존 처리로 인한 유지보수 위험
WeatherTransform의 Calendar.current.date(from: components)와 UserTravelTransform의 DateFormatter(timezone 미지정)는 모두 기기의 로컬 타임존을 사용합니다. 따라서 원래 지적한 UTC vs 로컬 타임존 불일치는 발생하지 않습니다.
다만 더 심각한 문제는 타임존을 명시적으로 지정하지 않은 암묵적 처리입니다:
DateFormatter의 timezone 속성 미설정 → 기기 설정에 의존Calendar.current→ 기기 로컬 타임존에 의존- 코드만으로는 어떤 타임존이 사용되는지 불명확
유지보수 시 개발자가 UTC, 서버 타임존 등을 가정할 수 있으며, 기기 설정 변경 시 예상 외 동작 가능성이 있습니다.
formatter.timeZone = TimeZone(abbreviation: "UTC") 또는 components.timeZone = TimeZone(secondsFromGMT: 0)으로 타임존을 명시적으로 지정하여 의도를 명확히 하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Data/Sources/Transform/WeatherTransform.swift` around lines 15 - 20,
The code in WeatherTransform uses Calendar.current.date(from: components)
without an explicit time zone, and elsewhere DateFormatter is used without
setting timezone, which makes the code implicitly depend on the device locale
and risks incorrect date calculations; update WeatherTransform to set
components.timeZone (e.g., TimeZone(secondsFromGMT: 0) or the intended TZ)
before calling Calendar.current.date(from:), and ensure any DateFormatter
instances (e.g., in UserTravelTransform) explicitly set formatter.timeZone to
the same intended zone so the intent is clear and consistent across
WeatherTransform and related formatting code.
| maxTemperature: maxTemperature?.degrees ?? 0, | ||
| minTemperature: minTemperature?.degrees ?? 0, |
There was a problem hiding this comment.
온도 nil 시 0 기본값 사용 — 오해 소지 있음
maxTemperature?.degrees ?? 0 / minTemperature?.degrees ?? 0에서 0은 실제 유효한 온도값(0°C)이므로, API가 온도 데이터를 반환하지 않은 경우와 실제 0°C를 구분할 수 없습니다. 사용자에게 "0°C"로 표시되어 혼동을 줄 수 있습니다.
🛡️ Double.nan 또는 별도 sentinel 사용 제안
도메인 모델(DailyWeatherInfo)의 온도 필드를 Double?로 바꾸거나, 표시 레이어에서 nil을 "–"로 렌더링하는 방법을 고려할 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Data/Sources/Transform/WeatherTransform.swift` around lines 24 - 25,
The mapping in WeatherTransform currently uses maxTemperature?.degrees ?? 0 and
minTemperature?.degrees ?? 0 which collapses missing data into a valid 0°C
value; change the transform so it preserves nil when the source is missing
(e.g., map to Optional Double by using maxTemperature?.degrees and
minTemperature?.degrees without the ?? 0) and then update the domain model
DailyWeatherInfo temperature fields to be Double? (or use Double.nan
consistently as a sentinel) and ensure the presentation layer renders nil (or
Double.nan) as a placeholder (e.g., "–") rather than "0°C".
| await MainActor.run { [weak self] in | ||
| guard let self else { return } | ||
| presenter.hideLoading() | ||
| router?.detachTripCalendar() | ||
|
|
||
| let tripTitle = "\(travelDetail?.city ?? "새로운") 여행" | ||
| listener?.followDetailDidAddTrip(title: tripTitle, startDate: startDate, endDate: endDate) | ||
|
|
||
| presenter.showTripCreatedModal( | ||
| onLater: { [weak self] in | ||
| self?.router?.detachTripCalendar() | ||
| self?.listener?.detachFollowDetail() | ||
| }, | ||
| onViewTrip: { [weak self] in | ||
| self?.router?.detachTripCalendar() | ||
| self?.listener?.followDetailDidViewTrip() | ||
| } | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*NDGLModal*" -o -name "*FollowDetailInteractor*" | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 218
🏁 Script executed:
cat -n Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift | sed -n '195,230p'Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1676
🏁 Script executed:
cat -n Projects/Modules/DSKit/Sources/Component/NDGLModalViewController.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 8140
🏁 Script executed:
cat -n Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift | sed -n '226,240p'Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 658
NDGLModalViewController의 시스템 스와이프 해제 시 RIB 분리 미처리 문제
NDGLModalViewController는 modalPresentationStyle = .overFullScreen으로 설정되어 있으며 isModalInPresentation을 별도로 설정하지 않습니다(line 83-84). 이로 인해 사용자가 모달을 아래로 스와이프하여 닫을 수 있으며, 이 경우 onCancelTapped, onActionTapped 어느 클로저도 호출되지 않습니다. dimViewTapped 메서드(line 213-217)는 탭 제스처만 처리하고 시스템 스와이프 제스처는 처리하지 않기 때문입니다.
결과적으로:
router?.detachTripCalendar()가 실행되지 않음tripCalendarRouter가 메모리에 남아있는 RIB 누수 발생- 후속 여행 생성 시 예상치 못한 상태 문제 가능
해결방안:
NDGLModalViewController에서isModalInPresentation = true설정하거나UIAdaptivePresentationControllerDelegate의presentationControllerDidDismiss메서드로 모든 해제 경로에서 클로저 실행
또한 라인 214의 print("여행 생성 성공 - userTravelId: \(response.userTravelId)") 디버그 로그는 제거하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift` around
lines 200 - 213, NDGLModalViewController currently allows system
swipe-to-dismiss so onLater/onViewTrip closures (triggered in
FollowDetailInteractor via presenter.showTripCreatedModal) are not always
called; fix by either setting isModalInPresentation = true in
NDGLModalViewController (around init/config where modalPresentationStyle =
.overFullScreen is set) or implement UIAdaptivePresentationControllerDelegate
(add presentationControllerDidDismiss(_:)) to invoke the same dismissal handling
that dimViewTapped/onCancelTapped/onActionTapped would (call the
onLater/onViewTrip handlers or directly call router?.detachTripCalendar() and
listener?.detachFollowDetail()/listener?.followDetailDidViewTrip() as
appropriate). Also remove the debug print("여행 생성 성공 - userTravelId:
\(response.userTravelId)") from the trip-creation success path so no stray logs
remain.
| self?.listener?.followDetailDidViewTrip() | ||
| } | ||
| ) | ||
| print("여행 생성 성공 - userTravelId: \(response.userTravelId)") |
There was a problem hiding this comment.
디버그 print 구문 제거 필요
print("여행 생성 성공 - userTravelId: \(response.userTravelId)")는 프로덕션 로그로 적합하지 않습니다. 제거하거나 구조화된 로거로 교체하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift` at line
214, Remove the debug print statement that logs "여행 생성 성공 - userTravelId:
\(response.userTravelId)" in FollowDetailInteractor (the response handling code
where response.userTravelId is used) and either delete it or replace it with a
structured logging call (e.g., use the app's logger/info method) so production
logging is consistent and not using print; ensure the replacement uses the same
context (userTravelId) and log level appropriate for success messages.
| private func fetchPopularTrips(categoryId: Int) { | ||
| Task { [weak self] in | ||
| guard let self else { return } | ||
|
|
||
| do { | ||
| let apiId: Int? = categoryId == HomeInteractor.allCategoryId ? nil : categoryId | ||
| let populars = try await self.usecase.fetchPopularTripList(id: apiId).map { $0.toPopularHomeModel() } | ||
|
|
||
| guard !Task.isCancelled, let model = self.homeDataRelay.value else { return } | ||
|
|
||
| let updated = HomePresentationModel( | ||
| banner: model.banner, | ||
| category: model.category, | ||
| popularTrip: populars, | ||
| recommendedTrip: model.recommendedTrip | ||
| ) | ||
| self.homeDataRelay.accept(updated) | ||
| } catch { | ||
| print(error) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
fetchPopularTrips의 Task가 저장되지 않아 경쟁 상태가 발생할 수 있습니다.
사용자가 카테고리를 빠르게 전환하면 여러 Task가 동시에 실행되고, 나중에 시작된 요청보다 이전 요청의 응답이 늦게 도착하여 homeDataRelay에 잘못된(이전 카테고리의) 데이터가 반영될 수 있습니다. fetchDataTask처럼 별도의 프로퍼티에 저장하고 새 요청 시 이전 Task를 취소하는 것이 좋습니다.
🐛 수정 제안
클래스에 프로퍼티 추가:
private var fetchPopularTask: Task<Void, Never>?그리고 메서드를 수정:
private func fetchPopularTrips(categoryId: Int) {
+ fetchPopularTask?.cancel()
- Task { [weak self] in
+ fetchPopularTask = Task { [weak self] in
guard let self else { return }
do {
let apiId: Int? = categoryId == HomeInteractor.allCategoryId ? nil : categoryId
let populars = try await self.usecase.fetchPopularTripList(id: apiId).map { $0.toPopularHomeModel() }
guard !Task.isCancelled, let model = self.homeDataRelay.value else { return }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Features/HomeFeature/Sources/HomeInteractor.swift` around lines 156
- 177, The Task created in fetchPopularTrips is not stored so concurrent tasks
can race and overwrite homeDataRelay; add a private var fetchPopularTask:
Task<Void, Never>? to the class, then in fetchPopularTrips cancel any existing
fetchPopularTask before assigning a new Task that performs the current fetch
(use usecase.fetchPopularTripList(...)), keep the [weak self] capture, check
Task.isCancelled as already done, and on completion or in the catch/finally
clear fetchPopularTask = nil so subsequent calls behave correctly (similar to
how fetchDataTask is managed).
| if let url = URL(string: imageUrl) { | ||
| imageView.kf.setImage(with: url) | ||
| } else { | ||
| imageView.backgroundColor = .systemGray5 | ||
| } |
There was a problem hiding this comment.
유효하지 않은 URL일 때 이전 이미지가 남아있을 수 있습니다.
configure가 유효한 URL로 호출된 후 다시 유효하지 않은 URL로 호출되면, Kingfisher가 설정한 이전 이미지가 그대로 표시됩니다. else 분기에서 이미지를 nil로 초기화해야 합니다. 또한 초기 backgroundColor(.systemGray6, Line 59)와 fallback 색상(.systemGray5)이 일치하지 않습니다.
🐛 수정 제안
if let url = URL(string: imageUrl) {
imageView.kf.setImage(with: url)
} else {
+ imageView.image = nil
- imageView.backgroundColor = .systemGray5
+ imageView.backgroundColor = .systemGray6
}📝 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.
| if let url = URL(string: imageUrl) { | |
| imageView.kf.setImage(with: url) | |
| } else { | |
| imageView.backgroundColor = .systemGray5 | |
| } | |
| if let url = URL(string: imageUrl) { | |
| imageView.kf.setImage(with: url) | |
| } else { | |
| imageView.image = nil | |
| imageView.backgroundColor = .systemGray6 | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolOnGoingView.swift`
around lines 45 - 49, In configure (or wherever imageUrl is handled) clear any
previously set image when the URL is invalid by setting imageView.image = nil in
the else branch that currently only sets imageView.backgroundColor; also make
the fallback background color consistent with the initial backgroundColor (use
the same .systemGray6 value instead of .systemGray5) so
imageView.kf.setImage(with:) doesn't leave the old image visible and the
background color matches across states.
| if let url = URL(string: imageUrl) { | ||
| imageView.kf.setImage(with: url) | ||
| } else { | ||
| imageView.backgroundColor = .systemGray5 | ||
| } |
There was a problem hiding this comment.
이미지 다운로드 실패 시 폴백 처리 누락
현재 코드는 URL(string:) 파싱 실패만 처리하고, URL이 유효하더라도 Kingfisher가 실제 다운로드에 실패하는 경우(네트워크 오류, 404 등)에는 imageView가 빈 상태로 남습니다. completionHandler를 추가해 다운로드 실패 시에도 폴백을 적용해야 합니다.
🛡️ 제안 수정
- if let url = URL(string: imageUrl) {
- imageView.kf.setImage(with: url)
- } else {
- imageView.backgroundColor = .systemGray5
- }
+ if let url = URL(string: imageUrl) {
+ imageView.kf.setImage(with: url) { [weak self] result in
+ if case .failure = result {
+ self?.imageView.backgroundColor = .systemGray5
+ }
+ }
+ } else {
+ imageView.backgroundColor = .systemGray5
+ }📝 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.
| if let url = URL(string: imageUrl) { | |
| imageView.kf.setImage(with: url) | |
| } else { | |
| imageView.backgroundColor = .systemGray5 | |
| } | |
| if let url = URL(string: imageUrl) { | |
| imageView.kf.setImage(with: url) { [weak self] result in | |
| if case .failure = result { | |
| self?.imageView.backgroundColor = .systemGray5 | |
| } | |
| } | |
| } else { | |
| imageView.backgroundColor = .systemGray5 | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolUpComingView.swift`
around lines 37 - 41, The image-loading block in TravelToolUpComingView.swift
only handles URL(string:) parsing and misses Kingfisher download failures;
update the imageView.kf.setImage call to include a completion handler (or use
the result closure) and on error/failure set a fallback (e.g.,
imageView.backgroundColor = .systemGray5 or a placeholder image) so the view
never stays blank; keep the existing else branch for invalid URLs and ensure the
completion handler checks for .failure and applies the same fallback.
| func configure(with info: DailyWeatherInfo) { | ||
| let dateFormatter = DateFormatter() | ||
| dateFormatter.locale = Locale(identifier: "ko_KR") | ||
|
|
||
| dateFormatter.dateFormat = "MM.dd" | ||
| dateLabel.setText(.bodyLSB, text: dateFormatter.string(from: info.date), color: DSKitAsset.Colors.black700.color) | ||
|
|
||
| dateFormatter.dateFormat = "EEEE" | ||
| dayOfWeekLabel.setText(.bodySR, text: dateFormatter.string(from: info.date), color: DSKitAsset.Colors.black400.color) | ||
|
|
||
| tempLabel.setText( | ||
| .bodyMR, | ||
| text: "\(Int(info.maxTemperature))° / \(Int(info.minTemperature))°", | ||
| color: DSKitAsset.Colors.black600.color | ||
| ) | ||
|
|
||
| iconImageView.image = WeatherIconMapper.icon(for: info.weatherType) | ||
| } |
There was a problem hiding this comment.
DateFormatter를 셀 구성마다 생성 — 성능 문제
DateFormatter 초기화는 로케일·타임존 데이터 로딩으로 인해 비용이 큽니다. 현재 구현은 스크롤 시 각 셀이 재사용될 때마다 새 인스턴스를 생성하므로 프레임 드롭을 유발할 수 있습니다. static 프로퍼티로 한 번만 생성해야 합니다.
🐛 static DateFormatter로 수정 제안
+private static let dateLabelFormatter: DateFormatter = {
+ let f = DateFormatter()
+ f.locale = Locale(identifier: "ko_KR")
+ f.dateFormat = "MM.dd"
+ return f
+}()
+
+private static let weekdayFormatter: DateFormatter = {
+ let f = DateFormatter()
+ f.locale = Locale(identifier: "ko_KR")
+ f.dateFormat = "EEEE"
+ return f
+}()
func configure(with info: DailyWeatherInfo) {
- let dateFormatter = DateFormatter()
- dateFormatter.locale = Locale(identifier: "ko_KR")
-
- dateFormatter.dateFormat = "MM.dd"
- dateLabel.setText(.bodyLSB, text: dateFormatter.string(from: info.date), color: DSKitAsset.Colors.black700.color)
-
- dateFormatter.dateFormat = "EEEE"
- dayOfWeekLabel.setText(.bodySR, text: dateFormatter.string(from: info.date), color: DSKitAsset.Colors.black400.color)
+ dateLabel.setText(.bodyLSB, text: Self.dateLabelFormatter.string(from: info.date), color: DSKitAsset.Colors.black700.color)
+ dayOfWeekLabel.setText(.bodySR, text: Self.weekdayFormatter.string(from: info.date), color: DSKitAsset.Colors.black400.color)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolWeatherView.swift`
around lines 191 - 208, The configure(with:) method creates a new DateFormatter
per cell causing performance issues; replace the local dateFormatter with one or
two static DateFormatter instances (e.g., static private let shortDateFormatter
and static private let weekdayFormatter) configured once with Locale(identifier:
"ko_KR") and the respective dateFormat ("MM.dd" and "EEEE"), then use those
static formatters when setting dateLabel and dayOfWeekLabel; update references
in configure(with:) to use the static formatters and leave other UI updates
(tempLabel, iconImageView) unchanged.
| case "LIGHT_SNOW", "SNOW", "HEAVY_SNOW", "BLIZZARD", "FLURRIES": | ||
| return DSKitAsset.Assets.icWeatherCloud.image |
There was a problem hiding this comment.
눈 날씨 조건이 구름 아이콘으로 표시됨
LIGHT_SNOW, SNOW, HEAVY_SNOW, BLIZZARD, FLURRIES 모두 icWeatherCloud를 반환합니다. DSKit에 눈 관련 아이콘이 없다면 불가피하지만, 적어도 블리자드와 폭설이 구름으로 표시되는 것은 사용자에게 혼란을 줄 수 있습니다. 전용 눈 아이콘 추가를 고려해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolWeatherView.swift`
around lines 333 - 334, The switch handling weather condition icons in
TravelToolWeatherView.swift currently maps LIGHT_SNOW, SNOW, HEAVY_SNOW,
BLIZZARD, and FLURRIES to DSKitAsset.Assets.icWeatherCloud.image, which
misrepresents snow/blizzard states; update the mapping so snow-related cases use
a dedicated snow icon (e.g., DSKitAsset.Assets.icWeatherSnow.image) and if no
snow asset exists, add one to DSKit and use it for at least HEAVY_SNOW and
BLIZZARD to avoid showing a cloud for severe snow—modify the switch case block
that currently returns DSKitAsset.Assets.icWeatherCloud.image for those
condition strings to return the new snow asset instead.
| public var baseURL: URL { | ||
| URL(string: "https://weather.googleapis.com")! | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for baseURL implementations in TargetType files
cd Projects/Modules/Networks/Sources/TargetType 2>/dev/null || cd .
rg -n 'var baseURL' --type=swift -A 3Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1098
🏁 Script executed:
#!/bin/bash
# Check the actual WeatherAPI.swift file to understand the full context
find . -name "WeatherAPI.swift" -type f | head -1 | xargs cat -nRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1691
🏁 Script executed:
#!/bin/bash
# Search for NetworkConfiguration.weatherApiKey to verify it's not hardcoded
rg -n 'weatherApiKey' --type=swift -B 2 -A 2Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1190
baseURL에서 강제 언래핑을 제거하세요.
프로젝트의 다른 모든 TargetType 구현(UserTravelAPI, TravelTemplateAPI, PlaceAPI, TravelProgramAPI, AuthAPI)은 NetworkConfiguration.baseURL을 사용하는 반면, WeatherAPI만 강제 언래핑된 문자열로 baseURL을 정의하고 있습니다. 정적 문자열이라 런타임 실패 가능성은 낮지만, 프로젝트 규약 일관성을 위해 다른 TargetType들과 동일한 패턴을 따르거나 안전한 URL 초기화 방식을 사용하세요.
🧰 Tools
🪛 SwiftLint (0.63.2)
[Warning] 18-18: Force unwrapping should be avoided
(force_unwrapping)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Modules/Networks/Sources/TargetType/WeatherAPI.swift` around lines
17 - 19, The WeatherAPI's baseURL property currently force-unwraps a literal URL
string; replace this with the same safe pattern used by other TargetType
implementations by either returning NetworkConfiguration.baseURL or by safely
initializing the URL (e.g., use optional URL(string:) with a fallback to
NetworkConfiguration.baseURL or guard/throw if initialization fails). Update the
WeatherAPI.baseURL implementation to avoid "!" and ensure it returns a
non-optional URL without force-unwrapping, referencing WeatherAPI.baseURL and
NetworkConfiguration.baseURL to locate where to change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift (2)
46-46: 미사용disposeBag및RxSwiftimport 제거
disposeBag이 선언만 되고 어디에도 사용되지 않습니다.RxSwift도 이 프로퍼티만을 위해 임포트된 상태이므로 함께 제거하면 됩니다.♻️ 제안 수정
-import RxSwift - // ... - private let disposeBag = DisposeBag()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift` at line 46, TravelToolInteractor contains an unused RxSwift dependency: remove the unused private let disposeBag = DisposeBag() declaration and delete the RxSwift import at the top of TravelToolInteractor.swift (or any unused RxSwift import in that file); ensure no other code references disposeBag or RxSwift types before removing so compilation isn't affected.
207-214:DateFormatter를 매 호출마다 새로 생성하지 말고static으로 캐싱
DateFormatter생성은 비용이 큰 작업입니다.toTravelToolKoreanMMdd()가 호출될 때마다 인스턴스를 새로 만드는 것은 불필요합니다.♻️ 제안 수정
extension Date { + private static let travelToolKoreanFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + formatter.dateFormat = "M월 d일" + return formatter + }() + func toTravelToolKoreanMMdd() -> String { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "ko_KR") - formatter.dateFormat = "M월 d일" - return formatter.string(from: self) + return Date.travelToolKoreanFormatter.string(from: self) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift` around lines 207 - 214, The DateFormatter in Date.toTravelToolKoreanMMdd() is recreated on every call; change the extension to use a cached static DateFormatter (e.g., a static let travelToolKoreanMMddFormatter initializer inside the Date extension that sets locale = Locale(identifier: "ko_KR") and dateFormat = "M월 d일") and then have toTravelToolKoreanMMdd() call that static formatter's string(from:); this avoids repeated expensive allocations while keeping the same formatter configuration.Projects/Modules/DSKit/Sources/Component/NDGLOnGoingView.swift (1)
43-64: SwiftLint 경고:configure파라미터 7개 (최대 5개 초과)SwiftLint
function_parameter_count규칙을 위반합니다. 파라미터를 전용 ViewModel 구조체로 묶으면 해소됩니다.♻️ 제안 수정
+struct NDGLOnGoingViewModel { + let title: String + let date: String + let transportIcon: UIImage? + let transport: String + let duration: String + let place: String + let imageUrl: String +} + public func configure( - title: String, - date: String, - transportIcon: UIImage?, - transport: String, - duration: String, - place: String, - imageUrl: String + _ viewModel: NDGLOnGoingViewModel ) { - titleLabel.setText(.bodyMSB, text: title, ...) + titleLabel.setText(.bodyMSB, text: viewModel.title, ...) // ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/DSKit/Sources/Component/NDGLOnGoingView.swift` around lines 43 - 64, The configure(...) method has too many parameters and triggers SwiftLint's function_parameter_count rule; create a dedicated view model (e.g., NDGLOnGoingViewModel) that bundles title, date, transportIcon, transport, duration, place, imageUrl, then change public func configure(...) to public func configure(with viewModel: NDGLOnGoingViewModel) and update the body to read from viewModel (set titleLabel, dateLabel, iconImageView, transportLabel, durationLabel, placeLabel, imageView.kf.setImage or placeholder). Update all call sites to pass the new viewModel and keep the existing UI assignment logic (titleLabel.setText, dateLabel.setText, iconImageView.image = ..., imageView.kf.setImage(with: URL(string: viewModel.imageUrl)) or fallback background color).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift`:
- Around line 59-63: Remove the redundant fetchTripInfo() invocation from the
TravelToolInteractor lifecycle so it is only called from the viewWillAppear
path: in TravelToolInteractor, delete the fetchTripInfo() call inside override
func didBecomeActive() and keep the existing call in viewWillAppear (ensuring
fetchTripInfo() remains intact there); also remove the duplicate fetchTripInfo()
invocation in the other duplicate didBecomeActive block referenced around the
second occurrence so that only the viewWillAppear-driven fetch is performed.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolTripCardView.swift`:
- Around line 9-11: Add explicit imports for SnapKit and Then at the top of
TravelToolTripCardView.swift so usages like stackView.snp.makeConstraints(...)
and stackView.do { ... } resolve without relying on transitive imports; update
the import block (after import DSKit) to include import SnapKit and import Then
so the file directly depends on those modules.
In `@Projects/Modules/DSKit/Sources/Component/NDGLOnGoingView.swift`:
- Around line 59-63: The image download currently only checks URL(string:
imageUrl) but doesn't handle Kingfisher download failures, leaving imageView
empty; update NDGLOnGoingView to call imageView.kf.setImage(with: url,
completionHandler: ...) (same pattern as NDGLUpComingView) and in the completion
closure detect an error or nil image and set imageView.backgroundColor =
.systemGray5 (or the existing fallback) so the fallback is applied when the
network fetch fails.
---
Duplicate comments:
In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift`:
- Around line 110-121: The branch that handles past trips uses daysFromToday
(computed from summary.endDay) and currently calls
presenter.updateWeather(.preparing) when daysFromToday <= 0, which conflicts
with convertToState returning .empty for past trips; change the branch to use
the same state as convertToState (e.g., call presenter.updateWeather(.empty) or
compute let state = convertToState(...) and pass that state) so the travel card
and weather UI remain consistent (refer to daysFromToday, summary.endDay,
convertToState, and presenter.updateWeather).
In `@Projects/Modules/DSKit/Sources/Component/NDGLUpComingView.swift`:
- Around line 42-46: NDGLUpComingView currently only sets
imageView.kf.setImage(with: URL(string: imageUrl)) and falls back to a gray
background only when URL creation fails; you must handle Kingfisher download
failures too by supplying a completion handler or placeholder to set a fallback
(e.g., imageView.backgroundColor = .systemGray5 or a default UIImage) when the
result is a failure. Update the call where imageView.kf.setImage is invoked
(referencing imageView, imageUrl, and NDGLUpComingView) to provide a completion
closure or placeholder option and set the fallback UI/state on error.
---
Nitpick comments:
In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift`:
- Line 46: TravelToolInteractor contains an unused RxSwift dependency: remove
the unused private let disposeBag = DisposeBag() declaration and delete the
RxSwift import at the top of TravelToolInteractor.swift (or any unused RxSwift
import in that file); ensure no other code references disposeBag or RxSwift
types before removing so compilation isn't affected.
- Around line 207-214: The DateFormatter in Date.toTravelToolKoreanMMdd() is
recreated on every call; change the extension to use a cached static
DateFormatter (e.g., a static let travelToolKoreanMMddFormatter initializer
inside the Date extension that sets locale = Locale(identifier: "ko_KR") and
dateFormat = "M월 d일") and then have toTravelToolKoreanMMdd() call that static
formatter's string(from:); this avoids repeated expensive allocations while
keeping the same formatter configuration.
In `@Projects/Modules/DSKit/Sources/Component/NDGLOnGoingView.swift`:
- Around line 43-64: The configure(...) method has too many parameters and
triggers SwiftLint's function_parameter_count rule; create a dedicated view
model (e.g., NDGLOnGoingViewModel) that bundles title, date, transportIcon,
transport, duration, place, imageUrl, then change public func configure(...) to
public func configure(with viewModel: NDGLOnGoingViewModel) and update the body
to read from viewModel (set titleLabel, dateLabel, iconImageView,
transportLabel, durationLabel, placeLabel, imageView.kf.setImage or
placeholder). Update all call sites to pass the new viewModel and keep the
existing UI assignment logic (titleLabel.setText, dateLabel.setText,
iconImageView.image = ..., imageView.kf.setImage(with: URL(string:
viewModel.imageUrl)) or fallback background color).
| override func didBecomeActive() { | ||
| super.didBecomeActive() | ||
|
|
||
| fetchTripInfo() | ||
| } |
There was a problem hiding this comment.
didBecomeActive에서의 중복 fetch 제거 필요
RIBs의 초기 활성화 시 didBecomeActive → (뷰 표시) → viewWillAppear 순서로 호출되어, fetchTripInfo()가 두 번 실행됩니다. 두 번째 호출(Line 73)이 첫 번째 Task를 즉시 취소하므로 실제 네트워크 응답은 하나지만, 매번 탭이 처음 빌드될 때마다 불필요한 Task가 생성되고 취소됩니다. viewWillAppear 기반 호출만으로 충분하므로 didBecomeActive의 fetchTripInfo() 호출을 제거하세요.
🐛 제안 수정
override func didBecomeActive() {
super.didBecomeActive()
-
- fetchTripInfo()
}Also applies to: 199-203
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift`
around lines 59 - 63, Remove the redundant fetchTripInfo() invocation from the
TravelToolInteractor lifecycle so it is only called from the viewWillAppear
path: in TravelToolInteractor, delete the fetchTripInfo() call inside override
func didBecomeActive() and keep the existing call in viewWillAppear (ensuring
fetchTripInfo() remains intact there); also remove the duplicate fetchTripInfo()
invocation in the other duplicate didBecomeActive block referenced around the
second occurrence so that only the viewWillAppear-driven fetch is performed.
| import UIKit | ||
|
|
||
| import DSKit |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if DSKit uses `@_exported` import for SnapKit/Then
echo "=== Checking `@_exported` imports in DSKit ==="
rg "@_exported import" Projects/Modules/DSKit/Sources/ -n
echo ""
echo "=== Checking imports in TravelToolTripCardView.swift ==="
# Get the full import list
rg "^import" Projects/Features/TravelToolFeature/Sources/Views/TravelToolTripCardView.swift -n
echo ""
echo "=== Checking TravelToolFeature project configuration ==="
# Check for project config files
fd "Project.swift|Package.swift" Projects/Features/TravelToolFeature/ -x cat {} \;
echo ""
echo "=== Checking DSKit imports (particularly in NDGLUpComingView) ==="
rg "^import" Projects/Modules/DSKit/Sources/ -A 0 | grep -E "(SnapKit|Then|import)" | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 2391
🏁 Script executed:
#!/bin/bash
# Find DependencyPlugin to see what baseFeatureDependency includes
echo "=== Searching for baseFeatureDependency definition ==="
rg "baseFeatureDependency" --type swift -A 10 -B 2
echo ""
echo "=== Searching for Features extension that defines baseFeatureDependency ==="
rg "extension.*Features" --type swift -A 50 | grep -A 50 "baseFeatureDependency"
echo ""
echo "=== Look for DependencyPlugin module file ==="
fd "DependencyPlugin" -type f | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 12548
🏁 Script executed:
#!/bin/bash
echo "=== Checking BaseFeatureDependency Project.swift ==="
fd "Project.swift" Projects/Features/BaseFeatureDependency/ -x cat {}
echo ""
echo "=== Checking BaseFeatureDependency source imports ==="
rg "^import" Projects/Features/BaseFeatureDependency/Sources/ -nRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 784
🏁 Script executed:
#!/bin/bash
echo "=== Checking DSKit main module/umbrella header ==="
fd -e swift "DSKit.swift|DSKit+.swift|\.h" Projects/Modules/DSKit/Sources/ | head -20
echo ""
echo "=== Checking DSKit Source structure ==="
tree -L 2 Projects/Modules/DSKit/Sources/ 2>/dev/null | head -40
echo ""
echo "=== Checking if DSKit has a public header or module map ==="
fd -e h -e modulemap Projects/Modules/DSKit/
echo ""
echo "=== Verify what happens with actual TravelToolTripCardView code ==="
cat Projects/Features/TravelToolFeature/Sources/Views/TravelToolTripCardView.swift | head -50Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 2395
🏁 Script executed:
#!/bin/bash
echo "=== Full TravelToolTripCardView.swift content ==="
cat Projects/Features/TravelToolFeature/Sources/Views/TravelToolTripCardView.swift
echo ""
echo "=== Check DSKit Project.swift for dependencies ==="
cat Projects/Modules/DSKit/Project.swift | head -60Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 3079
🏁 Script executed:
#!/bin/bash
echo "=== Check Core Project.swift ==="
cat Projects/Modules/Core/Project.swift | head -80
echo ""
echo "=== Check if SnapKit/Then are actually available via any transitive dependency ==="
rg "SnapKit|Then" Projects/Modules/Core/Project.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 352
🏁 Script executed:
#!/bin/bash
echo "=== Finding Core module location ==="
fd "Project.swift" | grep -i core
echo ""
echo "=== List all modules ==="
ls -la Projects/Modules/ 2>/dev/null || echo "Checking Plugins..."
fd . Projects/ -maxdepth 2 -type d | grep -i coreRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 696
🏁 Script executed:
#!/bin/bash
echo "=== Check Core Project.swift ==="
cat Projects/Core/Project.swift
echo ""
echo "=== Check ThirdPartyLibs Project.swift ==="
cat Projects/Modules/ThirdPartyLibs/Project.swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1228
🏁 Script executed:
#!/bin/bash
echo "=== Check if other views in TravelToolFeature explicitly import SnapKit/Then ==="
rg "import SnapKit|import Then" Projects/Features/TravelToolFeature/
echo ""
echo "=== Check if DSKit components export their dependencies ==="
head -20 Projects/Modules/DSKit/Sources/Component/NDGLUpComingView.swift
echo ""
echo "=== Check if BaseFeatureDependency re-exports ThirdPartyLibs ==="
rg "@_exported" Projects/Features/BaseFeatureDependency/Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 716
SnapKit 및 Then import 추가 필요
stackView.snp.makeConstraints(SnapKit)와 stackView.do { }(Then)를 사용하지만 명시적인 import 문이 없습니다. 코드는 ThirdPartyLibs 모듈의 전이 의존성을 통해 컴파일되지만, DSKit의 유사한 컴포넌트(NDGLUpComingView, NDGLOnGoingView)처럼 명시적으로 import하는 것이 코드의 명확성과 유지보수성을 높입니다.
import UIKit
import DSKit
import SnapKit
import Then🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/TravelToolFeature/Sources/Views/TravelToolTripCardView.swift`
around lines 9 - 11, Add explicit imports for SnapKit and Then at the top of
TravelToolTripCardView.swift so usages like stackView.snp.makeConstraints(...)
and stackView.do { ... } resolve without relying on transitive imports; update
the import block (after import DSKit) to include import SnapKit and import Then
so the file directly depends on those modules.
| if let url = URL(string: imageUrl) { | ||
| imageView.kf.setImage(with: url) | ||
| } else { | ||
| imageView.backgroundColor = .systemGray5 | ||
| } |
There was a problem hiding this comment.
Kingfisher 다운로드 실패 시 폴백 처리 누락
URL(string:) 파싱 성공 후에도 실제 네트워크 다운로드가 실패하면 imageView가 빈 상태로 남습니다. NDGLUpComingView와 동일하게 completion 클로저를 추가해 폴백 처리를 해야 합니다.
🛡️ 제안 수정
if let url = URL(string: imageUrl) {
- imageView.kf.setImage(with: url)
+ imageView.kf.setImage(with: url) { [weak self] result in
+ if case .failure = result {
+ self?.imageView.backgroundColor = .systemGray5
+ }
+ }
} else {
imageView.backgroundColor = .systemGray5
}📝 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.
| if let url = URL(string: imageUrl) { | |
| imageView.kf.setImage(with: url) | |
| } else { | |
| imageView.backgroundColor = .systemGray5 | |
| } | |
| if let url = URL(string: imageUrl) { | |
| imageView.kf.setImage(with: url) { [weak self] result in | |
| if case .failure = result { | |
| self?.imageView.backgroundColor = .systemGray5 | |
| } | |
| } | |
| } else { | |
| imageView.backgroundColor = .systemGray5 | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Modules/DSKit/Sources/Component/NDGLOnGoingView.swift` around lines
59 - 63, The image download currently only checks URL(string: imageUrl) but
doesn't handle Kingfisher download failures, leaving imageView empty; update
NDGLOnGoingView to call imageView.kf.setImage(with: url, completionHandler: ...)
(same pattern as NDGLUpComingView) and in the completion closure detect an error
or nil image and set imageView.backgroundColor = .systemGray5 (or the existing
fallback) so the fallback is applied when the network fetch fails.
🔗 연결된 이슈
📄 작업 내용
상태로 TripCardView 구성
💻 주요 코드 설명
TravelToolInteractor
// 여행 기간 일수 계산 후 최대 10일까지만 날씨 조회
TravelToolTripCardView
HomeInteractor
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항