Skip to content

Feat/#37 travel modify#39

Merged
KimNahun merged 8 commits into
developfrom
feat/#37-Travel-Modify
Feb 24, 2026
Merged

Feat/#37 travel modify#39
KimNahun merged 8 commits into
developfrom
feat/#37-Travel-Modify

Conversation

@KimNahun
Copy link
Copy Markdown
Contributor

@KimNahun KimNahun commented Feb 24, 2026

🔗 연결된 이슈

📄 작업 내용

  • AddPlace RIB 구현 (Google Places API 기반 장소 검색)
  • 내 여행 장소 단건 추가 API 연동 (POST /api/v1/placesPOST /api/v1/travels/{id}/itinerary)
  • 내 여행 일정 전체 교체 API 연동 (PUT /api/v1/travels/{id}/itinerary)
  • 편집 모드 장소 삭제 및 드래그 재정렬 구현
  • 저장된 내 여행 불러오기 및 홈화면 연동

💻 주요 코드 설명

AddPlaceInteractor

  • Google Places Text Search API로 검색 후 POST /api/v1/places로 백엔드 DB에 장소 등록, 이후 POST /api/v1/travels/{id}/itinerary로 일정에
    추가하는 2단계 흐름 구현
try await followDetailUsecase.registerPlace(googlePlaceId: place.googlePlaceId)
try await followDetailUsecase.addItinerary(
    travelId: travelId,
    googlePlaceId: place.googlePlaceId,
    day: currentDay,
    sequence: sequence
)

FollowDetailInteractor - editCompleted
- 편집 완료 시 현재 화면에 표시된 순서 그대로 PUT /api/v1/travels/{id}/itinerary 전송 (sequence = index + 1)
func editCompleted(orderedPlaces: [TravelPlace]) {
    placesByDay[currentDay] = orderedPlaces
    Task {
        try await followDetailUsecase.replaceItinerary(
            travelId: travelId,
            places: buildAllPlaces()
        )
    }
}

PlaceListCollectionView
- UICollectionViewDiffableDataSource reorderingHandlers로 드래그 재정렬 구현
- deleteSelected()  선택 항목만 로컬 삭제 후 남은 목록 반환 (편집 모드 유지)
func deleteSelected() -> [TravelPlace] {
    let remaining = places.filter { !selectedIds.contains($0.id) }
    selectedIds.removeAll()
    applySnapshot(places: remaining)
    return remaining
}

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • Google Places를 통한 장소 검색 기능 추가
    • 여행 일정 관리 기능 추가 (장소 조회, 추가, 편집, 삭제)
    • 편집 모드에서 다중 장소 선택 및 삭제 기능 추가
  • 개선 사항

    • 여행 세부 정보 및 일정 조회 기능 향상
    • 장소 목록 UI/UX 개선

@KimNahun KimNahun requested a review from ChoiAnYong February 24, 2026 13:52
@KimNahun KimNahun self-assigned this Feb 24, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 24, 2026

Walkthrough

이 PR은 Google Places API 통합을 통한 장소 검색 기능, 장소 등록, 여행 일정 관리(조회, 추가, 교체)를 구현합니다. AddPlace 기능, 여행 수정 모드, 새로운 도메인 모델 및 네트워크 계층을 추가하여 여행 일정 편집 워크플로우를 활성화합니다.

Changes

Cohort / File(s) Summary
Google Places Service Integration
Projects/Data/Sources/DI/GooglePlacesServiceFactory.swift, Projects/Modules/Networks/Sources/Service/GooglePlacesService.swift, Projects/Modules/Networks/Sources/TargetType/GooglePlacesAPI.swift, Projects/Modules/Networks/Sources/DTO/Place/GooglePlacesResponse.swift
Google Places API 서비스, 팩토리, TargetType, 응답 DTO 새로 추가; textQuery 기반 장소 검색 엔드포인트 구현
Place Repository & Service
Projects/Data/Sources/Repository/Place/PlaceRepository.swift, Projects/Domain/Sources/Interface/Place/PlaceRepositoryInterface.swift, Projects/Modules/Networks/Sources/Service/PlaceService.swift, Projects/Modules/Networks/Sources/TargetType/PlaceAPI.swift
PlaceRepository에 googlePlacesService 의존성 추가; searchPlaces 시그니처를 키워드 기반 비동기 메서드로 변경; registerPlace 메서드 추가
Travel Itinerary Management
Projects/Data/Sources/Repository/UserTravel /UserTravelRepository.swift, Projects/Domain/Sources/Interface/UserTravel/UserTravelRepositoryInterface.swift, Projects/Modules/Networks/Sources/Service/UserTravelService.swift, Projects/Modules/Networks/Sources/TargetType/UserTravelAPI.swift, Projects/Modules/Networks/Sources/DTO/Travel/TravelDTO.swift
일정 조회, 추가, 교체 메서드 구현; AddItineraryRequest, ReplaceItineraryRequest, UserTravelItineraryResponse 등 새로운 DTO 추가
Domain Models & Transformations
Projects/Domain/Sources/Model/Follow/PlaceSearchResult.swift, Projects/Data/Sources/Transform/PlaceTransform.swift, Projects/Data/Sources/Transform/UserTravelTransform.swift
PlaceSearchResult 도메인 모델 추가; GooglePlaceItem, UserContentCardResponse, UserTravelItineraryResponse 변환 메서드 추가
AddPlace Feature (RIBs)
Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceBuilder.swift, Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceInteractor.swift, Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceRouter.swift, Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceViewController.swift
새로운 AddPlace 기능 모듈; 키워드 기반 장소 검색, 지도 표시, 결과 추가 기능 포함
FollowDetail Feature Updates
Projects/Features/FollowFeature/Sources/FollowDetailBuilder.swift, Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift, Projects/Features/FollowFeature/Sources/FollowDetailRouter.swift, Projects/Features/FollowFeature/Sources/FollowDetailViewController.swift
FollowDetailMode enum 추가로 template/myTravel 모드 구분; 일정 편집 모드 UI, AddPlace 라우팅 통합; 일정 추가/교체/삭제 로직 구현
Place UI Components
Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift, Projects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift
PlaceCell에 체크박스, 드래그 핸들 추가; PlaceListCollectionView에 편집 모드, 다중 선택, 드래그 재정렬 기능 추가
Navigation & Routing Updates
Projects/Features/MainFeature/Sources/MainInteractor.swift, Projects/Features/MainFeature/Sources/MainRouter.swift, Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift, Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift
MyTravel 상세보기 라우팅 추가; FollowDetailMode 기반 라우팅 분기 구현
Domain UseCase
Projects/Domain/Sources/UseCase/FollowDetailUsecase.swift
일정 조회, 장소 검색, 등록, 일정 관리 메서드를 FollowDetailUsecaseProtocol에 추가
Home & Popular Feature
Projects/Features/HomeFeature/Sources/HomeInteractor.swift, Projects/Features/HomeFeature/Sources/HomeViewController.swift, Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift, Projects/Features/PopularTravelFeature/Sources/PopularTravelViewController.swift
viewWillAppear 라이프사이클 훅 추가; 카테고리 선택 상태 추적; 배너 새로고침 로직 추가
Dependency Injection & Config
Projects/Application/Sources/Application/AppComponent.swift
PlaceRepository 생성 시 googlePlacesService 의존성 전달
UI Layout Updates
Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift
PopularInfoCell 썸네일 높이 제약 조정; 고정 높이로 변경

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant VC as AddPlaceViewController
    participant Interactor as AddPlaceInteractor
    participant UseCase as FollowDetailUsecase
    participant Repository as PlaceRepository
    participant Service as GooglePlacesService
    participant API as Google Places API

    User->>VC: 키워드 입력
    VC->>Interactor: search(keyword:)
    Interactor->>Interactor: 이전 searchTask 취소
    Interactor->>VC: setLoading(true)
    
    Interactor->>UseCase: searchPlaces(keyword:)
    UseCase->>Repository: searchPlaces(keyword:)
    Repository->>Service: searchText(keyword:)
    Service->>API: POST /v1/places:searchText
    API-->>Service: GooglePlacesSearchResponse
    Service-->>Repository: [PlaceSearchResult]
    Repository-->>UseCase: [PlaceSearchResult]
    UseCase-->>Interactor: [PlaceSearchResult]
    
    alt 결과 있음
        Interactor->>Interactor: selectedPlace = 첫번째 결과
        Interactor->>VC: showResult(place)
        VC->>VC: 지도에 annotation 추가<br/>결과 카드 표시
        Interactor->>VC: setAddButtonEnabled(true)
    else 결과 없음
        Interactor->>VC: showNoResults()
        Interactor->>VC: setAddButtonEnabled(false)
    end
    
    Interactor->>VC: setLoading(false)
Loading
sequenceDiagram
    actor User
    participant VC as FollowDetailViewController
    participant Interactor as FollowDetailInteractor
    participant UseCase as FollowDetailUsecase
    participant PlaceRepo as PlaceRepository
    participant TravelRepo as UserTravelRepository
    participant Service as UserTravelService

    User->>VC: 장소 추가 버튼 탭
    VC->>Interactor: didTapAddToTrip()
    
    alt MyTravel 모드
        Interactor->>Interactor: routeToAddPlace()
        Note over Interactor: AddPlace 화면 표시
        
        User->>VC: 장소 선택 완료
        Interactor->>UseCase: registerPlace(googlePlaceId:)
        UseCase->>PlaceRepo: registerPlace(googlePlaceId:)
        PlaceRepo->>Service: registerPlace(googlePlaceId:)
        Service-->>PlaceRepo: Success
        PlaceRepo-->>UseCase: Success
        UseCase-->>Interactor: Success
        
        Interactor->>UseCase: addItinerary(travelId:, googlePlaceId:, day:, sequence:)
        UseCase->>TravelRepo: addItinerary(...)
        TravelRepo->>Service: addItinerary(...)
        Service-->>TravelRepo: Success
        TravelRepo-->>UseCase: Success
        UseCase-->>Interactor: Success
        
        Interactor->>Interactor: loadPlaces(for: currentDay)
        Interactor->>VC: showToast("장소 추가 완료")
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60분

Suggested labels

🧑🏻‍💻 feat, ✨ design

Suggested reviewers

  • ChoiAnYong

Poem

🐰 Google Places와 함께 춤을 춘다면,
여행 일정은 내 손 안에!
검색하고, 등록하고, 담고...
일정 편집의 마법이 완성되었다! ✨🗺️

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning PR의 모든 변경사항이 여행 수정(#37)의 범위 내에 있으나, PopularInfoCell 레이아웃 변경은 여행 수정과 직접적인 관련이 없음. PopularInfoCell.swift의 레이아웃 변경사항(thumbnail 높이 고정화)이 여행 수정 기능과 무관하므로 별도 PR로 분리하거나 변경 이유를 명확히 설명하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Feat/#37 travel modify'는 변경 사항의 주요 목적인 여행 수정 기능 구현을 일반적으로 나타내지만, 구체적인 기능(AddPlace RIB, 장소 검색, 일정 수정 등)을 충분히 설명하지 못함.
Description check ✅ Passed PR 설명은 템플릿의 주요 섹션(연결된 이슈, 작업 내용, 코드 설명)을 포함하고 있으며, 구체적인 구현 내용과 코드 예시를 제공함.
Linked Issues check ✅ Passed PR 변경사항이 이슈 #37의 요구사항인 여행 일정 수정 기능(장소 검색/추가, 삭제, 재정렬, API 연동, 내 여행 불러오기)을 완벽하게 구현함.

✏️ 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/#37-Travel-Modify

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

🧹 Nitpick comments (5)
Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift (2)

154-162: dragHandleImageViewcontainerView 뒤에 위치 — z-order 취약성

contentView.addSubview(dragHandleImageView)contentView.addSubview(containerView)보다 먼저 호출되어, dragHandleImageView의 z-index가 containerView보다 낮습니다. 현재는 편집 모드 inset이 28pt라 드래그 핸들(20pt)과 containerView 사이에 8pt 여유가 있어 가려지지 않지만, setEditMode의 inset 값이 20 이하로 바뀌면 핸들이 containerView 뒤로 숨어 탭/팬 인터랙션이 동작하지 않게 됩니다. dragHandleImageViewcontainerView 이후에 추가해야 안전합니다.

♻️ 리팩토링 제안
-        // 드래그 핸들 (편집 모드)
-        contentView.addSubview(dragHandleImageView)
-
         // 메인 컨테이너
         contentView.addSubview(containerView)

+        // 드래그 핸들 (편집 모드) — containerView 위에 위치해야 함
+        contentView.addSubview(dragHandleImageView)
+
         // 컨테이너 내부 요소들
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift` around
lines 154 - 162, The dragHandleImageView is added to contentView before
containerView, causing it to be behind containerView and vulnerable to being
obscured if insets change; fix by reordering the view additions so
contentView.addSubview(containerView) is called before
contentView.addSubview(dragHandleImageView) (update the initialization/placement
logic where dragHandleImageView and containerView are added, and ensure any
layout/setup in setEditMode still references dragHandleImageView after
containerView is added).

38-61: #28A745 색상 상수 중복 — 추출 권장

UIColor(hexCode: "#28A745")가 이 파일에서 sequenceView.backgroundColor(line 32), checkboxView.layer.borderColor(line 41), updateCheckboxAppearancecheckboxView.backgroundColor(line 274)까지 3곳에 중복 사용됩니다. DSKit의 디자인 토큰이 없다면 파일 상단에 private 상수로 추출하는 것이 낫습니다.

♻️ 리팩토링 제안
+    private enum Palette {
+        static let brandGreen = UIColor(hexCode: "#28A745")
+    }
+
     private let sequenceView = UIView().then {
-        $0.backgroundColor = UIColor(hexCode: "#28A745")
+        $0.backgroundColor = Palette.brandGreen
         $0.layer.cornerRadius = 10
     }
     ...
     private let checkboxView = UIView().then {
         $0.layer.borderWidth = 1.5
-        $0.layer.borderColor = UIColor(hexCode: "#28A745").cgColor
+        $0.layer.borderColor = Palette.brandGreen.cgColor
         ...
     }

그리고 updateCheckboxAppearance에서도 동일하게 Palette.brandGreen으로 교체.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift` around
lines 38 - 61, Extract the repeated hex color "#28A745" into a single private
constant at the top of the file (e.g., private let brandGreen = UIColor(hexCode:
"#28A745") or use Palette.brandGreen if available) and replace all direct uses
of UIColor(hexCode: "#28A745") with that constant; specifically update
sequenceView.backgroundColor, checkboxView.layer.borderColor (use
brandGreen.cgColor), and the assignment inside updateCheckboxAppearance that
sets checkboxView.backgroundColor so all three locations reference the new
constant or Palette.brandGreen.
Projects/Data/Sources/Transform/UserTravelTransform.swift (1)

15-33: 기본값/플레이스홀더 사용 의도 확인 권장.

budgetPerPerson=0, YouTubeInfo 빈값이 실제 데이터로 보일 수 있어요. API에 값이 있다면 매핑하거나, 없다는 전제를 명시해 두는 편이 안전합니다.

🤖 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 15 -
33, The toDomain() mapper in UserContentCardResponse currently hardcodes
budgetPerPerson = 0 and populates YouTubeInfo with empty strings/nils; instead,
in UserContentCardResponse.toDomain() map actual response properties into
TravelDetail and YouTubeInfo (use whatever response fields correspond to
budgetPerPerson, youtube title, youtuber, thumbnail, profileImage, link,
summary), or explicitly set those TravelDetail/YouTubeInfo properties to nil (or
optional) when the API did not provide them — avoid using silent placeholders
like ""/0; update TravelDetail/YouTubeInfo usage accordingly so absent data is
represented as optional/nil and present data is fully mapped.
Projects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift (1)

92-92: SwiftLint 경고: 변수 이름 및 암시적 반환

두 곳에서 SwiftLint 경고가 발생합니다.

  1. Line 92 — 변수명 ip가 최소 길이(3자) 미만입니다 (identifier_name).
  2. Line 109 — 클로저에서 return 키워드가 불필요합니다 (implicit_return).
✏️ 수정 제안
-                if let ip = self.indexPath(for: cell) {
-                    self.beginInteractiveMovementForItem(at: ip)
+                if let indexPath = self.indexPath(for: cell) {
+                    self.beginInteractiveMovementForItem(at: indexPath)
 diffableDataSource?.reorderingHandlers.canReorderItem = { [weak self] _ in
-    return self?.isEditMode ?? false
+    self?.isEditMode ?? false
 }

Also applies to: 109-109

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Projects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift`
at line 92, Rename the short variable ip to a longer, descriptive name (e.g.,
indexPath or cellIndexPath) where you call indexPath(for: cell) in
PlaceListCollectionView (replace "ip" in the if-let binding that uses
indexPath(for: cell)), and remove the unnecessary explicit return in the closure
at the other location (line 109) so the closure uses implicit return expression
syntax; ensure both changes compile and conform to SwiftLint rules
(identifier_name and implicit_return).
Projects/Data/Sources/Repository/Place/PlaceRepository.swift (1)

23-30: 위치 정보 없는 항목이 조용히 필터링됩니다

compactMap { $0.toDomain() }location이 없는 GooglePlaceItem을 아무 로그 없이 제외합니다. 여행 앱에서는 합리적인 동작이지만, 실제 검색 결과 수와 UI 표시 수가 다를 경우 원인 추적이 어렵습니다. 디버그 빌드에서라도 필터링된 항목을 로깅하면 QA에 도움이 됩니다.

💡 로깅 제안 (선택 사항)
-            return (response.places ?? []).compactMap { $0.toDomain() }
+            let items = response.places ?? []
+            let results = items.compactMap { $0.toDomain() }
+            `#if` DEBUG
+            let filtered = items.count - results.count
+            if filtered > 0 {
+                print("[PlaceRepository] \(filtered)개 항목이 위치 정보 부재로 제외됨")
+            }
+            `#endif`
+            return results
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Data/Sources/Repository/Place/PlaceRepository.swift` around lines 23
- 30, searchPlaces에서 response.places를 바로 compactMap { $0.toDomain() } 하여
location이 없는 GooglePlaceItem이 무음으로 필터링되므로, 디버그(또는 QA) 빌드에서 필터된 항목을 로깅하도록 수정하세요:
googlePlacesService.searchText 호출로 받은 response.places를 먼저 순회해 각 항목의 location 유무를
검사하고 location이 없는 항목에 대해 검색어·place id·이름 등 식별 가능한 정보를 포함해 로그를 남긴 후 정상 항목만
PlaceSearchResult로 변환(기존 toDomain 사용)해 반환하도록 searchPlaces를 변경하세요.
🤖 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/FollowFeature/Sources/AddPlace/AddPlaceViewController.swift`:
- Around line 139-143: The subscription to searchBar.searchButtonClicked in
AddPlaceViewController is using an unused parameter causing a SwiftLint warning;
either change the closure signature to use the anonymous parameter (_) — i.e.
subscribe(with: self) { owner, _ in ... } — or remove the entire subscription
block if it's redundant (since searchBar.searchText handles Return key),
ensuring disposeBag usage is updated accordingly.

In `@Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift`:
- Around line 194-214: buildAllPlaces uses placesByDay.values which is unordered
and can omit unloaded days causing replaceItinerary to receive an
incomplete/incorrect itinerary; update editCompleted/buildAllPlaces to (1)
iterate over sorted day keys (e.g., sort placesByDay.keys) to guarantee
deterministic order, and (2) ensure all expected days are present before calling
followDetailUsecase.replaceItinerary by either preloading missing days or
merging with a preserved original snapshot of the itinerary so no day is
accidentally deleted; keep references to placesByDay, buildAllPlaces,
editCompleted, replaceItinerary and travelId when implementing the fix.

In
`@Projects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift`:
- Around line 87-113: The custom pan gesture path (cell.onDragHandlePan closure
using
beginInteractiveMovementForItem/updateInteractiveMovementTargetPosition/endInteractiveMovement)
can bypass DiffableDataSource.didReorder when canReorderItem is false, so after
the pan ends you must defensively synchronize the model from the data source
snapshot; inside the onDragHandlePan closure's .ended case (near
beginInteractiveMovementForItem / endInteractiveMovement) fetch the current
snapshot from diffableDataSource (or diffableDataSource?.snapshot()), extract
the final order with snapshot.itemIdentifiers(inSection: 0) and assign it to
places (same property updated in reorderingHandlers.didReorder), ensuring you
still call endInteractiveMovement() and keep weak self/cell handling.

In `@Projects/Features/HomeFeature/Sources/HomeInteractor.swift`:
- Around line 167-169: The catch block in HomeInteractor.swift currently
swallows all errors (the catch after fetching trips), making debugging
impossible; update the catch in the HomeInteractor's trips refresh/fetch method
to log the caught error details (using the app's Logger or debugPrint) including
context like "fetchTrips failed" and the error, and only suppress silently when
you can explicitly detect the "no trips" condition (or handle specific errors
like a NotFound/empty response); do not remove existing behavior—just add
debug-level logging for network/auth/decoding errors and distinguish the "no
trips" case from other failures.
- Around line 153-171: The Task created in refreshBanner() must be stored so it
can be cancelled and not duplicated: add a Task property (e.g.,
refreshBannerTask: Task<Void, Never>?) on the Interactor, assign the Task
returned by refreshBanner() to that property (cancelling any existing
refreshBannerTask first), and update willResignActive() to cancel
refreshBannerTask alongside fetchDataTask; keep using usecase.fetchMyTripInfo(),
weak self capture, and homeDataRelay.accept(updated) logic but ensure the stored
task is cleared on completion or cancellation to avoid stale references.

In
`@Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift`:
- Around line 133-145: The fetchPopularTrips method passes the categoryId
directly to usecase.fetchPopularTripList, causing inconsistent handling for the
"전체" category compared to HomeInteractor; update fetchPopularTrips to map the
special "전체" category sentinel (e.g., id == -1) to nil before calling
usecase.fetchPopularTripList (so usecase.fetchPopularTripList receives nil for
"전체"), keeping the existing Task cancellation and error handling and referencing
the fetchPopularTrips function and usecase.fetchPopularTripList call to locate
where to apply the mapping.

In `@Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift`:
- Around line 118-123: In setLayout(), the thumbnailView uses mixed scaling
helpers (width uses adjusted, height uses adjustedH) which can break the
intended 140:88 aspect ratio on different devices; change the constraints so
both axes use the same scaling basis (e.g., both use adjusted) or, better,
constrain height relative to width to preserve aspect ratio (set thumbnailView
width via 140.adjusted and set thumbnailView height as a multiplier of that
width to match 88/140) so the 140:88 ratio remains consistent across devices.

In `@Projects/Modules/Networks/Sources/TargetType/GooglePlacesAPI.swift`:
- Around line 12-43: The headers for GooglePlacesAPI are incorrectly using
NetworkConfiguration.weatherApiKey; add a dedicated key (e.g.,
NetworkConfiguration.placesApiKey or googlePlacesApiKey) in NetworkConfiguration
and replace the reference in GooglePlacesAPI.headers so the "X-Goog-Api-Key"
header uses the new places API key constant; update any config loading/Env
parsing that assembles NetworkConfiguration to populate the new property as well
as any tests or docs that reference the old key.

---

Nitpick comments:
In `@Projects/Data/Sources/Repository/Place/PlaceRepository.swift`:
- Around line 23-30: searchPlaces에서 response.places를 바로 compactMap {
$0.toDomain() } 하여 location이 없는 GooglePlaceItem이 무음으로 필터링되므로, 디버그(또는 QA) 빌드에서
필터된 항목을 로깅하도록 수정하세요: googlePlacesService.searchText 호출로 받은 response.places를 먼저
순회해 각 항목의 location 유무를 검사하고 location이 없는 항목에 대해 검색어·place id·이름 등 식별 가능한 정보를 포함해
로그를 남긴 후 정상 항목만 PlaceSearchResult로 변환(기존 toDomain 사용)해 반환하도록 searchPlaces를
변경하세요.

In `@Projects/Data/Sources/Transform/UserTravelTransform.swift`:
- Around line 15-33: The toDomain() mapper in UserContentCardResponse currently
hardcodes budgetPerPerson = 0 and populates YouTubeInfo with empty strings/nils;
instead, in UserContentCardResponse.toDomain() map actual response properties
into TravelDetail and YouTubeInfo (use whatever response fields correspond to
budgetPerPerson, youtube title, youtuber, thumbnail, profileImage, link,
summary), or explicitly set those TravelDetail/YouTubeInfo properties to nil (or
optional) when the API did not provide them — avoid using silent placeholders
like ""/0; update TravelDetail/YouTubeInfo usage accordingly so absent data is
represented as optional/nil and present data is fully mapped.

In `@Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift`:
- Around line 154-162: The dragHandleImageView is added to contentView before
containerView, causing it to be behind containerView and vulnerable to being
obscured if insets change; fix by reordering the view additions so
contentView.addSubview(containerView) is called before
contentView.addSubview(dragHandleImageView) (update the initialization/placement
logic where dragHandleImageView and containerView are added, and ensure any
layout/setup in setEditMode still references dragHandleImageView after
containerView is added).
- Around line 38-61: Extract the repeated hex color "#28A745" into a single
private constant at the top of the file (e.g., private let brandGreen =
UIColor(hexCode: "#28A745") or use Palette.brandGreen if available) and replace
all direct uses of UIColor(hexCode: "#28A745") with that constant; specifically
update sequenceView.backgroundColor, checkboxView.layer.borderColor (use
brandGreen.cgColor), and the assignment inside updateCheckboxAppearance that
sets checkboxView.backgroundColor so all three locations reference the new
constant or Palette.brandGreen.

In
`@Projects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift`:
- Line 92: Rename the short variable ip to a longer, descriptive name (e.g.,
indexPath or cellIndexPath) where you call indexPath(for: cell) in
PlaceListCollectionView (replace "ip" in the if-let binding that uses
indexPath(for: cell)), and remove the unnecessary explicit return in the closure
at the other location (line 109) so the closure uses implicit return expression
syntax; ensure both changes compile and conform to SwiftLint rules
(identifier_name and implicit_return).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc7eef4 and 292aa92.

📒 Files selected for processing (37)
  • Projects/App/Sources/Application/AppComponent.swift
  • Projects/Data/Sources/DI/GooglePlacesServiceFactory.swift
  • Projects/Data/Sources/Repository/Place/PlaceRepository.swift
  • Projects/Data/Sources/Repository/UserTravel /UserTravelRepository.swift
  • Projects/Data/Sources/Transform/PlaceTransform.swift
  • Projects/Data/Sources/Transform/UserTravelTransform.swift
  • Projects/Domain/Sources/Interface/Place/PlaceRepositoryInterface.swift
  • Projects/Domain/Sources/Interface/UserTravel/UserTravelRepositoryInterface.swift
  • Projects/Domain/Sources/Model/Follow/PlaceSearchResult.swift
  • Projects/Domain/Sources/UseCase/FollowDetailUsecase.swift
  • Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceBuilder.swift
  • Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceInteractor.swift
  • Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceRouter.swift
  • Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceViewController.swift
  • Projects/Features/FollowFeature/Sources/FollowDetailBuilder.swift
  • Projects/Features/FollowFeature/Sources/FollowDetailInteractor.swift
  • Projects/Features/FollowFeature/Sources/FollowDetailRouter.swift
  • Projects/Features/FollowFeature/Sources/FollowDetailViewController.swift
  • Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift
  • Projects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift
  • Projects/Features/HomeFeature/Sources/HomeInteractor.swift
  • Projects/Features/HomeFeature/Sources/HomeViewController.swift
  • Projects/Features/MainFeature/Sources/MainInteractor.swift
  • Projects/Features/MainFeature/Sources/MainRouter.swift
  • Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift
  • Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift
  • Projects/Features/PopularTravelFeature/Sources/PopularTravelViewController.swift
  • Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift
  • Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift
  • Projects/Modules/Networks/Sources/DTO/Place/GooglePlacesResponse.swift
  • Projects/Modules/Networks/Sources/DTO/Travel/TravelDTO.swift
  • Projects/Modules/Networks/Sources/Service/GooglePlacesService.swift
  • Projects/Modules/Networks/Sources/Service/PlaceService.swift
  • Projects/Modules/Networks/Sources/Service/UserTravelService.swift
  • Projects/Modules/Networks/Sources/TargetType/GooglePlacesAPI.swift
  • Projects/Modules/Networks/Sources/TargetType/PlaceAPI.swift
  • Projects/Modules/Networks/Sources/TargetType/UserTravelAPI.swift

Comment on lines +139 to +143
searchBar.searchButtonClicked
.subscribe(with: self) { owner, _ in
// handled via searchBar.searchText on return key
}
.disposed(by: disposeBag)
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

searchButtonClicked 구독의 미사용 파라미터 처리 필요

SwiftLint 경고가 발생하므로 _로 치환하거나 구독 자체를 제거하세요.

🩹 수정 제안
         searchBar.searchButtonClicked
-            .subscribe(with: self) { owner, _ in
+            .subscribe(with: self) { _, _ in
                 // handled via searchBar.searchText on return key
             }
             .disposed(by: disposeBag)
📝 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
searchBar.searchButtonClicked
.subscribe(with: self) { owner, _ in
// handled via searchBar.searchText on return key
}
.disposed(by: disposeBag)
searchBar.searchButtonClicked
.subscribe(with: self) { _, _ in
// handled via searchBar.searchText on return key
}
.disposed(by: disposeBag)
🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 140-140: Unused parameter in a closure should be replaced with _

(unused_closure_parameter)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Projects/Features/FollowFeature/Sources/AddPlace/AddPlaceViewController.swift`
around lines 139 - 143, The subscription to searchBar.searchButtonClicked in
AddPlaceViewController is using an unused parameter causing a SwiftLint warning;
either change the closure signature to use the anonymous parameter (_) — i.e.
subscribe(with: self) { owner, _ in ... } — or remove the entire subscription
block if it's redundant (since searchBar.searchText handles Return key),
ensuring disposeBag usage is updated accordingly.

Comment on lines +194 to 214
func editCompleted(orderedPlaces: [TravelPlace]) {
placesByDay[currentDay] = orderedPlaces

Task {
do {
try await followDetailUsecase.replaceItinerary(
travelId: travelId,
places: buildAllPlaces()
)
await MainActor.run {
self.presenter.showToast("편집이 완료되었습니다.")
}
} catch {
print(error)
}
}
}

private func buildAllPlaces() -> [TravelPlace] {
placesByDay.values.flatMap { $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 | 🟠 Major

전체 일정 교체 시 누락/순서 비결정성 위험

buildAllPlaces()가 딕셔너리 values를 그대로 펼쳐서 순서가 비결정적이고, 로딩되지 않은 day가 있으면 해당 일정이 빠진 채 replaceItinerary로 전송될 수 있습니다. “전체 교체” API라면 누락된 day가 삭제될 위험이 있습니다. day 키를 정렬해 순서를 보장하고, 모든 day가 포함되었는지 확인/보강(미로딩 day 선조회 또는 원본 스냅샷 유지)하는 로직이 필요합니다.

🛠️ 최소한의 순서 보장 예시
-    private func buildAllPlaces() -> [TravelPlace] {
-        placesByDay.values.flatMap { $0 }
-    }
+    private func buildAllPlaces() -> [TravelPlace] {
+        let sortedDays = placesByDay.keys.sorted()
+        return sortedDays.flatMap { placesByDay[$0] ?? [] }
+    }
🤖 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 194 - 214, buildAllPlaces uses placesByDay.values which is unordered and
can omit unloaded days causing replaceItinerary to receive an
incomplete/incorrect itinerary; update editCompleted/buildAllPlaces to (1)
iterate over sorted day keys (e.g., sort placesByDay.keys) to guarantee
deterministic order, and (2) ensure all expected days are present before calling
followDetailUsecase.replaceItinerary by either preloading missing days or
merging with a preserved original snapshot of the itinerary so no day is
accidentally deleted; keep references to placesByDay, buildAllPlaces,
editCompleted, replaceItinerary and travelId when implementing the fix.

Comment on lines +87 to +113
cell.onDragHandlePan = { [weak self, weak cell] gesture in
guard let self, let cell else { return }
let location = gesture.location(in: self)
switch gesture.state {
case .began:
if let ip = self.indexPath(for: cell) {
self.beginInteractiveMovementForItem(at: ip)
}
case .changed:
self.updateInteractiveMovementTargetPosition(location)
case .ended:
self.endInteractiveMovement()
default:
self.cancelInteractiveMovement()
}
}

return cell
}

// 재정렬 핸들러 (iOS 14+)
diffableDataSource?.reorderingHandlers.canReorderItem = { [weak self] _ in
return self?.isEditMode ?? false
}
diffableDataSource?.reorderingHandlers.didReorder = { [weak self] transaction in
self?.places = transaction.finalSnapshot.itemIdentifiers(inSection: 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 | 🟠 Major

🧩 Analysis chain

🌐 Web query:

UICollectionViewDiffableDataSource reorderingHandlers didReorder beginInteractiveMovementForItem iOS interaction

💡 Result:

To get UICollectionViewDiffableDataSource.reorderingHandlers (canReorderItem / willReorder / didReorder) to fire, you must actually start an interactive reorder (either via the list reorder accessory or by calling the interactive movement APIs yourself). If you remove the reorder accessory in a list (or you’re using a grid/compositional layout), nothing initiates the reorder gesture, so the closures won’t be called. [1]

What to do (grid / custom layout / no reorder accessory)

  1. Enable reordering in the diffable data source:
dataSource.reorderingHandlers.canReorderItem = { item in true }

dataSource.reorderingHandlers.didReorder = { [weak self] transaction in
  guard let self else { return }
  // keep your “source of truth” in sync
  if let updated = self.items.applying(transaction.difference) {
    self.items = updated
  }
  // (alternatively) self.items = transaction.finalSnapshot.itemIdentifiers
}

Updating your backing store from the transaction is the key step—Apple’s sample pattern is to apply transaction.difference (or use finalSnapshot). [2][3]

  1. Provide a gesture that starts interactive movement and drives it:
`@objc` func handleLongPress(_ g: UILongPressGestureRecognizer) {
  let p = g.location(in: collectionView)

  switch g.state {
  case .began:
    guard let ip = collectionView.indexPathForItem(at: p) else { return }
    collectionView.beginInteractiveMovementForItem(at: ip)
  case .changed:
    collectionView.updateInteractiveMovementTargetPosition(p)
  case .ended:
    collectionView.endInteractiveMovement()
  default:
    collectionView.cancelInteractiveMovement()
  }
}

These beginInteractiveMovementForItem(at:), updateInteractiveMovementTargetPosition(_:), and endInteractiveMovement() calls are the standard way to run interactive reordering; once the move completes, the diffable data source can produce the reorder transaction and invoke didReorder. [4]

  1. Alternative (only if you’re using UICollectionViewController): you can enable Apple’s built-in gesture by setting installsStandardGestureForInteractiveMovement = true. [4]

Practical takeaway: reorderingHandlers tells diffable data source how to approve and persist a reorder, but you still need UI that initiates the interactive move (reorder accessory or interactive-movement gesture). [1]

Sources: Stack Overflow discussion of missing reorder accessory / need to call beginInteractiveMovement yourself [1]; explanation and sample for persisting the reorder using didReorder + transaction.difference [2][3]; interactive movement API sequence (begin/update/end/cancel) [4].


🌐 Web query:

UICollectionViewDiffableDataSource didReorder not called iOS 14 interactive movement endInteractiveMovement

💡 Result:

UICollectionViewDiffableDataSource.reorderingHandlers.didReorder is not a callback for the “old” interactive-movement API (beginInteractiveMovement… / endInteractiveMovement()). It’s called only when Diffable’s reordering feature is active (iOS 14+)—typically via the list reorder accessory, or by you starting interactive movement in a way that Diffable reordering supports. (biteinteractive.com)

What to do

  1. Enable Diffable reordering
dataSource.reorderingHandlers.canReorderItem = { _ in true }
dataSource.reorderingHandlers.didReorder = { transaction in
    // persist new order using transaction.finalSnapshot / etc.
}

Diffable won’t reorder (and didReorder won’t fire) unless canReorderItem allows it. (biteinteractive.com)

  1. If you’re not using list cells/reorder accessories
    You must start the drag yourself (e.g. long-press) and call beginInteractiveMovementForItem(at:) after determining the pressed index path (Apple’s reorder accessory is just a convenience). (stackoverflow.com)

  2. If you’re using the legacy interactive-movement path
    If you continue with interactive movement without Diffable’s reordering feature, don’t expect reorderingHandlers.didReorder to run—handle reordering by updating your model/snapshot yourself (this is how classic interactive movement works). Background: the classic API expects the data source to support moves. (stackoverflow.com)

  3. Check for drag/drop delegate interference
    Having a UICollectionViewDropDelegate can interfere with interactive reordering behavior; if you set one, try removing it or switching fully to drag/drop reordering. (stackoverflow.com)

Citations:


🌐 Web query:

UICollectionViewDiffableDataSource reorderingHandlers API documentation iOS 14 interactive movement deprecation

💡 Result:

  • reorderingHandlers (iOS 14+) is the diffable-data-source-specific API for user-driven reordering in UICollectionViewDiffableDataSource. It’s exposed as UICollectionViewDiffableDataSourceReorderingHandlers and is available on iOS / tvOS / Mac Catalyst 14.0+. [1]

  • The handlers commonly used are:

    • canReorderItem (gate which items are movable)
    • willReorder / didReorder (receive an NSDiffableDataSourceTransaction so you can update your backing store using the transaction’s difference). [2]
  • Interactive movement is not deprecated in iOS 14—it remains the underlying mechanism (beginInteractiveMovement…, updateInteractiveMovementTargetPosition, endInteractiveMovement, cancelInteractiveMovement). [3]

  • Practical implication (esp. for non-list / grid layouts): if you don’t use the list reordering accessory, you typically need to start interactive movement yourself (e.g., via a gesture recognizer) and ensure your diffable reordering setup allows the move (notably canReorderItem returning true). [4]

Sources:
[1] apisof.net: UICollectionViewDiffableDataSourceReorderingHandlers availability/platform notes (apisof.net)
[2] He(Heva) Wu blog: shows canReorderItem / willReorder / didReorder and applying transaction.difference (hevawu.github.io)
[3] NSHint article: lists interactive movement methods (beginInteractiveMovement…, update…, end…, cancel…) (nshint.io)
[4] Stack Overflow: notes reorder accessory is a convenience; manual gesture + beginInteractiveMovement + canReorderItem (stackoverflow.com)

Citations:


드래그 재정렬 후 places 배열이 갱신되지 않을 수 있습니다

코드는 두 가지 재정렬 경로를 동시에 사용합니다.

  • 커스텀 pan 제스처 경로 (Lines 87–102): beginInteractiveMovementForItemupdateInteractiveMovementTargetPositionendInteractiveMovement
  • DiffableDataSource 경로 (Lines 108–113): reorderingHandlers.canReorderItem / didReorder

beginInteractiveMovementForItem을 호출하면 DiffableDataSource의 reorderingHandlers가 자동으로 호출되어야 합니다. 다만 canReorderItemtrue를 반환할 때만 didReorder가 실행됩니다. 현재 코드에서 canReorderItemself?.isEditMode ?? false에 의존하므로, 편집 모드가 비활성화되면 didReorder는 호출되지 않고 self.places가 동기화되지 않습니다.

또한 Apple 공식 문서에서 이 두 API 경로의 통합을 명시적으로 보장하지 않으므로, 예상치 못한 상황에서 places가 갱신되지 않아 PUT /api/v1/travels/{id}/itinerary에 잘못된 sequence를 전송할 수 있습니다.

단기적 방어책으로, pan 제스처 ended 처리에서 DiffableDataSource 스냅샷으로부터 직접 순서를 동기화하는 방법을 고려하세요.

🛡️ 방어적 동기화 예시
 case .ended:
     self.endInteractiveMovement()
+    // didReorder가 호출되지 않을 경우를 대비한 폴백 동기화
+    if let snapshot = self.diffableDataSource?.snapshot() {
+        self.places = snapshot.itemIdentifiers(inSection: 0)
+    }
🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 92-92: Variable name 'ip' should be between 3 and 40 characters long

(identifier_name)


[Warning] 109-109: Prefer implicit returns in closures, functions and getters

(implicit_return)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Projects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swift`
around lines 87 - 113, The custom pan gesture path (cell.onDragHandlePan closure
using
beginInteractiveMovementForItem/updateInteractiveMovementTargetPosition/endInteractiveMovement)
can bypass DiffableDataSource.didReorder when canReorderItem is false, so after
the pan ends you must defensively synchronize the model from the data source
snapshot; inside the onDragHandlePan closure's .ended case (near
beginInteractiveMovementForItem / endInteractiveMovement) fetch the current
snapshot from diffableDataSource (or diffableDataSource?.snapshot()), extract
the final order with snapshot.itemIdentifiers(inSection: 0) and assign it to
places (same property updated in reorderingHandlers.didReorder), ensuring you
still call endInteractiveMovement() and keep weak self/cell handling.

Comment on lines +153 to +171
private func refreshBanner() {
Task { [weak self] in
guard let self else { return }
do {
let tripInfo = try await self.usecase.fetchMyTripInfo()
let banner = tripInfo.toPresention()
guard let model = self.homeDataRelay.value else { return }
let updated = HomePresentationModel(
banner: banner,
category: model.category,
popularTrip: model.popularTrip,
recommendedTrip: model.recommendedTrip
)
self.homeDataRelay.accept(updated)
} catch {
// 여행 없으면 empty 유지
}
}
}
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

refreshBanner Task가 저장되지 않아 취소 불가능하고 중복 실행될 수 있습니다.

refreshBanner() 내부의 Task는 어디에도 저장되지 않습니다. 이로 인해 두 가지 문제가 발생합니다.

  1. 취소 불가: willResignActive()fetchDataTask만 취소하므로, Interactor가 비활성화된 후에도 이 Task가 완료되어 homeDataRelay.accept(updated)를 호출할 수 있습니다.
  2. 중복 실행: 사용자가 홈 화면을 빠르게 반복 진입할 경우 매번 새 Task가 생성되어 동시에 여러 네트워크 요청이 진행되며, 마지막으로 완료된 응답이 최종 상태를 덮어씁니다.

기존 fetchDataTask와 동일한 패턴으로 별도 프로퍼티에 Task를 저장하고, willResignActive()에서 함께 취소하는 것을 권장합니다.

🔧 수정 제안
 private var fetchDataTask: Task<Void, Never>?
+private var refreshBannerTask: Task<Void, Never>?
 private let usecase: HomeUsecaseProtocol
 override func willResignActive() {
     super.willResignActive()
     
     fetchDataTask?.cancel()
     fetchDataTask = nil
+    refreshBannerTask?.cancel()
+    refreshBannerTask = nil
 }
 private func refreshBanner() {
-    Task { [weak self] in
+    refreshBannerTask?.cancel()
+    refreshBannerTask = Task { [weak self] in
         guard let self else { return }
         do {
             let tripInfo = try await self.usecase.fetchMyTripInfo()
             let banner = tripInfo.toPresention()
+            guard !Task.isCancelled else { return }
             guard let model = self.homeDataRelay.value else { return }
             let updated = HomePresentationModel(
                 banner: banner,
                 category: model.category,
                 popularTrip: model.popularTrip,
                 recommendedTrip: model.recommendedTrip
             )
             self.homeDataRelay.accept(updated)
         } catch {
             // 여행 없으면 empty 유지
         }
     }
 }
🤖 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 153
- 171, The Task created in refreshBanner() must be stored so it can be cancelled
and not duplicated: add a Task property (e.g., refreshBannerTask: Task<Void,
Never>?) on the Interactor, assign the Task returned by refreshBanner() to that
property (cancelling any existing refreshBannerTask first), and update
willResignActive() to cancel refreshBannerTask alongside fetchDataTask; keep
using usecase.fetchMyTripInfo(), weak self capture, and
homeDataRelay.accept(updated) logic but ensure the stored task is cleared on
completion or cancellation to avoid stale references.

Comment on lines +167 to +169
} catch {
// 여행 없으면 empty 유지
}
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

모든 에러를 묵묵히 무시하여 네트워크/인증 오류 디버깅이 어렵습니다.

주석에는 "여행 없으면 empty 유지"라고 명시되어 있지만, 현재 catch 블록은 네트워크 타임아웃, 인증 오류, 디코딩 실패 등 모든 에러를 동일하게 삼킵니다. 의도적인 백그라운드 refresh 전략이더라도, 실제 오류와 단순 "여행 없음" 케이스를 구분할 수 없어 디버깅이 어렵습니다.

🔧 수정 제안 (DEBUG 로깅 추가)
         } catch {
-            // 여행 없으면 empty 유지
+            `#if` DEBUG
+            print("[HomeInteractor] refreshBanner failed: \(error)")
+            `#endif`
         }
🤖 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 167
- 169, The catch block in HomeInteractor.swift currently swallows all errors
(the catch after fetching trips), making debugging impossible; update the catch
in the HomeInteractor's trips refresh/fetch method to log the caught error
details (using the app's Logger or debugPrint) including context like
"fetchTrips failed" and the error, and only suppress silently when you can
explicitly detect the "no trips" condition (or handle specific errors like a
NotFound/empty response); do not remove existing behavior—just add debug-level
logging for network/auth/decoding errors and distinguish the "no trips" case
from other failures.

Comment on lines +133 to 145
private func fetchPopularTrips(categoryId: Int) {
fetchTripsTask?.cancel()

fetchTripsTask = Task { [weak self] in
guard let self, !Task.isCancelled else { return }

do {
let trips = try await self.usecase.fetchPopularTripList(id: categoryId).map { $0.toPopularTravelModel() }
guard !Task.isCancelled else { return }
self.popularTripsRelay.accept(trips)
} catch {
// 카테고리 필터 실패 시 기존 목록 유지
}
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:

rg -n "allCategoryId" -g '*.swift'

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

Length of output: 601


🏁 Script executed:

rg -n "PopularTravelInteractor|PopularTravel.*Category" -g '*.swift' -A 3

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

Length of output: 5048


🏁 Script executed:

rg -n "HomeInteractor" -g '*.swift' -A 3 | head -50

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

Length of output: 3077


🏁 Script executed:

find . -name "*HomeInteractor*" -o -name "*PopularTravelInteractor*" | grep -E '\.swift$'

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

Length of output: 214


🏁 Script executed:

cat -n Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift

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

Length of output: 6561


🏁 Script executed:

rg -n "selectedCategoryRelay|categoryId" Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift -B 2 -A 2

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

Length of output: 1601


🏁 Script executed:

cat -n Projects/Features/HomeFeature/Sources/HomeInteractor.swift | sed -n '95,185p'

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

Length of output: 4059


"전체" 카테고리 ID 매핑 불일치 확인 필요

HomeInteractor와 다르게 PopularTravelInteractor에서 "전체" 카테고리에 대한 특별한 처리가 없습니다. HomeInteractor는 "전체" 카테고리를 ID -1로 정의하고 (line 97) API 호출 시 nil로 매핑하는데 (line 178), PopularTravelInteractor는 카테고리 ID를 그대로 전달합니다 (line 140). 동일한 usecase를 사용하므로 같은 API 규칙을 따라야 한다면 일관된 매핑이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swift`
around lines 133 - 145, The fetchPopularTrips method passes the categoryId
directly to usecase.fetchPopularTripList, causing inconsistent handling for the
"전체" category compared to HomeInteractor; update fetchPopularTrips to map the
special "전체" category sentinel (e.g., id == -1) to nil before calling
usecase.fetchPopularTripList (so usecase.fetchPopularTripList receives nil for
"전체"), keeping the existing Task cancellation and error handling and referencing
the fetchPopularTrips function and usecase.fetchPopularTripList call to locate
where to apply the mapping.

Comment on lines 118 to 123
func setLayout() {
thumbnailView.snp.makeConstraints {
$0.width.equalTo(140.adjusted)
$0.height.equalTo(thumbnailView.snp.width).multipliedBy(88.0 / 140.0)
$0.height.equalTo(88.adjustedH).priority(.high)
$0.leading.top.equalToSuperview()
$0.bottom.lessThanOrEqualToSuperview()
}
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

썸네일 종횡비가 기기별로 달라질 수 있음

widthadjusted(가로 비율), heightadjustedH(세로 비율)이라서 디바이스 비율에 따라 88:140 비율이 깨질 수 있습니다. 기존처럼 너비 기준 비율을 유지하거나 두 축을 동일 기준으로 스케일링하는 편이 안전합니다.

🔧 제안 수정
         thumbnailView.snp.makeConstraints {
             $0.width.equalTo(140.adjusted)
-            $0.height.equalTo(88.adjustedH).priority(.high)
+            $0.height.equalTo(thumbnailView.snp.width).multipliedBy(88.0 / 140.0).priority(.high)
             $0.leading.top.equalToSuperview()
         }
📝 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
func setLayout() {
thumbnailView.snp.makeConstraints {
$0.width.equalTo(140.adjusted)
$0.height.equalTo(thumbnailView.snp.width).multipliedBy(88.0 / 140.0)
$0.height.equalTo(88.adjustedH).priority(.high)
$0.leading.top.equalToSuperview()
$0.bottom.lessThanOrEqualToSuperview()
}
func setLayout() {
thumbnailView.snp.makeConstraints {
$0.width.equalTo(140.adjusted)
$0.height.equalTo(thumbnailView.snp.width).multipliedBy(88.0 / 140.0).priority(.high)
$0.leading.top.equalToSuperview()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Modules/DSKit/Sources/Component/PopularInfoCell.swift` around lines
118 - 123, In setLayout(), the thumbnailView uses mixed scaling helpers (width
uses adjusted, height uses adjustedH) which can break the intended 140:88 aspect
ratio on different devices; change the constraints so both axes use the same
scaling basis (e.g., both use adjusted) or, better, constrain height relative to
width to preserve aspect ratio (set thumbnailView width via 140.adjusted and set
thumbnailView height as a multiplier of that width to match 88/140) so the
140:88 ratio remains consistent across devices.

Comment on lines +12 to +43
public enum GooglePlacesAPI {
case searchText(keyword: String)
}

extension GooglePlacesAPI: TargetType {
public var baseURL: URL {
URL(string: "https://places.googleapis.com")!
}

public var path: String {
"/v1/places:searchText"
}

public var method: Moya.Method {
.post
}

public var task: Moya.Task {
switch self {
case .searchText(let keyword):
let body: [String: Any] = ["textQuery": keyword]
let data = (try? JSONSerialization.data(withJSONObject: body)) ?? Data()
return .requestData(data)
}
}

public var headers: [String: String]? {
[
"Content-Type": "application/json",
"X-Goog-Api-Key": NetworkConfiguration.weatherApiKey,
"X-Goog-FieldMask": "places.id,places.displayName,places.formattedAddress,places.location"
]
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

🧩 Analysis chain

🏁 Script executed:

rg -n "weatherApiKey|placesApiKey|googlePlacesApiKey" -g '*.swift'

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

Length of output: 459


🏁 Script executed:

fd -name "NetworkConfiguration*" -type f

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

Length of output: 305


🏁 Script executed:

cat -n Projects/Modules/Networks/Sources/TargetType/GooglePlacesAPI.swift

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

Length of output: 1472


Google Places API용 별도의 API 키 설정이 필요합니다.

현재 줄 41에서 NetworkConfiguration.weatherApiKey를 사용하고 있는데, 이는 WeatherAPI용 키입니다. Google Places API는 별도의 API 키를 필요로 하므로, NetworkConfiguration에 placesApiKey (또는 googlePlacesApiKey)를 추가하고 여기서 사용해야 합니다. 현재 설정은 API 인증 오류를 야기할 수 있습니다.

🧰 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/GooglePlacesAPI.swift` around
lines 12 - 43, The headers for GooglePlacesAPI are incorrectly using
NetworkConfiguration.weatherApiKey; add a dedicated key (e.g.,
NetworkConfiguration.placesApiKey or googlePlacesApiKey) in NetworkConfiguration
and replace the reference in GooglePlacesAPI.headers so the "X-Goog-Api-Key"
header uses the new places API key constant; update any config loading/Env
parsing that assembles NetworkConfiguration to populate the new property as well
as any tests or docs that reference the old key.

@KimNahun KimNahun merged commit 8751719 into develop Feb 24, 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.

feat: 여행 수정

1 participant