Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c8c19f2
feat: 탐색 지도에 카테고리별 핀 기능 추가 #9
Roy-wonji Mar 26, 2026
da48bfa
feat: 탐색 지도 스팟 상세 정보 카드 및 컨트롤 UI 추가 #9
Roy-wonji Mar 26, 2026
10c9ef3
feat: 프로필 알림 설정 기능 및 API 연동 구현 #14
Roy-wonji Mar 27, 2026
5fab765
feat: 알림 설정 도메인 로직 및 즐겨찾기 역 관리 개선 #14
Roy-wonji Mar 27, 2026
486ee14
refactor: 프로필 모듈 구조 개선 및 알림 설정 UI 추가 #14
Roy-wonji Mar 27, 2026
c30b4f7
feat: Place API 및 데이터 레이어 구현 #9
Roy-wonji Mar 27, 2026
44407f5
feat: Place 도메인 구현 및 로고 리소스 업데이트 #9
Roy-wonji Mar 27, 2026
691eb36
feat: 탐색 상세 UI 개선 및 장소 도메인 연동 #9
Roy-wonji Mar 27, 2026
8084150
feat: 장소 검색 및 페이지네이션 구현 #9
Roy-wonji Mar 27, 2026
8ed994a
feat: 탐색 UI 및 페이지네이션 로직 개선 #9
Roy-wonji Mar 27, 2026
2748a90
feat: 탐색 리스트 뷰 및 이미지 표시 기능 구현 #9
Roy-wonji Mar 27, 2026
bad788b
refactor: 탐색 페이징 0-base 전환 및 정렬 로직 개선 #9
Roy-wonji Mar 27, 2026
72d12e8
feat: 탐색 지도-리스트 화면 연동 및 위치 트리거 개선 #9
Roy-wonji Mar 27, 2026
d8638fe
feat: 탐색 상세 성능 최적화 및 상태 관리 개선 #9
Roy-wonji Mar 27, 2026
ffb95be
feat: 탐색 API 단순화 및 UI 개선 #9
Roy-wonji Mar 27, 2026
e756e39
feat: 현재위치 복귀 기능 개선 및 중복 auto-fit 방지 #9
Roy-wonji Mar 28, 2026
5a7b8f0
feat: 탐색 UI 개선 및 색상 체계 업데이트 #9
Roy-wonji Mar 28, 2026
3703e68
feat: 스켈레톤 UI 컴포넌트화 및 제목 레이아웃 개선 #9
Roy-wonji Mar 28, 2026
b311bdc
feat: ExploreDetail 화면 추가 및 네비게이션 연결 #15
Roy-wonji Mar 28, 2026
c77111e
refactor: rename lat/lng to userLat/userLon 수정 #14
Roy-wonji Mar 28, 2026
f734b6a
feat: 카메라 및 위치 도메인 레이어 기능 추가 #15
Roy-wonji Mar 28, 2026
ae9158b
feat: 장소 기능을 위한 공용 유틸리티 및 디자인 에셋 추가 #15
Roy-wonji Mar 28, 2026
29d4a5d
refactor: Explore 기능 아키텍처 개선 및 Detail 화면 강화 #15
Roy-wonji Mar 28, 2026
f43a0bf
refactor: Explore UI 레이아웃 최적화 및 성능 개선 #15
Roy-wonji Mar 28, 2026
2df3cc5
feat: Place 도메인 아키텍처 강화 및 상세 기능 추가 #15
Roy-wonji Mar 28, 2026
53ffc97
feat: Explore 상세 화면 지도 연동 및 기능 강화 #15
Roy-wonji Mar 28, 2026
bf57efc
feat: 탐색 기능 성능 최적화 및 UX 개선 #15
Roy-wonji Mar 28, 2026
5d80f0b
feat: 앱 아이콘 업데이트 및 fastlane 배포 환경 구축 #15
Roy-wonji Mar 28, 2026
92b775f
feat: update explore 랑 usecase 로직 개선 #15
Roy-wonji Mar 29, 2026
984683a
refactor: flatten place search API 과련 수정 #15
Roy-wonji Mar 29, 2026
7fceea0
refactor: explore view 상세 및 스켈레톤 작업 #15
Roy-wonji Mar 29, 2026
e7b12ce
feat: Google Places API 이미지 캐싱 최적화 설정 #15
Roy-wonji Mar 29, 2026
0541a8c
feat: Web 모듈 추가 및 프로젝트 구성 #15
Roy-wonji Mar 29, 2026
b1167a9
refactor: ProfileCoordinator 주석 수정 및 Web State Hashable 개선 315
Roy-wonji Mar 29, 2026
d13be3d
fix: 카테고리 탐색 뷰 개선 #15
Roy-wonji Mar 29, 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 @@ -10,6 +10,7 @@ import ProjectDescription
public extension TargetDependency.SPM {
static let asyncMoya = TargetDependency.external(name: "AsyncMoya", condition: .none)
static let composableArchitecture = TargetDependency.external(name: "ComposableArchitecture", condition: .none)
static let kingfisher = TargetDependency.external(name: "Kingfisher", condition: .none)
static let tcaCoordinator = TargetDependency.external(name: "TCACoordinators", condition: .none)
static let weaveDI = TargetDependency.external(name: "WeaveDI", condition: .none)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public extension ModulePath {
case Auth
case OnBoarding
case Profile
case Web


public static let name: String = "Presentation"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"value" : "dark"
}
],
"filename" : "darklogo.png",
"filename" : "darklogo 2.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
41 changes: 41 additions & 0 deletions Projects/App/Sources/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import UIKit
import WeaveDI
import Home
import Kingfisher


class AppDelegate: UIResponder, UIApplicationDelegate {
Expand All @@ -20,6 +21,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
await AppDIManager.shared.registerDefaultDependencies()
}

// Kingfisher 캐시 최적화 설정
configureImageCaching()

// 네이버맵 초기화 (Home 모듈의 NaverMapInitializer 사용)
NaverMapInitializer.initialize()

Expand All @@ -39,4 +43,41 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
didDiscardSceneSessions sceneSessions: Set<UISceneSession>
) {
}

// MARK: - Image Caching Configuration
private func configureImageCaching() {
let cache = ImageCache.default

// 메모리 캐시 설정 - 50MB로 제한
cache.memoryStorage.config.totalCostLimit = 50 * 1024 * 1024

// 디스크 캐시 설정 - 200MB로 제한, 1주일 보관
cache.diskStorage.config.sizeLimit = 200 * 1024 * 1024
cache.diskStorage.config.expiration = .days(7)

// 이미지 다운로드 설정 (Google Places API 최적화)
let modifier = AnyModifier { request in
var r = request
r.setValue("image/webp,image/*,*/*;q=0.8", forHTTPHeaderField: "Accept")
r.setValue("TimeSpot-iOS/1.0", forHTTPHeaderField: "User-Agent")
r.cachePolicy = .useProtocolCachePolicy
// Google Places API 이미지는 응답이 느릴 수 있으므로 타임아웃 증가
r.timeoutInterval = 30.0
return r
}

KingfisherManager.shared.defaultOptions = [
.requestModifier(modifier),
.backgroundDecode,
.diskCacheExpiration(.days(1)), // Google Places 이미지는 1일만 캐시
.memoryCacheExpiration(.seconds(300))
]

// Google Places API를 위한 네트워크 최적화
let config = KingfisherManager.shared.downloader.sessionConfiguration
config.httpMaximumConnectionsPerHost = 6
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 60.0
config.waitsForConnectivity = true
}
}
4 changes: 2 additions & 2 deletions Projects/App/Sources/Di/DiRegister.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ public final class AppDIManager {
.register(HistoryInterface.self) { HistoryRepositoryImpl() }
// MARK: - 역
.register(StationInterface.self) { StationRepositoryImpl() }


// MARK: - 장소
.register(PlaceInterface.self) { PlaceRepositoryImpl() }

.configure()
}
Expand Down
40 changes: 40 additions & 0 deletions Projects/App/Sources/Di/KeychainTokenProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
//

import Foundation
import Security

import DomainInterface
import Foundations

final class KeychainTokenProvider: TokenProviding, @unchecked Sendable {
private enum Constants {
static let cachedAccessTokenKey = "cached_access_token"
}

private let keychainManager: KeychainManagingInterface

init(keychainManager: KeychainManagingInterface) {
Expand All @@ -23,10 +28,25 @@ final class KeychainTokenProvider: TokenProviding, @unchecked Sendable {
return cached
}

if let persistedToken = UserDefaults.standard.string(forKey: Constants.cachedAccessTokenKey),
!persistedToken.isEmpty {
TokenCache.shared.token = persistedToken
return persistedToken
}

if let keychainToken = readAccessTokenFromKeychain(), !keychainToken.isEmpty {
TokenCache.shared.token = keychainToken
UserDefaults.standard.set(keychainToken, forKey: Constants.cachedAccessTokenKey)
return keychainToken
}

// 캐시가 없으면 비동기적으로 로드
Task {
let token = await keychainManager.accessToken()
TokenCache.shared.token = token
if let token, !token.isEmpty {
UserDefaults.standard.set(token, forKey: Constants.cachedAccessTokenKey)
}
}

// 현재는 캐시된 값 또는 nil 반환
Expand All @@ -36,6 +56,7 @@ final class KeychainTokenProvider: TokenProviding, @unchecked Sendable {
func saveAccessToken(_ token: String) {
// 캐시 업데이트
TokenCache.shared.token = token
UserDefaults.standard.set(token, forKey: Constants.cachedAccessTokenKey)

// 백그라운드에서 비동기적으로 저장
Task {
Expand All @@ -45,9 +66,28 @@ final class KeychainTokenProvider: TokenProviding, @unchecked Sendable {
print("Failed to save access token: \(error)")
// 저장 실패 시 캐시도 초기화
TokenCache.shared.token = nil
UserDefaults.standard.removeObject(forKey: Constants.cachedAccessTokenKey)
}
}
}

private func readAccessTokenFromKeychain() -> String? {
let service = Bundle.main.bundleIdentifier ?? "com.nomadspot.app"
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrService: service,
kSecAttrAccount: "ACCESS_TOKEN",
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]

var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else {
return nil
}
return String(data: data, encoding: .utf8)
}
}

// Thread-safe 토큰 캐시
Expand Down
25 changes: 25 additions & 0 deletions Projects/Data/API/Sources/API/Place/PlaceAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// PlaceAPI.swift
// API
//
// Created by Wonji Suh on 3/27/26.
//

import Foundation

public enum PlaceAPI: String, CaseIterable {
case fetchPlace
case searchPlace
case detailPlace

public var description: String {
switch self {
case .fetchPlace:
return ""
case .searchPlace:
return "/search"
case .detailPlace:
return "/detail"
}
}
}
8 changes: 7 additions & 1 deletion Projects/Data/API/Sources/API/Profile/ProfileAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,20 @@ import Foundation
public enum ProfileAPI: String, CaseIterable {
case user
case editUser
case fetchNotification
case editNotification

public var description : String {
switch self {
case .user:
return ""

case .editUser:
return ""
case .fetchNotification:
return "/notification-settings"

case .editNotification:
return "/notification-settings"
}
}
}
12 changes: 6 additions & 6 deletions Projects/Data/API/Sources/API/Station/StationAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import Foundation

public enum StationAPI {
case allStation
case addFavoriteStation
case deleteFavoriteStation(deleteStationId: Int)
case addFavoriteStation(stationID: Int)
case deleteFavoriteStation(stationID: Int)

public var description: String {
switch self {
case .allStation:
return ""
case .addFavoriteStation:
return "/favorites"
case .deleteFavoriteStation(let deleteStationId):
return "/favorites/\(deleteStationId)"
case .addFavoriteStation(let stationID):
return "/favorites/\(stationID)"
case .deleteFavoriteStation(let stationID):
return "/favorites/\(stationID)"
}
}
}
116 changes: 116 additions & 0 deletions Projects/Data/Model/Sources/Place/DTO/PlaceDTOModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
//
// PlaceDTOModel.swift
// Model
//
// Created by Wonji Suh on 3/27/26.
//

import Foundation


public typealias PlaceDTOModel = BaseResponseDTO<[PlaceResponseDTOModel]>
public typealias PlaceSearchDTOModel = BaseResponseDTO<PlaceSearchPageResponseDTO>

// MARK: - Datum
public struct PlaceResponseDTOModel: Decodable, Equatable {
public let placeID: Int
public let category: String
public let lat: Double
public let lon: Double
public let name: String?
public let address: String?
public let imageURL: String?
public let stayableMinutes: Int?
public let isOpen: Bool?
public let closingTime: String?

enum CodingKeys: String, CodingKey {
case placeID = "placeId"
case category, lat, lon, name, address, imageURL = "imageUrl", stayableMinutes, isOpen, closingTime
}
}

public struct PlaceSearchPageResponseDTO: Decodable, Equatable {
public let content: [PlaceResponseDTOModel]
public let number: Int
public let size: Int
public let hasNext: Bool

enum CodingKeys: String, CodingKey {
case content, number, size, hasNext
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.content = try container.decodeIfPresent([PlaceResponseDTOModel].self, forKey: .content) ?? []
self.number = try container.decodeIfPresent(Int.self, forKey: .number) ?? 0
self.size = try container.decodeIfPresent(Int.self, forKey: .size) ?? 10
self.hasNext = try container.decodeIfPresent(Bool.self, forKey: .hasNext) ?? false
}
}

public struct PlacePageableResponseDTO: Decodable, Equatable {
public let unpaged: Bool
public let paged: Bool
public let pageNumber: Int
public let pageSize: Int
public let offset: Int
public let sort: PlaceSortResponseDTO

enum CodingKeys: String, CodingKey {
case unpaged, paged, pageNumber, pageSize, offset, sort
}

public init(
unpaged: Bool = false,
paged: Bool = true,
pageNumber: Int = 0,
pageSize: Int = 10,
offset: Int = 0,
sort: PlaceSortResponseDTO = .init()
) {
self.unpaged = unpaged
self.paged = paged
self.pageNumber = pageNumber
self.pageSize = pageSize
self.offset = offset
self.sort = sort
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.unpaged = try container.decodeIfPresent(Bool.self, forKey: .unpaged) ?? false
self.paged = try container.decodeIfPresent(Bool.self, forKey: .paged) ?? true
self.pageNumber = try container.decodeIfPresent(Int.self, forKey: .pageNumber) ?? 0
self.pageSize = try container.decodeIfPresent(Int.self, forKey: .pageSize) ?? 10
self.offset = try container.decodeIfPresent(Int.self, forKey: .offset) ?? 0
self.sort = try container.decodeIfPresent(PlaceSortResponseDTO.self, forKey: .sort) ?? .init()
}
}

public struct PlaceSortResponseDTO: Decodable, Equatable {
public let unsorted: Bool
public let sorted: Bool
public let empty: Bool

public init(
unsorted: Bool = true,
sorted: Bool = false,
empty: Bool = true
) {
self.unsorted = unsorted
self.sorted = sorted
self.empty = empty
}

enum CodingKeys: String, CodingKey {
case unsorted, sorted, empty
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.unsorted = try container.decodeIfPresent(Bool.self, forKey: .unsorted) ?? true
self.sorted = try container.decodeIfPresent(Bool.self, forKey: .sorted) ?? false
self.empty = try container.decodeIfPresent(Bool.self, forKey: .empty) ?? true
}
}
28 changes: 28 additions & 0 deletions Projects/Data/Model/Sources/Place/DTO/PlaceDetailDTOModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// PlaceDetailDTOModel.swift
// Model
//
// Created by Wonji Suh on 3/28/26.
//

import Foundation

public typealias PlaceDetailDTOModel = BaseResponseDTO<PlaceDetailDTOResponseModel>

// MARK: - DataClass
public struct PlaceDetailDTOResponseModel: Decodable, Equatable {
let name, category, address: String
let distanceToStation, timeToStation, stayableMinutes: Int
let stationLat, stationLon: Double
let leaveTime: String
let imageURL: [String]
let weekday, weekend: [String]
let phoneNumber: String

enum CodingKeys: String, CodingKey {
case name, category, address, distanceToStation, timeToStation, stayableMinutes, stationLat, stationLon, leaveTime
case imageURL = "imageUrl"
case weekday, weekend, phoneNumber
}
}

Loading
Loading