SeatCatcher는 실시간으로 열차의 빈 좌석을 추적하고, 승객 간에 좌석을 공유하거나 교환할 수 있는 iOS 플랫폼입니다.
단순한 좌석 예약 앱을 넘어, WebSocket(STOMP) 을 활용한 실시간 상호작용과 크레딧 시스템을 도입하여 사용자가 능동적으로 좌석을 확보할 수 있는 경험을 제공합니다.
| 홈 (여정 시작 전) | 여정 설정 (역/노선) | 실시간 좌석 현황 |
|---|---|---|
![]() |
![]() |
![]() |
Highlight: 지하철 노선도처럼 직관적인 UI에서, 좌석이 점유되거나 해제되는 순간이 Live Animation으로 즉시 반영됩니다. "https://github.com/user-attachments/assets/c17156f7-df3c-4874-a1c6-76c6c72f4669"
- iOS Deployment Target: iOS 17.0+
- Language: Swift 6.0 (Swift 5 호환)
- Framework: SwiftUI, Combine
- Architecture: Clean Architecture + MVVM-C (Coordinator) + Modularization (SPM)
- Networking:
- REST: Moya (Alamofire Wrapper)
- Socket: SwiftStomp (WebSocket/STOMP)
- Dependency Injection: Pure Swift DI Container
- Authentication: Kakao SDK, Sign in with Apple
- 하이브리드 통신:
- 앱 진입 시
REST API로 최신 좌석 맵(TrainCar)을 로드하여 정합성을 맞춥니다. - 이후
WebSocket(STOMP)채널을 구독(subscribe)하여, 타 사용자의 점유(Occupied) / 해제(Free) 이벤트를 밀리초 단위로 수신합니다.
- 앱 진입 시
- 시각적 피드백: 좌석 상태 변경 시 Lottie 애니메이션과 햅틱 피드백을 통해 직관적인 경험을 제공합니다.
사용자의 복잡한 이동 경로 설정을 5단계의 상태 머신(Finite State Machine)으로 관리합니다.
- 승차 전 (Idle): 최근 경로 바로가기 및 즐겨찾기 제공
- 역 선택 (Station Selection): 실시간 검색 API (
/stations)를 통해 출발/도착역 지정 - 열차 선택 (Train Selection): 현재 운행 중인 열차 목록(
Incoming) 조회 및 선택 - 좌석 선택 (Seat Selection): 원하는 칸(
CarCode)과 구역 진입 - 여정 중 (On Journey): 하차 알림 및 실시간 좌석 요청 모드 활성화
단순 점유를 넘어, 이미 점유된 좌석을 요청할 수 있는 P2P 교환 로직을 구현했습니다.
- Request: 빈 좌석이 없을 때, 점유자에게 크레딧을 걸고 양도 요청 (
RequestSeatUseCase) - Interaction:
- 요청자: STOMP 채널을 통해 점유자의 수락(Accept) / 거절(Reject) 응답 대기
- 점유자: 푸시 알림 또는 인앱 팝업으로 요청 수신 후 결정
- Transaction: 수락 시 서버에서 크레딧이 즉시 이전되고 좌석 소유권이 변경됨
- 토큰 관리:
- Access/Refresh Token을 Keychain에 암호화하여 저장
- Moya
RequestInterceptor를 구현하여 401 에러 발생 시 자동으로 토큰 재발급(Silent Refresh) 수행
- 소셜 로그인: Kakao 및 Apple 인증을 지원하며, 회원 탈퇴 시 연동 해제 로직까지 완비
프로젝트는 **"관심사의 분리"**와 **"단방향 의존성"**을 위해 4개의 계층형 모듈로 설계되었습니다.
App (Composition Root)
└── Presentation (MVVM-C)
└── Domain (Business Logic) ◀── Data (Repository Impl)
└── Core (Shared / Store)
- View: SwiftUI로 작성된 순수 UI. 로직을 포함하지 않고
ViewModel의 상태(@Published)만 바인딩합니다. - Coordinator:
NavigationStack을 제어하는 객체입니다.- View는
coordinator.push(.seatDetail)처럼 목적지만 요청할 뿐, 화면 전환 방식이나 의존성 주입을 신경 쓰지 않습니다. - 이를 통해 뷰의 재사용성을 높이고, 딥링크 처리가 유연해졌습니다.
- View는
- 외부 프레임워크(UIKit, Moya 등)에 의존하지 않는 순수 비즈니스 로직입니다.
- UseCases:
YieldMySeatUseCase,StartJourneyUseCase등 앱의 핵심 기능을 캡슐화했습니다. - Repository Interfaces: 데이터 계층이 구현해야 할 규약(
Protocol)을 정의합니다. (DIP 적용)
- Moya + DTO: API 엔드포인트(
SeatCatcherAPI)를 Enum으로 관리하며, 응답 JSON을 도메인 Entity로 매핑(Mapper)합니다. - Repository Implementation:
- 로컬 캐시나 DB가 필요한 경우 여기서 처리하고, 최종적으로 Domain Entity를 반환합니다.
- 예:
SeatRepositoryImpl은 API 호출 후 결과를 반환하고, 동시에 WebSocket 구독을 트리거합니다.
- AppStore (@Observable):
- 앱 전역 상태(유저 정보, 현재 여정 상태, 소켓 연결 여부)를 관리하는 Single Source of Truth입니다.
- Combine
PassthroughSubject를 내장하여, 특정 이벤트(좌석 상태 변경 등)를 View나 ViewModel이 구독할 수 있게 합니다.
SeatCatcher-iOS/
├── SeatCatcher/ # [App Target]
│ ├── App/ # 앱 진입점 (@main) & 생명주기
│ ├── Coordinator/ # 최상위 AppCoordinator & Scene 빌더
│ └── DIContainer/ # 의존성 주입 컨테이너 (Assembler)
│
└── Modules/ # [Swift Packages]
├── SeatCatcherDomain/ # [Domain Layer]
│ ├── Entities/ # User, Seat, TrainCar 모델
│ ├── UseCases/ # RequestSeat, SubscribeTrain 등 비즈니스 로직
│ └── Repositories/ # Repository Protocols
│
├── SeatCatcherData/ # [Data Layer]
│ ├── Network/Moya/ # SeatCatcherAPI 정의 (Endpoint)
│ ├── Repositories/ # Repository 구현체 (Network 호출)
│ └── Services/ # WebSocket(Stomp), Keychain 서비스
│
├── SeatCatcherPresentation/ # [Presentation Layer]
│ ├── Flows/ # 화면별 View & ViewModel (Home, Onboarding...)
│ └── Common/ # 공통 UI 컴포넌트 (Button, Alert...)
│
└── SeatCatcherCore/ # [Core Layer]
├── Store/ # AppStore (전역 상태)
└── Coordinator/ # Coordinator Protocol 정의-
Repository Clone
git clone https://github.com/SeatCatcher/SeatCatcher-iOS.git cd SeatCatcher-iOS -
Open Project
SeatCatcher.xcodeproj파일을 실행합니다.- 주의: SPM 패키지 페칭(Fetching)이 완료될 때까지 기다려 주세요.
-
Environment Configuration
Configurations폴더 내의xcconfig또는Info.plist에서 API Key 설정을 확인합니다.- Kakao App Key:
KAKAO_APP_KEY환경 변수가 필요합니다. (빌드 오류 시 더미 값 입력 가능)
-
Build & Run
- 시뮬레이터를 선택하고
Cmd + R로 실행합니다.
- 시뮬레이터를 선택하고


