Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
57a4e79
del: #33 - 레거시 코드 삭제
ChoiAnYong Feb 21, 2026
e915b24
chore: #33 - MyTravel 모듈 생성
ChoiAnYong Feb 21, 2026
32a8a91
feat: #33 - upcomingList api 연결 및 MyPravelUsecase 구현
ChoiAnYong Feb 23, 2026
32b9db0
chore: #33 - DSKit 컴포넌트 이동
ChoiAnYong Feb 23, 2026
c4a8b5c
design: #33 - EmptyUpcomingCell 구현
ChoiAnYong Feb 23, 2026
2250ca2
chore: #33 - , 누락 수정
ChoiAnYong Feb 23, 2026
0ff2d8d
chore: #33 - 공통 유틸 함수 Core 모듈 이동
ChoiAnYong Feb 23, 2026
38435c3
feat: #33 - MyTravel CompositionalLayout 세팅
ChoiAnYong Feb 23, 2026
9f8545f
design: #33 - MyTravelBannerCell 구현
ChoiAnYong Feb 23, 2026
32ab446
design: #33 0 - UpcomingCell 구현
ChoiAnYong Feb 23, 2026
2c8fdf4
design: #33 - MyTravelHeaderView 구현
ChoiAnYong Feb 23, 2026
954ee2a
feat: #33 - MyTravelPresentationModel 구현
ChoiAnYong Feb 23, 2026
30bf109
feat: #33 - MyTravelFeature 연결
ChoiAnYong Feb 23, 2026
06ed7c6
feat: #33 - MyTravelUsecase 의존성 주입
ChoiAnYong Feb 23, 2026
8a27b75
feat: #33 - 애니메이션 추가
ChoiAnYong Feb 23, 2026
aab8670
fix: #33 - 이미지 변경
ChoiAnYong Feb 23, 2026
29ae329
refactor: #33 - 공용 컴포넌트 전환에 따른 매개변수 변경
ChoiAnYong Feb 23, 2026
fe9cc18
del: #33 - Core 모듈 이전에 따른 삭제
ChoiAnYong Feb 23, 2026
b489aa6
fix: #33 - 데이터 통신 생명주기 변경
ChoiAnYong Feb 23, 2026
b5e3ac6
feat: #33 - MyTravel RIBs 구현
ChoiAnYong Feb 23, 2026
882edea
feat: #33 - UserManager 구현
ChoiAnYong Feb 23, 2026
0e66c9a
design: #33 - padding 변경
ChoiAnYong Feb 23, 2026
889ad9d
feat: #33 - 서비스 이용약관 구현
ChoiAnYong Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ public extension TargetDependency {
public struct Home {}
public struct TabBar {}
public struct Follow {}
public struct Travel {}
public struct Search {}
public struct Setting {}
public struct PopularTravel {}
public struct MyTravel {}
public struct TravelTool {}
}

Expand Down Expand Up @@ -63,12 +63,6 @@ public extension TargetDependency.Features.Follow {
static let feature = TargetDependency.Features.project(name: "Feature", group: group)
}

public extension TargetDependency.Features.Travel {
static let group = "Travel"

static let feature = TargetDependency.Features.project(name: "Feature", group: group)
}

public extension TargetDependency.Features.Search {
static let group = "Search"

Expand All @@ -93,6 +87,12 @@ public extension TargetDependency.Features.PopularTravel {
static let feature = TargetDependency.Features.project(name: "Feature", group: group)
}

public extension TargetDependency.Features.MyTravel {
static let group = "MyTravel"

static let feature = TargetDependency.Features.project(name: "Feature", group: group)
}

public extension TargetDependency.Features.TravelTool {
static let group = "TravelTool"

Expand Down
9 changes: 9 additions & 0 deletions Projects/App/Sources/Application/AppComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ final class AppComponent: Component<EmptyDependency>, RootDependency {
TemplatesSearchUsecase(travelTemplateRepository: travelTemplateRepository)
}
}

var myTravelUsecase: MyTravelUsecaseProtocol {
shared {
MyTravelUsecase(
travelTemplateRepository: travelTemplateRepository,
userTravelRepository: userTravelRepository
)
}
}

var authRepository: AuthRepositoryInterface {
shared { makeAuthRepository() }
Expand Down
18 changes: 18 additions & 0 deletions Projects/Core/Sources/Extensions/Foundation+/Date+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Date+.swift
// Core
//
// Created by 최안용 on 2/22/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

public extension Date {
func toKoreanMMdd() -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateFormat = "M월 d일"
return formatter.string(from: self)
}
}
7 changes: 7 additions & 0 deletions Projects/Core/Sources/Extensions/Foundation+/String+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ public extension String {
let locale = Locale(identifier: "ko_KR")
return locale.localizedString(forRegionCode: self) ?? "알 수 없음"
}

func toDate() -> Date? {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
return formatter.date(from: self)
}
Comment on lines +35 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the toDate() implementation in String+.swift
find . -name "String+.swift" -type f | head -20

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

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

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

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

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

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

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

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

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

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

}
31 changes: 31 additions & 0 deletions Projects/Core/Sources/Storage/UserDefaultWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// UserDefaultWrapper.swift
// Core
//
// Created by 최안용 on 2/23/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

@propertyWrapper public struct UserDefaultWrapper<T> {
public var wrappedValue: T? {
get {
return UserDefaults.standard.object(forKey: self.key.rawValue) as? T
}

set {
if newValue == nil {
UserDefaults.standard.removeObject(forKey: self.key.rawValue)
} else {
UserDefaults.standard.setValue(newValue, forKey: self.key.rawValue)
}
}
}

private let key: UserDefaultKeys

public init(key: UserDefaultKeys) {
self.key = key
}
}
33 changes: 33 additions & 0 deletions Projects/Core/Sources/Storage/UserManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// UserManager.swift
// Core
//
// Created by 최안용 on 2/23/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

public enum UserDefaultKeys: String {
case uuid = "uuid"
case nickname = "nickname"
case isFirstOpen = "isFirstOpen"
}

public final class UserManager {
@UserDefaultWrapper(key: .uuid) public var uuid: String?
@UserDefaultWrapper(key: .nickname) public var nickname: String?
@UserDefaultWrapper(key: .isFirstOpen) private var isFirstOpen: Bool?

public static let shared = UserManager()

private init() {}

public func isFirstOpenApp() -> Bool {
guard let isFirstOpen else {
self.isFirstOpen = true
return true
}
return !isFirstOpen
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,12 @@ public final class UserTravelRepository: UserTravelRepositoryInterface {
throw error.toNDGLError()
}
}

public func fetchUpcomingList(page: Int?, size: Int?) async throws -> [UpcomingInfo] {
do {
return try await service.getUpcomingList(page: page, size: size).toDomain()
} catch {
throw error.toNDGLError()
}
}
}
23 changes: 17 additions & 6 deletions Projects/Data/Sources/Transform/UserTravelTransform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,22 @@ extension CreateUserTravelResponse {
}
}

extension String {
func toDate() -> Date? {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
return formatter.date(from: self)
extension UpcomingListResponse {
func toDomain() -> [UpcomingInfo] {
self.content.map {
.init(
id: $0.id,
title: $0.title,
country: $0.country,
city: $0.city,
startDate: $0.startDate.toDate() ?? .now,
endDate: $0.endDate.toDate() ?? .now,
nights: $0.nights,
days: $0.days,
templateId: $0.templateId,
thumbnail: $0.thumbnail,
profileImage: $0.profileImage
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ import Foundation

public protocol UserTravelRepositoryInterface {
func createUserTravel(request: CreateTravelRequest) async throws -> CreateTravelResponse
// func fetchContentCard(id: Int) async throws ->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

불완전한 주석 처리된 코드 제거 필요

반환 타입이 없는 미완성 선언입니다. 완성할 계획이 없다면 삭제하세요.

🗑️ 제거 제안
-//    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.

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

func fetchUpcoming() async throws -> MyTripSummary
func fetchUpcomingList(page: Int?, size: Int?) async throws -> [UpcomingInfo]
}
49 changes: 49 additions & 0 deletions Projects/Domain/Sources/Model/Home/UpcomingInfo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// UpcomingInfo.swift
// Domain
//
// Created by 최안용 on 2/22/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

public struct UpcomingInfo {
public let id: Int
public let title: String
public let country: String
public let city: String
public let startDate: Date
public let endDate: Date
public let nights: Int
public let days: Int
public let templateId: Int
public let thumbnail: String?
public let profileImage: String?

public init(
id: Int,
title: String,
country: String,
city: String,
startDate: Date,
endDate: Date,
nights: Int,
days: Int,
templateId: Int,
thumbnail: String?,
profileImage: String?
) {
self.id = id
self.title = title
self.country = country
self.city = city
self.startDate = startDate
self.endDate = endDate
self.nights = nights
self.days = days
self.templateId = templateId
self.thumbnail = thumbnail
self.profileImage = profileImage
}
}
58 changes: 58 additions & 0 deletions Projects/Domain/Sources/UseCase/MyTravelUsecase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// MyTravelUsecase.swift
// Domain
//
// Created by 최안용 on 2/22/26.
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

public protocol MyTravelUsecaseProtocol {
func fetchMyTripInfo() async throws -> MyTripSummary
func fetchUpcomingList(page: Int?, size: Int?) async throws -> [UpcomingInfo]
func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo]
}

public extension MyTravelUsecaseProtocol {
func fetchUpcomingList(
page: Int? = nil,
size: Int? = nil
) async throws -> [UpcomingInfo] {
try await fetchUpcomingList(page: page, size: size)
}

func fetchRecommendTripList(
page: Int? = nil,
size: Int? = nil
) async throws -> [TripInfo] {
try await fetchRecommendTripList(page: page, size: size)
}
}

public final class MyTravelUsecase {
private let travelTemplateRepository: TravelTemplateRepositoryInterface
private let userTravelRepository: UserTravelRepositoryInterface

public init(
travelTemplateRepository: TravelTemplateRepositoryInterface,
userTravelRepository: UserTravelRepositoryInterface
) {
self.travelTemplateRepository = travelTemplateRepository
self.userTravelRepository = userTravelRepository
}
}

extension MyTravelUsecase: MyTravelUsecaseProtocol {
public func fetchMyTripInfo() async throws -> MyTripSummary {
try await userTravelRepository.fetchUpcoming()
}

public func fetchUpcomingList(page: Int?, size: Int?) async throws -> [UpcomingInfo] {
try await userTravelRepository.fetchUpcomingList(page: page, size: size)
}

public func fetchRecommendTripList(page: Int?, size: Int?) async throws -> [TripInfo] {
try await travelTemplateRepository.fetchRecommendTripList(page: page, size: size)
}
}
6 changes: 2 additions & 4 deletions Projects/Features/HomeFeature/Sources/HomeInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ final class HomeInteractor: PresentableInteractor<HomePresentable>, HomeInteract

override func didBecomeActive() {
super.didBecomeActive()

setupStream()
fetchHomeData()
}

override func willResignActive() {
Expand Down Expand Up @@ -179,7 +176,8 @@ final class HomeInteractor: PresentableInteractor<HomePresentable>, HomeInteract

// MARK: - HomePresentableListener
extension HomeInteractor: HomePresentableListener {
func viewWillAppear() {
func viewDidLoad() {
setupStream()
fetchHomeData()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ protocol HomePresentableListener: AnyObject {
func itemSelected(item: HomeItem)
func moreBtnTapped()
func reloadBtnTapped()
func viewWillAppear()
func viewDidLoad()
}

// MARK: - HomeViewController
Expand Down Expand Up @@ -58,12 +58,12 @@ final class HomeViewController: UIViewController, HomeViewControllable {
setCollectionView()
setDataSource()
bindInteractor()
listener?.viewDidLoad()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
listener?.viewWillAppear()
}
}

Expand Down Expand Up @@ -239,7 +239,7 @@ private extension HomeViewController {
let headerRegistration = createHeaderRegistration()
let popularFooterRegistration = createPopularFooterRegistration()

dataSource?.supplementaryViewProvider = { collectionView, kind, indexPath in
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
guard HomeSectionKind(rawValue: indexPath.section) != nil else {
return UICollectionReusableView()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,3 @@ extension TripInfo {
)
}
}

extension Date {
func toKoreanMMdd() -> String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ko_KR")
formatter.dateFormat = "M월 d일"
return formatter.string(from: self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@ extension HomeViewController {

func createRecommedTripCellRegistration() -> UICollectionView.CellRegistration<RecommendInfoCell, HomePresentationModel.RecommendedTrip> {
return UICollectionView.CellRegistration { cell, indexPath, item in
cell.configure(item)
cell.configure(
title: item.title,
thumbnailUrl: item.thumbnailUrl,
countryCode: item.country,
creator: item.creator,
city: item.city,
schedule: item.schedule
)
}
}

Expand Down
Loading