Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 69 additions & 1 deletion .claude/rules/lessons-learned.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,5 +196,73 @@ MyPage: AuthInterceptor.retry → RefreshCoordinator (기다림)

---

## 6. TCA StackAction.popFrom 타이밍 (2026-02-25)

### ❌ 잘못된 접근: isEmpty로 root 복귀 체크

```swift
// 하지 말 것!
case .path(.popFrom(id: _)):
if state.path.isEmpty { // pop 완료 전 시점이므로 항상 false
return .send(.delegate(.showTabBar))
}
return .none
```

**문제점**:
- `.popFrom`은 pop **시작** 시점에 호출됨 (pop 완료 전)
- 1depth → root로 pop 시:
- `.popFrom` 호출 시점: `path.count = 1` (아직 pop 전)
- `isEmpty` 체크 → `false` ❌
- pop 완료 후: `path.count = 0`
- 결과: root로 돌아가도 TabBar가 표시되지 않음

### ✅ 올바른 접근: count == 1로 root 복귀 체크

```swift
case .path(.popFrom(id: _)):
// popFrom은 pop 시작 시점이므로 count == 1이면 root로 돌아감
if state.path.count == 1 {
return .send(.delegate(.showTabBar))
}
return .none
```

**타임라인**:
```
1depth → root로 pop:
1. .popFrom 호출 → path.count = 1 ✅
2. count == 1 체크 → true
3. TabBar 표시 액션 전송
4. pop 완료 → path.count = 0

2depth → 1depth로 pop:
1. .popFrom 호출 → path.count = 2
2. count == 1 체크 → false
3. TabBar 숨김 유지
4. pop 완료 → path.count = 1
```

**핵심**:
- **`.popFrom`은 pop 시작 시점, pop 완료 전**
- `path.count == 1`로 체크해야 root 복귀 감지 가능
- `isEmpty`는 절대 true가 될 수 없음 (pop 전이므로)

**참고**: TCA Navigation 문서 - StackAction lifecycle

---

## 요약

| 항목 | ❌ 하지 말 것 | ✅ 해야 할 것 |
|------|-------------|-------------|
| **토큰 검증** | 클라이언트에서 만료 시간 체크 | 서버 401 응답에 반응 |
| **Splash** | refresh 시도 | 토큰 존재만 체크 |
| **AuthInterceptor** | 메인 Session 사용 | 별도 Session 사용 |
| **복잡도** | 과도한 최적화 | 단순하고 검증된 방법 |
| **StackAction.popFrom** | isEmpty로 체크 | count == 1로 체크 |

---

**업데이트 일자**: 2026-02-25
**관련 이슈**: 자동 로그인 기능 구현
**관련 이슈**: 자동 로그인 기능 구현, History Stack 네비게이션 전환
86 changes: 86 additions & 0 deletions Projects/App/Sources/CustomTabBar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// CustomTabBar.swift
// DoriApp
//
// Created by 강동영 on 2/25/26.
// Copyright © 2026 com.arex. All rights reserved.
//

import SwiftUI
import DoriDesignSystem

// MARK: - Custom TabBar

struct CustomTabBar: View {
@Binding var selectedTab: MainTabFeature.State.Tab

var body: some View {
HStack(spacing: 0) {
TabBarItem(
icon: selectedTab == .calendar ? Image(.tabbarCalendarFill) : Image(.tabbarCalendar),
title: "캘린더",
isSelected: selectedTab == .calendar
) {
selectedTab = .calendar
}

TabBarItem(
icon: selectedTab == .history ? Image(.tabbarHistoryFill) : Image(.tabbarHistory),
title: "내역",
isSelected: selectedTab == .history
) {
selectedTab = .history
}

TabBarItem(
icon: selectedTab == .myPage ? Image(.tabbarMypageFill) : Image(.tabbarMypage),
title: "마이페이지",
isSelected: selectedTab == .myPage
) {
selectedTab = .myPage
}
}
.background(.doriWhite)
.shadow(
color: .black.opacity(0.05),
radius: 8,
x: 0,
y: -2
)
}
}

// MARK: - TabBarItem

private struct TabBarItem: View {
let icon: Image
let title: String
let isSelected: Bool
let action: () -> Void

var font: TypoToken {
isSelected ? .bold(.b11) : .regular(.r11)
}

var body: some View {
Button(action: action) {
VStack(spacing: 4) {
icon
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundStyle(isSelected ? .main : .grey600)

Text(title)
.pretendard(font)
.foregroundStyle(isSelected ? .main : .grey600)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
.padding(.horizontal, 20)
.padding(.top, 16)
.buttonStyle(.plain)
}
}
41 changes: 29 additions & 12 deletions Projects/App/Sources/MainTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ struct MainTabFeature {
enum Tab: Equatable {
case calendar, history, myPage
}

// 파생 상태: 모든 탭이 root depth면 TabBar 표시
var isTabBarVisible: Bool {
history.path.isEmpty &&
calendar.addDori == nil &&
myPage.navigationPath.isEmpty
}
}

enum Action {
Expand All @@ -31,7 +38,7 @@ struct MainTabFeature {
case history(HistoryFeature.Action)
case myPage(MyPageFeature.Action)
case delegate(Delegate)

enum Delegate: Equatable {
case needsAuthentication
}
Expand Down Expand Up @@ -73,19 +80,29 @@ struct MainTabView: View {
@Bindable var store: StoreOf<MainTabFeature>

var body: some View {
TabView(selection: $store.selectedTab.sending(\.tabSelected)) {
CalendarView(store: store.scope(state: \.calendar, action: \.calendar))
.tag(MainTabFeature.State.Tab.calendar)
.tabItem { Label("캘린더", systemImage: "calendar") }

HistoryView(store: store.scope(state: \.history, action: \.history))
.tag(MainTabFeature.State.Tab.history)
.tabItem { Label("내역", systemImage: "list.bullet.rectangle") }
VStack(spacing: 0) {
// Content
Group {
switch store.selectedTab {
case .calendar:
CalendarView(store: store.scope(state: \.calendar, action: \.calendar))
case .history:
HistoryView(store: store.scope(state: \.history, action: \.history))
case .myPage:
MyPageView(store: store.scope(state: \.myPage, action: \.myPage))
}
}

MyPageView(store: store.scope(state: \.myPage, action: \.myPage))
.tag(MainTabFeature.State.Tab.myPage)
.tabItem { Label("마이페이지", systemImage: "person.circle") }
// TabBar
if store.isTabBarVisible {
CustomTabBar(
selectedTab: $store.selectedTab.sending(\.tabSelected)
)
.transition(.move(edge: .bottom))
}
}
.ignoresSafeArea(.keyboard)
.animation(.easeInOut(duration: 0.2), value: store.isTabBarVisible)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "tabbar_calendar.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "tabbar_calendar_fill.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "tabbar_history.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "tabbar_history_fill.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "tabbar_mypage.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"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