Skip to content

Latest commit

 

History

History
112 lines (82 loc) · 8.34 KB

File metadata and controls

112 lines (82 loc) · 8.34 KB

Packing-iOS — 개발 가이드

여행 준비물 앱 iOS 클라이언트. Swift, UIKit + ReactorKit/RxSwift 기반. 공통 원칙은 루트 CLAUDE.md 참조. 이 문서는 iOS 고유 사항만 다룬다.

환경

  • 최소 지원: iOS 17.6 / Swift 5.0 (Packing.xcodeproj/project.pbxproj)
    • ⚠️ IPHONEOS_DEPLOYMENT_TARGET이 두 레벨에 따로 있다 — 타겟 레벨(:196 Debug / :235 Release) 17.6, 프로젝트 레벨(:307 / :365) 18.2. 타겟 값이 우선하므로 실효 최소는 17.6이지만, 타겟 설정이 지워지면 조용히 18.2로 올라간다.
  • 프로젝트 위치: Packing/Packing.xcodeproj (워크스페이스 아님, 순수 xcodeproj)
  • 앱 진입: Application/AppDelegate.swift + SceneDelegate.swift (UIWindowScene 기반, 최신 구조)

아키텍처 — 계층

Application/   앱 생명주기, AuthCoordinator(인증 분기), 화면 코디네이터
Data/          Network(APIClient·Endpoints), Storage(Keychain·UserManager)
Domain/        Models, Services(서버 호출 래퍼)
Presentation/  기능별 화면. 화면 = ViewController + Reactor 짝
Extensions/    공용 확장 (파일 3개뿐)
  • 네트워크: Domain/Services/*Service.swiftData/Network/APIClient.swift. 인증 토큰은 Keychain(KeychainManager)에서 읽어 Authorization: Bearer 자동 첨부.
  • Extensions/ 전체: DateFormatter.swift, NotificationConstants.swift, UIComponents.swift(공용 UI 팩토리). 이게 전부다.
  • Presentation/Common/이 공용 컴포넌트 저장소다 — 현재 FullScreenImageView(핀치줌 뷰어), ActivityView(공유 시트 래퍼), ButtonStyle, JourneySelectionViewController. 새 공용 UI를 만들기 전 여기부터 확인한다.

규약

  • 신규 화면은 ReactorKit로 작성한다. Presentation/Auth/ViewModel(MVVM) 방식은 레거시이며 따라 하지 않는다.
  • SwiftUI는 JourneyDetail 서브트리 + Presentation/Common/ 한정. 그 외 전 화면은 UIKit. 새 SwiftUI 도입은 이 경계를 넘지 않도록 신중히.
  • 레이아웃은 Auto Layout(SnapKit 없음, 순수 앵커/코드). 절대 프레임(CGRect(...)) 신규 사용 지양 — 회전·다크모드·동적폰트에 취약. 남아 있는 레거시 3곳:
    • Presentation/Journey/AddJourney/View/LocationSearchViewController.swift:87
    • Presentation/User/View/ProfileViewController.swift:269
    • Presentation/Notification/NotificationsViewController.swift:214
  • 이미지 로딩은 Kingfisher로 통일(kf.setImage). SwiftUI AsyncImage는 캐싱 이점이 없어 지양(현재 JourneyDetail만 예외, 정리 대상).

의존성 (전부 SPM)

ReactorKit / RxSwift / RxDataSources / Kingfisher / KeychainAccess. 버전은 Package.resolved가 단일 진실 소스다 — 이 문서에 버전 숫자를 복사해두지 않는다(복사본이 낡아 잘못된 TODO를 낳은 전례가 있다). Podfile·CocoaPods 없음. 의존성 변경은 Xcode SPM UI 또는 pbxproj XCRemoteSwiftPackageReference.

빌드 / 배포 (fastlane)

실행 위치는 Packing-iOS/Packing/(Fastfile:7 기준 — 저장소 루트가 아니다).

LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 bundle exec fastlane beta version:1.5
LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 bundle exec fastlane release version:1.5

⚠️ 실행 전에 반드시 챙길 것 — 둘 다 안 챙기면 아카이브를 다 끝낸 뒤에 실패한다.

  • LANG/LC_ALL을 US-UTF8로 준다. 비대화형 셸에서는 로케일이 없어 fastlane이 한글 출력에 인코딩 에러로 죽는다.
  • version:을 명시한다. 빌드 번호는 latest_testflight_build_number + 1로 자동 증가하지만(그래서 pbxproj의 CURRENT_PROJECT_VERSION은 실제 TestFlight 번호와 다르다), 마케팅 버전은 자동으로 안 오른다. App Store에서 이미 마감된 버전 트레인이면 업로드가 409로 거부된다.

iOS 고유 시크릿은 fastlane/.envAuthKey_*.p8 둘이다(gitignore됨).

딥링크 라우팅 (초대 코드)

인증 분기를 건드릴 때 반드시 아는 상태로 시작할 것. 상세는 DEEPLINK-INVITE-HANDOFF.md.

  • SceneDelegate.handleIncomingURL양쪽 형식을 받는다 — 유니버설 링크 https://packing-api.iyungui.dev/join/<code>, 커스텀 스킴 packingapp://join?code=<code>. 파싱한 코드는 AuthCoordinator.handleInviteCode로 넘긴다.
  • handleInviteCode참여 전에 GET /journeys/preview/:code를 먼저 부른다.
    • 이미 참가자면(isParticipant == true) 시트 없이 즉시 참여 — 링크 재탭 마찰은 0으로 유지한다.
    • 아니면 Presentation/Invite/의 참여 확인 시트를 띄우고, "참여하기"를 누른 뒤에 로그인을 요구한다.
    • 404면 만료 안내 알럿, 통신 실패면 예전 경로(즉시 참여/로그인 유도)로 폴백한다. 새 기능이 기존 동작을 퇴행시키지 않게 한 것이므로 이 폴백을 지우지 말 것.
  • 미로그인이면 코드를 UserDefaults pendingInviteCode(+pendingInviteSavedAt)에 저장하고 로그인을 유도한다. 로그인/가입 완료 후 showMainScreen에서 redeemPendingInviteIfNeeded가 참여를 처리한다. 저장 후 24시간이 지난 코드는 버린다 — 다른 계정이 남의 코드를 소진하는 걸 막는다.
  • ⚠️ 같은 코드의 중복 처리는 inFlightInviteCode가 막는다. 콜드스타트에서 start()의 redeem과 SceneDelegate의 0.3초 지연 라우팅이 겹쳐 실제로 두 번 흐른다. 이 가드를 빼면 시트가 두 번 뜬다.
  • 미리보기는 비로그인도 호출 가능한 유일한 여행 API다. APIClient.attachesTokenWhenAvailable가 이 엔드포인트만 "토큰 있으면 붙이고, 없어도 그대로 보낸다"로 다룬다.
  • 코드 형식 판정과 URL 파싱Extensions/PackingCode.swift 하나로 모았다 (normalize / isValid / extractCode(from:) / extractCode(fromUserInput:)). SceneDelegate(딥링크)와 로그인 화면(클립보드)이 같은 파서를 쓴다 — 두 벌 만들지 말 것. 대소문자를 구분하므로 항상 대문자로 정규화한 뒤 다룬다. ⚠️ 정규식 범위를 손으로 쓰지 말 것. 예전 ^[A-HJ-NP-Z2-9]{8}$J-N 구간 때문에 알파벳에 없는 L을 허용하고 있었다. PackingCodealphabet 상수에서 직접 판정한다.

설치 직후 클립보드 인수 (Presentation/Invite/InvitePasteBannerView)

앱을 새로 깐 사람에게서 코드가 끊기지 않게 하는 장치다. 랜딩의 "App Store에서 받기"를 누르면 웹이 초대 링크 전체 URL을 클립보드에 넣고(서버 public/join.v1.js) 스토어로 보낸다.

  • 🔴 클립보드 값을 직접 읽지 않는다. UIPasteboard.general.string을 읽으면 붙여넣기 경고 배너가 뜬다. detectPatterns(for: [.probableWebURL])는 "URL이 있다"는 사실만 알려주고 경고를 띄우지 않으며, 실제 값은 사용자가 UIPasteControl을 눌렀을 때 paste(itemProviders:)로 들어온다. 이 구분을 깨지 말 것.
  • UIPasteControl은 응답 체인에 pasteConfiguration을 가진 responder가 있어야 활성화된다 (LoginViewController.viewDidLoad).
  • 배너는 URL이 감지될 때만 뜬다. 오탐(무관한 URL)은 붙여넣은 뒤 검증에서 걸러 안내한다.

친구 기능은 없다 (2026-07-27 전면 제거)

친구 맺기·친구 검색·친구 코드는 전부 걷어냈다. 비동기 대기 2회(친구 요청 수락 → 초대 수락)를 거쳐야 동행을 추가할 수 있었고, 미가입자는 아예 초대할 수 없었다. 초대 경로는 링크 하나로 단일화됐다. 탭바도 2개(내 여행 / 내 프로필)로 줄었다.

  • 서버에는 아직 친구 라우트·모델이 남아 있다(구버전 앱 보호). 제거는 별도 작업이다.
  • PackingCode는 이제 여행 초대 코드 전용이다(예전엔 친구 코드와 공유했다).

파일 추가

프로젝트가 PBXFileSystemSynchronizedRootGroup을 쓴다 → .swift 파일은 해당 폴더에 넣기만 하면 타겟에 자동 포함된다. project.pbxproj를 수동으로 편집하지 않는다.

테스트

현재 테스트 타겟 없음. 추가는 후순위(Phase 0.5+).