Feat/#37 travel modify#39
Conversation
Walkthrough이 PR은 Google Places API 통합을 통한 장소 검색 기능, 장소 등록, 여행 일정 관리(조회, 추가, 교체)를 구현합니다. AddPlace 기능, 여행 수정 모드, 새로운 도메인 모델 및 네트워크 계층을 추가하여 여행 일정 편집 워크플로우를 활성화합니다. Changes
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)
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분 Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (5)
Projects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swift (2)
154-162:dragHandleImageView가containerView뒤에 위치 — z-order 취약성
contentView.addSubview(dragHandleImageView)가contentView.addSubview(containerView)보다 먼저 호출되어,dragHandleImageView의 z-index가containerView보다 낮습니다. 현재는 편집 모드 inset이 28pt라 드래그 핸들(20pt)과containerView사이에 8pt 여유가 있어 가려지지 않지만,setEditMode의 inset 값이 20 이하로 바뀌면 핸들이containerView뒤로 숨어 탭/팬 인터랙션이 동작하지 않게 됩니다.dragHandleImageView는containerView이후에 추가해야 안전합니다.♻️ 리팩토링 제안
- // 드래그 핸들 (편집 모드) - 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),updateCheckboxAppearance내checkboxView.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 경고가 발생합니다.
- Line 92 — 변수명
ip가 최소 길이(3자) 미만입니다 (identifier_name).- 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
📒 Files selected for processing (37)
Projects/App/Sources/Application/AppComponent.swiftProjects/Data/Sources/DI/GooglePlacesServiceFactory.swiftProjects/Data/Sources/Repository/Place/PlaceRepository.swiftProjects/Data/Sources/Repository/UserTravel /UserTravelRepository.swiftProjects/Data/Sources/Transform/PlaceTransform.swiftProjects/Data/Sources/Transform/UserTravelTransform.swiftProjects/Domain/Sources/Interface/Place/PlaceRepositoryInterface.swiftProjects/Domain/Sources/Interface/UserTravel/UserTravelRepositoryInterface.swiftProjects/Domain/Sources/Model/Follow/PlaceSearchResult.swiftProjects/Domain/Sources/UseCase/FollowDetailUsecase.swiftProjects/Features/FollowFeature/Sources/AddPlace/AddPlaceBuilder.swiftProjects/Features/FollowFeature/Sources/AddPlace/AddPlaceInteractor.swiftProjects/Features/FollowFeature/Sources/AddPlace/AddPlaceRouter.swiftProjects/Features/FollowFeature/Sources/AddPlace/AddPlaceViewController.swiftProjects/Features/FollowFeature/Sources/FollowDetailBuilder.swiftProjects/Features/FollowFeature/Sources/FollowDetailInteractor.swiftProjects/Features/FollowFeature/Sources/FollowDetailRouter.swiftProjects/Features/FollowFeature/Sources/FollowDetailViewController.swiftProjects/Features/FollowFeature/Sources/Views/Cells/PlaceCell.swiftProjects/Features/FollowFeature/Sources/Views/CollectionViews/PlaceListCollectionView.swiftProjects/Features/HomeFeature/Sources/HomeInteractor.swiftProjects/Features/HomeFeature/Sources/HomeViewController.swiftProjects/Features/MainFeature/Sources/MainInteractor.swiftProjects/Features/MainFeature/Sources/MainRouter.swiftProjects/Features/MyTravelFeature/Sources/MyTravelInteractor.swiftProjects/Features/PopularTravelFeature/Sources/PopularTravelInteractor.swiftProjects/Features/PopularTravelFeature/Sources/PopularTravelViewController.swiftProjects/Features/TabBarFeature/Sources/TabBarInteractor.swiftProjects/Modules/DSKit/Sources/Component/PopularInfoCell.swiftProjects/Modules/Networks/Sources/DTO/Place/GooglePlacesResponse.swiftProjects/Modules/Networks/Sources/DTO/Travel/TravelDTO.swiftProjects/Modules/Networks/Sources/Service/GooglePlacesService.swiftProjects/Modules/Networks/Sources/Service/PlaceService.swiftProjects/Modules/Networks/Sources/Service/UserTravelService.swiftProjects/Modules/Networks/Sources/TargetType/GooglePlacesAPI.swiftProjects/Modules/Networks/Sources/TargetType/PlaceAPI.swiftProjects/Modules/Networks/Sources/TargetType/UserTravelAPI.swift
| searchBar.searchButtonClicked | ||
| .subscribe(with: self) { owner, _ in | ||
| // handled via searchBar.searchText on return key | ||
| } | ||
| .disposed(by: disposeBag) |
There was a problem hiding this comment.
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.
| 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.
| 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 } | ||
| } |
There was a problem hiding this comment.
전체 일정 교체 시 누락/순서 비결정성 위험
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.
| 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) | ||
| } |
There was a problem hiding this comment.
🧩 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)
- 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]
- 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]
- Alternative (only if you’re using
UICollectionViewController): you can enable Apple’s built-in gesture by settinginstallsStandardGestureForInteractiveMovement = 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
- 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)
-
If you’re not using list cells/reorder accessories
You must start the drag yourself (e.g. long-press) and callbeginInteractiveMovementForItem(at:)after determining the pressed index path (Apple’s reorder accessory is just a convenience). (stackoverflow.com) -
If you’re using the legacy interactive-movement path
If you continue with interactive movement without Diffable’s reordering feature, don’t expectreorderingHandlers.didReorderto 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) -
Check for drag/drop delegate interference
Having aUICollectionViewDropDelegatecan interfere with interactive reordering behavior; if you set one, try removing it or switching fully to drag/drop reordering. (stackoverflow.com)
Citations:
- 1: https://www.biteinteractive.com/collection-view-lists-in-ios-14-part-2/?utm_source=openai
- 2: https://www.biteinteractive.com/collection-view-lists-in-ios-14-part-2/?utm_source=openai
- 3: https://stackoverflow.com/questions/69844189/how-can-i-use-uicollectionviewdiffabledatasource-reorderhandlers-with-a-custom-c?utm_source=openai
- 4: https://stackoverflow.com/questions/43136378/collection-view-begininteractivemovementforitem-returns-false?utm_source=openai
- 5: https://stackoverflow.com/questions/47084733/uicollectionview-not-calling-moveitematto-data-source-method-in-response-to-en?utm_source=openai
🌐 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 inUICollectionViewDiffableDataSource. It’s exposed asUICollectionViewDiffableDataSourceReorderingHandlersand is available on iOS / tvOS / Mac Catalyst 14.0+. [1] -
The handlers commonly used are:
canReorderItem(gate which items are movable)willReorder/didReorder(receive anNSDiffableDataSourceTransactionso you can update your backing store using the transaction’sdifference). [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
canReorderItemreturningtrue). [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:
- 1: https://apisof.net/catalog/6df2dd0d75ab1f733706e6e59c04f36e?fx=netstandard1.6&utm_source=openai
- 2: https://hevawu.github.io/blog/2021/05/27/UICollectionView-Updates-in-WWDC2020?utm_source=openai
- 3: https://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering?utm_source=openai
- 4: https://stackoverflow.com/questions/69844189/how-can-i-use-uicollectionviewdiffabledatasource-reorderhandlers-with-a-custom-c?utm_source=openai
드래그 재정렬 후 places 배열이 갱신되지 않을 수 있습니다
코드는 두 가지 재정렬 경로를 동시에 사용합니다.
- 커스텀 pan 제스처 경로 (Lines 87–102):
beginInteractiveMovementForItem→updateInteractiveMovementTargetPosition→endInteractiveMovement - DiffableDataSource 경로 (Lines 108–113):
reorderingHandlers.canReorderItem/didReorder
beginInteractiveMovementForItem을 호출하면 DiffableDataSource의 reorderingHandlers가 자동으로 호출되어야 합니다. 다만 canReorderItem이 true를 반환할 때만 didReorder가 실행됩니다. 현재 코드에서 canReorderItem이 self?.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.
| 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 유지 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
refreshBanner Task가 저장되지 않아 취소 불가능하고 중복 실행될 수 있습니다.
refreshBanner() 내부의 Task는 어디에도 저장되지 않습니다. 이로 인해 두 가지 문제가 발생합니다.
- 취소 불가:
willResignActive()는fetchDataTask만 취소하므로, Interactor가 비활성화된 후에도 이 Task가 완료되어homeDataRelay.accept(updated)를 호출할 수 있습니다. - 중복 실행: 사용자가 홈 화면을 빠르게 반복 진입할 경우 매번 새 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.
| } catch { | ||
| // 여행 없으면 empty 유지 | ||
| } |
There was a problem hiding this comment.
모든 에러를 묵묵히 무시하여 네트워크/인증 오류 디버깅이 어렵습니다.
주석에는 "여행 없으면 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.
| 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 { | ||
| // 카테고리 필터 실패 시 기존 목록 유지 | ||
| } |
There was a problem hiding this comment.
🧩 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 3Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 5048
🏁 Script executed:
rg -n "HomeInteractor" -g '*.swift' -A 3 | head -50Repository: 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.swiftRepository: 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 2Repository: 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.
| 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() | ||
| } |
There was a problem hiding this comment.
썸네일 종횡비가 기기별로 달라질 수 있음
width는 adjusted(가로 비율), height는 adjustedH(세로 비율)이라서 디바이스 비율에 따라 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.
| 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.
| 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" | ||
| ] |
There was a problem hiding this comment.
🧩 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 fRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 305
🏁 Script executed:
cat -n Projects/Modules/Networks/Sources/TargetType/GooglePlacesAPI.swiftRepository: 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.
🔗 연결된 이슈
📄 작업 내용
POST /api/v1/places→POST /api/v1/travels/{id}/itinerary)PUT /api/v1/travels/{id}/itinerary)💻 주요 코드 설명
AddPlaceInteractorPOST /api/v1/places로 백엔드 DB에 장소 등록, 이후POST /api/v1/travels/{id}/itinerary로 일정에추가하는 2단계 흐름 구현
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항