Picke 는 일상의 가치관 차이를 1:1 토론으로 풀어내는 모바일 토론·투표 플랫폼입니다. "오늘의 배틀" 주제에 대한 사전·사후 투표, 실시간 1:1 채팅 토론, 그리고 리캡 카드까지 한 흐름으로 이어집니다.
💡 왜 만들었나? SNS 의 단방향 의견 표출 대신, 짧고 명확한 1:1 토론을 통해 "내가 왜 그렇게 생각하는지" 를 정리하고 다른 가치관을 마주하는 경험을 제공합니다.
프로젝트 규칙은 AGENTS.md / CLAUDE.md 에 정의되어 있습니다.
ln -s AGENTS.md CLAUDE.md- Google / Kakao: WKWebView 기반
authorize → code 가로채기→ 백엔드 토큰 교환 - Apple Sign-In:
ASAuthorizationAppleIDProvider네이티브 통합 - 자동 토큰 갱신:
AccessTokenCredentialJWT exp 디코딩 + 만료 5분 전 자동 refresh - 401 자동 처리:
AuthInterceptor가 401 감지 → refresh 시도 → 실패 시 자동 로그아웃 알림 발송
- 사전 투표 → 1:1 채팅 토론 → 사후 투표 의 한 흐름
- 재투표 로 가치관이 바뀌었는지 추적
- 리캡 카드 자동 생성 + 공유
- 채팅방 오디오 재생 + 로딩 실패 시 상단 floating 오류 배너(
FloatingErrorView)
- 채팅방형 1:1 음성 토론
- 관점(=댓글) 등록·수정·삭제 + 대댓글, 좋아요, 신고
- 투표 진영(optionId)별 관점 등록 / 진영 탭 필터
- 본인 글 "나" 표시 + 수정·삭제 메뉴, 등록·갱신 시 스켈레톤
- 큐레이팅된 홈 피드
- 흥미 기반 배틀 추천(큐레이팅 화면) —
GET /battles/{id}/recommendations/interesting - 카테고리·태그 탐색
- 토픽 검색
- 프로필 카드 / 보유 포인트 + 무료 충전(리워드 광고)
- 포인트 내역, 내 배틀 기록, 내 콘텐츠 활동(댓글/좋아요), 공지사항·이벤트
- 나의 철학자 유형(recap) — 배틀 5개 미만 시 잠금 화면 분기, 애니메이션 레이더 차트 + 공유
- 배틀 주제 제안, 설정·탈퇴·알림 설정
- 알림받기 목록 — 카테고리 탭(전체·콘텐츠·공지사항·이벤트) + 무한 스크롤
- 탭 시 읽음 처리 / 모두 읽음,
GET·POST /api/v1/notifications - 미읽음 빨간점 — 전역 공유 상태(
HasUnreadNotification)로 홈·프로필 종 아이콘에 표시, 모두 읽음 시 제거
- GoogleMobileAds 리워드 동영상 시청 → 포인트 충전 (광고 유닛 ID 는
REWARD_AD_UNITconfig 주입) RewardedAdClient(UseCase) — 로드·표시·보상 콜백을 async 로 추상화
AnalyticsUseCase— 타입 안전 이벤트 + PICKé 핵심 이벤트 명세서 준수(이벤트 통합 전략)sign_up/battle_step(pre_vote·audio_end·post_vote) /report_action/community_action/ad_revenue+ 로그인 시identify
Picke-iOS/
├── 📱 Projects/
│ ├── App/ # 메인 애플리케이션 타겟
│ │ ├── Sources/
│ │ │ ├── Application/ # AppDelegate, SceneDelegate
│ │ │ ├── Di/ # WeaveDI 등록 (DiRegister, AppPresentationContextProvider)
│ │ │ ├── Reducer/ # TCA Root AppReducer
│ │ │ └── View/ # Root Views
│ │ └── Derived/ # Tuist 생성 plist
│ │
│ ├── Presentation/ # 🎨 UI Layer
│ │ ├── Auth/ # 로그인 / 온보딩 / 인증 코디네이터
│ │ ├── Battle/ # 오늘의 배틀 / 빠른 배틀 화면 모델과 메인 플로우
│ │ ├── Chat/ # 투표 / 채팅방 / 관점·대댓글 / 큐레이팅
│ │ ├── Hifi/ # 탐색·검색 기반 Hi-Fi 화면
│ │ ├── Home/ # 홈 피드 / 추천 / 스켈레톤
│ │ ├── MainTab/ # 탭 라우팅 / GNB
│ │ ├── Notification/ # 알림받기 목록 / 카테고리 탭 / 미읽음 뱃지
│ │ ├── Profile/ # 마이페이지 / 포인트 / 설정 / 배틀제안 / 배틀기록 / 콘텐츠활동 / 공지 / 리캡 / 무료충전
│ │ ├── Splash/ # 스플래시
│ │ ├── Web/ # 약관 / 외부 링크 WebView
│ │ └── Presentation/ # 공통 프레젠테이션 유틸
│ │
│ ├── Domain/ # 🔥 Business Logic Layer
│ │ ├── Entity/ # Auth / Battle / Comment / Home / OAuth / Profile / Notification / Share / Error 엔티티
│ │ ├── DomainInterface/ # Repository / Manager 인터페이스
│ │ └── UseCase/ # Auth / Battle / Comment / Home / OAuth / Profile / Notification / Analytics / Ad 유스케이스
│ │
│ ├── Data/ # 📡 Data Layer
│ │ ├── API/ # Base / Auth / Battle / Comment / Home / Perspective / Profile / Notification endpoint
│ │ ├── Service/ # Moya TargetType + 요청 바디
│ │ ├── Model/ # BaseResponseDTO + DTO → Entity 매퍼
│ │ └── Repository/ # RepositoryImpl + OAuth / AudioPlayer 구현
│ │
│ ├── Network/ # 🌐 Network Layer
│ │ ├── Networking/ # 네트워크 클라이언트 export
│ │ ├── Foundations/ # APIHeader / TokenProviding / KeychainTokenProvider
│ │ └── ThirdPartys/ # AsyncMoya / WeaveDI 등 SPM 재노출
│ │
│ └── Shared/ # 🔧 Shared Layer
│ ├── DesignSystem/ # 공통 UI / 컬러 토큰 / 이미지 / Toast / Floating 배너 / 팝업
│ ├── Shared/ # 공유 모델·확장
│ ├── ThirdParty/ # 써드파티 래퍼
│ └── Utill/ # 날짜 / 숫자 / 문자열 표시 유틸리티
│
├── 🔧 Tuist/
│ ├── Package.swift # SPM 의존성 정의
│ └── ProjectDescriptionHelpers/ # 모듈 템플릿 / Plist 헬퍼
└── 🧩 Plugins/
├── DependencyPlugin/ # 모듈 의존성 헬퍼 (.Data / .Domain / .Network ...)
├── DependencyPackagePlugin/ # SPM 의존성 헬퍼 (.SPM.asyncMoya ...)
└── ProjectTemplatePlugin/ # ProjectConfig / Project.makeModule
graph TD
A[Presentation: SwiftUI + TCA Feature] --> B[Domain: UseCase + Entity]
B --> C[DomainInterface: Repository Protocol]
D[Data: RepositoryImpl] --> C
D --> E[Data: DTO Model + Service + API]
E --> F[Network: AsyncMoya + Header + Token]
G[Shared: DesignSystem / Utill] --> A
G --> B
G --> D
A -.-> H[Auth / Battle / Chat / Home / Hifi / MainTab / Splash / Web]
B -.-> I[Auth / Battle / Comment / Perspective / Search UseCase]
D -.-> J[Auth / Battle / Comment / Home / OAuth / Perspective / Search Repository]
레이어별로 묶어 보거나(Grouped) 모든 모듈을 펼쳐 본(Expanded) 시각화입니다. (TuistSpider 결과)
Presentation → Domain (UseCase Protocol)
↓
Domain/UseCase → Domain (Repository Protocol)
↓
Data/Repository → Domain (Entity + Repository Protocol)
↓
Data/Model → Domain (Entity 변환)
↓
Data/Service → Data/API + Network/Foundations
핵심 설계 원칙
- ✅ Presentation 은 Domain 의 UseCase 만 의존합니다.
- ✅ Domain/UseCase 는 Repository Protocol 을 통해 외부 IO 를 호출합니다.
- ✅ Data/Repository 는 Domain 의 Repository Protocol 을 구현하고 Entity 를 반환합니다.
- ✅ Data/Model 은 DTO 와 Entity 변환을 담당합니다.
- ✅ Data/Service 는 endpoint / method / parameter 정의만 담당합니다.
- ✅ 모든 데이터 흐름은 Domain 을 중심으로 진행합니다.
앱
│ authorize URL (response_type=code, redirect_uri=https://picke.store/oauth/<p>)
▼
WKWebView (OAuthWebViewController)
│ 사용자 동의 → 구글/카카오가 redirect_uri 로 302
│ WKNavigationDelegate.decidePolicyFor 가 picke.store/oauth/<p>?code=... 가로채기
│ decisionHandler(.cancel) ← 401 응답 송신 차단
▼
authorizationCode 추출 → dismiss
▼
UnifiedOAuthUseCase
│ POST /api/v1/auth/login/<provider>
│ body: { authorizationCode, redirectUri }
▼
AuthRepositoryImpl
│ BaseResponseDTO<LoginDataDTO> 디코딩 → LoginEntity
▼
KeychainManager 저장 + AuthSessionManager.credential 갱신
ASAuthorizationAppleIDProvider 로 받은 credential / nonce / authorizationCode 를 그대로 백엔드에 전달.
AccessTokenCredential가 access token JWT 의exp를 디코딩해 만료 시점 보관AuthInterceptor.adapt에서 만료 5분 전이면TokenRefreshManager가 단일화된 refresh 수행- 401 응답 시
retry로 토큰 갱신 후 재시도, 실패 시NSNotification.refreshTokenExpired발송 + 자동 로그아웃
- 🎯 Architecture: The Composable Architecture (TCA)
- 📦 Modularization: Tuist 4.x (Micro Feature Architecture)
- 💉 Dependency Injection: WeaveDI 3.4.1
- 🔀 Navigation: TCAFlow (커스텀)
- ⚡ Concurrency: Swift Concurrency (async/await)
- ComposableArchitecture — 단방향 상태 관리
- TCAFlow ⭐️ — TCA 기반 화면 전환 / 네비게이션 (커스텀)
- WeaveDI ⭐️ — 의존성 주입 컨테이너 (커스텀)
- AuthenticationServices — Apple Sign-In, ASWebAuthenticationSession
- WebKit — WKWebView 기반 server-mediated OAuth (Google / Kakao)
- AppAuth-iOS — OAuth 2.0 / OpenID Connect 클라이언트 (옵션)
- AsyncMoya ⭐️ — async/await 기반 HTTP 클라이언트 (커스텀)
- Alamofire / Moya — AsyncMoya 의 기반 스택
- SwiftUI — 선언형 UI
- SDWebImageSwiftUI — 비동기 이미지 로딩 / 캐싱
- Firebase iOS SDK — Crashlytics / Messaging
- Mixpanel — 행동 분석 / Session Replay
- Google Mobile Ads — 광고
- LogMacro — 커스텀 로깅 매크로
- IssueReporting — 개발 단계 이슈 추적
- XCTestDynamicOverlay — 테스트 환경 오버레이
- Clocks — 시간 관련 유틸리티
- ConcurrencyExtras — Swift Concurrency 확장
- Swift 6.0 — 최신 Swift 언어 기능
- Tuist — 프로젝트 생성 / 모듈 의존성 관리
- Swift Package Manager — 패키지 의존성 관리
- fastlane + Bundler — TestFlight / App Store 빌드·업로드 자동화
- 💻 Xcode: 16.0 이상
- 📱 iOS: 17.0 이상
- ⚡ Swift: 6.0 이상
- 🔧 Tuist: 4.x 이상
- 💻 Xcode: 16.0 이상
- 📱 iOS: 17.0 이상
- ⚡ Swift: 6.0 이상
- 🔧 Tuist: 4.x 이상
git clone https://github.com/Roy-wonji/Picke-iOS.git
cd Picke-iOScurl -Ls https://install.tuist.io | bashrbenv install 3.3.9
rbenv global 3.3.9
bundle install# 전체 워크플로우 (권장)
./make build # clean → install → generate
# 단계별 실행
./make clean # 빌드 산출물 정리
./make install # SPM 의존성 설치
./make generate # Xcode 프로젝트 생성open Picke.xcworkspace다음 키들을 Picke-Dev.xcconfig / Picke-Stage.xcconfig / Picke-Prod.xcconfig 에 채워주세요.
BASE_URL = picke.store
GOOGLE_CLIENT_ID = YOUR_GOOGLE_WEB_CLIENT_ID
GOOGLE_IOS_CLIENT_ID = YOUR_GOOGLE_IOS_CLIENT_ID
REVERSED_CLIENT_ID = YOUR_REVERSED_CLIENT_ID
KAKAO_REST_API_KEY = YOUR_KAKAO_REST_API_KEY
| Provider | redirect_uri | 비고 |
|---|---|---|
https://picke.store/oauth/google |
Web client ID + Google Cloud Console 등록 필요 | |
| Kakao | https://picke.store/oauth/kakao |
Kakao Developers 콘솔 등록 필요 |
| Apple | (네이티브) | App Store Connect → Sign in with Apple |
./make build # 전체 빌드 프로세스 (권장)
./make generate # 프로젝트 생성만
./make clean # 빌드 산출물 정리
./make install # 의존성 설치tuist clean # Tuist 캐시 정리
./make clean # 모든 빌드 파일 정리tuist graph # 의존성 그래프 생성
tuist test # 전체 테스트 실행export MATCH_KEYCHAIN_PASSWORD="<match keychain password>"
bundle exec fastlane ios QA # TestFlight 업로드
bundle exec fastlane ios release # App Store 배포MATCH_KEYCHAIN_PASSWORD가 설정되어 있으면 fastlane이 match_keychain을 먼저 unlock해서 macOS 키체인 비밀번호 팝업을 줄입니다.
이 프로젝트는 MIT 라이선스 하에 배포됩니다. 자세한 내용은 LICENSE 파일을 참고하세요.
- iOS Lead Developer: 서원지 (@Roy-wonji)
- main: 프로덕션 배포용
- develop: 개발 통합 브랜치
- feature/*: 기능별 개발 브랜치
- fix/*: 버그 픽스 브랜치
- develop 에서 feature/ 브랜치 생성
- 기능 개발 → 자체 커밋 단위 SRP 분리
- feature/ → develop Pull Request, 코드 리뷰
- develop → main 배포 Pull Request
- 한국어 사용
- 관련 GitHub 이슈 번호 매칭 (예:
#20 #2) - 형식:
<type>: <요약> #<issue> feat / fix / refactor / chore / docs / test
- 📧 이메일: suhwj81@gmail.com
- 🐛 버그 신고: Issues
- 💡 기능 제안: Discussions

