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.
Files changed (196) hide show
  1. package/README.ko.md +288 -0
  2. package/README.md +158 -151
  3. package/dist/commands/compile.d.ts +3 -0
  4. package/dist/commands/compile.d.ts.map +1 -0
  5. package/dist/commands/compile.js +170 -0
  6. package/dist/commands/compile.js.map +1 -0
  7. package/dist/commands/daemon.d.ts.map +1 -1
  8. package/dist/commands/daemon.js +95 -5
  9. package/dist/commands/daemon.js.map +1 -1
  10. package/dist/commands/full.js +1 -0
  11. package/dist/commands/full.js.map +1 -1
  12. package/dist/commands/git/pr.js +6 -5
  13. package/dist/commands/git/pr.js.map +1 -1
  14. package/dist/commands/git/release.js +2 -7
  15. package/dist/commands/git/release.js.map +1 -1
  16. package/dist/commands/improve.js +2 -2
  17. package/dist/commands/improve.js.map +1 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +12 -3
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/log.d.ts.map +1 -1
  22. package/dist/commands/log.js +2 -2
  23. package/dist/commands/log.js.map +1 -1
  24. package/dist/commands/metrics.d.ts.map +1 -1
  25. package/dist/commands/metrics.js +6 -2
  26. package/dist/commands/metrics.js.map +1 -1
  27. package/dist/commands/retro.js +8 -8
  28. package/dist/commands/retro.js.map +1 -1
  29. package/dist/commands/session.js +3 -3
  30. package/dist/commands/session.js.map +1 -1
  31. package/dist/commands/skills.d.ts +12 -0
  32. package/dist/commands/skills.d.ts.map +1 -0
  33. package/dist/commands/skills.js +228 -0
  34. package/dist/commands/skills.js.map +1 -0
  35. package/dist/commands/status.js +1 -1
  36. package/dist/commands/status.js.map +1 -1
  37. package/dist/commands/upgrade.d.ts.map +1 -1
  38. package/dist/commands/upgrade.js +23 -1
  39. package/dist/commands/upgrade.js.map +1 -1
  40. package/dist/daemon/entry.js +3 -3
  41. package/dist/daemon/entry.js.map +1 -1
  42. package/dist/daemon/event-queue.d.ts.map +1 -1
  43. package/dist/daemon/event-queue.js +2 -2
  44. package/dist/daemon/event-queue.js.map +1 -1
  45. package/dist/daemon/index.d.ts +4 -2
  46. package/dist/daemon/index.d.ts.map +1 -1
  47. package/dist/daemon/index.js +214 -52
  48. package/dist/daemon/index.js.map +1 -1
  49. package/dist/daemon/jsonl-watcher.d.ts +1 -0
  50. package/dist/daemon/jsonl-watcher.d.ts.map +1 -1
  51. package/dist/daemon/jsonl-watcher.js.map +1 -1
  52. package/dist/daemon/meta-cache.d.ts +1 -0
  53. package/dist/daemon/meta-cache.d.ts.map +1 -1
  54. package/dist/daemon/meta-cache.js +9 -0
  55. package/dist/daemon/meta-cache.js.map +1 -1
  56. package/dist/daemon/session-notes.d.ts +33 -0
  57. package/dist/daemon/session-notes.d.ts.map +1 -0
  58. package/dist/daemon/session-notes.js +74 -0
  59. package/dist/daemon/session-notes.js.map +1 -0
  60. package/dist/daemon/session-state.d.ts +27 -0
  61. package/dist/daemon/session-state.d.ts.map +1 -0
  62. package/dist/daemon/session-state.js +165 -0
  63. package/dist/daemon/session-state.js.map +1 -0
  64. package/dist/daemon/shutdown.d.ts.map +1 -1
  65. package/dist/daemon/shutdown.js +9 -1
  66. package/dist/daemon/shutdown.js.map +1 -1
  67. package/dist/index.js +4 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/lib/agent-generator.d.ts +4 -0
  70. package/dist/lib/agent-generator.d.ts.map +1 -1
  71. package/dist/lib/agent-generator.js +52 -3
  72. package/dist/lib/agent-generator.js.map +1 -1
  73. package/dist/lib/compile-rules.d.ts +66 -0
  74. package/dist/lib/compile-rules.d.ts.map +1 -0
  75. package/dist/lib/compile-rules.js +114 -0
  76. package/dist/lib/compile-rules.js.map +1 -0
  77. package/dist/lib/compiler.d.ts +105 -0
  78. package/dist/lib/compiler.d.ts.map +1 -0
  79. package/dist/lib/compiler.js +368 -0
  80. package/dist/lib/compiler.js.map +1 -0
  81. package/dist/lib/config.d.ts +1 -0
  82. package/dist/lib/config.d.ts.map +1 -1
  83. package/dist/lib/config.js +8 -1
  84. package/dist/lib/config.js.map +1 -1
  85. package/dist/lib/project.d.ts.map +1 -1
  86. package/dist/lib/project.js +8 -3
  87. package/dist/lib/project.js.map +1 -1
  88. package/dist/lib/skill-generator.d.ts.map +1 -1
  89. package/dist/lib/skill-generator.js +22 -1
  90. package/dist/lib/skill-generator.js.map +1 -1
  91. package/dist/lib/template.d.ts.map +1 -1
  92. package/dist/lib/template.js +6 -0
  93. package/dist/lib/template.js.map +1 -1
  94. package/dist/types/config.d.ts +1 -0
  95. package/dist/types/config.d.ts.map +1 -1
  96. package/dist/types/config.js +12 -1
  97. package/dist/types/config.js.map +1 -1
  98. package/dist/types/project.d.ts +1 -1
  99. package/dist/types/project.d.ts.map +1 -1
  100. package/dist/types/project.js +2 -0
  101. package/dist/types/project.js.map +1 -1
  102. package/package.json +4 -4
  103. package/templates/base/agents/base/tsq-architect.md +2 -2
  104. package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
  105. package/templates/base/knowledge/checklists/plan-quality.md +31 -0
  106. package/templates/base/knowledge/checklists/stability-verification.md +14 -0
  107. package/templates/base/skills/controller/SKILL.md +111 -0
  108. package/templates/base/skills/controller/references/README.md +35 -0
  109. package/templates/base/skills/controller/rules/README.md +18 -0
  110. package/templates/base/skills/mobile/dart/SKILL.md +69 -0
  111. package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
  112. package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
  113. package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
  114. package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
  115. package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
  116. package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
  117. package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
  118. package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
  119. package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
  120. package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
  121. package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
  122. package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
  123. package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
  124. package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
  125. package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
  126. package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
  127. package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
  128. package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
  129. package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
  130. package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
  131. package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
  132. package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
  133. package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
  134. package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
  135. package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
  136. package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
  137. package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
  138. package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
  139. package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
  140. package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
  141. package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
  142. package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
  143. package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
  144. package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
  145. package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
  146. package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
  147. package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
  148. package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
  149. package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
  150. package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
  151. package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
  152. package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
  153. package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
  154. package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
  155. package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
  156. package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
  157. package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
  158. package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
  159. package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
  160. package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
  161. package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
  162. package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
  163. package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
  164. package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
  165. package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
  166. package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
  167. package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
  168. package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
  169. package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
  170. package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
  171. package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
  172. package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
  173. package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
  174. package/templates/base/skills/stability-verification/SKILL.md +64 -0
  175. package/templates/base/skills/stability-verification/references/release-checklist.md +34 -0
  176. package/templates/base/skills/stability-verification/references/security-fix-patterns.md +112 -0
  177. package/templates/base/skills/stability-verification/rules/verification-layers.md +67 -0
  178. package/templates/base/skills/stability-verification/rules/verification-workflow.md +69 -0
  179. package/templates/base/skills/stability-verification/scripts/verify.sh +294 -0
  180. package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
  181. package/templates/platforms/claude-code/rules/build-gate.md +28 -0
  182. package/templates/platforms/claude-code/rules/completion-verification.md +30 -0
  183. package/templates/platforms/claude-code/rules/context-monitor.md +23 -0
  184. package/templates/platforms/claude-code/rules/plan-review.md +45 -0
  185. package/templates/platforms/claude-code/rules/quality-guards.md +43 -0
  186. package/templates/platforms/claude-code/rules/session-notes.md +18 -0
  187. package/templates/platforms/claude-code/rules/skill-suggest.md +27 -0
  188. package/templates/platforms/claude-code/scripts/build-gate.sh +73 -0
  189. package/templates/platforms/claude-code/scripts/completion-guard.sh +93 -0
  190. package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
  191. package/templates/platforms/claude-code/scripts/safe-guard.sh +83 -0
  192. package/templates/platforms/claude-code/scripts/skill-rules.json +85 -0
  193. package/templates/platforms/claude-code/scripts/skill-suggest.sh +105 -0
  194. package/templates/platforms/claude-code/settings.json +111 -3
  195. package/templates/project-types/mobile-app/config.yaml +123 -0
  196. package/templates/project-types/mobile-app/process/workflow.xml +191 -0
@@ -0,0 +1,213 @@
1
+ ---
2
+ title: Connectivity & Offline Queue
3
+ impact: HIGH
4
+ impactDescription: "오프라인 미처리 → 요청 유실, 무한 로딩, 사용자 혼란"
5
+ tags: connectivity, offline, queue, retry, connectivity-plus
6
+ ---
7
+
8
+ ## Connectivity & Offline Queue
9
+
10
+ **Impact: HIGH (오프라인 미처리 → 요청 유실, 무한 로딩, 사용자 혼란)**
11
+
12
+ connectivity_plus로 네트워크 상태 실시간 감지.
13
+ 오프라인 시 요청 큐잉, 연결 복구 시 자동 재시도.
14
+
15
+ ### 의존성
16
+
17
+ ```yaml
18
+ # pubspec.yaml
19
+ dependencies:
20
+ connectivity_plus: ^6.1.0
21
+ flutter_riverpod: ^2.6.0
22
+ ```
23
+
24
+ ### ConnectivityNotifier
25
+
26
+ **Incorrect (단발 체크만):**
27
+ ```dart
28
+ Future<bool> isOnline() async {
29
+ final result = await Connectivity().checkConnectivity();
30
+ return result != ConnectivityResult.none;
31
+ // → 상태 변화 미감지, UI 갱신 없음
32
+ }
33
+ ```
34
+
35
+ **Correct (Riverpod + 실시간 스트림):**
36
+ ```dart
37
+ /// 연결 상태 열거형
38
+ enum NetworkStatus { online, offline }
39
+
40
+ /// 연결 상태 Provider — 앱 전역 스트림
41
+ final connectivityProvider =
42
+ StreamNotifierProvider<ConnectivityNotifier, NetworkStatus>(
43
+ ConnectivityNotifier.new,
44
+ );
45
+
46
+ class ConnectivityNotifier extends StreamNotifier<NetworkStatus> {
47
+ @override
48
+ Stream<NetworkStatus> build() {
49
+ return Connectivity().onConnectivityChanged.map((results) {
50
+ final hasConnection = results.any((r) => r != ConnectivityResult.none);
51
+ return hasConnection ? NetworkStatus.online : NetworkStatus.offline;
52
+ });
53
+ }
54
+ }
55
+
56
+ /// 현재 온라인 여부 (동기 체크용)
57
+ final isOnlineProvider = Provider<bool>((ref) {
58
+ final status = ref.watch(connectivityProvider).valueOrNull;
59
+ return status == NetworkStatus.online;
60
+ });
61
+ ```
62
+
63
+ ### 오프라인 배너 UI
64
+
65
+ ```dart
66
+ /// 오프라인 상태 배너 — 앱 최상단에 배치
67
+ class OfflineBanner extends ConsumerWidget {
68
+ const OfflineBanner({super.key});
69
+
70
+ @override
71
+ Widget build(BuildContext context, WidgetRef ref) {
72
+ final isOnline = ref.watch(isOnlineProvider);
73
+
74
+ if (isOnline) return const SizedBox.shrink();
75
+
76
+ return MaterialBanner(
77
+ content: const Text('인터넷 연결이 끊어졌습니다'),
78
+ leading: const Icon(Icons.wifi_off, color: Colors.white),
79
+ backgroundColor: Colors.red.shade700,
80
+ actions: [
81
+ TextButton(
82
+ onPressed: () async {
83
+ // 수동 재시도
84
+ final result = await Connectivity().checkConnectivity();
85
+ if (result.any((r) => r != ConnectivityResult.none)) {
86
+ ref.invalidate(connectivityProvider);
87
+ }
88
+ },
89
+ child: const Text('재시도', style: TextStyle(color: Colors.white)),
90
+ ),
91
+ ],
92
+ );
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### 오프라인 요청 큐
98
+
99
+ ```dart
100
+ /// 오프라인 시 보류된 요청
101
+ class PendingRequest {
102
+ final String id;
103
+ final RequestOptions options;
104
+ final DateTime createdAt;
105
+ final int retryCount;
106
+
107
+ const PendingRequest({
108
+ required this.id,
109
+ required this.options,
110
+ required this.createdAt,
111
+ this.retryCount = 0,
112
+ });
113
+
114
+ bool get isExpired =>
115
+ DateTime.now().difference(createdAt) > const Duration(hours: 1);
116
+ }
117
+
118
+ /// 오프라인 큐 매니저
119
+ class OfflineQueueManager {
120
+ final Dio _dio;
121
+ final List<PendingRequest> _queue = [];
122
+ bool _isProcessing = false;
123
+
124
+ OfflineQueueManager({required Dio dio}) : _dio = dio;
125
+
126
+ /// 오프라인 시 큐에 추가
127
+ void enqueue(RequestOptions options) {
128
+ _queue.add(PendingRequest(
129
+ id: const Uuid().v4(),
130
+ options: options,
131
+ createdAt: DateTime.now(),
132
+ ));
133
+ }
134
+
135
+ /// 연결 복구 시 큐 처리
136
+ Future<void> processQueue() async {
137
+ if (_isProcessing || _queue.isEmpty) return;
138
+ _isProcessing = true;
139
+
140
+ try {
141
+ // 만료된 요청 제거
142
+ _queue.removeWhere((r) => r.isExpired);
143
+
144
+ final pending = List<PendingRequest>.from(_queue);
145
+ for (final request in pending) {
146
+ try {
147
+ await _dio.fetch(request.options);
148
+ _queue.remove(request);
149
+ } on DioException {
150
+ // 실패 → 큐에 유지, 다음 복구 시 재시도
151
+ break; // 첫 실패 시 중단 (아직 불안정할 수 있음)
152
+ }
153
+ }
154
+ } finally {
155
+ _isProcessing = false;
156
+ }
157
+ }
158
+
159
+ int get pendingCount => _queue.length;
160
+ bool get hasPending => _queue.isNotEmpty;
161
+ }
162
+
163
+ /// Provider
164
+ final offlineQueueProvider = Provider<OfflineQueueManager>((ref) {
165
+ final queue = OfflineQueueManager(dio: ref.watch(dioProvider));
166
+
167
+ // 온라인 복구 시 자동 처리
168
+ ref.listen(connectivityProvider, (prev, next) {
169
+ if (next.valueOrNull == NetworkStatus.online) {
170
+ queue.processQueue();
171
+ }
172
+ });
173
+
174
+ return queue;
175
+ });
176
+ ```
177
+
178
+ ### Connectivity Interceptor
179
+
180
+ ```dart
181
+ /// Dio 인터셉터로 오프라인 감지 (선택적)
182
+ class ConnectivityInterceptor extends Interceptor {
183
+ final OfflineQueueManager _queueManager;
184
+ final Ref _ref;
185
+
186
+ ConnectivityInterceptor({
187
+ required OfflineQueueManager queueManager,
188
+ required Ref ref,
189
+ }) : _queueManager = queueManager,
190
+ _ref = ref;
191
+
192
+ @override
193
+ void onError(DioException err, ErrorInterceptorHandler handler) {
194
+ if (err.type == DioExceptionType.connectionError) {
195
+ // POST/PUT 요청을 큐에 추가 (GET은 캐시 fallback)
196
+ if (err.requestOptions.method != 'GET') {
197
+ _queueManager.enqueue(err.requestOptions);
198
+ }
199
+ }
200
+ handler.next(err);
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### 규칙
206
+
207
+ - `connectivity_plus` 스트림으로 실시간 감지 — 단발 체크 금지
208
+ - 오프라인 배너를 앱 최상단에 항상 표시
209
+ - 오프라인 큐: POST/PUT/DELETE 요청만 큐잉 (GET은 캐시 fallback)
210
+ - 만료된 큐 요청 자동 제거 (1시간 기본)
211
+ - 연결 복구 시 큐 자동 처리 — `ref.listen(connectivityProvider)` 활용
212
+ - 큐 처리 중 실패 시 중단 (네트워크 아직 불안정할 수 있음)
213
+ - Wi-Fi 연결 ≠ 인터넷 접속 — 실제 HTTP 핑으로 검증 고려
@@ -0,0 +1,159 @@
1
+ ---
2
+ title: Dio Setup & Configuration
3
+ impact: CRITICAL
4
+ impactDescription: "잘못된 Dio 설정 → 타임아웃 누락, 인스턴스 중복 생성, 메모리 낭비"
5
+ tags: dio, http, setup, riverpod, baseOptions
6
+ ---
7
+
8
+ ## Dio Setup & Configuration
9
+
10
+ **Impact: CRITICAL (잘못된 Dio 설정 → 타임아웃 누락, 인스턴스 중복 생성, 메모리 낭비)**
11
+
12
+ Dio 싱글톤 인스턴스 관리, BaseOptions 설정, 환경별 분리.
13
+ 모든 네트워크 통신의 기반이 되는 핵심 인프라.
14
+
15
+ ### 의존성
16
+
17
+ ```yaml
18
+ # pubspec.yaml
19
+ dependencies:
20
+ dio: ^5.7.0
21
+ flutter_riverpod: ^2.6.0
22
+ # 또는 riverpod (non-Flutter)
23
+ ```
24
+
25
+ ### Dio 인스턴스 관리
26
+
27
+ **Incorrect (매번 새 인스턴스 생성):**
28
+ ```dart
29
+ class UserRepository {
30
+ Future<User> getUser(String id) async {
31
+ // 매 호출마다 Dio 생성 → 인터셉터 미적용, 커넥션 풀 미활용
32
+ final dio = Dio();
33
+ final response = await dio.get('https://api.example.com/users/$id');
34
+ return User.fromJson(response.data);
35
+ }
36
+ }
37
+ ```
38
+
39
+ **Correct (Riverpod Provider로 싱글톤 관리):**
40
+ ```dart
41
+ /// 환경 설정
42
+ enum AppEnvironment { dev, staging, prod }
43
+
44
+ class AppConfig {
45
+ final AppEnvironment environment;
46
+ final String baseUrl;
47
+
48
+ const AppConfig({required this.environment, required this.baseUrl});
49
+
50
+ static const dev = AppConfig(
51
+ environment: AppEnvironment.dev,
52
+ baseUrl: 'https://dev-api.example.com/v1',
53
+ );
54
+
55
+ static const staging = AppConfig(
56
+ environment: AppEnvironment.staging,
57
+ baseUrl: 'https://staging-api.example.com/v1',
58
+ );
59
+
60
+ static const prod = AppConfig(
61
+ environment: AppEnvironment.prod,
62
+ baseUrl: 'https://api.example.com/v1',
63
+ );
64
+ }
65
+
66
+ /// 앱 설정 Provider
67
+ final appConfigProvider = Provider<AppConfig>((ref) {
68
+ // main.dart에서 ProviderScope overrides로 주입
69
+ return AppConfig.dev;
70
+ });
71
+
72
+ /// Dio Provider — 앱 전역 싱글톤
73
+ final dioProvider = Provider<Dio>((ref) {
74
+ final config = ref.watch(appConfigProvider);
75
+
76
+ final dio = Dio(BaseOptions(
77
+ baseUrl: config.baseUrl,
78
+ connectTimeout: const Duration(seconds: 15),
79
+ receiveTimeout: const Duration(seconds: 15),
80
+ sendTimeout: const Duration(seconds: 15),
81
+ headers: {
82
+ 'Content-Type': 'application/json',
83
+ 'Accept': 'application/json',
84
+ },
85
+ validateStatus: (status) => status != null && status < 500,
86
+ ));
87
+
88
+ // 인터셉터 체인 (순서 중요)
89
+ dio.interceptors.addAll([
90
+ ref.watch(authInterceptorProvider),
91
+ ref.watch(retryInterceptorProvider),
92
+ if (config.environment == AppEnvironment.dev)
93
+ ref.watch(loggingInterceptorProvider),
94
+ ref.watch(errorTransformInterceptorProvider),
95
+ ]);
96
+
97
+ return dio;
98
+ });
99
+ ```
100
+
101
+ ### main.dart 환경 주입
102
+
103
+ ```dart
104
+ Future<void> main() async {
105
+ WidgetsFlutterBinding.ensureInitialized();
106
+
107
+ // 환경 결정 (빌드 플래그, .env 등)
108
+ const config = String.fromEnvironment('ENV') == 'prod'
109
+ ? AppConfig.prod
110
+ : AppConfig.dev;
111
+
112
+ runApp(
113
+ ProviderScope(
114
+ overrides: [
115
+ appConfigProvider.overrideWithValue(config),
116
+ ],
117
+ child: const MyApp(),
118
+ ),
119
+ );
120
+ }
121
+ ```
122
+
123
+ ### BaseOptions 상세
124
+
125
+ ```dart
126
+ BaseOptions(
127
+ // 서버 기본 URL — 환경별 분리 필수
128
+ baseUrl: 'https://api.example.com/v1',
129
+
130
+ // 타임아웃 — 15초 권장 (모바일 네트워크 고려)
131
+ connectTimeout: const Duration(seconds: 15),
132
+ receiveTimeout: const Duration(seconds: 15),
133
+ sendTimeout: const Duration(seconds: 15),
134
+
135
+ // 기본 헤더
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ 'Accept': 'application/json',
139
+ 'X-App-Version': appVersion, // 서버 호환성 체크용
140
+ 'X-Platform': Platform.isIOS ? 'ios' : 'android',
141
+ },
142
+
143
+ // 응답 타입
144
+ responseType: ResponseType.json,
145
+
146
+ // 상태 코드 검증 — 5xx는 DioException으로 처리
147
+ validateStatus: (status) => status != null && status < 500,
148
+ )
149
+ ```
150
+
151
+ ### 규칙
152
+
153
+ - Dio 인스턴스는 Riverpod Provider로 싱글톤 관리 — 직접 생성 금지
154
+ - `connectTimeout`, `receiveTimeout`, `sendTimeout` 모두 15초 설정
155
+ - `baseUrl` 은 환경별 분리 (dev/staging/prod) — 하드코딩 금지
156
+ - `Content-Type: application/json` 기본 헤더
157
+ - `validateStatus` — 5xx만 에러, 4xx는 응답 데이터로 처리
158
+ - Interceptor 등록은 Dio Provider 내에서 순서대로
159
+ - Logging 인터셉터는 dev 환경에서만 활성화
@@ -0,0 +1,209 @@
1
+ ---
2
+ title: Network Error Handling
3
+ impact: CRITICAL
4
+ impactDescription: "에러 미변환 → DioException UI 노출, 사용자 혼란, 디버깅 불가"
5
+ tags: dio, error, failure, sealed-class, result-pattern
6
+ ---
7
+
8
+ ## Network Error Handling
9
+
10
+ **Impact: CRITICAL (에러 미변환 → DioException UI 노출, 사용자 혼란, 디버깅 불가)**
11
+
12
+ DioException을 앱 도메인 NetworkFailure로 변환.
13
+ Repository에서 Result 패턴으로 래핑하여 UI까지 안전하게 전달.
14
+
15
+ ### NetworkFailure Sealed Class
16
+
17
+ **Incorrect (DioException 직접 노출):**
18
+ ```dart
19
+ class UserRepository {
20
+ Future<User> getUser(String id) async {
21
+ final response = await dio.get('/users/$id');
22
+ return User.fromJson(response.data);
23
+ // → DioException이 그대로 UI까지 전파
24
+ // → try-catch 누락 시 앱 크래시
25
+ }
26
+ }
27
+ ```
28
+
29
+ **Correct (도메인 에러 변환 + Result 패턴):**
30
+ ```dart
31
+ /// 네트워크 에러 도메인 모델
32
+ sealed class NetworkFailure {
33
+ final String message;
34
+ final int? statusCode;
35
+ final dynamic originalError;
36
+
37
+ const NetworkFailure({
38
+ required this.message,
39
+ this.statusCode,
40
+ this.originalError,
41
+ });
42
+
43
+ /// DioException → NetworkFailure 팩토리
44
+ factory NetworkFailure.fromDioException(DioException e) {
45
+ return switch (e.type) {
46
+ DioExceptionType.connectionTimeout ||
47
+ DioExceptionType.sendTimeout ||
48
+ DioExceptionType.receiveTimeout =>
49
+ TimeoutFailure(
50
+ message: '서버 응답 시간이 초과되었습니다',
51
+ originalError: e,
52
+ ),
53
+ DioExceptionType.connectionError => NoConnectionFailure(
54
+ message: '인터넷 연결을 확인해주세요',
55
+ originalError: e,
56
+ ),
57
+ DioExceptionType.badResponse => ServerFailure(
58
+ message: _extractServerMessage(e.response),
59
+ statusCode: e.response?.statusCode,
60
+ originalError: e,
61
+ ),
62
+ DioExceptionType.cancel => RequestCancelledFailure(
63
+ message: '요청이 취소되었습니다',
64
+ originalError: e,
65
+ ),
66
+ _ => UnknownNetworkFailure(
67
+ message: '알 수 없는 네트워크 오류가 발생했습니다',
68
+ originalError: e,
69
+ ),
70
+ };
71
+ }
72
+
73
+ static String _extractServerMessage(Response? response) {
74
+ try {
75
+ final data = response?.data;
76
+ if (data is Map<String, dynamic>) {
77
+ return data['message'] as String? ??
78
+ data['error'] as String? ??
79
+ 'Server error';
80
+ }
81
+ } catch (_) {}
82
+ return 'Server error (${response?.statusCode})';
83
+ }
84
+ }
85
+
86
+ class TimeoutFailure extends NetworkFailure {
87
+ const TimeoutFailure({required super.message, super.originalError});
88
+ }
89
+
90
+ class NoConnectionFailure extends NetworkFailure {
91
+ const NoConnectionFailure({required super.message, super.originalError});
92
+ }
93
+
94
+ class ServerFailure extends NetworkFailure {
95
+ const ServerFailure({
96
+ required super.message,
97
+ super.statusCode,
98
+ super.originalError,
99
+ });
100
+
101
+ bool get isUnauthorized => statusCode == 401;
102
+ bool get isForbidden => statusCode == 403;
103
+ bool get isNotFound => statusCode == 404;
104
+ bool get isConflict => statusCode == 409;
105
+ bool get isValidationError => statusCode == 422;
106
+ bool get isRateLimited => statusCode == 429;
107
+ }
108
+
109
+ class RequestCancelledFailure extends NetworkFailure {
110
+ const RequestCancelledFailure({required super.message, super.originalError});
111
+ }
112
+
113
+ class UnknownNetworkFailure extends NetworkFailure {
114
+ const UnknownNetworkFailure({required super.message, super.originalError});
115
+ }
116
+ ```
117
+
118
+ ### Result 패턴
119
+
120
+ ```dart
121
+ /// 범용 Result 타입
122
+ sealed class Result<T> {
123
+ const Result();
124
+ }
125
+
126
+ class Success<T> extends Result<T> {
127
+ final T data;
128
+ const Success(this.data);
129
+ }
130
+
131
+ class Failure<T> extends Result<T> {
132
+ final NetworkFailure failure;
133
+ const Failure(this.failure);
134
+ }
135
+
136
+ /// Repository에서 사용
137
+ class UserRepository {
138
+ final Dio _dio;
139
+
140
+ UserRepository({required Dio dio}) : _dio = dio;
141
+
142
+ Future<Result<User>> getUser(String id) async {
143
+ try {
144
+ final response = await _dio.get('/users/$id');
145
+ final user = User.fromJson(response.data);
146
+ return Success(user);
147
+ } on DioException catch (e) {
148
+ return Failure(NetworkFailure.fromDioException(e));
149
+ }
150
+ }
151
+
152
+ Future<Result<List<User>>> getUsers({int page = 1}) async {
153
+ try {
154
+ final response = await _dio.get('/users', queryParameters: {'page': page});
155
+ final users = (response.data as List)
156
+ .map((json) => User.fromJson(json))
157
+ .toList();
158
+ return Success(users);
159
+ } on DioException catch (e) {
160
+ return Failure(NetworkFailure.fromDioException(e));
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ ### UI에서 Result 처리
167
+
168
+ ```dart
169
+ /// Provider에서 Result 소비
170
+ class UserNotifier extends AsyncNotifier<User> {
171
+ @override
172
+ Future<User> build() async {
173
+ final result = await ref.read(userRepositoryProvider).getUser('me');
174
+ return switch (result) {
175
+ Success(data: final user) => user,
176
+ Failure(failure: final f) => throw f, // AsyncError로 전환
177
+ };
178
+ }
179
+ }
180
+
181
+ /// Widget에서 에러 표시
182
+ Widget build(BuildContext context, WidgetRef ref) {
183
+ final userAsync = ref.watch(userProvider);
184
+
185
+ return userAsync.when(
186
+ data: (user) => UserProfile(user: user),
187
+ loading: () => const LoadingIndicator(),
188
+ error: (error, _) {
189
+ if (error is NoConnectionFailure) {
190
+ return const OfflineWidget();
191
+ }
192
+ if (error is ServerFailure && error.isNotFound) {
193
+ return const UserNotFoundWidget();
194
+ }
195
+ return ErrorWidget(message: (error as NetworkFailure).message);
196
+ },
197
+ );
198
+ }
199
+ ```
200
+
201
+ ### 규칙
202
+
203
+ - DioException을 UI 레이어까지 전파 금지 — 반드시 NetworkFailure로 변환
204
+ - `NetworkFailure.fromDioException()` 팩토리 단일 변환 지점
205
+ - Repository 메서드는 `Result<T>` 반환 — 절대 throw 하지 않음
206
+ - sealed class 사용 — switch 완전성 검사로 누락 방지
207
+ - 서버 에러 메시지 추출 → 사용자 친화적 메시지로 매핑
208
+ - 401 → 토큰 리프레시는 Auth 인터셉터 담당 (Repository 아님)
209
+ - 에러 로깅은 Logging 인터셉터에서 처리 (Repository에서 중복 로깅 금지)