timsquad 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +288 -0
- package/README.md +158 -151
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +170 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +95 -5
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/full.js +1 -0
- package/dist/commands/full.js.map +1 -1
- package/dist/commands/git/pr.js +6 -5
- package/dist/commands/git/pr.js.map +1 -1
- package/dist/commands/git/release.js +2 -7
- package/dist/commands/git/release.js.map +1 -1
- package/dist/commands/improve.js +2 -2
- package/dist/commands/improve.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/log.d.ts.map +1 -1
- package/dist/commands/log.js +2 -2
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +6 -2
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/retro.js +8 -8
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/session.js +3 -3
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/skills.d.ts +12 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +228 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +23 -1
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/daemon/entry.js +3 -3
- package/dist/daemon/entry.js.map +1 -1
- package/dist/daemon/event-queue.d.ts.map +1 -1
- package/dist/daemon/event-queue.js +2 -2
- package/dist/daemon/event-queue.js.map +1 -1
- package/dist/daemon/index.d.ts +4 -2
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +214 -52
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jsonl-watcher.d.ts +1 -0
- package/dist/daemon/jsonl-watcher.d.ts.map +1 -1
- package/dist/daemon/jsonl-watcher.js.map +1 -1
- package/dist/daemon/meta-cache.d.ts +1 -0
- package/dist/daemon/meta-cache.d.ts.map +1 -1
- package/dist/daemon/meta-cache.js +9 -0
- package/dist/daemon/meta-cache.js.map +1 -1
- package/dist/daemon/session-notes.d.ts +33 -0
- package/dist/daemon/session-notes.d.ts.map +1 -0
- package/dist/daemon/session-notes.js +74 -0
- package/dist/daemon/session-notes.js.map +1 -0
- package/dist/daemon/session-state.d.ts +27 -0
- package/dist/daemon/session-state.d.ts.map +1 -0
- package/dist/daemon/session-state.js +165 -0
- package/dist/daemon/session-state.js.map +1 -0
- package/dist/daemon/shutdown.d.ts.map +1 -1
- package/dist/daemon/shutdown.js +9 -1
- package/dist/daemon/shutdown.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-generator.d.ts +4 -0
- package/dist/lib/agent-generator.d.ts.map +1 -1
- package/dist/lib/agent-generator.js +52 -3
- package/dist/lib/agent-generator.js.map +1 -1
- package/dist/lib/compile-rules.d.ts +66 -0
- package/dist/lib/compile-rules.d.ts.map +1 -0
- package/dist/lib/compile-rules.js +114 -0
- package/dist/lib/compile-rules.js.map +1 -0
- package/dist/lib/compiler.d.ts +105 -0
- package/dist/lib/compiler.d.ts.map +1 -0
- package/dist/lib/compiler.js +368 -0
- package/dist/lib/compiler.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/project.d.ts.map +1 -1
- package/dist/lib/project.js +8 -3
- package/dist/lib/project.js.map +1 -1
- package/dist/lib/skill-generator.d.ts.map +1 -1
- package/dist/lib/skill-generator.js +22 -1
- package/dist/lib/skill-generator.js.map +1 -1
- package/dist/lib/template.d.ts.map +1 -1
- package/dist/lib/template.js +6 -0
- package/dist/lib/template.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +12 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +2 -0
- package/dist/types/project.js.map +1 -1
- package/package.json +4 -4
- package/templates/base/agents/base/tsq-architect.md +2 -2
- package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
- package/templates/base/knowledge/checklists/plan-quality.md +31 -0
- package/templates/base/knowledge/checklists/stability-verification.md +14 -0
- package/templates/base/skills/controller/SKILL.md +111 -0
- package/templates/base/skills/controller/references/README.md +35 -0
- package/templates/base/skills/controller/rules/README.md +18 -0
- package/templates/base/skills/mobile/dart/SKILL.md +69 -0
- package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
- package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
- package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
- package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
- package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
- package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
- package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
- package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
- package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
- package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
- package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
- package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
- package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
- package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
- package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
- package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
- package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
- package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
- package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
- package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
- package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
- package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
- package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
- package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
- package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
- package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
- package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
- package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
- package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
- package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
- package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
- package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
- package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
- package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
- package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
- package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
- package/templates/base/skills/stability-verification/SKILL.md +64 -0
- package/templates/base/skills/stability-verification/references/release-checklist.md +34 -0
- package/templates/base/skills/stability-verification/references/security-fix-patterns.md +112 -0
- package/templates/base/skills/stability-verification/rules/verification-layers.md +67 -0
- package/templates/base/skills/stability-verification/rules/verification-workflow.md +69 -0
- package/templates/base/skills/stability-verification/scripts/verify.sh +294 -0
- package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
- package/templates/platforms/claude-code/rules/build-gate.md +28 -0
- package/templates/platforms/claude-code/rules/completion-verification.md +30 -0
- package/templates/platforms/claude-code/rules/context-monitor.md +23 -0
- package/templates/platforms/claude-code/rules/plan-review.md +45 -0
- package/templates/platforms/claude-code/rules/quality-guards.md +43 -0
- package/templates/platforms/claude-code/rules/session-notes.md +18 -0
- package/templates/platforms/claude-code/rules/skill-suggest.md +27 -0
- package/templates/platforms/claude-code/scripts/build-gate.sh +73 -0
- package/templates/platforms/claude-code/scripts/completion-guard.sh +93 -0
- package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
- package/templates/platforms/claude-code/scripts/safe-guard.sh +83 -0
- package/templates/platforms/claude-code/scripts/skill-rules.json +85 -0
- package/templates/platforms/claude-code/scripts/skill-suggest.sh +105 -0
- package/templates/platforms/claude-code/settings.json +111 -3
- package/templates/project-types/mobile-app/config.yaml +123 -0
- package/templates/project-types/mobile-app/process/workflow.xml +191 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: API Client Architecture
|
|
3
|
+
category: reference
|
|
4
|
+
source: internal
|
|
5
|
+
tags: architecture, directory, provider, repository, mock, testing
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# API Client Architecture
|
|
9
|
+
|
|
10
|
+
네트워크 레이어 전체 아키텍처. 디렉토리 구조, Provider 그래프, Repository 패턴, Mock 테스트 전략.
|
|
11
|
+
|
|
12
|
+
## Key Concepts
|
|
13
|
+
|
|
14
|
+
- **중앙화**: 네트워크 관련 코드를 `core/networking/` 에 집중 (feature 횡단 관심사)
|
|
15
|
+
- **계층 분리**: Dio → Interceptor → API Client → Repository → Provider → UI
|
|
16
|
+
- **테스트 가능성**: API Client 추상화로 Mock 구현 용이
|
|
17
|
+
- **오프라인 우선**: 캐시 + 큐 + connectivity 감지를 기본 탑재
|
|
18
|
+
|
|
19
|
+
## Directory Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
lib/
|
|
23
|
+
├── core/
|
|
24
|
+
│ └── networking/
|
|
25
|
+
│ ├── dio_provider.dart # Dio 싱글톤 Provider
|
|
26
|
+
│ ├── app_config.dart # 환경 설정 (baseUrl 등)
|
|
27
|
+
│ ├── interceptors/
|
|
28
|
+
│ │ ├── auth_interceptor.dart # 토큰 주입 + 401 리프레시
|
|
29
|
+
│ │ ├── retry_interceptor.dart # 지수 백오프 재시도
|
|
30
|
+
│ │ ├── logging_interceptor.dart # 요청/응답 로깅
|
|
31
|
+
│ │ └── error_transform_interceptor.dart # DioException → NetworkFailure
|
|
32
|
+
│ ├── errors/
|
|
33
|
+
│ │ ├── network_failure.dart # sealed class 에러 타입
|
|
34
|
+
│ │ └── result.dart # Result<T> 타입
|
|
35
|
+
│ ├── cache/
|
|
36
|
+
│ │ ├── cache_config.dart # 캐시 정책 팩토리
|
|
37
|
+
│ │ ├── cache_store_provider.dart # HiveCacheStore Provider
|
|
38
|
+
│ │ └── cache_manager.dart # 캐시 클리어/무효화
|
|
39
|
+
│ ├── connectivity/
|
|
40
|
+
│ │ ├── connectivity_notifier.dart # 연결 상태 스트림
|
|
41
|
+
│ │ ├── offline_queue.dart # 오프라인 요청 큐
|
|
42
|
+
│ │ └── offline_banner.dart # 오프라인 UI 위젯
|
|
43
|
+
│ └── api/
|
|
44
|
+
│ ├── user_api.dart # @RestApi 클라이언트
|
|
45
|
+
│ ├── match_api.dart # @RestApi 클라이언트
|
|
46
|
+
│ └── ...
|
|
47
|
+
│
|
|
48
|
+
├── features/
|
|
49
|
+
│ └── user/
|
|
50
|
+
│ ├── data/
|
|
51
|
+
│ │ ├── models/
|
|
52
|
+
│ │ │ ├── user_response.dart # freezed 응답 모델
|
|
53
|
+
│ │ │ └── create_user_request.dart # freezed 요청 모델
|
|
54
|
+
│ │ └── repositories/
|
|
55
|
+
│ │ └── user_repository_impl.dart # API 호출 + 에러 변환
|
|
56
|
+
│ ├── domain/
|
|
57
|
+
│ │ ├── entities/
|
|
58
|
+
│ │ │ └── user.dart # 도메인 엔티티
|
|
59
|
+
│ │ └── repositories/
|
|
60
|
+
│ │ └── user_repository.dart # abstract
|
|
61
|
+
│ └── presentation/
|
|
62
|
+
│ └── providers/
|
|
63
|
+
│ └── user_provider.dart # UI 상태 관리
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Provider Dependency Graph
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
AppConfig
|
|
70
|
+
│
|
|
71
|
+
▼
|
|
72
|
+
DioProvider ──────────────────────────────────┐
|
|
73
|
+
│ │
|
|
74
|
+
├─ AuthInterceptor ← TokenStorage │
|
|
75
|
+
├─ RetryInterceptor │
|
|
76
|
+
├─ LoggingInterceptor (dev only) │
|
|
77
|
+
├─ ErrorTransformInterceptor │
|
|
78
|
+
└─ DioCacheInterceptor ← CacheStore │
|
|
79
|
+
│
|
|
80
|
+
┌──────────────────────────────────────────┘
|
|
81
|
+
│
|
|
82
|
+
▼
|
|
83
|
+
API Clients (Retrofit)
|
|
84
|
+
│ UserApi, MatchApi, ...
|
|
85
|
+
│
|
|
86
|
+
▼
|
|
87
|
+
Repositories
|
|
88
|
+
│ UserRepository, MatchRepository, ...
|
|
89
|
+
│ (API 호출 + Result<T> 반환)
|
|
90
|
+
│
|
|
91
|
+
▼
|
|
92
|
+
Notifiers / Providers
|
|
93
|
+
│ UserNotifier, MatchNotifier, ...
|
|
94
|
+
│ (비즈니스 로직 + UI 상태)
|
|
95
|
+
│
|
|
96
|
+
▼
|
|
97
|
+
Widgets (UI)
|
|
98
|
+
|
|
99
|
+
ConnectivityNotifier ────► OfflineQueueManager
|
|
100
|
+
│ │
|
|
101
|
+
└── OfflineBanner (UI) └── 연결 복구 시 큐 처리
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Provider 코드
|
|
105
|
+
|
|
106
|
+
```dart
|
|
107
|
+
// core/networking/providers.dart — 네트워크 레이어 Provider 모음
|
|
108
|
+
|
|
109
|
+
/// 앱 설정
|
|
110
|
+
final appConfigProvider = Provider<AppConfig>((ref) => AppConfig.dev);
|
|
111
|
+
|
|
112
|
+
/// Dio 싱글톤
|
|
113
|
+
final dioProvider = Provider<Dio>((ref) {
|
|
114
|
+
final config = ref.watch(appConfigProvider);
|
|
115
|
+
final dio = Dio(BaseOptions(
|
|
116
|
+
baseUrl: config.baseUrl,
|
|
117
|
+
connectTimeout: const Duration(seconds: 15),
|
|
118
|
+
receiveTimeout: const Duration(seconds: 15),
|
|
119
|
+
sendTimeout: const Duration(seconds: 15),
|
|
120
|
+
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
|
|
121
|
+
));
|
|
122
|
+
|
|
123
|
+
dio.interceptors.addAll([
|
|
124
|
+
ref.watch(authInterceptorProvider),
|
|
125
|
+
ref.watch(retryInterceptorProvider),
|
|
126
|
+
if (config.environment == AppEnvironment.dev)
|
|
127
|
+
ref.watch(loggingInterceptorProvider),
|
|
128
|
+
ref.watch(errorTransformInterceptorProvider),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
return dio;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/// API Clients
|
|
135
|
+
final userApiProvider = Provider<UserApi>((ref) {
|
|
136
|
+
return UserApi(ref.watch(dioProvider));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/// Repositories
|
|
140
|
+
final userRepositoryProvider = Provider<UserRepository>((ref) {
|
|
141
|
+
return UserRepositoryImpl(api: ref.watch(userApiProvider));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/// Connectivity
|
|
145
|
+
final connectivityProvider =
|
|
146
|
+
StreamNotifierProvider<ConnectivityNotifier, NetworkStatus>(
|
|
147
|
+
ConnectivityNotifier.new,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
final isOnlineProvider = Provider<bool>((ref) {
|
|
151
|
+
return ref.watch(connectivityProvider).valueOrNull == NetworkStatus.online;
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Repository Pattern
|
|
156
|
+
|
|
157
|
+
```dart
|
|
158
|
+
/// Repository 추상 (domain 레이어)
|
|
159
|
+
abstract class UserRepository {
|
|
160
|
+
Future<Result<User>> getUser(String id);
|
|
161
|
+
Future<Result<List<User>>> getUsers({int page = 1});
|
|
162
|
+
Future<Result<User>> createUser(CreateUserRequest request);
|
|
163
|
+
Future<Result<void>> deleteUser(String id);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Repository 구현 (data 레이어)
|
|
167
|
+
class UserRepositoryImpl implements UserRepository {
|
|
168
|
+
final UserApi _api;
|
|
169
|
+
|
|
170
|
+
UserRepositoryImpl({required UserApi api}) : _api = api;
|
|
171
|
+
|
|
172
|
+
@override
|
|
173
|
+
Future<Result<User>> getUser(String id) async {
|
|
174
|
+
try {
|
|
175
|
+
final response = await _api.getUser(id);
|
|
176
|
+
return Success(response.toDomain());
|
|
177
|
+
} on DioException catch (e) {
|
|
178
|
+
return Failure(NetworkFailure.fromDioException(e));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@override
|
|
183
|
+
Future<Result<List<User>>> getUsers({int page = 1}) async {
|
|
184
|
+
try {
|
|
185
|
+
final response = await _api.getUsers(page, 20);
|
|
186
|
+
return Success(response.data.map((r) => r.toDomain()).toList());
|
|
187
|
+
} on DioException catch (e) {
|
|
188
|
+
return Failure(NetworkFailure.fromDioException(e));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ... createUser, deleteUser 동일 패턴
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Mock Testing Strategy
|
|
197
|
+
|
|
198
|
+
```dart
|
|
199
|
+
/// Mock API Client
|
|
200
|
+
class MockUserApi implements UserApi {
|
|
201
|
+
@override
|
|
202
|
+
Future<UserResponse> getUser(String id) async {
|
|
203
|
+
return UserResponse(
|
|
204
|
+
id: id,
|
|
205
|
+
name: 'Test User',
|
|
206
|
+
email: 'test@example.com',
|
|
207
|
+
createdAt: DateTime.now(),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@override
|
|
212
|
+
Future<PaginatedResponse<UserResponse>> getUsers(int page, int limit) async {
|
|
213
|
+
return PaginatedResponse(
|
|
214
|
+
data: List.generate(limit, (i) => UserResponse(
|
|
215
|
+
id: 'user_$i',
|
|
216
|
+
name: 'User $i',
|
|
217
|
+
email: 'user$i@example.com',
|
|
218
|
+
createdAt: DateTime.now(),
|
|
219
|
+
)),
|
|
220
|
+
total: 100,
|
|
221
|
+
page: page,
|
|
222
|
+
lastPage: 5,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ...
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// 테스트에서 Provider 오버라이드
|
|
230
|
+
void main() {
|
|
231
|
+
group('UserNotifier', () {
|
|
232
|
+
late ProviderContainer container;
|
|
233
|
+
|
|
234
|
+
setUp(() {
|
|
235
|
+
container = ProviderContainer(overrides: [
|
|
236
|
+
userApiProvider.overrideWithValue(MockUserApi()),
|
|
237
|
+
]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
tearDown(() => container.dispose());
|
|
241
|
+
|
|
242
|
+
test('loads user successfully', () async {
|
|
243
|
+
final repository = container.read(userRepositoryProvider);
|
|
244
|
+
final result = await repository.getUser('user_1');
|
|
245
|
+
|
|
246
|
+
expect(result, isA<Success<User>>());
|
|
247
|
+
expect((result as Success).data.name, 'Test User');
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// 에러 시나리오 Mock
|
|
253
|
+
class ErrorUserApi implements UserApi {
|
|
254
|
+
@override
|
|
255
|
+
Future<UserResponse> getUser(String id) async {
|
|
256
|
+
throw DioException(
|
|
257
|
+
requestOptions: RequestOptions(path: '/users/$id'),
|
|
258
|
+
type: DioExceptionType.connectionTimeout,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ...
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// 에러 테스트
|
|
266
|
+
test('returns TimeoutFailure on connection timeout', () async {
|
|
267
|
+
final container = ProviderContainer(overrides: [
|
|
268
|
+
userApiProvider.overrideWithValue(ErrorUserApi()),
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
final repository = container.read(userRepositoryProvider);
|
|
272
|
+
final result = await repository.getUser('user_1');
|
|
273
|
+
|
|
274
|
+
expect(result, isA<Failure<User>>());
|
|
275
|
+
expect((result as Failure).failure, isA<TimeoutFailure>());
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Initialization Flow
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
앱 시작 (main.dart)
|
|
283
|
+
│
|
|
284
|
+
├─ 1. WidgetsFlutterBinding.ensureInitialized()
|
|
285
|
+
├─ 2. AppConfig 결정 (환경 변수 / 빌드 플래그)
|
|
286
|
+
├─ 3. CacheStore 초기화 (HiveCacheStore)
|
|
287
|
+
├─ 4. ProviderScope(overrides: [appConfig, cacheStore])
|
|
288
|
+
└─ 5. runApp()
|
|
289
|
+
│
|
|
290
|
+
└─ App 위젯 build
|
|
291
|
+
├─ DioProvider 자동 생성 (lazy)
|
|
292
|
+
├─ ConnectivityNotifier 스트림 시작
|
|
293
|
+
└─ OfflineBanner 조건부 표시
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Common Pitfalls
|
|
297
|
+
|
|
298
|
+
1. **Dio 다중 인스턴스**: Feature마다 Dio 생성 → 인터셉터 미적용, 토큰 누락
|
|
299
|
+
2. **인터셉터 순서**: Error Transform이 Auth 앞 → 401 리프레시 불가
|
|
300
|
+
3. **토큰 리프레시 순환**: Auth 인터셉터가 메인 Dio로 리프레시 → 무한 루프
|
|
301
|
+
4. **캐시 + 인증 에러**: 401 응답 캐시 → 로그인 후에도 에러 반환
|
|
302
|
+
5. **오프라인 큐 영속성**: 메모리 큐만 사용 → 앱 재시작 시 유실
|
|
303
|
+
6. **connectTimeout vs receiveTimeout**: connect는 TCP 연결, receive는 데이터 수신 — 둘 다 설정 필수
|
|
304
|
+
7. **validateStatus 미설정**: 4xx도 DioException → 정상 에러 응답 처리 불가
|
|
305
|
+
8. **build_runner 미실행**: Retrofit `.g.dart` 미생성 → 컴파일 에러
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: HTTP Caching Strategy
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: "캐시 미사용 → 불필요한 네트워크 호출, 느린 로딩, 데이터 요금 낭비"
|
|
5
|
+
tags: cache, dio-cache-interceptor, etag, offline, performance
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## HTTP Caching Strategy
|
|
9
|
+
|
|
10
|
+
**Impact: MEDIUM (캐시 미사용 → 불필요한 네트워크 호출, 느린 로딩, 데이터 요금 낭비)**
|
|
11
|
+
|
|
12
|
+
dio_cache_interceptor로 HTTP 응답 캐시.
|
|
13
|
+
ETag/Last-Modified 검증, 오프라인 fallback, 정책별 캐시 전략.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
dio: ^5.7.0
|
|
21
|
+
dio_cache_interceptor: ^3.5.0
|
|
22
|
+
dio_cache_interceptor_hive_store: ^3.2.0 # 영구 저장소
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 캐시 인터셉터 설정
|
|
26
|
+
|
|
27
|
+
**Incorrect (캐시 없이 매번 네트워크 호출):**
|
|
28
|
+
```dart
|
|
29
|
+
final dio = Dio();
|
|
30
|
+
// → 동일 데이터 반복 요청, 배터리 + 데이터 낭비
|
|
31
|
+
// → 오프라인 시 데이터 표시 불가
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct (dio_cache_interceptor 통합):**
|
|
35
|
+
```dart
|
|
36
|
+
/// 캐시 저장소 초기화
|
|
37
|
+
Future<CacheStore> initCacheStore() async {
|
|
38
|
+
final dir = await getApplicationDocumentsDirectory();
|
|
39
|
+
return HiveCacheStore(
|
|
40
|
+
'${dir.path}/http_cache',
|
|
41
|
+
hiveBoxName: 'dio_cache',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// 캐시 옵션 팩토리
|
|
46
|
+
class CacheConfig {
|
|
47
|
+
/// 기본 캐시 정책 — 네트워크 우선, 실패 시 캐시
|
|
48
|
+
static CacheOptions defaultPolicy(CacheStore store) {
|
|
49
|
+
return CacheOptions(
|
|
50
|
+
store: store,
|
|
51
|
+
policy: CachePolicy.request, // 네트워크 요청 + 캐시 갱신
|
|
52
|
+
maxStale: const Duration(days: 7), // 오프라인 시 7일간 캐시 유효
|
|
53
|
+
hitCacheOnErrorExcept: [401, 403], // 에러 시 캐시 반환 (인증 에러 제외)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// 강제 캐시 — 네트워크 미사용 (빠른 로딩)
|
|
58
|
+
static CacheOptions forceCache(CacheStore store) {
|
|
59
|
+
return CacheOptions(
|
|
60
|
+
store: store,
|
|
61
|
+
policy: CachePolicy.forceCache,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// 캐시 무시 — 항상 최신 데이터
|
|
66
|
+
static CacheOptions noCache(CacheStore store) {
|
|
67
|
+
return CacheOptions(
|
|
68
|
+
store: store,
|
|
69
|
+
policy: CachePolicy.noCache,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// stale-while-revalidate — 캐시 먼저 반환, 백그라운드 갱신
|
|
74
|
+
static CacheOptions refreshIfStale(CacheStore store) {
|
|
75
|
+
return CacheOptions(
|
|
76
|
+
store: store,
|
|
77
|
+
policy: CachePolicy.refreshForceCache,
|
|
78
|
+
maxStale: const Duration(hours: 1),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Dio에 캐시 인터셉터 추가
|
|
84
|
+
final cacheStoreProvider = FutureProvider<CacheStore>((ref) async {
|
|
85
|
+
return await initCacheStore();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
final cacheDioProvider = Provider<Dio>((ref) {
|
|
89
|
+
final dio = ref.watch(dioProvider);
|
|
90
|
+
final cacheStore = ref.watch(cacheStoreProvider).valueOrNull;
|
|
91
|
+
|
|
92
|
+
if (cacheStore != null) {
|
|
93
|
+
final cacheOptions = CacheConfig.defaultPolicy(cacheStore);
|
|
94
|
+
dio.interceptors.add(
|
|
95
|
+
DioCacheInterceptor(options: cacheOptions),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return dio;
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 요청별 캐시 정책 오버라이드
|
|
104
|
+
|
|
105
|
+
```dart
|
|
106
|
+
/// API에서 요청별 캐시 정책 지정
|
|
107
|
+
class ProductRepository {
|
|
108
|
+
final Dio _dio;
|
|
109
|
+
final CacheStore _cacheStore;
|
|
110
|
+
|
|
111
|
+
ProductRepository({required Dio dio, required CacheStore cacheStore})
|
|
112
|
+
: _dio = dio,
|
|
113
|
+
_cacheStore = cacheStore;
|
|
114
|
+
|
|
115
|
+
/// 상품 목록 — 기본 캐시 (네트워크 + 캐시 갱신)
|
|
116
|
+
Future<Result<List<Product>>> getProducts() async {
|
|
117
|
+
try {
|
|
118
|
+
final response = await _dio.get('/products');
|
|
119
|
+
return Success(_parseProducts(response.data));
|
|
120
|
+
} on DioException catch (e) {
|
|
121
|
+
return Failure(NetworkFailure.fromDioException(e));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// 상품 상세 — 캐시 우선 (자주 변경되지 않음)
|
|
126
|
+
Future<Result<Product>> getProduct(String id) async {
|
|
127
|
+
try {
|
|
128
|
+
final response = await _dio.get(
|
|
129
|
+
'/products/$id',
|
|
130
|
+
options: CacheConfig.refreshIfStale(_cacheStore).toOptions(),
|
|
131
|
+
);
|
|
132
|
+
return Success(Product.fromJson(response.data));
|
|
133
|
+
} on DioException catch (e) {
|
|
134
|
+
return Failure(NetworkFailure.fromDioException(e));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// 결제 정보 — 캐시 금지 (항상 최신)
|
|
139
|
+
Future<Result<PaymentInfo>> getPaymentInfo() async {
|
|
140
|
+
try {
|
|
141
|
+
final response = await _dio.get(
|
|
142
|
+
'/payment/info',
|
|
143
|
+
options: CacheConfig.noCache(_cacheStore).toOptions(),
|
|
144
|
+
);
|
|
145
|
+
return Success(PaymentInfo.fromJson(response.data));
|
|
146
|
+
} on DioException catch (e) {
|
|
147
|
+
return Failure(NetworkFailure.fromDioException(e));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### ETag / Last-Modified 활용
|
|
154
|
+
|
|
155
|
+
```dart
|
|
156
|
+
// dio_cache_interceptor가 자동 처리하는 HTTP 헤더:
|
|
157
|
+
//
|
|
158
|
+
// 서버 응답:
|
|
159
|
+
// ETag: "abc123"
|
|
160
|
+
// Last-Modified: Wed, 15 Jan 2026 10:00:00 GMT
|
|
161
|
+
// Cache-Control: max-age=3600
|
|
162
|
+
//
|
|
163
|
+
// 클라이언트 재요청 (자동):
|
|
164
|
+
// If-None-Match: "abc123"
|
|
165
|
+
// If-Modified-Since: Wed, 15 Jan 2026 10:00:00 GMT
|
|
166
|
+
//
|
|
167
|
+
// 서버 응답:
|
|
168
|
+
// 304 Not Modified → 캐시된 데이터 사용 (전송 비용 없음)
|
|
169
|
+
// 200 OK → 새 데이터로 캐시 갱신
|
|
170
|
+
|
|
171
|
+
// 서버가 ETag/Last-Modified를 지원하면 자동으로 최적화됨
|
|
172
|
+
// 별도 클라이언트 코드 필요 없음
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 캐시 관리
|
|
176
|
+
|
|
177
|
+
```dart
|
|
178
|
+
/// 캐시 클리어 (로그아웃 시, 디버깅 시)
|
|
179
|
+
class CacheManager {
|
|
180
|
+
final CacheStore _store;
|
|
181
|
+
|
|
182
|
+
CacheManager({required CacheStore store}) : _store = store;
|
|
183
|
+
|
|
184
|
+
/// 전체 캐시 클리어
|
|
185
|
+
Future<void> clearAll() async {
|
|
186
|
+
await _store.clean();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// 특정 URL 캐시 삭제
|
|
190
|
+
Future<void> invalidate(String url) async {
|
|
191
|
+
await _store.delete(CacheOptions.defaultCacheKeyBuilder(
|
|
192
|
+
RequestOptions(path: url),
|
|
193
|
+
));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
final cacheManagerProvider = Provider<CacheManager>((ref) {
|
|
198
|
+
final store = ref.watch(cacheStoreProvider).valueOrNull;
|
|
199
|
+
if (store == null) throw StateError('Cache store not initialized');
|
|
200
|
+
return CacheManager(store: store);
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 규칙
|
|
205
|
+
|
|
206
|
+
- GET 요청에만 캐시 적용 — POST/PUT/DELETE는 캐시 금지
|
|
207
|
+
- 기본 정책: 네트워크 우선 + 실패 시 캐시 fallback (`hitCacheOnErrorExcept`)
|
|
208
|
+
- 결제/인증 관련 API는 `CachePolicy.noCache` 강제
|
|
209
|
+
- `maxStale: 7일` — 오프라인 시 캐시 유효 기간
|
|
210
|
+
- 인증 에러(401, 403)는 캐시 반환 제외 — 만료 토큰으로 캐시된 데이터 방지
|
|
211
|
+
- 로그아웃 시 전체 캐시 클리어 — 사용자 데이터 잔류 방지
|
|
212
|
+
- HiveCacheStore 사용 — 앱 재시작 후에도 캐시 유지 (메모리 캐시보다 안정)
|