Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
77ddaa4
feat: #32 로그인 성공 후 홈 진입 흐름 연결
Roy-wonji May 14, 2026
ddebebf
docs: #32 TCA Store 보유 규칙 문서화
Roy-wonji May 14, 2026
91dcfa4
chore: #2 온보딩 일러스트 자산을 PNG @3x → SVG 로 교체 (벡터 스케일링 지원)
Roy-wonji May 15, 2026
3d82fb4
refactor: #20 Kakao authorize URL 에서 prompt=login 쿼리 제거
Roy-wonji May 15, 2026
1ca32b0
fix: 탭바 색상을 디자인시스템 토큰으로 통일
Roy-wonji May 15, 2026
03d1bfa
feat: #3 홈 메인 화면 도메인 모델 + UI 구현
Roy-wonji May 15, 2026
d786f9f
fix: #20 Kakao OAuth 웹뷰에 ephemeral 세션 적용 + 콜백 직후 webView 가림
Roy-wonji May 15, 2026
1069609
chore: #32 Splash → Auth / MainTab 전환에 3초 대기 효과 추가
Roy-wonji May 15, 2026
2b0bc13
chore: TCAFlow 1.1.2 → 1.1.3 버전 업데이트
Roy-wonji May 15, 2026
fb9ae42
docs: AGENTS.md SwiftUI 컬러 단축형 규칙 stroke/fill/background(in:) 까지 확장
Roy-wonji May 15, 2026
bc6101d
chore: #3 홈 카드 spacing 을 Pencil .pen 값과 1:1 정렬
Roy-wonji May 15, 2026
9ca4334
chore: #3 헤더 아래 16pt 상단 padding 제거 — .pen 디자인에 없는 갭
Roy-wonji May 15, 2026
67a0df9
feat: #3 홈 헤더용 PicKé 로고 + 알림 벨 SVG 자산 추가
Roy-wonji May 15, 2026
2297709
feat: #3 홈 도메인 Entity 확장 + HomeInterface / Mock Repository 추가
Roy-wonji May 15, 2026
6d1994b
feat: #3 GET /api/v1/home 데이터 레이어 (API · Service · DTO · RepositoryIm…
Roy-wonji May 15, 2026
0004cbb
chore: #3 Kingfisher SPM 의존성 + HomeRepositoryImpl DI 등록
Roy-wonji May 15, 2026
3a90701
feat: #3 HomeFeature 에 fetchHome API 연동 + 카드 컴포넌트 KFImage 적용
Roy-wonji May 15, 2026
3f611c1
refactor: #19 AuthRepositoryImpl provider 선언을 팩토리 기본값 직접 주입 방식으로 정리
Roy-wonji May 15, 2026
09d9317
docs: #19 AGENTS.md RepositoryImpl Provider 선언 패턴 + State init / Resu…
Roy-wonji May 15, 2026
3089bac
fix: #19 refresh token 헤더 전송 방식 수정
Roy-wonji May 15, 2026
31e502d
fix: #3 홈 데이터 반복 호출과 빈 응답 처리 보정
Roy-wonji May 15, 2026
da73c58
feat: #18 홈 스켈레톤 로더 애니메이션 적용
Roy-wonji May 15, 2026
7a01182
docs: #19 모듈 그래프 문서화와 Service 의존성 정리
Roy-wonji May 15, 2026
dc3fab6
docs: #19 README 모듈 구조 최신화
Roy-wonji May 15, 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
188 changes: 187 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,28 @@ Color.bgSubtle.edgesIgnoringSafeArea(.all)
.foregroundStyle(Color.neutral900)
```

`SwiftUI.Color` 의 정적 멤버로 디자인 토큰 (`neutral50` … `neutral900`, `primary50` … `primary900`, `secondary50` …, `bgSubtle`) 이 등록되어 있어 `.foregroundStyle / .fill / .background / .tint` 등에서 모두 점 단축형 사용 가능.
`SwiftUI.Color` 의 정적 멤버로 디자인 토큰 (`neutral50` … `neutral900`, `primary50` … `primary900`, `secondary50` …, `beige50` … `beige900`, `bgSubtle`) 이 등록되어 있어 **ShapeStyle 을 받는 모든 modifier 에서 점 단축형 사용**:

```swift
// ✅ 점 단축형 — ShapeStyle 컨텍스트 모두 적용
.foregroundStyle(.neutral900)
.fill(.beige200)
.stroke(.beige700, lineWidth: 1)
.background(.beige50, in: RoundedRectangle(cornerRadius: 2))
.tint(.primary500)

// ❌ 금지 — Color 타입 명시
.foregroundStyle(Color.neutral900)
.fill(Color.beige200)
.stroke(Color.beige700, lineWidth: 1)
.background(Color.beige50, in: ...)
```

> **예외:** `Color` 가 View 자체로 쓰여 메서드 체이닝을 받는 경우는 그대로 둔다.
> ```swift
> Color.beige50.ignoresSafeArea() // ✅ View 로 쓰임 — Color 명시 필요
> Color.neutral500.opacity(0.4) // ✅ View 로 쓰임 — Color 명시 필요
> ```

#### 🖼 이미지 — `Image(asset: .xxx)` + 데이터 모델은 `ImageAsset` 타입

Expand All @@ -203,6 +224,36 @@ Image(ImageAsset.onboarding1.rawValue)
2. `ImageAsset` enum 에 `case <name>` 추가 (raw value = imageset 폴더명과 동일)
3. `tuist generate` 로 리소스 재인덱싱

#### 🧲 Store 보유 — `@Bindable private var store: StoreOf<Feature>` 고정

TCA View 가 `store` 를 들고 있을 때는 **항상 `@Bindable`** 로 선언한다. 단순 표시뿐이라도 future-proof 하기 위해 동일.

```swift
// ✅ 올바른 패턴
public struct OnBoardingView: View {
@Bindable var store: StoreOf<OnBoardingFeature>
}

public struct MainTabView: View {
@Bindable private var store: StoreOf<MainTabCoordinator>
}

// ❌ 금지 — 그냥 let / var
public struct TabFeatureView: View {
let store: StoreOf<TabFeature> // ← @Bindable 누락
var store: StoreOf<HomeFeature> // ← 동일하게 누락
}
```

근거:
- `$store.binding` 형태가 필요한 시점이 거의 반드시 옴 (TabView selection, TextField, NavigationDestination 등)
- `@Bindable` 은 read-only 사용 시에도 비용이 없고, 후에 binding 이 추가될 때 시그니처 변경 없이 받음
- LoginView / OnBoardingView / MainTabView / AuthCoordinatorView 모두 이 규칙 따름

가시성:
- 외부에서 store 를 주입받는 표면은 `public` 또는 그대로 두고,
- 그 외 내부에서만 쓸 store 는 `private` 으로 가린다 (`@Bindable private var store`)

#### 🧮 텍스트 / 라벨 — `body` 안에 인라인 표현 금지, State computed 로

표시용 파생값은 View 가 아니라 `State` 의 computed property 로 정의해서 View 에서는 그대로 꺼내기만 한다.
Expand All @@ -228,6 +279,141 @@ private var primaryButtonTitle: String {
}
```

#### 🪟 State 초기값 — inline default + `public init() {}` 만 노출

`@ObservableState` 의 `State` 는 프로퍼티마다 **inline default 값**을 박고, `public init() {}` 만 외부에 노출한다. 긴 파라미터 리스트의 `public init(x:, y:, ...)` 는 쓰지 않는다.

```swift
// ✅ 올바른 패턴 — 외부는 .init() 만 호출, 변경은 reducer 내부에서
@ObservableState
public struct State: Equatable {
public var isLoading: Bool = false
public var newNotice: Bool = false
public var heroes: [HeroBattle] = []
public var heroIndex: Int = 0
public var hotBattles: [HotBattle] = []

public var currentHero: HeroBattle? { heroes[safe: heroIndex] }

public init() {}
}

// ❌ 금지 — 모든 필드를 init 파라미터로 펼침
public init(
isLoading: Bool = false,
newNotice: Bool = false,
heroes: [HeroBattle] = [],
heroIndex: Int = 0,
hotBattles: [HotBattle] = []
) {
self.isLoading = isLoading
// …
}
```

근거:
- 호출처는 `Feature.State()` 한 줄이면 충분 — 사용 시점에 노이즈 없음
- 초기값은 한 곳에서만 정의 — 프로퍼티 추가/제거 시 init 도 따라 고칠 일 없음
- 테스트/프리뷰에서 다른 값을 넣고 싶으면 `var state = Feature.State(); state.heroes = ... ` 로 직접 mutate
- Shared / Presents / AppStorage 도 동일하게 inline 으로 선언 (`@Shared(...) var foo: Foo = .empty`)

레퍼런스: `HomeFeature.State`, attendance `ProfileFeature.State`

#### ⚡ AsyncAction — `Result { try await }` + `mapError` + 단일 `Response` Inner 액션

`do/catch + 별도 Loaded / Failed 액션` 분리하지 말고, `Result` 로 감싸서 단일 `xxxResponse(Result<Success, AuthError>)` Inner 액션으로 보낸다. State 캡쳐는 `[키 = state.xxx]` 형태.

```swift
// ✅ 올바른 패턴
public enum InnerAction: Equatable {
case homeResponse(Result<HomeBundle, AuthError>)
}

case .fetchHome:
state.isLoading = true
return .run { [repository = homeRepository] send in
let result = await Result {
try await repository.fetchHome()
}
.mapError(AuthError.from)
return await send(.inner(.homeResponse(result)))
}
.cancellable(id: CancelID.fetchHome, cancelInFlight: true)

// 핸들러에서 한 자리에서 success/failure 분기
case let .homeResponse(result):
state.isLoading = false
switch result {
case let .success(bundle): /* state 갱신 */
case let .failure(error): Log.error("\(error.localizedDescription)")
}
return .none

// ❌ 금지 — do/catch 로 두 액션을 발사
return .run { send in
do {
let bundle = try await repository.fetchHome()
await send(.inner(.homeLoaded(bundle))) // ← 분리됨
} catch {
await send(.inner(.homeFailed(error.localizedDescription)))
}
}
```

규칙:
- 성공/실패 상태 머지 → 하나의 `xxxResponse(Result<Success, AuthError>)` 케이스
- 에러 타입은 `AuthError` 로 통일하고 `AuthError.from(_:)` 로 변환 (이미 Entity 에 정의됨)
- 캡쳐는 `[repository = self.repository, userSession = state.userSession]` 처럼 명시
- `.cancellable(id: CancelID.xxx, cancelInFlight: true)` 로 중복 호출 방지
- 레퍼런스: `AuthUseCaseImpl.withDraw` / `HomeFeature.fetchHome`

#### 🔌 RepositoryImpl — Provider 선언 패턴

Repository 구현체의 `MoyaProvider` 는 `let` 으로 직접 선언하고, init 기본값으로 `.default` / `.authorized` 팩토리를 그대로 사용한다. `Optional + nil 합치기`나 `MoyaProviderPool` 인다이렉션 금지.

```swift
// ✅ 올바른 패턴 — 단일 provider (인증 필요)
public final class HomeRepositoryImpl: HomeInterface, @unchecked Sendable {
private let provider: MoyaProvider<HomeService>

public init(
provider: MoyaProvider<HomeService> = MoyaProvider<HomeService>.authorized
) {
self.provider = provider
}
}

// ✅ 올바른 패턴 — default + authorized 두 개 필요 (로그인/로그아웃 분리)
public final class AuthRepositoryImpl: AuthInterface, @unchecked Sendable {
private let provider: MoyaProvider<AuthService>
private let authProvider: MoyaProvider<AuthService>

public init(
provider: MoyaProvider<AuthService> = MoyaProvider<AuthService>.default,
authProvider: MoyaProvider<AuthService> = MoyaProvider<AuthService>.authorized
) {
self.provider = provider
self.authProvider = authProvider
}
}

// ❌ 금지 — Optional + nil 합치기 + Pool 인다이렉션
public init(
provider: MoyaProvider<AuthService>? = nil,
authProvider: MoyaProvider<AuthService>? = nil
) {
self.provider = provider ?? MoyaProviderPool.shared.defaultProvider(for: AuthService.self)
self.authProvider = authProvider ?? MoyaProviderPool.shared.authorizedProvider(for: AuthService.self)
}
```

규칙:
- 토큰 인증이 필요한 API → `.authorized` (OptimizedSessionManager 인터셉터 부착)
- 로그인/회원가입 같이 헤더 없는 API → `.default` (로그 플러그인만)
- 테스트/프리뷰는 Mock provider 를 init 으로 그대로 주입 — `MockProvider` 변수 따로 둘 필요 없음
- `MoyaProviderPool` 은 더 이상 RepositoryImpl 에서 직접 호출하지 않는다 (필요 시 풀 자체에서 내부적으로 캐시 처리)
- 레퍼런스: `HomeRepositoryImpl`, `AuthRepositoryImpl`, AsyncMoya `MoyaProvider+Factory.default`, `Extension+MoyaProvider+Auth.authorized`

### 📏 Swift 코딩 규칙 (`docs/agent/swift-coding-rules.md`)
- Swift 스타일 가이드
- 에러 처리 패턴
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import ProjectDescription
public extension TargetDependency.SPM {
static let asyncMoya = TargetDependency.external(name: "AsyncMoya", condition: .none)
static let logMarco = TargetDependency.external(name: "LogMacro", condition: .none)

static let composableArchitecture = TargetDependency.external(name: "ComposableArchitecture", condition: .none)
static let dependencies = TargetDependency.external(name: "Dependencies", condition: .none)
static let identifiedCollections = TargetDependency.external(name: "IdentifiedCollections", condition: .none)
static let tcaFlow = TargetDependency.external(name: "TCAFlow", condition: .none)
static let concurrencyExtras = TargetDependency.external(name: "ConcurrencyExtras", condition: .none)
static let sdwebImage = TargetDependency.external(name: "SDWebImageSwiftUI", condition: .none)
static let kingfisher = TargetDependency.external(name: "Kingfisher", condition: .none)
static let weaveDI = TargetDependency.external(name: "WeaveDI", condition: .none)

static let googleSignIn = TargetDependency.external(name: "GoogleSignIn", condition: .none)
static let appAuth: TargetDependency = .external(name: "AppAuth")
static let firebaseCrashlytics = TargetDependency.external(name: "FirebaseCrashlytics", condition: .none)
Expand All @@ -27,5 +28,4 @@ public extension TargetDependency.SPM {
static let googleMobileAds = TargetDependency.external(name: "GoogleMobileAds", condition: .none)
static let mixpanel = TargetDependency.external(name: "Mixpanel", condition: .none)
static let mixpanelSessionReplay = TargetDependency.external(name: "MixpanelSessionReplay", condition: .none)

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public extension ModulePath {
public static let name: String = "Presentation"

case Auth
case MainTab
case Home
}
}

Expand Down
1 change: 1 addition & 0 deletions Projects/App/Sources/Di/DiRegister.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public final class AppDIManager: Sendable {

// 🏗️ Repository 계층 (Clean Architecture + PFW)
.register { AuthRepositoryImpl() as AuthInterface }
.register { HomeRepositoryImpl() as HomeInterface }
// .register { ProfileRepositoryImpl() as ProfileInterface }
// .register { AppUpdateRepositoryImpl() as AppUpdateInterface }

Expand Down
Loading
Loading