Skip to content

Feat/#34 travel tool#35

Merged
KimNahun merged 9 commits into
developfrom
feat/#34-TravelTool
Feb 21, 2026
Merged

Feat/#34 travel tool#35
KimNahun merged 9 commits into
developfrom
feat/#34-TravelTool

Conversation

@KimNahun
Copy link
Copy Markdown
Contributor

@KimNahun KimNahun commented Feb 21, 2026

🔗 연결된 이슈

📄 작업 내용

  • TravelTool 탭 신규 구현 — RIBs 기반 모듈 생성 및 화면 이동 연결
  • 내 여행 상태에 따른 UI 분기 처리 — 여행 없음(Empty) / 다가오는 여행(UpComing) / 진행 중인 여행(OnGoing) 세 가지
    상태로 TripCardView 구성
  • Google Weather API 연동 — 여행 목적지 좌표 기반으로 날씨 예보 조회, 여행 기간에 해당하는 날씨만 필터링해서 표시
  • 날씨 데이터 유무에 따른 상태 분기 — noTrip / preparing / hasWeather 상태별 WeatherView 처리
  • 홈화면 인기여행 API 연결 — 카테고리 탭 선택 시 해당 카테고리 인기 여행 목록 재조회
  • 검색 화면 UX 개선 — 검색 결과를 새 화면이 아닌 동일 화면 내에서 표시하도록 수정

💻 주요 코드 설명

TravelToolInteractor

  • 여행 정보 조회 → 트립 카드 상태 업데이트 → 날씨 예보 조회 순으로 비동기 처리
  • 여행 시작일/종료일 기준으로 현재 상태(onGoing / upComing / empty)를 판별
  • 날씨 예보는 여행 기간 최대 10일치만 요청하고, 여행 날짜 범위에 해당하는 것만 필터링

// 여행 기간 일수 계산 후 최대 10일까지만 날씨 조회

  let forecastDays = min(daysFromToday, 10)
  let filtered = all.filter { info in
      let day = calendar.startOfDay(for: info.date)
      return day >= startOfTravel && day <= endOfTravel
  }

TravelToolTripCardView

  - TravelToolTripState enum으로 상태 관리, 상태에 따라 EmptyView / UpComingView / OnGoingView를 토글

  func configure(_ state: TravelToolTripState) {
      [emptyView, upComingView, onGoingView].forEach { $0.isHidden = true }
      switch state {
      case .empty:       emptyView.isHidden = false
      case .upComing:    upComingView.isHidden = false; upComingView.configure(...)
      case .onGoing:     onGoingView.isHidden = false; onGoingView.configure(...)
      }
  }

HomeInteractor

  • 홈 진입 시 내 여행 정보, 카테고리, 인기 여행, 추천 여행을 async let으로 병렬 조회
  • 카테고리 탭 선택 시 해당 id로 인기 여행만 별도 재조회하여 homeDataRelay 업데이트

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • TravelTool 탭에서 현재 여행 상태와 날씨 예보 정보 표시
    • 여행 생성 후 모달 창을 통한 여행 정보 확인 옵션 추가
  • 개선 사항

    • 검색 결과 표시 인터페이스 개선
    • 여행 탭 네비게이션 구조 업데이트
    • 여행 정보에 위치 기반 데이터(도시, 국가, 좌표) 통합

@KimNahun KimNahun self-assigned this Feb 21, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 21, 2026

Walkthrough

새로운 TravelTool 기능을 도입하며 날씨 데이터 통합, 도메인 모델 확장(위도/경도 및 위치 정보 추가), DI 구조 업데이트, 기존 기능들의 네비게이션 및 UI 리팩토링을 수행합니다.

Changes

Cohort / File(s) Summary
TravelTool 새 기능
Projects/Features/TravelToolFeature/Project.swift, Projects/Features/TravelToolFeature/Sources/TravelToolBuilder.swift, Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift, Projects/Features/TravelToolFeature/Sources/TravelToolRouter.swift, Projects/Features/TravelToolFeature/Sources/TravelToolViewController.swift, Projects/Features/TravelToolFeature/Sources/Views/...
TravelTool 기능 전체 구현: RIBs 아키텍처 기반 빌더, 인터랙터, 라우터, 뷰컨트롤러, 상태 관리(TravelToolTripState, TravelToolWeatherState), 여행 카드/날씨 뷰 UI 컴포넌트.
날씨 API 및 네트워킹
Projects/Modules/Networks/Sources/TargetType/WeatherAPI.swift, Projects/Modules/Networks/Sources/Service/WeatherService.swift, Projects/Modules/Networks/Sources/DTO/Weather/WeatherResponse.swift, Projects/Modules/Networks/Sources/Extensions/MoyaProvider+Async.swift
Google 날씨 API 클라이언트(Moya TargetType), WeatherService 프로토콜 및 구현, 날씨 응답 데이터 모델, async/await 기반 MoyaProvider 확장 메서드.
날씨 저장소 및 팩토리
Projects/Data/Sources/DI/WeatherServiceFactory.swift, Projects/Data/Sources/Repository/Weather/WeatherRepository.swift
WeatherService 생성 팩토리, WeatherRepositoryInterface 구현체로 API 응답을 도메인 모델로 변환.
도메인 모델 확장
Projects/Domain/Sources/Model/Home/MyTripSummary.swift, Projects/Domain/Sources/Model/Weather/WeatherInfo.swift, Projects/Domain/Sources/Interface/Weather/WeatherRepositoryInterface.swift
MyTripSummary에 city/country 속성, Schedule에 latitude/longitude 추가; DailyWeatherInfo 모델 신규; WeatherRepositoryInterface 프로토콜 정의.
데이터 변환 및 매핑
Projects/Data/Sources/Transform/UserTravelTransform.swift, Projects/Data/Sources/Transform/WeatherTransform.swift
UpcomingResponse에서 MyTripSummary 매핑 시 위도/경도 및 위치 정보 포함; ForecastDayResponse → DailyWeatherInfo 변환 로직 신규.
의존성 주입 구성
Projects/App/Sources/Application/AppComponent.swift, Projects/Features/MainFeature/Sources/MainBuilder.swift, Projects/Features/RootFeature/Sources/RootBuilder.swift, Projects/Features/TabBarFeature/Sources/TabBarBuilder.swift, Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift, Projects/Features/TabBarFeature/Sources/TabBarRouter.swift, Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency+Project.swift, Plugins/EnvPlugin/ProjectDescriptionHelpers/InfoPlist.swift
AppComponent에 weatherRepository 추가, 상위 컴포넌트들의 의존성 전파, TabBar에 TravelTool 통합, TravelTool 플러그인 추가, GOOGLE_WEATHER_API_KEY 환경 설정.
Follow 기능 리팩토링
Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift, Projects/Features/FollowFeature/Sources/FollowDetailViewController.swift, Projects/Features/FollowFeature/Sources/PlaceDetail/PlaceDetailViewController.swift, Projects/Features/FollowFeature/Sources/TripCalendar/TripCalendarViewController.swift
followDetailDidAddTrip 콜백을 followDetailDidViewTrip으로 교체, 여행 생성 모달 UI 추가, 커스텀 NDGLNavigationBar 도입, TripCalendar에 RxSwift 통합 및 네비게이션 바 추가.
Home 기능 업데이트
Projects/Features/HomeFeature/Sources/HomeInteractor.swift, Projects/Features/HomeFeature/Sources/HomeViewController.swift, Projects/Features/HomeFeature/Sources/Views/Cells/HomeBannerCell.swift
"전체" 카테고리 추가, 카테고리 선택 시 인기 여행지 리로드, viewWillAppear 라이프사이클 훅 추가, 뷰 타입을 NDGLUpComingView/NDGLOnGoingView로 업데이트.
Search 기능 리팩토링
Projects/Features/SearchFeature/Sources/SearchBuilder.swift, Projects/Features/SearchFeature/Sources/SearchInteractor.swift, Projects/Features/SearchFeature/Sources/SearchRouter.swift, Projects/Features/SearchFeature/Sources/SearchViewController.swift
SearchResult 중첩 기능 제거, 인라인 검색 결과 표시로 변경, 로딩/에러 상태 UI 추가, 컬렉션 뷰 기반 데이터 소스 구현, 검색 항목 선택 및 재로드 기능.
Travel 기능 업데이트
Projects/Features/TravelFeature/Sources/TravelInteractor.swift, Projects/Features/TravelFeature/Sources/TravelViewController.swift
didTapMenuButton 제거, viewWillAppear에서 데이터 로드, NDGLNavigationBar로 UI 단순화.
Main 및 TabBar 라우팅
Projects/Features/MainFeature/Sources/MainInteractor.swift, Projects/Features/MainFeature/Sources/MainRouter.swift, Projects/Features/TabBarFeature/Sources/TabBarViewController.swift, Projects/Features/TabBarFeature/Project.swift
switchToTab(at:) 라우팅 메서드 추가, TravelTool 탭 추가(인덱스 0), 탭 전환 로직 구현.
UI 컴포넌트 및 자산
Projects/Modules/DSKit/Sources/Component/NDGLNavigationBar.swift, Projects/Modules/DSKit/Sources/Component/NDGLUpComingView.swift, Projects/Modules/DSKit/Sources/Component/NDGLOnGoingView.swift, Projects/Modules/DSKit/Resources/Assets.xcassets/ic_weather_*.imageset/Contents.json
NDGLNavigationBar 패딩 조정, HomeBannerUpCommingView → NDGLUpComingView 공개 컴포넌트로 전환, HomeBannerOnGoingView → NDGLOnGoingView 공개 컴포넌트로 전환, 10개의 날씨 아이콘 자산 추가.
네트워킹 및 설정
Projects/Modules/Networks/Sources/Foundation/NetworkConfiguration.swift, Projects/App/Resources/PrivacyInfo.xcprivacy
weatherApiKey 설정 추가, 개인정보 보호 정책 파일(UserDefaults 접근 선언).

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feat/#27 검색api연결 #32: 네트워킹 설정 및 앱 plist에 API 키 추가 관련 — 두 PR 모두 NetworkConfiguration API 키 접근자와 대응하는 InfoPlist 키를 추가하므로 코드 수준에서 연관됨.

Suggested labels

🧑🏻‍💻 feat

Poem

🐰 여행 도구로 하늘 그리고,
날씨 데이터 펄펄 날리며,
도메인 모델 쌓고 쌓아,
의존성 연결 요정처럼,
탭을 넘나들며 춤을 춘다! ✨🌤️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 PR의 주요 변경 사항인 TravelTool 탭 신규 구현과 관련 기능들을 간결하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 주요 섹션(연결된 이슈, 작업 내용, 주요 코드 설명)을 포함하고 있으며, 상세한 구현 내용과 코드 예시를 제공하고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#34-TravelTool

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

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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이 최초 활성화되면 didBecomeActiveloadTrips() 직후 viewWillAppearloadTrips()가 연속 실행되어 동일 데이터를 두 번 로드합니다.

현재는 목 데이터라 영향이 없지만, 실제 API로 교체되면 초기 진입 시 불필요한 네트워크 요청이 두 번 발생합니다. viewWillAppear()가 데이터 로딩을 담당하는 것이 의도라면, didBecomeActiveloadTrips() 호출을 제거하는 것이 좋습니다.

🛠 수정 제안
 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 / dark appearance 변형을 추가하는 것을 고려해 주세요.

프로젝트의 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()에서 CellRegistrationSupplementaryRegistration API를 사용하고 있으므로, 셀과 헤더 등록이 자동으로 처리됩니다. 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에서 dataSourceUICollectionViewDiffableDataSource<...>! (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 경고)

NetworksMoya의 순서가 알파벳 순서(MoyaNetworks)와 반대입니다.

♻️ 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: StringWeatherIconMapper에서 문자열 리터럴과 직접 비교됩니다. 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.swiftWeatherIconMapper도 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: didBecomeActiveviewWillAppear에서 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.

Comment on lines +15 to +20
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 }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.swift" | grep -E "(Weather|Travel)" | head -20

Repository: 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.swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1063


🏁 Script executed:

cat -n Projects/Data/Sources/Transform/UserTravelTransform.swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 1935


🏁 Script executed:

rg -n "displayDate" Projects/Data/Sources/Transform/WeatherTransform.swift

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 212


🏁 Script executed:

rg -n "ForecastDayResponse" Projects/Modules/Networks/Sources/DTO/Weather/ -A 15

Repository: 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


암묵적 타임존 처리로 인한 유지보수 위험

WeatherTransformCalendar.current.date(from: components)UserTravelTransformDateFormatter(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.

Comment on lines +24 to +25
maxTemperature: maxTemperature?.degrees ?? 0,
minTemperature: minTemperature?.degrees ?? 0,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

온도 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".

Comment on lines +200 to +213
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()
}
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*NDGLModal*" -o -name "*FollowDetailInteractor*" | head -20

Repository: 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.swift

Repository: 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 분리 미처리 문제

NDGLModalViewControllermodalPresentationStyle = .overFullScreen으로 설정되어 있으며 isModalInPresentation을 별도로 설정하지 않습니다(line 83-84). 이로 인해 사용자가 모달을 아래로 스와이프하여 닫을 수 있으며, 이 경우 onCancelTapped, onActionTapped 어느 클로저도 호출되지 않습니다. dimViewTapped 메서드(line 213-217)는 탭 제스처만 처리하고 시스템 스와이프 제스처는 처리하지 않기 때문입니다.

결과적으로:

  • router?.detachTripCalendar()가 실행되지 않음
  • tripCalendarRouter가 메모리에 남아있는 RIB 누수 발생
  • 후속 여행 생성 시 예상치 못한 상태 문제 가능

해결방안:

  1. NDGLModalViewController에서 isModalInPresentation = true 설정하거나
  2. UIAdaptivePresentationControllerDelegatepresentationControllerDidDismiss 메서드로 모든 해제 경로에서 클로저 실행

또한 라인 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)")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

디버그 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.

Comment on lines +156 to +177
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)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

fetchPopularTripsTask가 저장되지 않아 경쟁 상태가 발생할 수 있습니다.

사용자가 카테고리를 빠르게 전환하면 여러 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).

Comment on lines +45 to +49
if let url = URL(string: imageUrl) {
imageView.kf.setImage(with: url)
} else {
imageView.backgroundColor = .systemGray5
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

유효하지 않은 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.

Suggested change
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.

Comment on lines +37 to +41
if let url = URL(string: imageUrl) {
imageView.kf.setImage(with: url)
} else {
imageView.backgroundColor = .systemGray5
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

이미지 다운로드 실패 시 폴백 처리 누락

현재 코드는 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.

Suggested change
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.

Comment on lines +191 to +208
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +333 to +334
case "LIGHT_SNOW", "SNOW", "HEAVY_SNOW", "BLIZZARD", "FLURRIES":
return DSKitAsset.Assets.icWeatherCloud.image
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

눈 날씨 조건이 구름 아이콘으로 표시됨

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.

Comment on lines +17 to +19
public var baseURL: URL {
URL(string: "https://weather.googleapis.com")!
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for baseURL implementations in TargetType files
cd Projects/Modules/Networks/Sources/TargetType 2>/dev/null || cd . 
rg -n 'var baseURL' --type=swift -A 3

Repository: 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 -n

Repository: 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 2

Repository: 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.

KimNahun and others added 2 commits February 22, 2026 03:44
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
Projects/Features/TravelToolFeature/Sources/TravelToolInteractor.swift (2)

46-46: 미사용 disposeBagRxSwift import 제거

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).

Comment on lines +59 to +63
override func didBecomeActive() {
super.didBecomeActive()

fetchTripInfo()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

didBecomeActive에서의 중복 fetch 제거 필요

RIBs의 초기 활성화 시 didBecomeActive → (뷰 표시) → viewWillAppear 순서로 호출되어, fetchTripInfo()가 두 번 실행됩니다. 두 번째 호출(Line 73)이 첫 번째 Task를 즉시 취소하므로 실제 네트워크 응답은 하나지만, 매번 탭이 처음 빌드될 때마다 불필요한 Task가 생성되고 취소됩니다. viewWillAppear 기반 호출만으로 충분하므로 didBecomeActivefetchTripInfo() 호출을 제거하세요.

🐛 제안 수정
 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.

Comment on lines +9 to +11
import UIKit

import DSKit
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check 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 -20

Repository: 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 -20

Repository: 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/ -n

Repository: 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 -50

Repository: 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 -60

Repository: 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.swift

Repository: 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 core

Repository: 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.swift

Repository: 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


SnapKitThen 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.

Comment on lines 59 to 63
if let url = URL(string: imageUrl) {
imageView.kf.setImage(with: url)
} else {
imageView.backgroundColor = .systemGray5
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@KimNahun KimNahun merged commit 9a9db00 into develop Feb 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant