Design/#33 내여행탭#36
Hidden character warning
Conversation
🎯 요약WalkthroughTravel 기능을 MyTravel로 완전히 교체하면서 새로운 도메인 모델, UseCase, Repository 메서드, UI 컴포넌트, 저장소 관리 레이어를 추가했습니다. 기존 Travel Feature의 모든 파일을 삭제하고 MyTravel Feature로 재구현하며, 의존성 그래프 전체에서 참조를 업데이트했습니다. Changes
Sequence DiagramsequenceDiagram
participant User
participant MyTravelVC as MyTravelViewController
participant MyTravelInteractor
participant MyTravelUsecase
participant UserTravelRepository
participant TravelTemplateRepository
participant Network as UserTravelService
User->>MyTravelVC: viewDidLoad
activate MyTravelVC
MyTravelVC->>MyTravelInteractor: viewWillAppear()
deactivate MyTravelVC
activate MyTravelInteractor
MyTravelInteractor->>MyTravelInteractor: fetchMyTravelData()
MyTravelInteractor->>MyTravelVC: setLoading(true)
activate MyTravelVC
deactivate MyTravelVC
par 병렬 조회
MyTravelInteractor->>MyTravelUsecase: fetchMyTripInfo()
activate MyTravelUsecase
MyTravelUsecase->>UserTravelRepository: fetchUpcoming()
activate UserTravelRepository
UserTravelRepository->>Network: getUpcoming()
activate Network
Network-->>UserTravelRepository: MyTripSummary
deactivate Network
UserTravelRepository-->>MyTravelUsecase: MyTripSummary
deactivate UserTravelRepository
MyTravelUsecase-->>MyTravelInteractor: MyTripSummary
deactivate MyTravelUsecase
MyTravelInteractor->>MyTravelUsecase: fetchUpcomingList()
activate MyTravelUsecase
MyTravelUsecase->>UserTravelRepository: fetchUpcomingList(page:size:)
activate UserTravelRepository
UserTravelRepository->>Network: getUpcomingList()
activate Network
Network-->>UserTravelRepository: [UpcomingInfo]
deactivate Network
UserTravelRepository-->>MyTravelUsecase: [UpcomingInfo]
deactivate UserTravelRepository
MyTravelUsecase-->>MyTravelInteractor: [UpcomingInfo]
deactivate MyTravelUsecase
MyTravelInteractor->>MyTravelUsecase: fetchRecommendTripList()
activate MyTravelUsecase
MyTravelUsecase->>TravelTemplateRepository: fetchRecommendTripList(page:size:)
activate TravelTemplateRepository
TravelTemplateRepository->>Network: getRecommendTripList()
activate Network
Network-->>TravelTemplateRepository: [TripInfo]
deactivate Network
TravelTemplateRepository-->>MyTravelUsecase: [TripInfo]
deactivate TravelTemplateRepository
MyTravelUsecase-->>MyTravelInteractor: [TripInfo]
deactivate MyTravelUsecase
end
MyTravelInteractor->>MyTravelInteractor: 섹션 구성
MyTravelInteractor->>MyTravelVC: update(with sections:)
activate MyTravelVC
deactivate MyTravelVC
MyTravelInteractor->>MyTravelVC: setLoading(false)
activate MyTravelVC
MyTravelVC->>User: 화면 렌더링
deactivate MyTravelVC
deactivate MyTravelInteractor
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes 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)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Projects/Features/RootFeature/Sources/RootInteractor.swift (1)
67-100:⚠️ Potential issue | 🟠 Major인증 실패 시 앱이 무응답 상태에 빠질 수 있습니다.
performAuthFlow에서 에러 발생 시router?.attachMain()이 호출되지 않아 사용자는 빈 화면에 머물게 됩니다. 또한,willResignActive에서Task를 취소하지 않아 RIB 비활성화 후에도 비동기 작업이 계속 실행될 수 있습니다.🛠️ Task 취소 및 에러 핸들링 개선 제안
+ private var authTask: Task<Void, Never>? + private func performAuthFlow() { - Task { [weak self] in + authTask = Task { [weak self] in guard let self else { return } // ... existing code ... } catch { print("[RootInteractor] Auth flow failed: \(error)") + // TODO: 에러 화면 표시 또는 재시도 로직 추가 } } } + + override func willResignActive() { + super.willResignActive() + authTask?.cancel() + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/RootFeature/Sources/RootInteractor.swift` around lines 67 - 100, performAuthFlow currently spawns an untracked Task and only prints errors, which can leave the UI empty on failure and allows the work to continue after RIB deactivation; change performAuthFlow to store the Task in a property (e.g., authTask) so it can be cancelled from willResignActive, add cancellation checks or handle Task.isCancelled inside the Task, and improve error handling by ensuring a fallback UI flow is always invoked (e.g., call router?.attachMain() or an error/placeholder routing path) in the catch path or via a defer so the app never remains blank; also replace the plain print with proper logging. Reference: performAuthFlow, router?.attachMain(), willResignActive, and add a stored Task property (authTask) to manage cancellation.
🧹 Nitpick comments (27)
Projects/Core/Sources/Extensions/Foundation+/Date+.swift (1)
12-17:DateFormatter인스턴스를 매 호출마다 생성하지 않도록static캐싱 권장
DateFormatter초기화 비용이 높습니다. 셀 표시 등 반복 호출 경로에서 매번 생성하면 불필요한 오버헤드가 발생합니다. UI(메인 스레드) 전용 유틸리티이므로static let으로 캐싱해도 안전합니다.참고:
DateFormatter는 스레드 안전하지 않으므로, 백그라운드 스레드에서 호출되는 경우엔 별도 처리가 필요합니다.♻️ static 캐싱 적용 제안
public extension Date { + private static let koreanMMddFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "ko_KR") + formatter.dateFormat = "M월 d일" + return formatter + }() + func toKoreanMMdd() -> String { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "ko_KR") - formatter.dateFormat = "M월 d일" - return formatter.string(from: self) + return Self.koreanMMddFormatter.string(from: self) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Core/Sources/Extensions/Foundation`+/Date+.swift around lines 12 - 17, The toKoreanMMdd() function creates a new DateFormatter on every call; change it to use a cached static DateFormatter (e.g. a static let koreanMMddFormatter) configured with locale "ko_KR" and dateFormat "M월 d일" and return koreanMMddFormatter.string(from: self) inside toKoreanMMdd(); if this utility may be called off the main thread, either document that it’s main-thread-only or create/return a thread-confined formatter per thread (or use a DateFormatter factory) to avoid thread-safety issues.Projects/Features/SettingFeature/Sources/SettingViewController.swift (1)
142-146: 주석 처리된 코드 정리 권장주석 코드가 남아 있으면 유지보수 시 혼란을 줄 수 있습니다. 해당 로직이 불필요하다면 제거하고, 추후 필요하면 버전 관리(Git)에서 복원하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/SettingFeature/Sources/SettingViewController.swift` around lines 142 - 146, Remove the commented-out conditional that checks `menu == .notification` and the alternate `cell.separatorInset` assignment in SettingViewController.swift; keep only the active line setting `cell.separatorInset = UIEdgeInsets(top: 0, left: 25.adjusted, bottom: 0, right: 24.adjusted)` and delete the commented block referencing `.notification` so the codebase is cleaner (the removed logic can be recovered from Git history if needed).Projects/Core/Sources/Storage/UserManager.swift (2)
11-15: SwiftLint:redundant_string_enum_value— rawValue 생략 가능case 이름과 동일한 rawValue는 Swift에서 자동 합성되므로 명시적 선언이 불필요합니다.
♻️ 중복 rawValue 제거 제안
public enum UserDefaultKeys: String { - case uuid = "uuid" - case nickname = "nickname" - case isFirstOpen = "isFirstOpen" + case uuid + case nickname + case isFirstOpen }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Core/Sources/Storage/UserManager.swift` around lines 11 - 15, The enum UserDefaultKeys currently specifies raw string values identical to the case names; update UserDefaultKeys (the enum and its cases uuid, nickname, isFirstOpen) to omit the redundant rawValue literals so Swift auto-synthesizes them (e.g. declare the cases without = "..."), keeping the enum conforming to String.
26-32: 코드 명확성 개선 제안 — 로직은 정상이나 가독성 향상 가능현재 구현은 첫 실행 감지 로직으로 정상 작동합니다. 첫 호출 시
isFirstOpen이nil이므로true로 설정 후true를 반환하고, 이후 호출 시는!isFirstOpen으로false를 반환합니다. 다만self.isFirstOpen = true로 저장한 뒤!isFirstOpen을 반환하는 방식은 의도와 코드가 직관적으로 연결되지 않아 가독성을 해칩니다.제안된 리팩토링은 이 명확성을 개선합니다:
♻️ 개선된 구현
public func isFirstOpenApp() -> Bool { - guard let isFirstOpen else { - self.isFirstOpen = true - return true - } - return !isFirstOpen + guard isFirstOpen == nil else { return false } + self.isFirstOpen = true + return true }변경된 방식은 "아직 설정되지 않았으면
true저장 후 반환, 이미 설정되었으면false반환"이라는 의도를 더 명확하게 표현합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Core/Sources/Storage/UserManager.swift` around lines 26 - 32, 현재 isFirstOpenApp()의 논리는 동작하지만 self.isFirstOpen = true 후 반환값을 !isFirstOpen으로 처리해 의도가 직관적이지 않습니다; isFirstOpenApp() 내에서 옵셔널 바인딩 실패 시 self.isFirstOpen을 true로 설정하고 즉시 true를 반환하고, 옵셔널이 존재할 경우는 false를 반환하도록 리팩토링해 가독성을 개선하세요 (참조 심볼: isFirstOpenApp(), isFirstOpen).Projects/Features/MyTravelFeature/Project.swift (1)
9-11: SwiftLint:sorted_imports경고 — import 순서 조정 필요현재 import 순서가 알파벳 정렬을 따르지 않습니다.
♻️ 정렬 수정 제안
-import ProjectDescription -import ProjectDescriptionHelpers -import DependencyPlugin +import DependencyPlugin +import ProjectDescription +import ProjectDescriptionHelpers🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Project.swift` around lines 9 - 11, The imports at the top of Project.swift are not alphabetically sorted, triggering SwiftLint's sorted_imports rule; reorder the two import statements so they follow alphabetical order (import DependencyPlugin before import ProjectDescriptionHelpers) to satisfy the lint rule and keep imports consistently sorted.Projects/Core/Sources/Storage/UserDefaultWrapper.swift (1)
13-15: SwiftLint:implicit_return— getter의 명시적return생략 가능♻️ 암묵적 반환으로 변경 제안
get { - return UserDefaults.standard.object(forKey: self.key.rawValue) as? T + UserDefaults.standard.object(forKey: self.key.rawValue) as? T }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Core/Sources/Storage/UserDefaultWrapper.swift` around lines 13 - 15, The getter in UserDefaultWrapper's computed property uses an explicit "return" that SwiftLint flags; change the get block to use implicit return by replacing "get { return UserDefaults.standard.object(forKey: self.key.rawValue) as? T }" with "get { UserDefaults.standard.object(forKey: self.key.rawValue) as? T }" (reference the computed property in UserDefaultWrapper that accesses self.key and UserDefaults.standard).Projects/Modules/DSKit/Sources/Component/RecommendInfoCell.swift (2)
58-75:configure메서드의 파라미터가 6개로 SwiftLint 규칙(5개 이하)을 초과합니다.설정 구조체를 도입하면 가독성과 유지보수성이 개선됩니다.
♻️ 구조체 도입 제안
+struct RecommendInfoCellModel { + let title: String + let thumbnailUrl: String + let countryCode: String + let creator: String + let city: String + let schedule: String +} + - public func configure( - title: String, - thumbnailUrl: String, - countryCode: String, - creator: String, - city: String, - schedule: String - ) { + public func configure(with model: RecommendInfoCellModel) { + let title = model.title + let thumbnailUrl = model.thumbnailUrl + let countryCode = model.countryCode + let creator = model.creator + let city = model.city + let schedule = model.schedule🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/DSKit/Sources/Component/RecommendInfoCell.swift` around lines 58 - 75, The configure(title:thumbnailUrl:countryCode:creator:city:schedule:) method on RecommendInfoCell exceeds the SwiftLint 5-parameter rule; refactor by introducing a single configuration struct (e.g., RecommendInfo or RecommendInfoViewModel) that contains title, thumbnailUrl, countryCode, creator, city, and schedule, update the Configure signature to accept that struct (e.g., configure(with: RecommendInfo)), and update all call sites to build and pass the struct; ensure internal usage maps struct properties to thumbnailView.kf.setImage, nationalFlagLabel, nationLabel, titleLabel, nameLabel, cityLabel and scheduleLabel as before.
78-122: 일부 spacing/cornerRadius 값이 스케일링 확장 없이 하드코딩되어 있습니다.Line 81(
cornerRadius = 8), Line 107/113/120(spacing = 4)에서.adjusted/.adjustedH없이 하드코딩된 값이 사용되고 있어 다양한 화면 크기에서 일관성이 깨질 수 있습니다.Based on learnings: "In the NDGL iOS design system, ensure all components use the defined scaling extensions: adjustedH scales by height ratio, and adjustedW and adjusted scale by width ratio."
♻️ 스케일링 확장 적용 제안
- $0.layer.cornerRadius = 8 + $0.layer.cornerRadius = 8.adjusted - $0.spacing = 4 + $0.spacing = 4.adjusted - $0.spacing = 4 + $0.spacing = 4.adjusted - $0.spacing = 4 + $0.spacing = 4.adjustedH🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/DSKit/Sources/Component/RecommendInfoCell.swift` around lines 78 - 122, The setStyle() function uses hardcoded geometry; replace the literal values with the project scaling extensions: change thumbnailView.layer.cornerRadius = 8 to use a scaled value (e.g. 8.adjusted), and change the stack view spacing assignments (nationStackView.spacing, subInfoStackView.spacing, infoStackView.spacing) from 4 to scaled values (e.g. 4.adjusted or 4.adjustedH depending on whether you want width- or height-based scaling), keeping use of .adjustedH where vertical scaling is intended; update these in the RecommendInfoCell.setStyle() method referencing thumbnailView, nationStackView, subInfoStackView, and infoStackView.Projects/Features/MainFeature/Sources/MainBuilder.swift (1)
26-26: SwiftLint: 쉼표 뒤 공백 누락.
PopularTravelDependency,SearchDependency사이에 공백이 없습니다 (SwiftLintcomma규칙). 기존 코드이지만 이번 변경과 함께 수정하면 좋겠습니다.♻️ 수정 제안
-final class MainComponent: Component<MainDependency>, FollowDetailDependency, PopularTravelDependency,SearchDependency, SettingDependency, TabBarDependency { +final class MainComponent: Component<MainDependency>, FollowDetailDependency, PopularTravelDependency, SearchDependency, SettingDependency, TabBarDependency {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MainFeature/Sources/MainBuilder.swift` at line 26, The dependency list in the MainComponent declaration has a missing space after a comma between PopularTravelDependency and SearchDependency; update the class declaration (final class MainComponent: Component<MainDependency>, FollowDetailDependency, PopularTravelDependency,SearchDependency, SettingDependency, TabBarDependency) to insert a space after the comma so it reads ..., PopularTravelDependency, SearchDependency, ... to satisfy SwiftLint's comma rule.Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift (1)
96-96:// MARK: - MyTravelListener주석 누락
HomeListener,TravelToolListener확장에는 모두 MARK 주석이 있으나 이 확장만 빠져 있습니다.♻️ 제안
+// MARK: - MyTravelListener + extension TabBarInteractor: MyTravelListener {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift` at line 96, Add the missing section comment by inserting a "// MARK: - MyTravelListener" comment immediately above the TabBarInteractor extension declaration for MyTravelListener (extension TabBarInteractor: MyTravelListener) so it matches the existing MARK comments used for HomeListener and TravelToolListener and keeps the file's sectioning consistent.Projects/Features/MyTravelFeature/Sources/Views/CollectionView/SectionKind/MyTravelSectionKind.swift (1)
16-24: 하드코딩된 한국어 문자열 — 현지화 고려
"다가오는 여행","추천하는 따라가기 여행"등의 UI 문자열이 소스에 직접 포함되어 있습니다. 향후 다국어 지원을 고려한다면NSLocalizedString으로 추출하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/SectionKind/MyTravelSectionKind.swift` around lines 16 - 24, The headerTitle computed property currently returns hardcoded Korean strings; update it to use NSLocalizedString (or a String extension/localization helper) instead of literals so UI text is localized: replace the direct returns in headerTitle (for cases .upcomingTrips and .recommendedTrip) with NSLocalizedString calls using clear keys (e.g. "section.upcoming_trips.title", "section.recommended_trip.title") and provide those keys in Localizable.strings; keep the same enum/case names and behavior (headerTitle) and ensure the default still returns an empty localized string if needed.Projects/Domain/Sources/Model/Home/UpcomingInfo.swift (1)
11-49:public init명시적 제공은 올바르나, 파일 위치 일관성 확인 권장
public struct의 멤버와이즈 이니셜라이저는 Swift에서internal로 생성되므로public init을 명시적으로 제공한 것은 정확합니다. 다만 이 파일도Domain/Sources/Model/Home/에 위치하고 있는데, MyTravel 도메인 모델이라면UserContentCardResponse와 마찬가지로 적절한 MyTravel 하위 경로에 두는 것이 일관성에 맞습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Domain/Sources/Model/Home/UpcomingInfo.swift` around lines 11 - 49, The UpcomingInfo struct currently defines a public init correctly but lives under Domain/Sources/Model/Home/, which is inconsistent with other MyTravel domain models (e.g., UserContentCardResponse); move the file to the MyTravel domain model folder (mirror the location used by UserContentCardResponse) so UpcomingInfo and its public init remain in the appropriate MyTravel module/namespace, and update any imports/usages to the new location.Projects/Features/MainFeature/Sources/MainInteractor.swift (1)
60-64:UserManager.shared직접 접근 — RIBs 의존성 주입 권장RIBs 아키텍처에서 싱글턴을 인터랙터 내부에서 직접 접근하는 것은 테스트 가능성 및 의존성 방향성 원칙에 어긋납니다.
UserManager를MainDependency프로토콜에 추가하고 빌더에서 주입하는 방식을 권장합니다.또한 PR 설명에 따르면
isFirstOpenApp()은 조회와 동시에isFirstOpen기본값을 설정하는 부수 효과가 있어 CQS(Command-Query Separation) 원칙에 위배됩니다. 읽기(isFirstOpen)와 쓰기(기본값 설정)를 분리하는 것을 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MainFeature/Sources/MainInteractor.swift` around lines 60 - 64, The interactor directly accesses UserManager.shared in checkFirstOpenStatus(), which breaks RIBs dependency-injection and testability and also relies on isFirstOpenApp() that mixes read and write; update the design by adding a UserManager (or a minimal interface exposing isFirstOpen and a separate initializer/markFirstOpen method) to the MainDependency protocol, have the builder inject that dependency into MainInteractor, replace UserManager.shared usage in checkFirstOpenStatus() to read the injected dependency's isFirstOpen property, and ensure any side-effect that sets the default is separated into an explicit write method (e.g., markFirstOpen) invoked where appropriate instead of inside isFirstOpenApp().Projects/Features/MyTravelFeature/Sources/MyTravelViewController.swift (3)
143-155:guard let으로 바인딩한 지역 변수dataSource를 사용해 주세요.Line 145에서
guard let self, let dataSource = self.dataSource로 바인딩했지만, Line 153에서self.dataSource.apply(...)로 다시self를 통해 접근하고 있습니다. 바인딩된 지역 변수dataSource를 사용하는 것이 더 안전하고 일관적입니다.♻️ 제안된 수정
- self.dataSource.apply(snapshot, animatingDifferences: true) + dataSource.apply(snapshot, animatingDifferences: true)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelViewController.swift` around lines 143 - 155, In update(with sections:) change the use of self.dataSource to the locally bound dataSource from the guard; inside the DispatchQueue.main.async block use the dataSource variable (bound in guard let dataSource = self.dataSource) when calling apply(snapshot, animatingDifferences:) to avoid re-accessing self.dataSource and ensure the safe, consistent use of the guarded reference.
66-73:contentInset이 중복 설정되고 있습니다.Line 70에서
.zero로 설정한 후 Line 72에서 바로 덮어쓰고 있습니다. Line 70은 불필요합니다.♻️ 제안된 수정
collectionView.do { $0.showsVerticalScrollIndicator = false $0.showsHorizontalScrollIndicator = false $0.backgroundColor = .clear - $0.contentInset = .zero $0.isScrollEnabled = true $0.contentInset = .init(top: 21.adjustedH, left: 0, bottom: 0, right: 0) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelViewController.swift` around lines 66 - 73, collectionView 설정 블록에서 contentInset이 두 번 설정되어 .zero가 바로 덮어씌워지므로 중복을 제거하세요: locate the collectionView.do closure and remove the line setting $0.contentInset = .zero, leaving only the intended $0.contentInset = .init(top: 21.adjustedH, left: 0, bottom: 0, right: 0) so the final layout uses the adjusted top inset.
111-139:UICollectionView.CellRegistration과SupplementaryRegistration을 사용할 때 수동register호출은 불필요합니다.
setDataSource()와configureSupplementaryView()에서dequeueConfiguredReusableCell과dequeueConfiguredReusableSupplementary를 사용하고 있으므로,setCollectionView()의 수동register호출은 모두 중복입니다. 현대적 등록 API는 자동으로 셀과 보조 뷰를 등록 처리하므로 lines 113-137의 모든register호출을 제거할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelViewController.swift` around lines 111 - 139, The setCollectionView() function currently manually registers cells and a supplementary view even though you use UICollectionView.CellRegistration/SupplementaryRegistration with dequeueConfiguredReusableCell and dequeueConfiguredReusableSupplementary in setDataSource() and configureSupplementaryView(); remove the manual register calls inside setCollectionView() for MyTravelBannerCell, EmptyUpcomingCell, UpcomingCell, RecommendInfoCell and MyTravelHeaderView so the modern registration API handles registration automatically, leaving any other non-registration setup in setCollectionView() intact.Projects/Features/MainFeature/Sources/VC/ServiceNoticeViewController.swift (2)
32-44: 하드코딩된 hex 컬러 문자열 대신 DSKit 에셋 사용을 고려해 주세요.Line 34에서
UIColor.init(hexCode: "#000000", alpha: 0.7)로 직접 색상을 생성하고 있습니다. 다른 색상들은 모두DSKitAsset.Colors를 통해 참조하고 있으므로, 일관성을 위해 DSKit에 정의된 색상 에셋이나 공통 유틸리티를 사용하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MainFeature/Sources/VC/ServiceNoticeViewController.swift` around lines 32 - 44, Replace the hardcoded UIColor.init(hexCode: "#000000", alpha: 0.7) in ServiceNoticeViewController.setStyle with the DSKit asset equivalent to keep styling consistent; use the DSKitAsset.Colors entry (for example DSKitAsset.Colors.black900.color) and apply the alpha via withAlphaComponent(0.7) before assigning to view.backgroundColor so the background uses the shared design token instead of a literal hex string.
86-111:confirmButton에 하단 제약 조건이 없어containerView에서 콘텐츠가 잘릴 수 있습니다.
containerView의 높이가318.adjustedH로 고정되어 있으나confirmButton의 bottom이containerView에 연결되어 있지 않습니다. 디바이스 크기에 따라adjustedH스케일링이 달라질 경우, 버튼이 컨테이너 영역 밖으로 나갈 수 있습니다.고정 높이 대신
confirmButton.bottom을containerView.bottom에 연결하여 컨테이너가 콘텐츠에 맞게 자동으로 크기를 조절하도록 하는 것을 권장합니다.♻️ 제안된 수정
containerView.snp.makeConstraints { $0.center.equalToSuperview() $0.width.equalTo(314.adjusted) - $0.height.equalTo(318.adjustedH) }confirmButton.snp.makeConstraints { $0.top.equalTo(termsButton.snp.bottom).offset(28.adjustedH) $0.horizontalEdges.equalToSuperview().inset(28.adjusted) + $0.bottom.equalToSuperview().inset(28.adjustedH) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MainFeature/Sources/VC/ServiceNoticeViewController.swift` around lines 86 - 111, In setLayout(), tie the confirmButton's bottom to the containerView so the container won't cut off content: update confirmButton.snp.makeConstraints to include $0.bottom.equalToSuperview().inset(28.adjustedH) (or an appropriate inset), and remove or relax the fixed containerView height (containerView.snp.makeConstraints: delete height.equalTo(318.adjustedH) or change to height.greaterThanOrEqualTo(318.adjustedH)) so containerView can size to its content; ensure these changes reference containerView, setLayout(), and confirmButton so the constraints are adjusted in the same method.Projects/Data/Sources/Transform/UserTravelTransform.swift (1)
52-69: 날짜 파싱 실패 시.now폴백이 데이터 문제를 숨길 수 있습니다.
$0.startDate.toDate() ?? .now패턴은 기존UpcomingResponse.toDomain()(Line 38-39)과 동일하지만, 서버에서 잘못된 날짜 형식이 반환되면 현재 날짜로 대체되어 UI에 잘못된 여행 일정이 표시될 수 있습니다.향후 디버깅을 위해 파싱 실패 시 로깅을 추가하는 것을 고려해 주세요.
🤖 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 52 - 69, In UpcomingListResponse.toDomain(), avoid silently falling back to .now when $0.startDate.toDate() or $0.endDate.toDate() returns nil: detect the failed parse for $0.startDate and $0.endDate, log a clear error (including the source string, the item's id/title/templateId) using the project's logger (same approach as used in UpcomingResponse.toDomain() if present), and then decide a safe fallback or skip/mark the item; update the mapping that constructs UpcomingInfo to perform the parse+logging before calling .init so parsing failures are recorded for debugging.Projects/Features/MyTravelFeature/Sources/Views/CollectionView/Registration/MyTravelRegistraion.swift (1)
24-47: 미사용 클로저 파라미터indexPath를_로 교체하세요.SwiftLint
unused_closure_parameter경고가 여러 registration 클로저에서 발생합니다. 사용하지 않는indexPath파라미터를_로 변경하세요.🔧 수정 제안 (예시: createBannerCellRegistration)
- return UICollectionView.CellRegistration { cell, indexPath, item in + return UICollectionView.CellRegistration { cell, _, item in
createRecommedTripCellRegistration(Line 31)과createUpcomingCell(Line 44)에도 동일하게 적용하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/Registration/MyTravelRegistraion.swift` around lines 24 - 47, Replace the unused closure parameter indexPath with _ in each CellRegistration closure to satisfy SwiftLint's unused_closure_parameter rule: update the closures in createBannerCellRegistration, createRecommedTripCellRegistration, and createUpcomingCell so their registration blocks use the signature (cell, _, item) instead of (cell, indexPath, item); no logic changes needed inside the closures.Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift (5)
79-79:프로덕션 코드에서
os.Logger또는 프로젝트 로깅 유틸을 사용하는 것이 좋습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift` at line 79, Replace the use of print in MyTravelInteractor (the print("Log: MyTripInfo 로드 실패 - \(error)") call) with the project's structured logger (e.g., an os.Logger instance or the shared logging utility) so logs can be filtered and configured; update the MyTravelInteractor to inject or obtain a Logger (or use the existing logging utility), and change the print call to a structured log call that includes the error as context (error: or metadata) and a clear message about MyTripInfo load failure.
127-129:viewWillAppear마다 데이터를 매번 fetch합니다.탭 전환 시마다 네트워크 요청이 발생합니다. 캐싱이나 마지막 fetch 시간 기반 throttle을 고려하면 불필요한 네트워크 호출을 줄일 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift` around lines 127 - 129, The viewWillAppear() currently calls fetchMyTravelData() on every appearance causing redundant network requests; modify MyTravelInteractor to add a cachedData property and a lastFetchDate (or similar) and change viewWillAppear() to check the cache/lastFetchDate before calling fetchMyTravelData(), e.g., only call fetchMyTravelData() if cachedData is nil or lastFetchDate is older than a defined TTL (or add a forceRefresh flag to fetchMyTravelData()); ensure fetchMyTravelData() updates cachedData and lastFetchDate after a successful load and provide a way to invalidate the cache when necessary.
51-53:super호출만 하는 불필요한 오버라이드를 제거하세요.SwiftLint
unneeded_override경고에 해당합니다.didBecomeActive에서 추가 로직이 없으므로 제거 가능합니다.🔧 수정 제안
- override func didBecomeActive() { - super.didBecomeActive() - } -🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift` around lines 51 - 53, 현재 MyTravelInteractor.swift의 override func didBecomeActive()는 오직 super.didBecomeActive()만 호출하므로 SwiftLint의 unneeded_override 경고 대상입니다; MyTravelInteractor 클래스에서 불필요한 오버라이드를 제거하여 didBecomeActive() 메서드를 삭제하면 해결됩니다 (즉, 해당 override 블록을 제거하여 기본 구현인 super.didBecomeActive()가 호출되도록 함).
62-119: Task 취소 후에도 UI 업데이트가 실행될 수 있습니다.Line 69에서
Task.isCancelled를 한 번 체크하지만, 이후 여러await호출(fetchUpcomingList,fetchRecommendTripList,MainActor.run) 사이에는 취소 여부를 재확인하지 않습니다.willResignActive에서 task를 취소해도, 진행 중인 네트워크 응답 이후의 UI 업데이트는 그대로 수행됩니다.
try Task.checkCancellation()을 주요await지점 사이에 추가하면 불필요한 작업을 조기에 중단할 수 있습니다.🔧 수정 제안 (예시)
upcomingList = try await usecase.fetchUpcomingList().map { $0.toMyTravelModel() } + try Task.checkCancellation() var recommendItems: [MyTravelPresentationModel.RecommendedTrip] = [] if myTripBanner == nil && upcomingList.isEmpty { recommendItems = try await usecase.fetchRecommendTripList().map { $0.toRecommendMyTravelModel() } + try Task.checkCancellation() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift` around lines 62 - 119, fetchMyTravelData uses a single early check of Task.isCancelled but does not re-check cancellation between await points, so cancelled tasks can still continue and update the UI; modify the Task body (associated with fetchDataTask) to call try Task.checkCancellation() (or guard !Task.isCancelled else { return }) immediately after each major await (after usecase.fetchMyTripInfo(), after usecase.fetchUpcomingList(), after fetchRecommendTripList() if called, and before MainActor.run) so work bails out promptly and no UI updates occur for cancelled tasks; keep the existing weak self capture and use the same presenter update path when not cancelled.
41-41:myTravelRelay가 선언만 되고 사용되지 않습니다.
BehaviorRelay가 초기화되었지만 구독하거나 값을 설정하는 코드가 없습니다. 사용 계획이 없다면 제거하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift` at line 41, The declared BehaviorRelay myTravelRelay in MyTravelInteractor.swift is never used; either remove the unused property or wire it up: if not needed delete the private let myTravelRelay = BehaviorRelay<[(MyTravelSectionKind, [MyTravelItem])]>(value: []) to remove dead code, otherwise update the interactor methods that build/receive travel data to .accept(...) new values into myTravelRelay, expose it as an observable via myTravelRelay.asObservable() or a read-only Relay for callers, and ensure consumers subscribe to that observable (references: myTravelRelay, MyTravelSectionKind, MyTravelItem, MyTravelInteractor).Projects/Features/MyTravelFeature/Sources/Views/CollectionView/CompositionalLayout/MyTravelCompositionalLayout.swift (1)
15-15: 미사용 클로저 파라미터environment를_로 교체하세요.SwiftLint
unused_closure_parameter경고에 해당합니다.🔧 수정 제안
- return UICollectionViewCompositionalLayout { [weak self] sectionIndex, environment in + return UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/CompositionalLayout/MyTravelCompositionalLayout.swift` at line 15, 클로저의 미사용 파라미터 `environment` 때문에 SwiftLint의 unused_closure_parameter 경고가 발생하므로 UICollectionViewCompositionalLayout 생성 클로저 시그니처에서 `environment`를 `_`로 교체하세요 (즉, the closure passed to UICollectionViewCompositionalLayout that currently reads `{ [weak self] sectionIndex, environment in ... }` should become `{ [weak self] sectionIndex, _ in ... }`) so the unused parameter is ignored while preserving `sectionIndex` and the `[weak self]` capture.Projects/Domain/Sources/UseCase/MyTravelUsecase.swift (1)
33-44:travelTemplateRepository사용 범위를 확인하세요.현재
travelTemplateRepository는fetchRecommendTripList에서만 사용되고,userTravelRepository는 나머지 두 메서드에서 사용됩니다. 향후 확장 계획이 없다면, 각 리포지토리를 별도 UseCase로 분리하는 것도 고려할 수 있지만, 현재 규모에서는 문제 없습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Domain/Sources/UseCase/MyTravelUsecase.swift` around lines 33 - 44, MyTravelUsecase declares two repositories but travelTemplateRepository is only used by fetchRecommendTripList while userTravelRepository is used by the other methods; verify and either (a) keep both in MyTravelUsecase if you expect future shared behavior, or (b) refactor by moving travelTemplateRepository and fetchRecommendTripList into a dedicated TravelTemplateUsecase and keep UserTravelRepository methods in a separate UserTravelUsecase; locate symbols MyTravelUsecase, travelTemplateRepository, fetchRecommendTripList, and userTravelRepository to perform the split or confirm retention.
🤖 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/Core/Sources/Extensions/Foundation`+/String+.swift:
- Around line 35-40: The String.toDate() implementation creates a new
DateFormatter on every call which is expensive; update it to avoid per-call
allocations: if toDate() is only ever invoked on the main thread, replace the
local formatter with a cached static DateFormatter used on the main thread
(e.g., a lazily-initialized static property referenced from toDate()); if
toDate() can be called from background threads (e.g., from UserTravelTransform),
keep the current per-call creation or protect a shared formatter via
thread-local storage or a serial queue/lock, or use a thread-safe alternative
like ISO8601DateFormatter where appropriate; modify the String.toDate() method
accordingly to use one of these safe caching strategies.
In
`@Projects/Domain/Sources/Interface/UserTravel/UserTravelRepositoryInterface.swift`:
- Line 13: 제거할 미완성 주석 선언 func fetchContentCard(id: Int) async throws -> 는 반환 타입이
없어 불완전하므로 유지할 계획이 없다면 해당 주석(미완성 선언)을 삭제하세요; 만약 추후 구현할 계획이면 완전한 시그니처(예: func
fetchContentCard(id: Int) async throws -> ContentCard) 또는 적절한 placeholder 반환 타입을
명시하고 구현 또는 TODO 주석을 추가해 주세요 — 참조 심볼: fetchContentCard(id: Int) async throws ->.
In `@Projects/Features/MainFeature/Sources/MainViewController.swift`:
- Around line 59-61: The Notion link assigned in noticeVC.termsHandler uses an
unnecessary tracking query parameter and is hard-coded; remove the trailing
"?source=copy_link" from the URL and replace the literal string with a
centralized constant (e.g., TermsURL or NotionTermsURL) defined in your
constants/config file, then update the handler to call URLHelper.openURL with
that constant (referencing noticeVC.termsHandler and URLHelper.openURL to locate
the change).
In
`@Projects/Features/MyTravelFeature/Sources/Models/MyTravelPresentationModel.swift`:
- Around line 123-137: UpcomingInfo.toMyTravelModel currently computes dDay and
MyTravelBannerCell.configure duplicates that logic; remove the duplication by
having the cell accept the precomputed dDay from the presentation model. Update
UpcomingInfo.toMyTravelModel to continue providing dDay (it already does) and
modify MyTravelBannerCell.configure to stop recomputing dDay and instead use the
dDay property from MyTravelPresentationModel.Upcoming (refer to the
UpcomingInfo.toMyTravelModel, MyTravelPresentationModel.Upcoming, and
MyTravelBannerCell.configure symbols) so all D-day computation lives in the
presentation model.
In `@Projects/Features/MyTravelFeature/Sources/MyTravelViewController.swift`:
- Around line 193-198: The code uses a magic number check (banner.id == 0)
inside the switch over MyTravelItem (case .banner(let banner)) to decide between
emptyReg and bannerReg; replace this fragile check by modeling empty state
explicitly—either add a new MyTravelItem case (e.g., .emptyBanner) and switch on
that, or add an explicit flag/property on the banner presentation model (e.g.,
banner.isEmpty) and use that instead—then update the switch in
MyTravelViewController (the case .banner branch and any callers that construct
items) to dequeueConfiguredReusableCell using emptyReg only for the explicit
empty case/flag and bannerReg for normal banners, leaving
collectionView.dequeueConfiguredReusableCell calls and emptyReg/bannerReg usage
unchanged.
In
`@Projects/Features/MyTravelFeature/Sources/Views/Cells/MyTravelBannerCell.swift`:
- Around line 43-73: The cell contains business logic: move date comparison and
D-day calculation out of MyTravelBannerCell.configure(_ model:
MyTravelPresentationModel.Banner) into the presentation/model mapping layer so
the cell only receives a resolved MyTravelBannerType (e.g., .onGoing /
.upComming) or a Banner that already exposes bannerType, dDay and transport
info; remove Calendar/date math and setting of self.type inside configure and
instead accept the precomputed type and call updateViewWithCurrentType(). Also
stop hardcoding transport assets/strings: expose transportIcon and
transportLabel (or a transportType enum) from
MyTravelPresentationModel.Banner/tripSchedule and use those in configure rather
than DSKitAsset.Assets.icBus2.image or the literal "대중교통".
- Around line 127-137: The enum case name MyTravelBannerType.upComming is
misspelled; rename it to MyTravelBannerType.upcoming (and update any related
view identifiers that used "upComming", e.g., MyTravelBannerCell references) and
then update all usages: switch/case patterns, initializers, unit tests,
storyboard/xib identifiers, and any string-based keys so the code compiles and
runtime lookups remain consistent. Ensure you update associated labels or
factory methods that construct the case with the new name and run tests/build to
catch any remaining references.
- Around line 62-69: The current else branch treats any non-future case as
upcoming, causing past trips to be shown as "upComming" with negative D-day;
update the conditional logic around startOfToday, startOfTravel and startOfEnd
so only true future trips set self.type = .upComming (keep the existing
dDayValue computation), add an explicit branch for past trips (startOfEnd <
startOfToday) to set a .past or other appropriate type/handling, and optionally
handle ongoing trips when startOfTravel <= startOfToday && startOfEnd >=
startOfToday; modify the code that sets self.type = .upComming(...) to be inside
the strict future check and implement the past/ongoing branches instead of the
current else.
In
`@Projects/Features/MyTravelFeature/Sources/Views/Cells/MyTravelHeaderView.swift`:
- Around line 28-32: prepareForReuse currently only clears titleLabel.text, but
configure(title:) uses DSKit's setText which sets titleLabel.attributedText, so
previous styled text can persist; update the MyTravelHeaderView.prepareForReuse
implementation to also clear titleLabel.attributedText (and keep text = nil) so
any attributed styling from prior reuse is removed before the cell is reused.
In `@Projects/Features/MyTravelFeature/Sources/Views/Cells/UpcomingCell.swift`:
- Line 110: 현재 셀에 직접 addSubview(stackView) 하고 있어서 선택/하이라이트/레이아웃에서 문제를 일으킵니다;
stackView를 셀이 아닌 contentView에 추가하도록 변경하고, setLayout에서 stackView의
제약(leading/trailing/top/bottom, center 등)을 contentView의 anchors(예:
contentView.topAnchor, contentView.leadingAnchor 등)를 기준으로 설정하도록 수정하세요; 관련 식별자는
stackView와 setLayout 메서드(또는 레이아웃 설정 코드)를 찾아 대상을 contentView로 바꾸면 됩니다.
- Around line 38-48: The configure(title:date:dDay:imageUrl:) method currently
formats dDay poorly and leaves a debug print; update d-day formatting so that
when dDay == 0 you set dDayLabel to "D-Day", when dDay > 0 you set "D-\(dDay)",
and when dDay < 0 you set "D+\(abs(dDay))"; remove the debug print(imageUrl) and
instead keep the existing fallback behavior for imageView (e.g., set
backgroundColor or a placeholder image) when URL(string: imageUrl) is nil;
modify the dDayLabel assignment inside configure and delete the print call
referencing imageUrl to avoid leaking debug logs.
In
`@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/Registration/MyTravelRegistraion.swift`:
- Line 30: Rename the misspelled function createRecommedTripCellRegistration to
createRecommendTripCellRegistration and update all references/usages
accordingly; specifically rename the declaration of
createRecommedTripCellRegistration() -> createRecommendTripCellRegistration()
and update any call sites or tests that reference it (including places using
RecommendInfoCell and MyTravelPresentationModel.RecommendedTrip) so the new
symbol name is used consistently across the codebase.
- Around line 1-3: Rename the file MyTravelRegistraion.swift to
MyTravelRegistration.swift and fix the header comment lines to spell
"Registration" correctly; search the file for any symbols or identifiers that
use "MyTravelRegistraion" (file-level comments, MARKs,
class/struct/enum/extension names such as a type named MyTravelRegistraion) and
rename them to "MyTravelRegistration" so the filename, header comment, and any
identifier names are consistent.
In
`@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/SectionKind/MyTravelSectionKind.swift`:
- Around line 11-14: 현재 MyTravelSectionKind의 upcomingTrips(isEmpty: Bool) 연관값
때문에 섹션 식별자가 상태 변경시 달라져서 섹션 전체가 삭제·재삽입됩니다; 수정 방법은 MyTravelSectionKind의
upcomingTrips 케이스에서 연관값을 제거(예: case upcomingTrips)하고 빈 상태 표현을 섹션 식별자에서 제거한 뒤,
MyTravelItem에 빈 상태용 케이스(예: .emptyUpcomingTrip)를 추가하여 인터랙터(또는 해당 데이터를 만드는 메서드)가
아이템 배열이 비어있을 때 해당 빈 아이템을 반환하도록 변경하세요; 스냅샷 생성 로직을 업데이트해 이제 upcomingTrips 섹션에는 실제
아이템 또는 .emptyUpcomingTrip이 포함되도록 하고, MyTravelSectionKind 및 스냅샷 관련 코드에서 더 이상
isEmpty 연관값을 사용하지 않도록 정리하세요.
In `@Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift`:
- Around line 96-108: The three methods in TabBarInteractor that implement
MyTravelListener are misspelled (missing the "l"): update the method names
myTraveDidTapFollowDetail, myTraveDidTapSearch, and myTraveDidTapPopularTravel
to myTravelDidTapFollowDetail, myTravelDidTapSearch, and
myTravelDidTapPopularTravel respectively so they match the MyTravelListener
protocol, and ensure any references like listener?.routeToFollow,
listener?.routeToSearch, and listener?.routeToPopularTravel remain unchanged;
also run a compile to find and fix any other callsites or protocol declarations
that need the same rename.
In `@Projects/Modules/DSKit/Sources/Component/EmptyUpcomingCell.swift`:
- Around line 45-47: In EmptyUpcomingCell, change the prepareForReuse
declaration to put the override modifier before the access level (use "override
public func prepareForReuse()") and inside the method call
super.prepareForReuse() as the first statement, then reset the disposeBag
(disposeBag = DisposeBag()) so the cell is properly reset and adheres to
SwiftLint modifier order.
In `@Projects/Modules/Networks/Sources/TargetType/UserTravelAPI.swift`:
- Around line 33-34: The enum case .getUpcomingList returns a path missing the
leading '/', causing incorrect URL composition; update the path returned for
.getUpcomingList to include a leading slash (i.e.,
"/api/v1/travels/upcoming/list") so it matches the other cases and composes
correctly with Moya's baseURL; verify in the TargetType implementation that
other paths follow the same leading-slash pattern for consistency.
---
Outside diff comments:
In `@Projects/Features/RootFeature/Sources/RootInteractor.swift`:
- Around line 67-100: performAuthFlow currently spawns an untracked Task and
only prints errors, which can leave the UI empty on failure and allows the work
to continue after RIB deactivation; change performAuthFlow to store the Task in
a property (e.g., authTask) so it can be cancelled from willResignActive, add
cancellation checks or handle Task.isCancelled inside the Task, and improve
error handling by ensuring a fallback UI flow is always invoked (e.g., call
router?.attachMain() or an error/placeholder routing path) in the catch path or
via a defer so the app never remains blank; also replace the plain print with
proper logging. Reference: performAuthFlow, router?.attachMain(),
willResignActive, and add a stored Task property (authTask) to manage
cancellation.
---
Nitpick comments:
In `@Projects/Core/Sources/Extensions/Foundation`+/Date+.swift:
- Around line 12-17: The toKoreanMMdd() function creates a new DateFormatter on
every call; change it to use a cached static DateFormatter (e.g. a static let
koreanMMddFormatter) configured with locale "ko_KR" and dateFormat "M월 d일" and
return koreanMMddFormatter.string(from: self) inside toKoreanMMdd(); if this
utility may be called off the main thread, either document that it’s
main-thread-only or create/return a thread-confined formatter per thread (or use
a DateFormatter factory) to avoid thread-safety issues.
In `@Projects/Core/Sources/Storage/UserDefaultWrapper.swift`:
- Around line 13-15: The getter in UserDefaultWrapper's computed property uses
an explicit "return" that SwiftLint flags; change the get block to use implicit
return by replacing "get { return UserDefaults.standard.object(forKey:
self.key.rawValue) as? T }" with "get { UserDefaults.standard.object(forKey:
self.key.rawValue) as? T }" (reference the computed property in
UserDefaultWrapper that accesses self.key and UserDefaults.standard).
In `@Projects/Core/Sources/Storage/UserManager.swift`:
- Around line 11-15: The enum UserDefaultKeys currently specifies raw string
values identical to the case names; update UserDefaultKeys (the enum and its
cases uuid, nickname, isFirstOpen) to omit the redundant rawValue literals so
Swift auto-synthesizes them (e.g. declare the cases without = "..."), keeping
the enum conforming to String.
- Around line 26-32: 현재 isFirstOpenApp()의 논리는 동작하지만 self.isFirstOpen = true 후
반환값을 !isFirstOpen으로 처리해 의도가 직관적이지 않습니다; isFirstOpenApp() 내에서 옵셔널 바인딩 실패 시
self.isFirstOpen을 true로 설정하고 즉시 true를 반환하고, 옵셔널이 존재할 경우는 false를 반환하도록 리팩토링해 가독성을
개선하세요 (참조 심볼: isFirstOpenApp(), isFirstOpen).
In `@Projects/Data/Sources/Transform/UserTravelTransform.swift`:
- Around line 52-69: In UpcomingListResponse.toDomain(), avoid silently falling
back to .now when $0.startDate.toDate() or $0.endDate.toDate() returns nil:
detect the failed parse for $0.startDate and $0.endDate, log a clear error
(including the source string, the item's id/title/templateId) using the
project's logger (same approach as used in UpcomingResponse.toDomain() if
present), and then decide a safe fallback or skip/mark the item; update the
mapping that constructs UpcomingInfo to perform the parse+logging before calling
.init so parsing failures are recorded for debugging.
In `@Projects/Domain/Sources/Model/Home/UpcomingInfo.swift`:
- Around line 11-49: The UpcomingInfo struct currently defines a public init
correctly but lives under Domain/Sources/Model/Home/, which is inconsistent with
other MyTravel domain models (e.g., UserContentCardResponse); move the file to
the MyTravel domain model folder (mirror the location used by
UserContentCardResponse) so UpcomingInfo and its public init remain in the
appropriate MyTravel module/namespace, and update any imports/usages to the new
location.
In `@Projects/Domain/Sources/UseCase/MyTravelUsecase.swift`:
- Around line 33-44: MyTravelUsecase declares two repositories but
travelTemplateRepository is only used by fetchRecommendTripList while
userTravelRepository is used by the other methods; verify and either (a) keep
both in MyTravelUsecase if you expect future shared behavior, or (b) refactor by
moving travelTemplateRepository and fetchRecommendTripList into a dedicated
TravelTemplateUsecase and keep UserTravelRepository methods in a separate
UserTravelUsecase; locate symbols MyTravelUsecase, travelTemplateRepository,
fetchRecommendTripList, and userTravelRepository to perform the split or confirm
retention.
In `@Projects/Features/MainFeature/Sources/MainBuilder.swift`:
- Line 26: The dependency list in the MainComponent declaration has a missing
space after a comma between PopularTravelDependency and SearchDependency; update
the class declaration (final class MainComponent: Component<MainDependency>,
FollowDetailDependency, PopularTravelDependency,SearchDependency,
SettingDependency, TabBarDependency) to insert a space after the comma so it
reads ..., PopularTravelDependency, SearchDependency, ... to satisfy SwiftLint's
comma rule.
In `@Projects/Features/MainFeature/Sources/MainInteractor.swift`:
- Around line 60-64: The interactor directly accesses UserManager.shared in
checkFirstOpenStatus(), which breaks RIBs dependency-injection and testability
and also relies on isFirstOpenApp() that mixes read and write; update the design
by adding a UserManager (or a minimal interface exposing isFirstOpen and a
separate initializer/markFirstOpen method) to the MainDependency protocol, have
the builder inject that dependency into MainInteractor, replace
UserManager.shared usage in checkFirstOpenStatus() to read the injected
dependency's isFirstOpen property, and ensure any side-effect that sets the
default is separated into an explicit write method (e.g., markFirstOpen) invoked
where appropriate instead of inside isFirstOpenApp().
In `@Projects/Features/MainFeature/Sources/VC/ServiceNoticeViewController.swift`:
- Around line 32-44: Replace the hardcoded UIColor.init(hexCode: "#000000",
alpha: 0.7) in ServiceNoticeViewController.setStyle with the DSKit asset
equivalent to keep styling consistent; use the DSKitAsset.Colors entry (for
example DSKitAsset.Colors.black900.color) and apply the alpha via
withAlphaComponent(0.7) before assigning to view.backgroundColor so the
background uses the shared design token instead of a literal hex string.
- Around line 86-111: In setLayout(), tie the confirmButton's bottom to the
containerView so the container won't cut off content: update
confirmButton.snp.makeConstraints to include
$0.bottom.equalToSuperview().inset(28.adjustedH) (or an appropriate inset), and
remove or relax the fixed containerView height
(containerView.snp.makeConstraints: delete height.equalTo(318.adjustedH) or
change to height.greaterThanOrEqualTo(318.adjustedH)) so containerView can size
to its content; ensure these changes reference containerView, setLayout(), and
confirmButton so the constraints are adjusted in the same method.
In `@Projects/Features/MyTravelFeature/Project.swift`:
- Around line 9-11: The imports at the top of Project.swift are not
alphabetically sorted, triggering SwiftLint's sorted_imports rule; reorder the
two import statements so they follow alphabetical order (import DependencyPlugin
before import ProjectDescriptionHelpers) to satisfy the lint rule and keep
imports consistently sorted.
In `@Projects/Features/MyTravelFeature/Sources/MyTravelInteractor.swift`:
- Line 79: Replace the use of print in MyTravelInteractor (the print("Log:
MyTripInfo 로드 실패 - \(error)") call) with the project's structured logger (e.g.,
an os.Logger instance or the shared logging utility) so logs can be filtered and
configured; update the MyTravelInteractor to inject or obtain a Logger (or use
the existing logging utility), and change the print call to a structured log
call that includes the error as context (error: or metadata) and a clear message
about MyTripInfo load failure.
- Around line 127-129: The viewWillAppear() currently calls fetchMyTravelData()
on every appearance causing redundant network requests; modify
MyTravelInteractor to add a cachedData property and a lastFetchDate (or similar)
and change viewWillAppear() to check the cache/lastFetchDate before calling
fetchMyTravelData(), e.g., only call fetchMyTravelData() if cachedData is nil or
lastFetchDate is older than a defined TTL (or add a forceRefresh flag to
fetchMyTravelData()); ensure fetchMyTravelData() updates cachedData and
lastFetchDate after a successful load and provide a way to invalidate the cache
when necessary.
- Around line 51-53: 현재 MyTravelInteractor.swift의 override func
didBecomeActive()는 오직 super.didBecomeActive()만 호출하므로 SwiftLint의
unneeded_override 경고 대상입니다; MyTravelInteractor 클래스에서 불필요한 오버라이드를 제거하여
didBecomeActive() 메서드를 삭제하면 해결됩니다 (즉, 해당 override 블록을 제거하여 기본 구현인
super.didBecomeActive()가 호출되도록 함).
- Around line 62-119: fetchMyTravelData uses a single early check of
Task.isCancelled but does not re-check cancellation between await points, so
cancelled tasks can still continue and update the UI; modify the Task body
(associated with fetchDataTask) to call try Task.checkCancellation() (or guard
!Task.isCancelled else { return }) immediately after each major await (after
usecase.fetchMyTripInfo(), after usecase.fetchUpcomingList(), after
fetchRecommendTripList() if called, and before MainActor.run) so work bails out
promptly and no UI updates occur for cancelled tasks; keep the existing weak
self capture and use the same presenter update path when not cancelled.
- Line 41: The declared BehaviorRelay myTravelRelay in MyTravelInteractor.swift
is never used; either remove the unused property or wire it up: if not needed
delete the private let myTravelRelay = BehaviorRelay<[(MyTravelSectionKind,
[MyTravelItem])]>(value: []) to remove dead code, otherwise update the
interactor methods that build/receive travel data to .accept(...) new values
into myTravelRelay, expose it as an observable via myTravelRelay.asObservable()
or a read-only Relay for callers, and ensure consumers subscribe to that
observable (references: myTravelRelay, MyTravelSectionKind, MyTravelItem,
MyTravelInteractor).
In `@Projects/Features/MyTravelFeature/Sources/MyTravelViewController.swift`:
- Around line 143-155: In update(with sections:) change the use of
self.dataSource to the locally bound dataSource from the guard; inside the
DispatchQueue.main.async block use the dataSource variable (bound in guard let
dataSource = self.dataSource) when calling apply(snapshot,
animatingDifferences:) to avoid re-accessing self.dataSource and ensure the
safe, consistent use of the guarded reference.
- Around line 66-73: collectionView 설정 블록에서 contentInset이 두 번 설정되어 .zero가 바로
덮어씌워지므로 중복을 제거하세요: locate the collectionView.do closure and remove the line
setting $0.contentInset = .zero, leaving only the intended $0.contentInset =
.init(top: 21.adjustedH, left: 0, bottom: 0, right: 0) so the final layout uses
the adjusted top inset.
- Around line 111-139: The setCollectionView() function currently manually
registers cells and a supplementary view even though you use
UICollectionView.CellRegistration/SupplementaryRegistration with
dequeueConfiguredReusableCell and dequeueConfiguredReusableSupplementary in
setDataSource() and configureSupplementaryView(); remove the manual register
calls inside setCollectionView() for MyTravelBannerCell, EmptyUpcomingCell,
UpcomingCell, RecommendInfoCell and MyTravelHeaderView so the modern
registration API handles registration automatically, leaving any other
non-registration setup in setCollectionView() intact.
In
`@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/CompositionalLayout/MyTravelCompositionalLayout.swift`:
- Line 15: 클로저의 미사용 파라미터 `environment` 때문에 SwiftLint의 unused_closure_parameter
경고가 발생하므로 UICollectionViewCompositionalLayout 생성 클로저 시그니처에서 `environment`를 `_`로
교체하세요 (즉, the closure passed to UICollectionViewCompositionalLayout that
currently reads `{ [weak self] sectionIndex, environment in ... }` should become
`{ [weak self] sectionIndex, _ in ... }`) so the unused parameter is ignored
while preserving `sectionIndex` and the `[weak self]` capture.
In
`@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/Registration/MyTravelRegistraion.swift`:
- Around line 24-47: Replace the unused closure parameter indexPath with _ in
each CellRegistration closure to satisfy SwiftLint's unused_closure_parameter
rule: update the closures in createBannerCellRegistration,
createRecommedTripCellRegistration, and createUpcomingCell so their registration
blocks use the signature (cell, _, item) instead of (cell, indexPath, item); no
logic changes needed inside the closures.
In
`@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/SectionKind/MyTravelSectionKind.swift`:
- Around line 16-24: The headerTitle computed property currently returns
hardcoded Korean strings; update it to use NSLocalizedString (or a String
extension/localization helper) instead of literals so UI text is localized:
replace the direct returns in headerTitle (for cases .upcomingTrips and
.recommendedTrip) with NSLocalizedString calls using clear keys (e.g.
"section.upcoming_trips.title", "section.recommended_trip.title") and provide
those keys in Localizable.strings; keep the same enum/case names and behavior
(headerTitle) and ensure the default still returns an empty localized string if
needed.
In `@Projects/Features/SettingFeature/Sources/SettingViewController.swift`:
- Around line 142-146: Remove the commented-out conditional that checks `menu ==
.notification` and the alternate `cell.separatorInset` assignment in
SettingViewController.swift; keep only the active line setting
`cell.separatorInset = UIEdgeInsets(top: 0, left: 25.adjusted, bottom: 0, right:
24.adjusted)` and delete the commented block referencing `.notification` so the
codebase is cleaner (the removed logic can be recovered from Git history if
needed).
In `@Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift`:
- Line 96: Add the missing section comment by inserting a "// MARK: -
MyTravelListener" comment immediately above the TabBarInteractor extension
declaration for MyTravelListener (extension TabBarInteractor: MyTravelListener)
so it matches the existing MARK comments used for HomeListener and
TravelToolListener and keeps the file's sectioning consistent.
In `@Projects/Modules/DSKit/Sources/Component/RecommendInfoCell.swift`:
- Around line 58-75: The
configure(title:thumbnailUrl:countryCode:creator:city:schedule:) method on
RecommendInfoCell exceeds the SwiftLint 5-parameter rule; refactor by
introducing a single configuration struct (e.g., RecommendInfo or
RecommendInfoViewModel) that contains title, thumbnailUrl, countryCode, creator,
city, and schedule, update the Configure signature to accept that struct (e.g.,
configure(with: RecommendInfo)), and update all call sites to build and pass the
struct; ensure internal usage maps struct properties to
thumbnailView.kf.setImage, nationalFlagLabel, nationLabel, titleLabel,
nameLabel, cityLabel and scheduleLabel as before.
- Around line 78-122: The setStyle() function uses hardcoded geometry; replace
the literal values with the project scaling extensions: change
thumbnailView.layer.cornerRadius = 8 to use a scaled value (e.g. 8.adjusted),
and change the stack view spacing assignments (nationStackView.spacing,
subInfoStackView.spacing, infoStackView.spacing) from 4 to scaled values (e.g.
4.adjusted or 4.adjustedH depending on whether you want width- or height-based
scaling), keeping use of .adjustedH where vertical scaling is intended; update
these in the RecommendInfoCell.setStyle() method referencing thumbnailView,
nationStackView, subInfoStackView, and infoStackView.
| func toDate() -> Date? { | ||
| let formatter = DateFormatter() | ||
| formatter.locale = Locale(identifier: "en_US_POSIX") | ||
| formatter.dateFormat = "yyyy-MM-dd" | ||
| return formatter.date(from: self) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the toDate() implementation in String+.swift
find . -name "String+.swift" -type f | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 135
🏁 Script executed:
# Search for all usages of toDate()
rg -n "\.toDate\(\)" --type swift -B 2 -A 2Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1299
🏁 Script executed:
# Also check the Date+.swift file for comparison
find . -name "Date+.swift" -type f | head -20Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 133
🏁 Script executed:
# Search for UserTravelTransform to understand the context
rg -n "UserTravelTransform" --type swift -A 5 -B 5Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 625
🏁 Script executed:
# Read the String+.swift file to see the toDate() implementation
cat -n ./Projects/Core/Sources/Extensions/Foundation+/String+.swift | head -50Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1427
🏁 Script executed:
# Read the Date+.swift file to see the caching approach
cat -n ./Projects/Core/Sources/Extensions/Foundation+/Date+.swift | head -80Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 583
🏁 Script executed:
# Check the UserTravelTransform full context to understand the threading model
cat -n ./Projects/Data/Sources/Transform/UserTravelTransform.swift | head -80Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 2441
🏁 Script executed:
# Search for all DateFormatter usage in Date+.swift and other Date extensions
rg -n "DateFormatter" --type swift -B 2 -A 2 Projects/Core/Sources/Extensions/Foundation+/Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 1060
🏁 Script executed:
# Check if there are other Date extensions or if DateFormatter is cached elsewhere
rg -n "static.*DateFormatter\|DateFormatter.*static" --type swiftRepository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 57
🏁 Script executed:
# Look at where UpcomingResponse and UpcomingListResponse are used to understand the threading context
rg -n "\.toDomain\(\)" --type swift -B 3 -A 3Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 14607
🏁 Script executed:
# Double-check by examining the full context of how these formatters are used
rg -n "DateFormatter" --type swift -B 1 -A 3Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 4249
String.toDate() 메서드의 성능 최적화 고려
DateFormatter는 초기화 비용이 크므로 매 호출마다 새로 생성하는 것은 성능상 비효율적입니다. 다만, DateFormatter는 스레드 안전하지 않으므로 static으로 캐싱할 경우 UserTravelTransform처럼 비동기 작업 중 호출되는 시나리오에서는 동기화 문제가 발생할 수 있습니다.
메인 스레드에서만 호출된다면 캐싱을 적용하고, 백그라운드 스레드에서도 호출될 가능성이 있다면 현재 방식을 유지하거나 스레드 로컬 저장소(thread-local storage) 또는 직렬 큐로 보호하는 것을 검토해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Core/Sources/Extensions/Foundation`+/String+.swift around lines 35 -
40, The String.toDate() implementation creates a new DateFormatter on every call
which is expensive; update it to avoid per-call allocations: if toDate() is only
ever invoked on the main thread, replace the local formatter with a cached
static DateFormatter used on the main thread (e.g., a lazily-initialized static
property referenced from toDate()); if toDate() can be called from background
threads (e.g., from UserTravelTransform), keep the current per-call creation or
protect a shared formatter via thread-local storage or a serial queue/lock, or
use a thread-safe alternative like ISO8601DateFormatter where appropriate;
modify the String.toDate() method accordingly to use one of these safe caching
strategies.
|
|
||
| public protocol UserTravelRepositoryInterface { | ||
| func createUserTravel(request: CreateTravelRequest) async throws -> CreateTravelResponse | ||
| // func fetchContentCard(id: Int) async throws -> |
There was a problem hiding this comment.
불완전한 주석 처리된 코드 제거 필요
반환 타입이 없는 미완성 선언입니다. 완성할 계획이 없다면 삭제하세요.
🗑️ 제거 제안
-// func fetchContentCard(id: Int) async throws ->📝 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 fetchContentCard(id: Int) async throws -> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Domain/Sources/Interface/UserTravel/UserTravelRepositoryInterface.swift`
at line 13, 제거할 미완성 주석 선언 func fetchContentCard(id: Int) async throws -> 는 반환
타입이 없어 불완전하므로 유지할 계획이 없다면 해당 주석(미완성 선언)을 삭제하세요; 만약 추후 구현할 계획이면 완전한 시그니처(예: func
fetchContentCard(id: Int) async throws -> ContentCard) 또는 적절한 placeholder 반환 타입을
명시하고 구현 또는 TODO 주석을 추가해 주세요 — 참조 심볼: fetchContentCard(id: Int) async throws ->.
| noticeVC.termsHandler = { | ||
| URLHelper.openURL("https://repeated-tapir-33f.notion.site/2c8cbdc5a3838070a8d8ccdcd0631c9a?source=copy_link") | ||
| } |
There was a problem hiding this comment.
Notion URL의 source=copy_link 파라미터 및 URL 상수화 권장
- URL의
?source=copy_link는 Notion에서 링크 복사 시 자동으로 붙는 트래킹 파라미터로, 브라우저 주소에서 그대로 복사된 것으로 보입니다. 의도된 파라미터가 아니라면 제거하세요. - 하드코딩된 URL은 상수 파일로 분리하면 향후 변경 및 관리가 용이합니다.
🔧 수정 제안
- noticeVC.termsHandler = {
- URLHelper.openURL("https://repeated-tapir-33f.notion.site/2c8cbdc5a3838070a8d8ccdcd0631c9a?source=copy_link")
- }
+ noticeVC.termsHandler = {
+ URLHelper.openURL("https://repeated-tapir-33f.notion.site/2c8cbdc5a3838070a8d8ccdcd0631c9a")
+ }📝 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.
| noticeVC.termsHandler = { | |
| URLHelper.openURL("https://repeated-tapir-33f.notion.site/2c8cbdc5a3838070a8d8ccdcd0631c9a?source=copy_link") | |
| } | |
| noticeVC.termsHandler = { | |
| URLHelper.openURL("https://repeated-tapir-33f.notion.site/2c8cbdc5a3838070a8d8ccdcd0631c9a") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Features/MainFeature/Sources/MainViewController.swift` around lines
59 - 61, The Notion link assigned in noticeVC.termsHandler uses an unnecessary
tracking query parameter and is hard-coded; remove the trailing
"?source=copy_link" from the URL and replace the literal string with a
centralized constant (e.g., TermsURL or NotionTermsURL) defined in your
constants/config file, then update the handler to call URLHelper.openURL with
that constant (referencing noticeVC.termsHandler and URLHelper.openURL to locate
the change).
| extension UpcomingInfo { | ||
| func toMyTravelModel() -> MyTravelPresentationModel.Upcoming { | ||
| let calendar = Calendar.current | ||
| let startOfToday = calendar.startOfDay(for: Date()) | ||
| let startOfTravel = calendar.startOfDay(for: self.startDate) | ||
| let dDayValue = calendar.dateComponents([.day], from: startOfToday, to: startOfTravel).day ?? 0 | ||
|
|
||
| return MyTravelPresentationModel.Upcoming( | ||
| id: self.id, | ||
| title: self.title, | ||
| profileImage: self.thumbnail ?? "", | ||
| dDay: dDayValue, | ||
| duration: "\(self.startDate.toKoreanMMdd())~\(self.endDate.toKoreanMMdd())" | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
D-day 계산 로직이 MyTravelBannerCell.configure에도 중복 존재합니다.
이 프레젠테이션 모델이 D-day 계산의 올바른 위치입니다. MyTravelBannerCell.swift(Line 46-63)에서도 동일한 Calendar/dateComponents 로직으로 D-day를 계산하고 있으므로, 배너 셀은 이미 계산된 값을 받도록 통합하면 중복을 제거할 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/MyTravelFeature/Sources/Models/MyTravelPresentationModel.swift`
around lines 123 - 137, UpcomingInfo.toMyTravelModel currently computes dDay and
MyTravelBannerCell.configure duplicates that logic; remove the duplication by
having the cell accept the precomputed dDay from the presentation model. Update
UpcomingInfo.toMyTravelModel to continue providing dDay (it already does) and
modify MyTravelBannerCell.configure to stop recomputing dDay and instead use the
dDay property from MyTravelPresentationModel.Upcoming (refer to the
UpcomingInfo.toMyTravelModel, MyTravelPresentationModel.Upcoming, and
MyTravelBannerCell.configure symbols) so all D-day computation lives in the
presentation model.
| switch item { | ||
| case .banner(let banner): | ||
| if banner.id == 0 { | ||
| return collectionView.dequeueConfiguredReusableCell(using: emptyReg, for: indexPath, item: banner) | ||
| } | ||
| return collectionView.dequeueConfiguredReusableCell(using: bannerReg, for: indexPath, item: banner) |
There was a problem hiding this comment.
빈 상태 판별에 매직 넘버 0을 사용하고 있습니다.
banner.id == 0으로 빈 상태를 판별하는 것은 취약합니다. 실제 서버 데이터에서 id가 0인 배너가 있을 경우 의도치 않게 빈 상태 셀이 표시됩니다. 별도의 MyTravelItem case(예: .emptyBanner)를 추가하거나, Presentation Model에 명시적인 empty 상태 플래그를 두는 것이 더 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Features/MyTravelFeature/Sources/MyTravelViewController.swift`
around lines 193 - 198, The code uses a magic number check (banner.id == 0)
inside the switch over MyTravelItem (case .banner(let banner)) to decide between
emptyReg and bannerReg; replace this fragile check by modeling empty state
explicitly—either add a new MyTravelItem case (e.g., .emptyBanner) and switch on
that, or add an explicit flag/property on the banner presentation model (e.g.,
banner.isEmpty) and use that instead—then update the switch in
MyTravelViewController (the case .banner branch and any callers that construct
items) to dequeueConfiguredReusableCell using emptyReg only for the explicit
empty case/flag and bannerReg for normal banners, leaving
collectionView.dequeueConfiguredReusableCell calls and emptyReg/bannerReg usage
unchanged.
| } | ||
| } | ||
|
|
||
| func createRecommedTripCellRegistration() -> UICollectionView.CellRegistration<RecommendInfoCell, MyTravelPresentationModel.RecommendedTrip> { |
There was a problem hiding this comment.
메서드명 오타: createRecommedTripCellRegistration → createRecommendTripCellRegistration.
"Recommend"의 철자가 잘못되었습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/Registration/MyTravelRegistraion.swift`
at line 30, Rename the misspelled function createRecommedTripCellRegistration to
createRecommendTripCellRegistration and update all references/usages
accordingly; specifically rename the declaration of
createRecommedTripCellRegistration() -> createRecommendTripCellRegistration()
and update any call sites or tests that reference it (including places using
RecommendInfoCell and MyTravelPresentationModel.RecommendedTrip) so the new
symbol name is used consistently across the codebase.
| enum MyTravelSectionKind: Hashable { | ||
| case banner | ||
| case upcomingTrips(isEmpty: Bool) | ||
| case recommendedTrip |
There was a problem hiding this comment.
upcomingTrips(isEmpty: Bool) 섹션 식별자 설계 오류 — 섹션 전체가 삭제·재삽입됨
DiffableDataSource는 모든 식별자의 해시값을 사용해 변경 사항을 판별합니다. Swift가 연관 값이 있는 열거형에 대해 Hashable을 자동 합성하므로, upcomingTrips(isEmpty: true)와 upcomingTrips(isEmpty: false)는 서로 다른 해시값을 가집니다.
결과적으로 빈 상태(예: 데이터 로드 전 isEmpty: true)에서 데이터가 채워지는 시점(isEmpty: false)으로 스냅샷이 전환될 때, DiffableDataSource는 이전 스냅샷과 새 스냅샷의 차이를 계산하며, 이 변경 사항(삽입·삭제 등)을 애니메이션으로 처리합니다. 즉, 섹션 내 아이템만 교체되는 것이 아니라 섹션 자체가 삭제되고 새 섹션이 삽입되어 의도치 않은 애니메이션 또는 레이아웃 깨짐이 발생합니다.
섹션의 빈 상태는 섹션 식별자에서 분리하고, 아이템 타입으로 표현하는 방식을 권장합니다:
🔧 제안된 수정
enum MyTravelSectionKind: Hashable {
case banner
- case upcomingTrips(isEmpty: Bool)
+ case upcomingTrips
case recommendedTrip
}빈 상태는 MyTravelItem에 별도 케이스(예: .emptyUpcomingTrip)를 추가하고, 인터랙터에서 아이템 배열이 비어 있을 때 해당 아이템을 섹션에 포함시키는 방식으로 처리하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Projects/Features/MyTravelFeature/Sources/Views/CollectionView/SectionKind/MyTravelSectionKind.swift`
around lines 11 - 14, 현재 MyTravelSectionKind의 upcomingTrips(isEmpty: Bool) 연관값
때문에 섹션 식별자가 상태 변경시 달라져서 섹션 전체가 삭제·재삽입됩니다; 수정 방법은 MyTravelSectionKind의
upcomingTrips 케이스에서 연관값을 제거(예: case upcomingTrips)하고 빈 상태 표현을 섹션 식별자에서 제거한 뒤,
MyTravelItem에 빈 상태용 케이스(예: .emptyUpcomingTrip)를 추가하여 인터랙터(또는 해당 데이터를 만드는 메서드)가
아이템 배열이 비어있을 때 해당 빈 아이템을 반환하도록 변경하세요; 스냅샷 생성 로직을 업데이트해 이제 upcomingTrips 섹션에는 실제
아이템 또는 .emptyUpcomingTrip이 포함되도록 하고, MyTravelSectionKind 및 스냅샷 관련 코드에서 더 이상
isEmpty 연관값을 사용하지 않도록 정리하세요.
| extension TabBarInteractor: MyTravelListener { | ||
| func myTraveDidTapFollowDetail(with recommendationId: Int) { | ||
| listener?.routeToFollow(with: recommendationId) | ||
| } | ||
|
|
||
| func myTraveDidTapSearch() { | ||
| listener?.routeToSearch() | ||
| } | ||
|
|
||
| func myTraveDidTapPopularTravel() { | ||
| listener?.routeToPopularTravel() | ||
| } | ||
| } |
There was a problem hiding this comment.
myTravelListener 메서드명 오타 — myTraveDidTap... → myTravelDidTap...
세 메서드 모두 "Travel"에서 l이 누락된 채로 정의되어 있습니다. 이 오타는 MyTravelListener 프로토콜 정의에서 기인한 것으로, 현재 PR에서 수정되어야 합니다.
- func myTraveDidTapFollowDetail(with recommendationId: Int) {
+ func myTravelDidTapFollowDetail(with recommendationId: Int) {
listener?.routeToFollow(with: recommendationId)
}
- func myTraveDidTapSearch() {
+ func myTravelDidTapSearch() {
listener?.routeToSearch()
}
- func myTraveDidTapPopularTravel() {
+ func myTravelDidTapPopularTravel() {
listener?.routeToPopularTravel()
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Features/TabBarFeature/Sources/TabBarInteractor.swift` around lines
96 - 108, The three methods in TabBarInteractor that implement MyTravelListener
are misspelled (missing the "l"): update the method names
myTraveDidTapFollowDetail, myTraveDidTapSearch, and myTraveDidTapPopularTravel
to myTravelDidTapFollowDetail, myTravelDidTapSearch, and
myTravelDidTapPopularTravel respectively so they match the MyTravelListener
protocol, and ensure any references like listener?.routeToFollow,
listener?.routeToSearch, and listener?.routeToPopularTravel remain unchanged;
also run a compile to find and fix any other callsites or protocol declarations
that need the same rename.
| public override func prepareForReuse() { | ||
| disposeBag = DisposeBag() | ||
| } |
There was a problem hiding this comment.
super.prepareForReuse() 호출 누락 및 modifier 순서 오류.
prepareForReuse()에서 super.prepareForReuse()를 호출하지 않고 있습니다. UIKit 문서에 따르면 반드시 super를 호출해야 합니다. 또한 SwiftLint에서 지적한 대로 override modifier가 public 앞에 와야 합니다.
🐛 수정 제안
- public override func prepareForReuse() {
+ override public func prepareForReuse() {
+ super.prepareForReuse()
disposeBag = DisposeBag()
}🧰 Tools
🪛 SwiftLint (0.63.2)
[Warning] 45-45: override modifier should come before public
(modifier_order)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Modules/DSKit/Sources/Component/EmptyUpcomingCell.swift` around
lines 45 - 47, In EmptyUpcomingCell, change the prepareForReuse declaration to
put the override modifier before the access level (use "override public func
prepareForReuse()") and inside the method call super.prepareForReuse() as the
first statement, then reset the disposeBag (disposeBag = DisposeBag()) so the
cell is properly reset and adheres to SwiftLint modifier order.
| case .getUpcomingList: | ||
| return "api/v1/travels/upcoming/list" |
There was a problem hiding this comment.
getUpcomingList 경로에 선행 /가 누락되어 API 호출이 실패합니다.
다른 모든 경로(/api/v1/travels, /api/v1/travels/upcoming 등)는 /로 시작하지만, Line 34의 getUpcomingList는 "api/v1/..." 으로 /가 빠져 있습니다. Moya의 baseURL + path 결합 시 잘못된 URL이 생성됩니다.
🐛 수정 제안
case .getUpcomingList:
- return "api/v1/travels/upcoming/list"
+ return "/api/v1/travels/upcoming/list"📝 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.
| case .getUpcomingList: | |
| return "api/v1/travels/upcoming/list" | |
| case .getUpcomingList: | |
| return "/api/v1/travels/upcoming/list" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Projects/Modules/Networks/Sources/TargetType/UserTravelAPI.swift` around
lines 33 - 34, The enum case .getUpcomingList returns a path missing the leading
'/', causing incorrect URL composition; update the path returned for
.getUpcomingList to include a leading slash (i.e.,
"/api/v1/travels/upcoming/list") so it matches the other cases and composes
correctly with Moya's baseURL; verify in the TargetType implementation that
other paths follow the same leading-slash pattern for consistency.
🔗 연결된 이슈
📄 작업 내용
💻 주요 코드 설명
UserManager.swiftServiceNoticeViewController.swiftSummary by CodeRabbit
출시 노트
새로운 기능
버그 수정