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,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Interceptor Chain Architecture
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: "인터셉터 순서 오류 → 인증 누락, 무한 재시도, 에러 미변환"
|
|
5
|
+
tags: dio, interceptor, auth, retry, logging, chain
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Interceptor Chain Architecture
|
|
9
|
+
|
|
10
|
+
**Impact: CRITICAL (인터셉터 순서 오류 → 인증 누락, 무한 재시도, 에러 미변환)**
|
|
11
|
+
|
|
12
|
+
Dio 인터셉터 체인 설계. Auth → Retry → Logging → Error Transform 순서.
|
|
13
|
+
각 인터셉터의 책임과 구현 패턴.
|
|
14
|
+
|
|
15
|
+
### 인터셉터 체인 순서
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Request Flow:
|
|
19
|
+
Client → Auth → Retry → Logging → Error Transform → Server
|
|
20
|
+
|
|
21
|
+
Response Flow:
|
|
22
|
+
Server → Error Transform → Logging → Retry → Auth → Client
|
|
23
|
+
|
|
24
|
+
순서 근거:
|
|
25
|
+
1. Auth: 요청에 토큰 주입 (가장 먼저)
|
|
26
|
+
2. Retry: 실패 시 재시도 판단 (Auth 뒤 = 토큰 포함 상태로 재시도)
|
|
27
|
+
3. Logging: 최종 요청/응답 기록 (디버깅용)
|
|
28
|
+
4. Error Transform: DioException → 도메인 에러 변환 (가장 마지막)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Auth Interceptor
|
|
32
|
+
|
|
33
|
+
**Incorrect (토큰 만료 시 무한 실패):**
|
|
34
|
+
```dart
|
|
35
|
+
class BadAuthInterceptor extends Interceptor {
|
|
36
|
+
@override
|
|
37
|
+
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
|
38
|
+
options.headers['Authorization'] = 'Bearer $accessToken';
|
|
39
|
+
handler.next(options);
|
|
40
|
+
// → 토큰 만료 시 401 → 재시도 없이 실패
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Correct (QueuedInterceptorsWrapper로 토큰 리프레시):**
|
|
46
|
+
```dart
|
|
47
|
+
/// Auth 인터셉터 — 토큰 주입 + 401 리프레시
|
|
48
|
+
/// QueuedInterceptorsWrapper: 리프레시 중 다른 요청 큐잉 (중복 리프레시 방지)
|
|
49
|
+
class AuthInterceptor extends QueuedInterceptorsWrapper {
|
|
50
|
+
final TokenStorage _tokenStorage;
|
|
51
|
+
final Dio _tokenDio; // 토큰 리프레시 전용 Dio (인터셉터 미적용)
|
|
52
|
+
|
|
53
|
+
AuthInterceptor({
|
|
54
|
+
required TokenStorage tokenStorage,
|
|
55
|
+
required Dio tokenDio,
|
|
56
|
+
}) : _tokenStorage = tokenStorage,
|
|
57
|
+
_tokenDio = tokenDio;
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
|
61
|
+
final token = await _tokenStorage.getAccessToken();
|
|
62
|
+
if (token != null) {
|
|
63
|
+
options.headers['Authorization'] = 'Bearer $token';
|
|
64
|
+
}
|
|
65
|
+
handler.next(options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
|
70
|
+
if (err.response?.statusCode != 401) {
|
|
71
|
+
return handler.next(err);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 401 → 토큰 리프레시 시도
|
|
75
|
+
try {
|
|
76
|
+
final refreshToken = await _tokenStorage.getRefreshToken();
|
|
77
|
+
if (refreshToken == null) {
|
|
78
|
+
return handler.next(err); // 리프레시 토큰 없음 → 로그아웃
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
final response = await _tokenDio.post('/auth/refresh', data: {
|
|
82
|
+
'refresh_token': refreshToken,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
final newAccessToken = response.data['access_token'] as String;
|
|
86
|
+
final newRefreshToken = response.data['refresh_token'] as String;
|
|
87
|
+
await _tokenStorage.saveTokens(newAccessToken, newRefreshToken);
|
|
88
|
+
|
|
89
|
+
// 원래 요청 재시도 (새 토큰으로)
|
|
90
|
+
err.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
|
|
91
|
+
final retryResponse = await _tokenDio.fetch(err.requestOptions);
|
|
92
|
+
return handler.resolve(retryResponse);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// 리프레시 실패 → 로그아웃 이벤트 발행
|
|
95
|
+
return handler.next(err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
final authInterceptorProvider = Provider<AuthInterceptor>((ref) {
|
|
101
|
+
return AuthInterceptor(
|
|
102
|
+
tokenStorage: ref.watch(tokenStorageProvider),
|
|
103
|
+
tokenDio: Dio(BaseOptions(
|
|
104
|
+
baseUrl: ref.watch(appConfigProvider).baseUrl,
|
|
105
|
+
)), // 인터셉터 없는 별도 Dio
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Retry Interceptor
|
|
111
|
+
|
|
112
|
+
```dart
|
|
113
|
+
/// Retry 인터셉터 — 지수 백오프, 5xx/timeout만
|
|
114
|
+
class RetryInterceptor extends Interceptor {
|
|
115
|
+
final Dio _dio;
|
|
116
|
+
final int maxRetries;
|
|
117
|
+
|
|
118
|
+
RetryInterceptor({required Dio dio, this.maxRetries = 3}) : _dio = dio;
|
|
119
|
+
|
|
120
|
+
@override
|
|
121
|
+
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
|
122
|
+
if (!_shouldRetry(err) || _getRetryCount(err) >= maxRetries) {
|
|
123
|
+
return handler.next(err);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
final retryCount = _getRetryCount(err) + 1;
|
|
127
|
+
final delay = Duration(milliseconds: 1000 * pow(2, retryCount - 1).toInt());
|
|
128
|
+
|
|
129
|
+
await Future.delayed(delay);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
err.requestOptions.extra['retryCount'] = retryCount;
|
|
133
|
+
final response = await _dio.fetch(err.requestOptions);
|
|
134
|
+
return handler.resolve(response);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return handler.next(err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
bool _shouldRetry(DioException err) {
|
|
141
|
+
return err.type == DioExceptionType.connectionTimeout ||
|
|
142
|
+
err.type == DioExceptionType.sendTimeout ||
|
|
143
|
+
err.type == DioExceptionType.receiveTimeout ||
|
|
144
|
+
(err.response?.statusCode != null && err.response!.statusCode! >= 500);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
int _getRetryCount(DioException err) {
|
|
148
|
+
return err.requestOptions.extra['retryCount'] as int? ?? 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Logging Interceptor
|
|
154
|
+
|
|
155
|
+
```dart
|
|
156
|
+
/// Logging 인터셉터 — debug 빌드에서만 활성화
|
|
157
|
+
class AppLoggingInterceptor extends Interceptor {
|
|
158
|
+
@override
|
|
159
|
+
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
|
160
|
+
debugPrint('→ ${options.method} ${options.uri}');
|
|
161
|
+
if (options.data != null) {
|
|
162
|
+
debugPrint(' Body: ${options.data}');
|
|
163
|
+
}
|
|
164
|
+
handler.next(options);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
|
169
|
+
debugPrint('← ${response.statusCode} ${response.requestOptions.uri}');
|
|
170
|
+
handler.next(response);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@override
|
|
174
|
+
void onError(DioException err, ErrorInterceptorHandler handler) {
|
|
175
|
+
debugPrint('✗ ${err.type} ${err.requestOptions.uri}');
|
|
176
|
+
debugPrint(' ${err.message}');
|
|
177
|
+
handler.next(err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Error Transform Interceptor
|
|
183
|
+
|
|
184
|
+
```dart
|
|
185
|
+
/// Error Transform — DioException → NetworkFailure
|
|
186
|
+
/// (error-handling.md 참조)
|
|
187
|
+
class ErrorTransformInterceptor extends Interceptor {
|
|
188
|
+
@override
|
|
189
|
+
void onError(DioException err, ErrorInterceptorHandler handler) {
|
|
190
|
+
// NetworkFailure로 변환 후 그대로 전파
|
|
191
|
+
// Repository 레이어에서 catch하여 Result로 래핑
|
|
192
|
+
handler.next(err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 규칙
|
|
198
|
+
|
|
199
|
+
- 인터셉터 순서: Auth → Retry → Logging → Error Transform (변경 금지)
|
|
200
|
+
- Auth 인터셉터는 `QueuedInterceptorsWrapper` 사용 (동시 리프레시 방지)
|
|
201
|
+
- 토큰 리프레시용 Dio는 별도 인스턴스 (인터셉터 미적용, 순환 방지)
|
|
202
|
+
- Retry: 지수 백오프 (1s, 2s, 4s), 최대 3회, 5xx/timeout만
|
|
203
|
+
- Retry: POST/PUT/DELETE는 멱등성 확인 후 재시도 여부 결정
|
|
204
|
+
- Logging: `kDebugMode` 또는 dev 환경에서만 활성화
|
|
205
|
+
- 인터셉터 내에서 BuildContext, Navigator 접근 금지
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Retrofit Code Generation Patterns
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: "수동 API 코드 → 오타, 타입 불일치, 유지보수 비용 증가"
|
|
5
|
+
tags: retrofit, code-generation, build-runner, freezed, rest-api
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Retrofit Code Generation Patterns
|
|
9
|
+
|
|
10
|
+
**Impact: HIGH (수동 API 코드 → 오타, 타입 불일치, 유지보수 비용 증가)**
|
|
11
|
+
|
|
12
|
+
Retrofit으로 타입 안전한 API 클라이언트 자동 생성.
|
|
13
|
+
freezed 모델과 연동하여 불변 데이터 클래스 + JSON 직렬화.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
dio: ^5.7.0
|
|
21
|
+
retrofit: ^4.4.0
|
|
22
|
+
json_annotation: ^4.9.0
|
|
23
|
+
freezed_annotation: ^2.4.0
|
|
24
|
+
|
|
25
|
+
dev_dependencies:
|
|
26
|
+
retrofit_generator: ^9.1.0
|
|
27
|
+
build_runner: ^2.4.0
|
|
28
|
+
json_serializable: ^6.8.0
|
|
29
|
+
freezed: ^2.5.0
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### API 클라이언트 정의
|
|
33
|
+
|
|
34
|
+
**Incorrect (수동 HTTP 호출):**
|
|
35
|
+
```dart
|
|
36
|
+
class UserApi {
|
|
37
|
+
final Dio dio;
|
|
38
|
+
UserApi(this.dio);
|
|
39
|
+
|
|
40
|
+
Future<User> getUser(String id) async {
|
|
41
|
+
final response = await dio.get('/users/$id');
|
|
42
|
+
return User.fromJson(response.data);
|
|
43
|
+
// → 경로 오타, 파라미터 누락 가능, 타입 안전하지 않음
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Correct (Retrofit 어노테이션 + 코드 생성):**
|
|
49
|
+
```dart
|
|
50
|
+
// lib/core/networking/api/user_api.dart
|
|
51
|
+
import 'package:dio/dio.dart';
|
|
52
|
+
import 'package:retrofit/retrofit.dart';
|
|
53
|
+
|
|
54
|
+
part 'user_api.g.dart';
|
|
55
|
+
|
|
56
|
+
@RestApi()
|
|
57
|
+
abstract class UserApi {
|
|
58
|
+
factory UserApi(Dio dio, {String baseUrl}) = _UserApi;
|
|
59
|
+
|
|
60
|
+
@GET('/users/{id}')
|
|
61
|
+
Future<UserResponse> getUser(@Path('id') String id);
|
|
62
|
+
|
|
63
|
+
@GET('/users')
|
|
64
|
+
Future<PaginatedResponse<UserResponse>> getUsers(
|
|
65
|
+
@Query('page') int page,
|
|
66
|
+
@Query('limit') int limit,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
@POST('/users')
|
|
70
|
+
Future<UserResponse> createUser(@Body() CreateUserRequest request);
|
|
71
|
+
|
|
72
|
+
@PUT('/users/{id}')
|
|
73
|
+
Future<UserResponse> updateUser(
|
|
74
|
+
@Path('id') String id,
|
|
75
|
+
@Body() UpdateUserRequest request,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
@DELETE('/users/{id}')
|
|
79
|
+
Future<void> deleteUser(@Path('id') String id);
|
|
80
|
+
|
|
81
|
+
@POST('/users/{id}/avatar')
|
|
82
|
+
@MultiPart()
|
|
83
|
+
Future<AvatarResponse> uploadAvatar(
|
|
84
|
+
@Path('id') String id,
|
|
85
|
+
@Part(name: 'file') File avatar,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### freezed 응답 모델
|
|
91
|
+
|
|
92
|
+
```dart
|
|
93
|
+
// lib/features/user/data/models/user_response.dart
|
|
94
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
95
|
+
|
|
96
|
+
part 'user_response.freezed.dart';
|
|
97
|
+
part 'user_response.g.dart';
|
|
98
|
+
|
|
99
|
+
@freezed
|
|
100
|
+
class UserResponse with _$UserResponse {
|
|
101
|
+
const factory UserResponse({
|
|
102
|
+
required String id,
|
|
103
|
+
required String name,
|
|
104
|
+
required String email,
|
|
105
|
+
String? avatarUrl,
|
|
106
|
+
@JsonKey(name: 'created_at') required DateTime createdAt,
|
|
107
|
+
@Default(false) bool isVerified,
|
|
108
|
+
}) = _UserResponse;
|
|
109
|
+
|
|
110
|
+
factory UserResponse.fromJson(Map<String, dynamic> json) =>
|
|
111
|
+
_$UserResponseFromJson(json);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// 페이지네이션 래퍼
|
|
115
|
+
@freezed
|
|
116
|
+
class PaginatedResponse<T> with _$PaginatedResponse<T> {
|
|
117
|
+
const factory PaginatedResponse({
|
|
118
|
+
required List<T> data,
|
|
119
|
+
required int total,
|
|
120
|
+
required int page,
|
|
121
|
+
required int lastPage,
|
|
122
|
+
}) = _PaginatedResponse<T>;
|
|
123
|
+
|
|
124
|
+
factory PaginatedResponse.fromJson(
|
|
125
|
+
Map<String, dynamic> json,
|
|
126
|
+
T Function(Object?) fromJsonT,
|
|
127
|
+
) => _$PaginatedResponseFromJson(json, fromJsonT);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 요청 모델
|
|
132
|
+
|
|
133
|
+
```dart
|
|
134
|
+
@freezed
|
|
135
|
+
class CreateUserRequest with _$CreateUserRequest {
|
|
136
|
+
const factory CreateUserRequest({
|
|
137
|
+
required String name,
|
|
138
|
+
required String email,
|
|
139
|
+
String? phoneNumber,
|
|
140
|
+
}) = _CreateUserRequest;
|
|
141
|
+
|
|
142
|
+
factory CreateUserRequest.fromJson(Map<String, dynamic> json) =>
|
|
143
|
+
_$CreateUserRequestFromJson(json);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Riverpod Provider 구성
|
|
148
|
+
|
|
149
|
+
```dart
|
|
150
|
+
/// API Provider
|
|
151
|
+
final userApiProvider = Provider<UserApi>((ref) {
|
|
152
|
+
return UserApi(ref.watch(dioProvider));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/// Repository — API + 에러 변환
|
|
156
|
+
final userRepositoryProvider = Provider<UserRepository>((ref) {
|
|
157
|
+
return UserRepository(api: ref.watch(userApiProvider));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
class UserRepository {
|
|
161
|
+
final UserApi _api;
|
|
162
|
+
|
|
163
|
+
UserRepository({required UserApi api}) : _api = api;
|
|
164
|
+
|
|
165
|
+
Future<Result<UserResponse>> getUser(String id) async {
|
|
166
|
+
try {
|
|
167
|
+
final response = await _api.getUser(id);
|
|
168
|
+
return Success(response);
|
|
169
|
+
} on DioException catch (e) {
|
|
170
|
+
return Failure(NetworkFailure.fromDioException(e));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 코드 생성
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# 1회 실행
|
|
180
|
+
dart run build_runner build --delete-conflicting-outputs
|
|
181
|
+
|
|
182
|
+
# 개발 중 자동 생성 (파일 변경 감지)
|
|
183
|
+
dart run build_runner watch --delete-conflicting-outputs
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 규칙
|
|
187
|
+
|
|
188
|
+
- `@RestApi(baseUrl: '')` — baseUrl은 빈 문자열, Dio BaseOptions에서 관리
|
|
189
|
+
- 응답/요청 모델은 freezed + json_serializable (불변 + 자동 직렬화)
|
|
190
|
+
- `@JsonKey(name: 'snake_case')` — 서버 필드명과 클라이언트 필드명 매핑
|
|
191
|
+
- `@Part(name: 'file')` — multipart 업로드 시 서버 기대 필드명 명시
|
|
192
|
+
- `.g.dart` 파일은 버전 관리에 포함하지 않음 (`.gitignore` 에 추가 가능)
|
|
193
|
+
- `build_runner watch` 로 개발 중 자동 생성 — 수동 생성 후 커밋 잊음 방지
|
|
194
|
+
- API 클라이언트와 Repository 분리 — API는 순수 HTTP, Repository에서 에러 변환
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: push-notifications
|
|
3
|
+
description: |
|
|
4
|
+
Flutter 푸시 알림 + 백그라운드 처리 가이드라인.
|
|
5
|
+
FCM(Firebase Cloud Messaging) 설정, 포그라운드/백그라운드/종료 상태 메시지 핸들링,
|
|
6
|
+
flutter_local_notifications 통합, 딥링크 네비게이션, workmanager 백그라운드 태스크,
|
|
7
|
+
리치 알림(이미지, 액션 버튼, 그룹), 플랫폼별 설정(APNs, Android 채널).
|
|
8
|
+
알림 기능 구현 시 이 스킬을 참조.
|
|
9
|
+
version: "1.0.0"
|
|
10
|
+
tags: [flutter, fcm, push-notification, background, local-notification, deep-link]
|
|
11
|
+
user-invocable: false
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Push Notifications & Background Processing
|
|
15
|
+
|
|
16
|
+
Flutter FCM + Local Notifications + Background Processing 통합 가이드.
|
|
17
|
+
iOS APNs / Android FCM 채널 기반, 모든 앱 상태에서의 알림 수신 및 처리.
|
|
18
|
+
|
|
19
|
+
## Philosophy
|
|
20
|
+
|
|
21
|
+
- 알림은 서비스 — Feature-first 구조에서 `core/notifications/` 로 중앙화
|
|
22
|
+
- 권한은 맥락 — 앱 첫 실행이 아닌, 사용자가 가치를 인지한 순간에 요청
|
|
23
|
+
- 백그라운드는 격리 — top-level 함수 + 최소 의존성, UI 코드 접근 금지
|
|
24
|
+
- 플랫폼 차이 흡수 — 단일 인터페이스로 iOS/Android 분기 캡슐화
|
|
25
|
+
|
|
26
|
+
## Resources
|
|
27
|
+
|
|
28
|
+
7개 규칙 + 2개 참조. 알림 파이프라인 전체를 커버.
|
|
29
|
+
|
|
30
|
+
| Priority | Type | Resource | Description |
|
|
31
|
+
|----------|------|----------|-------------|
|
|
32
|
+
| CRITICAL | rule | [fcm-setup](rules/fcm-setup.md) | FCM 초기화, 토큰 관리, 서버 연동 |
|
|
33
|
+
| CRITICAL | rule | [notification-handling](rules/notification-handling.md) | 포그라운드/백그라운드/종료 상태 메시지 처리 |
|
|
34
|
+
| CRITICAL | rule | [local-notifications](rules/local-notifications.md) | flutter_local_notifications 설정, 채널, 스케줄링 |
|
|
35
|
+
| HIGH | rule | [notification-permissions](rules/notification-permissions.md) | 권한 요청 타이밍, 프리프롬프트, 설정 유도 |
|
|
36
|
+
| HIGH | rule | [deep-linking](rules/deep-linking.md) | 알림 탭 → 특정 화면 네비게이션, 페이로드 처리 |
|
|
37
|
+
| HIGH | rule | [rich-notifications](rules/rich-notifications.md) | 이미지, 액션 버튼, 그룹 알림, 사운드 |
|
|
38
|
+
| HIGH | rule | [background-processing](rules/background-processing.md) | workmanager, 백그라운드 fetch, Isolate 태스크 |
|
|
39
|
+
| — | ref | [platform-setup](references/platform-setup.md) | iOS APNs + Android 채널 + 권한 설정 상세 |
|
|
40
|
+
| — | ref | [notification-architecture](references/notification-architecture.md) | 알림 서비스 아키텍처, 디렉토리 구조, 테스트 전략 |
|
|
41
|
+
|
|
42
|
+
## Quick Rules
|
|
43
|
+
|
|
44
|
+
### FCM 설정
|
|
45
|
+
- `Firebase.initializeApp()` → `main()` 에서 최우선 호출
|
|
46
|
+
- FCM 토큰은 서버에 저장, `onTokenRefresh` 구독으로 갱신
|
|
47
|
+
- iOS: APNs 인증 키(.p8) 방식 사용 (인증서보다 관리 용이)
|
|
48
|
+
- `@pragma('vm:entry-point')` 로 백그라운드 핸들러 보호
|
|
49
|
+
|
|
50
|
+
### 메시지 핸들링
|
|
51
|
+
- `FirebaseMessaging.onMessage` → 포그라운드 (직접 로컬 알림 표시)
|
|
52
|
+
- `FirebaseMessaging.onBackgroundMessage` → top-level 함수 필수
|
|
53
|
+
- `getInitialMessage()` → 종료 상태에서 알림 탭 처리 (1회만 호출)
|
|
54
|
+
- `onMessageOpenedApp` → 백그라운드에서 알림 탭 처리
|
|
55
|
+
|
|
56
|
+
### 로컬 알림
|
|
57
|
+
- Android: 채널 ID 필수 (Android 8+), 중요도별 분리
|
|
58
|
+
- iOS: `DarwinInitializationSettings` 권한 플래그
|
|
59
|
+
- 스케줄링: `zonedSchedule` (timezone 패키지 필수)
|
|
60
|
+
- 페이로드: JSON 문자열로 직렬화, 탭 시 파싱
|
|
61
|
+
|
|
62
|
+
### 백그라운드 처리
|
|
63
|
+
- `workmanager` → periodic/one-off 백그라운드 태스크
|
|
64
|
+
- iOS: `BGTaskScheduler` (최소 15분 간격, OS 제어)
|
|
65
|
+
- Android: `WorkManager` (정확한 주기, 제약 조건 설정 가능)
|
|
66
|
+
- 백그라운드 핸들러에서 Flutter 엔진 직접 접근 금지
|
|
67
|
+
|
|
68
|
+
### 권한
|
|
69
|
+
- iOS 13+: 임시 알림 (provisional) → 조용히 전달, 사용자 결정 유도
|
|
70
|
+
- Android 13+: `POST_NOTIFICATIONS` 런타임 권한 필수
|
|
71
|
+
- 권한 거부 시 설정 앱으로 유도 (`openAppSettings()`)
|
|
72
|
+
|
|
73
|
+
## Checklist
|
|
74
|
+
|
|
75
|
+
| Priority | Item |
|
|
76
|
+
|----------|------|
|
|
77
|
+
| CRITICAL | `Firebase.initializeApp()` 호출 후 FCM 초기화 |
|
|
78
|
+
| CRITICAL | 백그라운드 핸들러가 top-level 함수인지 확인 |
|
|
79
|
+
| CRITICAL | Android notification channel 생성 (앱 시작 시) |
|
|
80
|
+
| CRITICAL | iOS APNs capability + Background Modes 활성화 |
|
|
81
|
+
| HIGH | FCM 토큰 서버 동기화 + 갱신 리스너 |
|
|
82
|
+
| HIGH | 알림 탭 → 딥링크 네비게이션 3가지 상태 모두 처리 |
|
|
83
|
+
| HIGH | 권한 요청 타이밍 (첫 실행 X, 가치 인지 후 O) |
|
|
84
|
+
| HIGH | Android 13+ POST_NOTIFICATIONS 런타임 권한 처리 |
|
|
85
|
+
| MEDIUM | 리치 알림 (이미지, 액션 버튼) 테스트 |
|
|
86
|
+
| MEDIUM | 백그라운드 태스크 배터리 최적화 고려 |
|
|
87
|
+
| MEDIUM | 알림 분석 (열림률, 전환율) 수집 |
|