TimeSpot은 KTX 출발 전 대기 시간을 효율적으로 활용할 수 있도록 도와주는 iOS 애플리케이션입니다. 역 주변의 관광지, 맛집, 카페 등을 탐색하고 출발 시간에 맞춰 안전하게 플랫폼으로 돌아올 수 있도록 가이드합니다.
💡 우리는 왜 이 앱을 만들었을까요? 기차를 기다리는 시간, 그저 대기실에서 시간을 보내기엔 아깝지 않나요? TimeSpot과 함께 여행의 시작부터 특별한 경험을 만들어보세요.
- 전국 주요 KTX역 지원: 서울, 부산, 대전, 광주 등 전국 26개 역
- 정확한 출발 시간 관리: 다음날 23:59까지 자유로운 시간 설정
- 대기 시간 계산: 최소 20분 이상의 여유 시간 확보
- 다양한 POI 정보: 관광지, 맛집, 카페, 쇼핑, 문화시설 등
- 실시간 위치 기반: GPS를 활용한 정확한 주변 정보
- 상세 정보 제공: 영업시간, 리뷰, 연락처 등
- NaverMap 연동: 정확하고 빠른 경로 안내
- 외부 앱 지원: 네이버맵, 구글맵, 애플 지도 등 선택 가능
- 실시간 소요 시간: 도보 경로 및 예상 도착 시간 계산
- 체류 시간 계산: 왕복 도보 시간 + 플랫폼 대기 시간을 고려한 안전한 체류 시간 제공
- 원터치 길찾기: 장소 선택 후 바로 외부 지도 앱으로 연결
- 15분 전: "활동을 차분히 마무리해 주세요"
- 10분 전: "슬슬 일어날 준비를 해볼까요?"
- 5분 전: "출발 채비를 할 시간이에요"
- 즉시 출발: "지금 바로 출발해야 해요!"
- 방문 히스토리: 다녀온 장소와 여행 기록 보관
- 여정 추적: 출발부터 복귀까지의 전체 여정 관리
TimeSpot-iOS/
├── 📱 Projects/
│ ├── App/ # 메인 애플리케이션 타겟
│ │ └── Sources/
│ │ ├── Application/ # AppDelegate, SceneDelegate
│ │ └── Root/ # 루트 화면 설정
│ │
│ ├── Presentation/ # 🎨 UI Layer
│ │ └── Home/ # 홈 기능 모듈
│ │ ├── Sources/
│ │ │ ├── Main/ # 메인 홈 화면
│ │ │ ├── TrainStation/ # 역 선택
│ │ │ ├── Explore/ # 주변 탐색
│ │ │ ├── Route/ # 경로 안내
│ │ │ ├── RouteNotification/ # 알림
│ │ │ └── Components/ # 공통 컴포넌트
│ │ ├── Tests/ # 단위 테스트
│ │ └── Testing/ # 테스트 Mock
│ │
│ ├── Domain/ # 🔥 Business Logic Layer
│ │ ├── Entity/ # 도메인 엔티티
│ │ ├── UseCase/ # 비즈니스 로직 구현
│ │ └── DomainInterface/ # 인터페이스 정의 (Protocol)
│ │
│ ├── Data/ # 📡 Data Layer
│ │ ├── Service/ # REST API 서비스
│ │ ├── Repository/ # Repository 구현체
│ │ └── Model/ # DTO, Response Models
│ │
│ ├── Network/ # 🌐 Network Layer
│ │ ├── Networks/ # 네트워크 설정
│ │ ├── Foundations/ # 네트워크 유틸리티
│ │ └── ThirdPartys/ # AsyncMoya, WeaveDI
│ │
│ └── Shared/ # 🔧 Shared Layer
│ ├── DesignSystem/ # 디자인 시스템
│ ├── Utill/ # 공통 유틸리티
│ └── ThirdPartyLib/ # 외부 라이브러리 래핑
│
├── 🔧 Tuist/ # 프로젝트 설정
│ ├── Package.swift
│ └── ProjectDescriptionHelpers/
│
└── 🤖 docs/ # 커스텀 Claude 에이전트
└── ios-performance-optimizer.md
graph TD
A[🎨 Presentation Layer] --> B[🔥 Domain Layer]
B --> C[📡 Data Layer]
D[🌐 Network Layer] --> C
E[🔧 Shared Layer] --> A
E --> B
E --> C
A -.-> F[SwiftUI Views]
A -.-> G[TCA Reducers]
B -.-> H[Use Cases]
B -.-> I[Entities]
C -.-> J[Repositories]
C -.-> K[API Services]
프로젝트 모듈 간 의존성 관계도 (자동 생성)
Presentation → Domain (UseCase Protocol)
↓
Domain/UseCase → Domain (Repository Protocol)
↓
Data/Repository → Domain (Entity + Repository Protocol)
↓
Data/Model → Domain (Entity 변환)
핵심 설계 원칙:
- ✅ Presentation은 Domain의 UseCase Protocol만 의존
- ✅ Domain은 외부 계층에 의존하지 않는 순수 비즈니스 로직
- ✅ Data는 Domain의 Entity와 Repository Protocol을 구현
- ✅ 모든 데이터 흐름은 Domain을 중심으로 진행
- 🎯 Architecture: The Composable Architecture (TCA) 1.25.5
- 📦 Modularization: Tuist 4.97.2 (Micro Feature Architecture)
- 💉 Dependency Injection: WeaveDI 3.4.0
- 🔀 Navigation: TCAFlow 1.1.0 (커스텀 라이브러리)
- ⚡ Concurrency: Swift Concurrency (async/await)
- ComposableArchitecture 1.25.5 - 단방향 데이터 플로우 및 상태 관리
- swift-dependencies 1.6.0 - 의존성 관리 시스템
- TCAFlow 1.1.0 ⭐️ - TCA 기반 화면 전환 및 네비게이션 (커스텀)
- WeaveDI 3.4.0 ⭐️ - 의존성 주입 컨테이너 (커스텀 포크)
- GoogleSignIn-iOS 9.0.0 - Google OAuth 2.0 인증
- AppAuth-iOS 2.0.0 - OAuth 2.0 및 OpenID Connect 클라이언트
- AsyncMoya 1.1.8 ⭐️ - async/await 기반 HTTP 클라이언트 (커스텀)
- Kingfisher 8.2.0 - 비동기 이미지 로딩 및 캐싱
- SDWebImageSwiftUI 2.0.0 - SwiftUI 이미지 로딩
- ReactiveSwift 6.7.0 - 함수형 리액티브 프로그래밍
- Firebase iOS SDK 12.7.0 - Analytics, Crashlytics, Remote Config
- Mixpanel 5.1.3 - 사용자 행동 분석 및 이벤트 추적
- Mixpanel Session Replay 1.2.1 - 세션 리플레이 및 사용자 경험 분석
- CustomAlert: TCA 기반 커스텀 알림 (
@Presents+PresentationAction) - Toast: 전역
ToastManager를 통한 간단한 메시지 - CustomModal: 드래그 지원 커스텀 모달 시스템
- ios-performance-optimizer: iOS 전용 성능 최적화 에이전트
- SwiftUI 렌더링 최적화, TCA Store 성능 개선
- 메모리 누수 감지, 네트워크 성능 분석
- 빌드 시간 최적화, 모듈 구조 개선
- 📁 에이전트 파일:
docs/ios-performance-optimizer.md
- LogMacro: 커스텀 로깅 매크로
- IssueReporting: 개발 단계 이슈 추적
- XCTestDynamicOverlay: 테스트 환경 오버레이
- Clocks: 시간 관련 유틸리티
- ConcurrencyExtras: Swift Concurrency 확장
- Swift 6.0: 최신 Swift 언어 기능
- Tuist: 프로젝트 생성 및 의존성 관리
- Swift Package Manager (SPM): 패키지 의존성 관리
- fastlane: 자동화된 빌드 및 배포
- 🗺️ Map Service: Naver Maps SDK
- 📍 Location: Core Location Framework
- 🛣️ Places: Google Places API
- 💻 Xcode: 16.0 이상
- 📱 iOS: 17.0 이상
- ⚡ Swift: 6.0 이상
- 🔧 Tuist: 4.97.2 이상
- 💻 Xcode: 16.0 이상
- 📱 iOS: 17.0 이상
- ⚡ Swift: 6.0 이상
- 🔧 Tuist: 4.97.2 이상
git clone https://github.com/Roy-wonji/TimeSpot-iOS.git
cd TimeSpot-iOScurl -Ls https://install.tuist.io | bash# 전체 워크플로우 (권장)
./make build # clean → install → generate
# 또는 단계별 실행
./make clean # 기존 파일 정리
./make install # 의존성 설치
./make generate # 프로젝트 생성open TimeSpot.xcworkspace프로젝트 실행을 위해 다음 API 키가 필요합니다:
// Config.swift에서 설정
enum APIKeys {
static let naverMapsClientID = "YOUR_NAVER_MAPS_KEY"
static let googlePlacesAPI = "YOUR_GOOGLE_PLACES_KEY"
static let timeSpotServerURL = "YOUR_SERVER_URL"
}./make build # 전체 빌드 프로세스 (권장)
./make generate # 프로젝트 생성만
./make moduleinit # 새 모듈 생성 (대화형)./make reset # 강력한 클린 + 캐시 삭제 + 재생성
./make clean # 빌드 아티팩트 정리
./make install # 의존성 재설치./make inspect-imports # 모듈 의존성 검사
./make inspect-coverage # 코드 커버리지 분석
./make graph # 의존성 그래프 생성./make moduleinit # 새 Feature 모듈 생성
./make test # 전체 테스트 실행fastlane snapshot # 전체 스크린샷 생성 (메인, 역선택, 탐색, 상세, 알림)
fastlane snapshot --scheme TimeSpot # 특정 스킴만스크린샷 구성:
- 메인 화면, 역 선택, 주변 탐색
- 장소 상세, 복귀 알림
- 출발역 선택: 전국 26개 주요 KTX역 중 선택
- 출발 시간 설정: 현재 시각부터 다음날 23:59까지
- 대기 시간 확인: 최소 20분 이상의 여유 시간 필요
- "주변 탐색 시작하기" 버튼 터치
- 위치 권한 허용 (정확한 주변 정보 제공을 위해 필요)
- 카테고리별 탐색: 관광지 🏛️, 맛집 🍴, 카페 ☕, 쇼핑 🛍️ 등
- 장소 선택: 방문하고 싶은 장소 터치
- 체류 시간 확인: 안전한 왕복 시간을 고려한 체류 가능 시간 확인
- "경로 확인하기" 버튼 터치
- 지도 앱 선택: 네이버맵, 구글맵, 애플 지도 중 선택
- 실시간 네비게이션: 선택한 앱으로 바로 길찾기 시작
- 복귀 알림: 설정한 시간에 맞춰 자동 복귀 알림
- 📱 스마트 알림: 출발 시간에 맞춰 단계별 알림
- ⏰ 실시간 추적: 현재 위치에서 역까지의 실시간 경로
- 🔔 맞춤 알림: 개인 일정에 맞춘 최적의 출발 타이밍
이 프로젝트는 TCA + Clean Architecture 기반의 멀티모듈 구조로 설계되어 있습니다.
새로운 기능 개발이나 코드 리뷰 시 다음 가이드라인을 따라주세요.
@Reducer
public struct FeatureName {
@ObservableState
public struct State: Equatable {
// 상태 정의
}
@CasePathable
public enum Action {
case view(View) // 뷰 액션
case async(AsyncAction) // 비동기 액션
case inner(InnerAction) // 내부 로직 액션
case delegate(Delegate) // 부모에게 전달할 액션
}
// Action 세분화
public enum View { /* 사용자 인터랙션 */ }
public enum AsyncAction { /* 비동기 처리 */ }
public enum InnerAction { /* 내부 로직 */ }
public enum Delegate { /* 부모 통신 */ }
}TCA 핵심 규칙:
@Reducer매크로 +@ObservableState필수 사용- Action 네이밍: 이벤트 기반 (
loginButtonTapped,userInfoReceived) - Effect: 부작용 없으면
.none, 비동기는.run - Store:
StoreOf<Feature>타입 활용 - Extension 활용: Action 처리 메서드와 State computed property 분리
@FlowCoordinator(screen: "ScreenName", navigation: true)
public struct FeatureCoordinator {
@ObservableState
public struct State: Equatable {
var routes: [Route<FeatureScreen.State>]
public init() {
self.routes = [.root(.login(.init()), embedInNavigationView: true)]
}
}
}
// 네비게이션 동작
state.routes.push(.nextScreen(.init())) // Push
state.routes.present(.modalScreen(.init())) // Present (Modal)
state.routes.goBack() // Go Back
state.routes.goBackToRoot() // Go Back to RootAction 처리 메서드 분리:
// 메인 body에서는 라우팅만
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .view(let viewAction):
return handleViewAction(state: &state, action: viewAction)
case .async(let asyncAction):
return handleAsyncAction(state: &state, action: asyncAction)
}
}
}
// Extension으로 핸들러 분리
extension FeatureName {
private func handleViewAction(state: inout State, action: View) -> Effect<Action> {
// 구현
}
}State Extension (Computed Properties):
extension FeatureName.State {
var isExploreNearbyEnabled: Bool {
isStationReady && isDepartureTimeSet && remainingTotalMinutes > 20
}
var progressPercentage: Double {
guard remainingTotalMinutes > 0 else { return 0 }
return min(Double(remainingTotalMinutes) / 120.0, 1.0)
}
}
// View Extension 패턴
extension HomeView {
@ViewBuilder
private func selectStationView() -> some View {
Button {
store.send(.view(.stationSelectionTapped))
} label: {
HStack {
Text("출발역 선택")
Spacer()
if let station = store.selectedStation {
Text(station.name)
}
}
}
}
// Computed Property로 데이터 가공
private var hasValidStation: Bool {
store.selectedStation != nil
}
}// AppDIManager에서 등록
WeaveDI.builder
.register { AuthRepositoryImpl() as AuthInterface }
.register { ProfileRepositoryImpl() as ProfileInterface }
.configure()
// TCA Feature에서 사용
@Reducer
public struct LoginFeature {
@Dependency(\.authRepository) var authRepository
// 구현...
}기본 규칙:
- 들여쓰기: 2 spaces
- View suffix 생략:
LoginView→Login - SubView 분리:
@ViewBuilder함수 대신 별도 struct 사용 - Spacer 대신:
.frame(maxWidth: .infinity)활용 - View Extension:
@ViewBuilder로 컴포넌트 분리, computed property로 데이터 가공
// ✅ 올바른 패턴
struct ProfileView: View {
@Bindable var store: StoreOf<ProfileFeature>
var body: some View {
VStack {
profileHeader
profileContent
actionButtons
}
}
private var profileHeader: some View {
ProfileHeaderView(user: store.user)
}
}Domain Error 구조:
Projects/Domain/Entity/Sources/Error/
├── AuthError.swift # 인증 관련 에러
├── SignUpError.swift # 회원가입 관련 에러
├── ProfileError.swift # 프로필 관련 에러
├── PlaceError.swift # 장소 검색 관련 에러
├── DirectionError.swift # 경로 안내 관련 에러
├── StationError.swift # 역 정보 및 즐겨찾기 관련 에러
└── HistoryError.swift # 여행 기록 및 여정 관리 관련 에러
Result 래퍼 활용 패턴:
return .run { [userSession = state.userSession] send in
let signupResult = await Result {
try await signUpUseCase.registerUser(userSession: userSession)
}
.mapError(SignUpError.from) // 에러 타입 변환
await send(.inner(.signUpResponse(signupResult)))
}
// 결과 처리
case .signUpResponse(let result):
switch result {
case .success(let data):
state.loginEntity = data
return .send(.navigation(.completed))
case .failure(let error):
state.errorMessage = error.localizedDescription
// 재시도 가능한 에러인 경우
if error.isRetryable {
state.canRetry = true
}
return .none
}팝업/모달 사용 예시:
// CustomAlert - 중요한 확인이 필요한 경우
state.customAlert = .withdrawAccount()
state.customAlert = .logout()
// Toast - 간단한 피드백
ToastManager.shared.showSuccess("저장되었습니다")
ToastManager.shared.showError("네트워크 오류")
// Modal - 복잡한 UI
.presentDSModal(
item: $store.scope(state: \.trainStationSheet, action: \.trainStationSheet),
height: .fraction(0.8)
) { store in
TrainStationView(store: store)
}// ✅ Guard early return
guard let user = currentUser else {
return
}
// ✅ Final class 기본
public final class ServiceManager {
private let networking: NetworkingInterface
public init(networking: NetworkingInterface) {
self.networking = networking
}
}
// ❌ Force unwrap 금지
let result = optionalValue! // 절대 사용 금지
// ✅ Safe unwrapping
guard let result = optionalValue else { return }[{Header}]: {Message}
Header 종류:
- FEAT: 새 기능 추가
- REFACTOR: 코드 리팩토링
- ADD: 파일, 의존성 추가
- FIX: 버그 수정
- HOTFIX: 긴급 버그 수정
- DOCS: 문서 수정
- TEST: 테스트 코드 작성/수정
- CHORE: 기타 작업
예시:
[FEAT]: 로그인 화면 UI 구현
[REFACTOR]: AuthRepository DI 패턴 적용
[FIX]: 토큰 만료 시 자동 갱신 로직 수정
Git 규칙:
- 메시지: 한국어 사용, 한 줄, 최대 50자
- 브랜치:
feature/#{issue-number}→develop머지 - 특수기호: 마침표, 특수기호 사용 금지
- PR 크기: 파일 변경 30개 내외로 제한, 대형 작업은 분할
./make generate 실행
- 파일 생성/삭제 시
Project.swift수정 시- 의존성 추가/제거 시
# 권장 워크플로우
./make build # clean → install → generate 순차 실행
./make generate # Xcode 프로젝트 생성
./make install # 의존성 설치
./make clean # 프로젝트 정리# Claude에서 에이전트 호출
@ios-performance-optimizer "HomeView 스크롤 성능 개선 필요"
@ios-performance-optimizer "TCA Store 메모리 사용량 최적화"
@ios-performance-optimizer "빌드 시간 단축 방법 제안"📚 상세한 개발 가이드: AGENTS.md 참고
🤖 Claude Code 연동:ln -s AGENTS.md CLAUDE.md로 심볼릭 링크 연결됨
프로젝트의 상세한 개발 가이드라인과 성능 최적화 방법은 다음 문서들을 참고하세요:
- iOS 성능 최적화 통합 시스템 - 16개 서브에이전트 기반 자동화 최적화
- Point-Free Workshop 패턴 가이드 - TCA/SwiftUI/PFW 전문 성능 분석
- 성능 통합 가이드 - 두 성능 스킬의 협업 워크플로우
# Claude Code에서 성능 최적화 스킬 사용법
@ios-performance-optimizer "TimeSpot 프로젝트 전체 최적화해줘"
@ios-performance-pfw "HomeFeature TCA 성능 분석해줘"
# 협업 패턴 (분석 → 적용 → 검증)
1. @ios-performance-pfw "성능 분석 및 개선점 찾아줘"
2. @ios-performance-optimizer "분석 결과 기반으로 자동 최적화해줘"
3. @ios-performance-pfw "자동 최적화 후 TCA 패턴 검증해줘"이 프로젝트는 MIT 라이선스 하에 배포됩니다. 자세한 내용은 LICENSE 파일을 참고하세요.
- iOS Developer & Architecture: 서원지 (@Roy-wonji)
- Backend Developer: @loadingKKamo21
- Designer: 박미란
- Product Manager: 전희정
- 📧 이메일: suhwj81@gmail.com
- 🐛 버그 신고: Issues
- 💡 기능 제안: Discussions
- 📱 App Store: TimeSpot 다운로드






