Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
0159ec9
feat: AtchaV2 Phase 14 — 재실행 정합성 (스냅샷 + 고아 LA 재부착 + 도보 시간)
YuGeonHui Aug 23, 2026
5e3e073
docs: AtchaV2 Phase 15 구현 프롬프트 — 인지 채널 방어선 (자동 검수 규약 기준)
YuGeonHui Aug 23, 2026
87c7fa1
feat: AtchaV2 Phase 15 — 인지 채널 방어선 (폴백 확대 + time-sensitive + 노티 라우팅 +…
YuGeonHui Aug 23, 2026
a666e1e
docs: AtchaV2 Phase 16 구현 프롬프트 — 갱신 신뢰성 (수동 갱신 + 타임아웃·재시도 + 오프라인 구분 +…
YuGeonHui Aug 23, 2026
c9a517a
feat: AtchaV2 Phase 16 — 갱신 신뢰성 (pull-to-refresh + 타임아웃·재시도 + 오프라인 구분…
YuGeonHui Aug 23, 2026
77ede3c
docs: Phase 16 자동 검수 결과 기록 + CLAUDE.md 갱신 채널 서술 갱신
YuGeonHui Aug 23, 2026
d8ce38b
docs: AtchaV2 Phase 17 구현 프롬프트 — 검색·홈 마찰 팩 (빈 상태 + 필드 정합 + 진입·이탈 경로 +…
YuGeonHui Aug 23, 2026
a1a1530
feat: AtchaV2 Phase 17 — 검색·홈 마찰 팩 (빈 상태 2종+로딩 + 도착지 바인딩·필드 진입 + didS…
YuGeonHui Aug 23, 2026
6e7d3dc
fix: 검색 빈 상태·스피너를 키보드 위 가시 영역 중앙에 배치 (Phase 17 자동 검수에서 발견 — 타이핑 중 빈 상…
YuGeonHui Aug 23, 2026
2e1c680
docs: Phase 17 자동 검수 결과 기록 + 규약에 잠금 화면 대체 경로(XCUITest 하니스) 실측 추가
YuGeonHui Aug 23, 2026
620ba04
docs: AtchaV2 Phase 18 구현 프롬프트 — 최근 경로 원탭 칩 (3탭→1탭, 최근 검색 표면 확장, 자동 검…
YuGeonHui Aug 23, 2026
5f53636
feat: AtchaV2 Phase 18 — 최근 경로 원탭 칩 (DSChip + 홈 칩 상태·원탭 재검색 + 도착지 승격 …
YuGeonHui Aug 23, 2026
2770032
docs: Phase 18 자동 검수 결과 기록(①~⑧ 통과) + 규약에 위치 권한 알럿 Allow Once 함정·위치 주입…
YuGeonHui Aug 23, 2026
d6281eb
feat: 레거시 스플래시 비주얼 V2 이식 (배경+캐릭터+로고 PNG 3종, 런치스크린 storyboard 픽셀 동일 구성…
YuGeonHui Aug 24, 2026
2d1e3cc
Merge pull request #357 from Atcha-Project/feat/v2-ui-redesign
YuGeonHui Aug 24, 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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ AtchaV2(앱, 조합 루트: 어댑터·스플래시·AlarmSyncService) ─► Ho

- **익명 인증 발급 엔드포인트 미확정** — `UnconfiguredAnonymousSessionIssuer` 스텁이 주입돼 있어 실서버에서는 토큰 없이 동작한다(알람 서버 기능 전부 불가). Debug(DEV)는 `DevDemoFallbacks`가 데모 데이터로 가린다 — 실서버 스펙 확정 시 이 폴백과 `AppDIContainer`의 `#if DEV` 주입을 제거할 것.
- `AppEnvironment`의 base URL은 dev/live 실주소 반영 완료. **Stage는 dev 호스트를 공유 중** — 전용 호스트만 미정.
- `Projects/App/Resources/GoogleService-Info.plist`는 **레거시 번들 ID(`com.atcha.iOS`)용 파일**이라 존재 가드만 통과할 뿐 V2(`com.atcha.iOS.v2`)로의 사일런트 푸시가 성립하지 않는다 — V2용 재발급·교체 필요. FCM 토큰 서버 전달도 미구현(로깅만)이라 갱신 채널은 현재 폴링(앱 시작·포그라운드 복귀)뿐.
- `Projects/App/Resources/GoogleService-Info.plist`는 **레거시 번들 ID(`com.atcha.iOS`)용 파일**이라 존재 가드만 통과할 뿐 V2(`com.atcha.iOS.v2`)로의 사일런트 푸시가 성립하지 않는다 — V2용 재발급·교체 필요. FCM 토큰 서버 전달도 미구현(로깅만)이라 갱신 채널은 현재 폴링(앱 시작·포그라운드 복귀)과 홈 pull-to-refresh(수동)뿐.
- AtchaV2는 iOS 26 전용. AlarmKit(CoreAlarm)·Live Activity(CoreLiveActivity + AtchaWidget 익스텐션)는 Phase 9~12에서 구축 완료.
- Phase 12 이후의 갭 분석·후속 로드맵: `docs/planning/atcha-v2-post12-roadmap.md` / Phase 13·14(알람 이후 + 재실행 정합성) 구현 프롬프트: `docs/prompts/atcha-v2-session-lifecycle-prompt.md`.
- Phase 검수는 사람 검수 대신 **자동 검수 규약**(`docs/prompts/atcha-v2-auto-verification.md`)을 따른다 — 에이전트가 computer use로 시뮬레이터 검수를 직접 수행·증적 보고하고, 실기기 잔여 항목만 사용자에게 이관.
21 changes: 19 additions & 2 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ let appTarget = Target.target(
deploymentTargets: Atcha.v2Deployment,
infoPlist: .extendingDefault(with: [
"CFBundleDisplayName": "앗차",
"UILaunchScreen": [:],
"UILaunchStoryboardName": "LaunchScreen",
"NSAlarmKitUsageDescription": "막차 시간에 맞춰 알람을 울리기 위해 권한이 필요합니다.",
"NSLocationWhenInUseUsageDescription": "현재 위치를 출발지로 사용하기 위해 위치 정보 접근 권한이 필요합니다.",
// 사일런트 푸시(content-available=1) 수신용 — 사용자 알림 권한과 무관.
Expand All @@ -35,6 +35,8 @@ let appTarget = Target.target(
// 배포 서명 시 프로비저닝이 production으로 치환한다.
entitlements: .dictionary([
"aps-environment": "development",
// 폴백 노티의 .timeSensitive interruptionLevel용(Phase 15) — 집중 모드 관통.
"com.apple.developer.usernotifications.time-sensitive": true,
]),
dependencies: [
.target(name: "AtchaWidget"),
Expand Down Expand Up @@ -63,6 +65,20 @@ let appTarget = Target.target(
])
)

// App 타겟 테스트 타겟(Phase 16) — host app 방식: 앱 타겟 의존으로 internal 심볼을
// @testable 접근한다. AlarmSyncService(판정·폴백·만료 분기)의 회귀 방어가 목적.
let testTarget = Target.target(
name: "AtchaV2Tests",
destinations: Atcha.destinations,
product: .unitTests,
bundleId: "\(Atcha.v2BundleID).tests",
deploymentTargets: Atcha.v2Deployment,
infoPlist: .default,
sources: ["Tests/**"],
dependencies: [.target(name: "AtchaV2")],
settings: .atchaV2()
)

let widgetTarget = Target.widgetExtension(
name: "AtchaWidget",
bundleId: "\(Atcha.v2BundleID).widget",
Expand All @@ -82,12 +98,13 @@ let project = Project(
developmentRegion: Atcha.developmentRegion
),
settings: .atchaV2(),
targets: [appTarget, widgetTarget],
targets: [appTarget, widgetTarget, testTarget],
schemes: [
.scheme(
name: "AtchaV2",
shared: true,
buildAction: .buildAction(targets: ["AtchaV2"]),
testAction: .targets(["AtchaV2Tests"]),
runAction: .runAction(configuration: "Debug", executable: "AtchaV2"),
archiveAction: .archiveAction(configuration: "Debug"),
profileAction: .profileAction(configuration: "Debug", executable: "AtchaV2"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Splash_BG.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Splash_BG@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Splash_BG@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "img_Atcha_character.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img_Atcha_character@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img_Atcha_character@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "img_Atcha.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img_Atcha@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img_Atcha@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions Projects/App/Resources/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="0qB-dO-3U6"/>
<viewControllerLayoutGuide type="bottom" id="2LT-NI-Tm7"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="splashBackground" translatesAutoresizingMaskIntoConstraints="NO" id="09a-iA-Mxf">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="935-rF-KUC">
<rect key="frame" x="146.66666666666666" y="363" width="100" height="126.33333333333331"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="splashCharacter" translatesAutoresizingMaskIntoConstraints="NO" id="csS-KK-yCf">
<rect key="frame" x="0.0" y="0.0" width="100" height="62.333333333333336"/>
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="splashLogo" translatesAutoresizingMaskIntoConstraints="NO" id="nnR-L7-YKX">
<rect key="frame" x="0.0" y="74.333333333333314" width="100" height="52"/>
</imageView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" red="0.074509803921568626" green="0.074509803921568626" blue="0.082352941176470587" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="935-rF-KUC" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="4oV-rM-in1"/>
<constraint firstItem="09a-iA-Mxf" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="FzF-6x-XiG"/>
<constraint firstAttribute="bottom" secondItem="09a-iA-Mxf" secondAttribute="bottom" id="GHy-tu-oxr"/>
<constraint firstAttribute="trailing" secondItem="09a-iA-Mxf" secondAttribute="trailing" id="fXW-wu-UoH"/>
<constraint firstItem="935-rF-KUC" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="nRk-dD-4MT"/>
<constraint firstItem="09a-iA-Mxf" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="xeK-Hf-fno"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-815" y="305"/>
</scene>
</scenes>
<resources>
<image name="splashBackground" width="360" height="800"/>
<image name="splashCharacter" width="59.333332061767578" height="62.333332061767578"/>
<image name="splashLogo" width="100" height="52"/>
</resources>
</document>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import CoreStorage
import Domain
import Foundation

/// Domain `AlarmSessionSnapshotStore` → CoreStorage(UserDefaults 백엔드) 어댑터 (Phase 14).
/// 스냅샷은 재실행 브리지이지 정본이 아니다 — 저장·삭제 실패는 조용히 흡수하고(다음
/// 등록/sync가 자가치유), 필드 추가 등으로 인한 디코딩 실패도 nil로 무해화한다
/// (최근 검색 저장소의 자가치유 패턴 재사용).
struct AlarmSessionSnapshotStoreAdapter: AlarmSessionSnapshotStore {
private static let storageKey = "alarm.sessionSnapshot"

private let store: any KeyValueStore

init(store: any KeyValueStore = UserDefaultsKeyValueStore()) {
self.store = store
}

func load() async -> AlarmSessionSnapshot? {
(try? store.value(AlarmSessionSnapshot.self, forKey: Self.storageKey)) ?? nil
}

func save(_ snapshot: AlarmSessionSnapshot) async {
try? store.setValue(snapshot, forKey: Self.storageKey)
}

func clear() async {
try? store.removeValue(forKey: Self.storageKey)
}
}
20 changes: 16 additions & 4 deletions Projects/App/Sources/Adapters/CoreLocationServiceAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,25 @@ import Domain
/// App 모듈 기본 격리가 MainActor라 이 클래스는 암시적 Sendable — 프로토콜의
/// nonisolated async 요구사항은 격리 witness로 충족된다.
final class CoreLocationServiceAdapter: LocationService {
/// CLLocationUpdate의 거부 플래그 3종 → LocationError 매핑(Phase 17). nil = 진행 중.
/// 우선순위: restricted > 전역 OFF > 앱 권한 거부 — 더 좁은 회복 경로가 이긴다
/// (restricted는 설정으로 못 풀고, 전역 OFF는 앱 권한 상태를 무의미하게 만든다).
static func classify(denied: Bool, deniedGlobally: Bool, restricted: Bool) -> LocationError? {
if restricted { return .restricted }
if deniedGlobally { return .servicesDisabled }
if denied { return .permissionDenied }
return nil
}

func currentLocation() async throws -> Coordinate {
do {
for try await update in CLLocationUpdate.liveUpdates() {
if update.authorizationDenied
|| update.authorizationDeniedGlobally
|| update.authorizationRestricted {
throw LocationError.permissionDenied
if let error = Self.classify(
denied: update.authorizationDenied,
deniedGlobally: update.authorizationDeniedGlobally,
restricted: update.authorizationRestricted
) {
throw error
}
if let location = update.location {
return Coordinate(
Expand Down
Loading
Loading