@walwal-harness/cli 4.0.0-alpha.9 → 4.0.0-beta.2
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.md +235 -273
- package/assets/templates/config.json +1 -48
- package/assets/templates/gitignore-append.txt +1 -0
- package/bin/init.js +42 -18
- package/package.json +1 -1
- package/scripts/harness-dashboard-v4.sh +27 -88
- package/scripts/harness-next.sh +4 -15
- package/scripts/harness-user-prompt-submit.sh +10 -0
- package/skills/dispatcher/SKILL.md +7 -2
- package/skills/team-action/SKILL.md +26 -0
- package/skills/team-stop/SKILL.md +19 -0
- package/scripts/harness-control-v4.sh +0 -97
- package/scripts/harness-studio-v4.sh +0 -122
- package/scripts/harness-team-worker.sh +0 -415
- package/skills/evaluator-functional-flutter/SKILL.md +0 -206
- package/skills/evaluator-functional-flutter/references/ia-compliance.md +0 -77
- package/skills/evaluator-functional-flutter/references/scoring-rubric.md +0 -132
- package/skills/evaluator-functional-flutter/references/static-check-rules.md +0 -99
- package/skills/generator-frontend-flutter/SKILL.md +0 -173
- package/skills/generator-frontend-flutter/references/anti-patterns.md +0 -320
- package/skills/generator-frontend-flutter/references/api-layer-pattern.md +0 -233
- package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +0 -273
- package/skills/generator-frontend-flutter/references/i18n-pattern.md +0 -102
- package/skills/generator-frontend-flutter/references/riverpod-pattern.md +0 -199
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
docmeta:
|
|
3
|
-
id: anti-patterns
|
|
4
|
-
title: Flutter Anti-Patterns & Forbidden APIs
|
|
5
|
-
type: output
|
|
6
|
-
createdAt: 2026-04-09T00:00:00Z
|
|
7
|
-
updatedAt: 2026-04-09T00:00:00Z
|
|
8
|
-
source:
|
|
9
|
-
producer: agent
|
|
10
|
-
skillId: harness-generator-frontend-flutter
|
|
11
|
-
inputs:
|
|
12
|
-
- documentId: clue-fe-flutter-skill
|
|
13
|
-
uri: ../../../../../moon_web/clue-fe-flutter.skill
|
|
14
|
-
relation: output-from
|
|
15
|
-
sections:
|
|
16
|
-
- sourceRange:
|
|
17
|
-
startLine: 62
|
|
18
|
-
endLine: 103
|
|
19
|
-
targetRange:
|
|
20
|
-
startLine: 32
|
|
21
|
-
endLine: 180
|
|
22
|
-
tags:
|
|
23
|
-
- flutter
|
|
24
|
-
- anti-patterns
|
|
25
|
-
- forbidden
|
|
26
|
-
- lint
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
# Flutter Anti-Patterns & Forbidden APIs
|
|
30
|
-
|
|
31
|
-
**Generator는 구현 완료 전 이 문서를 보고 셀프 체크한다.**
|
|
32
|
-
**Evaluator-Functional-Flutter 는 이 문서의 패턴을 `grep` 기반 정적 검증에 사용한다.**
|
|
33
|
-
|
|
34
|
-
각 항목은 **패턴 → 이유 → 대체 방법** 형태.
|
|
35
|
-
|
|
36
|
-
## 1. 웹 전용 API 직접 참조 (fe_target 별 차이)
|
|
37
|
-
|
|
38
|
-
### `fe_target = mobile` 또는 `desktop` — 금지
|
|
39
|
-
```dart
|
|
40
|
-
import 'dart:html'; // ✗
|
|
41
|
-
import 'package:universal_html/html.dart'; // ✗
|
|
42
|
-
import 'package:web/web.dart'; // ✗ (가드 없으면)
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 이유
|
|
46
|
-
Flutter 모바일/데스크톱 빌드에서 `dart:html` 참조는 즉시 빌드 실패한다.
|
|
47
|
-
가드 없이 import 하면 cross-platform 코드가 깨진다.
|
|
48
|
-
|
|
49
|
-
### 대체
|
|
50
|
-
- `kIsWeb` 분기로 플랫폼 체크 후 조건부 import
|
|
51
|
-
- `if (kIsWeb) { ... }` 가드 안에서만 web API 사용
|
|
52
|
-
- 또는 conditional import (`stub.dart` / `web.dart` / `io.dart`)
|
|
53
|
-
|
|
54
|
-
### `fe_target = web` — 허용 (단, 권장 패턴 준수)
|
|
55
|
-
```dart
|
|
56
|
-
import 'package:web/web.dart' as web; // ✓ 권장 (modern)
|
|
57
|
-
import 'dart:js_interop'; // ✓ JS interop
|
|
58
|
-
import 'dart:html'; // ✓ (legacy, 신규 코드는 package:web 권장)
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Web 권장 패턴
|
|
62
|
-
- 새 코드는 `package:web` + `dart:js_interop` 사용 (Flutter 3.7+ 에서 stable)
|
|
63
|
-
- `dart:html` 은 legacy — 마이그레이션 대상이지만 즉시 금지는 아님
|
|
64
|
-
- 가능하면 web 전용 코드를 별도 파일로 분리하고 conditional import:
|
|
65
|
-
```dart
|
|
66
|
-
// _web_helper.dart 또는 conditional import
|
|
67
|
-
import 'platform_helper.dart'
|
|
68
|
-
if (dart.library.html) 'platform_helper_web.dart'
|
|
69
|
-
if (dart.library.io) 'platform_helper_io.dart';
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### 검증
|
|
73
|
-
```bash
|
|
74
|
-
# fe_target = mobile/desktop: 0개여야 함
|
|
75
|
-
grep -rn "dart:html\|universal_html\|package:web" lib/ integrated_data_layer/lib/
|
|
76
|
-
|
|
77
|
-
# fe_target = web: 매치 OK, 단 'kIsWeb' 가드 또는 conditional import 와 함께 쓰는지 인접 라인 확인
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
## 2. print / console 남발
|
|
83
|
-
|
|
84
|
-
### 금지
|
|
85
|
-
```dart
|
|
86
|
-
print("user clicked");
|
|
87
|
-
debugPrint("response: $data");
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### 이유
|
|
91
|
-
프로덕션 빌드에 로그가 섞이면 성능/보안 문제. 통일된 로거가 없으면 분석 불가.
|
|
92
|
-
|
|
93
|
-
### 대체
|
|
94
|
-
프로젝트의 `logger` 모듈 사용 (`Logger().d()`, `.i()`, `.w()`, `.e()`).
|
|
95
|
-
디버그 전용 출력도 `kDebugMode` 가드 필수.
|
|
96
|
-
|
|
97
|
-
### 검증
|
|
98
|
-
```bash
|
|
99
|
-
grep -rn "^\s*print(" lib/ integrated_data_layer/lib/
|
|
100
|
-
grep -rn "console\.log" lib/
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## 3. 하드코딩 색상
|
|
106
|
-
|
|
107
|
-
### 금지
|
|
108
|
-
```dart
|
|
109
|
-
Container(color: Color(0xFF6682FF))
|
|
110
|
-
Text('Hello', style: TextStyle(color: Colors.blue))
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### 이유
|
|
114
|
-
디자인 시스템 붕괴 + 다크모드/브랜딩 변경 시 전역 수정 불가.
|
|
115
|
-
|
|
116
|
-
### 대체
|
|
117
|
-
```dart
|
|
118
|
-
Container(color: ColorManager.primary)
|
|
119
|
-
Text('Hello', style: TextStyle(color: ColorManager.textPrimary))
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
`ColorManager` (또는 프로젝트의 Design Token) 경유 필수.
|
|
123
|
-
|
|
124
|
-
### 검증
|
|
125
|
-
```bash
|
|
126
|
-
grep -rn "Color(0x" lib/ui/ | grep -v "ColorManager\|color_manager"
|
|
127
|
-
```
|
|
128
|
-
신규 파일에서 결과 0개여야 함 (레거시 파일은 예외).
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
## 4. StatefulWidget 내 직접 API 호출
|
|
133
|
-
|
|
134
|
-
### 금지
|
|
135
|
-
```dart
|
|
136
|
-
class _MyPageState extends State<MyPage> {
|
|
137
|
-
@override
|
|
138
|
-
void initState() {
|
|
139
|
-
super.initState();
|
|
140
|
-
DataLayer.instance.ac.getExample(exampleId: 1).then((res) { // ✗
|
|
141
|
-
setState(() { _data = res; });
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### 이유
|
|
148
|
-
테스트 불가능, 상태 공유 불가, UI와 비즈니스 로직 결합.
|
|
149
|
-
|
|
150
|
-
### 대체
|
|
151
|
-
VM (`NotifierProvider`) 에 API 호출을 옮기고 Page는 `ConsumerWidget` 으로.
|
|
152
|
-
상세 → [riverpod-pattern.md](./riverpod-pattern.md)
|
|
153
|
-
|
|
154
|
-
### 검증
|
|
155
|
-
`grep` 으로 자동 감지 어려움 — Evaluator가 신규 `*_page.dart` 파일에서
|
|
156
|
-
`dataLayer\|DataLayer` 호출 패턴을 확인하고, 파트너 `*_page_vm.dart` 파일에
|
|
157
|
-
동일 호출이 있는지 대조한다.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## 5. bridges/ 신규 사용
|
|
162
|
-
|
|
163
|
-
### 금지
|
|
164
|
-
```dart
|
|
165
|
-
import 'package:clue_mobile_app/bridges/xxx.dart';
|
|
166
|
-
```
|
|
167
|
-
신규 코드에서 참조 금지.
|
|
168
|
-
|
|
169
|
-
### 이유
|
|
170
|
-
BLOC 기반 레거시 — Riverpod 마이그레이션 방침에 따라 유지보수만 허용.
|
|
171
|
-
|
|
172
|
-
### 대체
|
|
173
|
-
신규 상태관리는 `NotifierProvider` + `Notifier<State>`.
|
|
174
|
-
|
|
175
|
-
### 검증
|
|
176
|
-
신규/수정 파일의 git diff에서 `bridges/` import 추가 여부 확인.
|
|
177
|
-
```bash
|
|
178
|
-
git diff --name-only HEAD~1 | xargs grep -l "bridges/" 2>/dev/null
|
|
179
|
-
```
|
|
180
|
-
새로 추가된 줄이 있으면 FAIL.
|
|
181
|
-
|
|
182
|
-
---
|
|
183
|
-
|
|
184
|
-
## 6. 하드코딩 문자열 (다국어 미처리)
|
|
185
|
-
|
|
186
|
-
### 금지
|
|
187
|
-
```dart
|
|
188
|
-
Text('취소')
|
|
189
|
-
Text('로그인 실패')
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### 이유
|
|
193
|
-
i18n 원칙 위반 — 다국어 전환 시 즉시 깨짐.
|
|
194
|
-
|
|
195
|
-
### 대체
|
|
196
|
-
```dart
|
|
197
|
-
Text(LocaleAssist().of.cancel)
|
|
198
|
-
Text(LocaleAssist().of.loginFailed)
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
상세 → [i18n-pattern.md](./i18n-pattern.md)
|
|
202
|
-
|
|
203
|
-
### 검증
|
|
204
|
-
```bash
|
|
205
|
-
grep -rn "Text('[가-힣ぁ-んァ-ヶ一-龯]" lib/ui/
|
|
206
|
-
```
|
|
207
|
-
신규 파일에서 결과 0개여야 함.
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## 7. JsonSerializable 누락 / includeIfNull 잘못 설정
|
|
212
|
-
|
|
213
|
-
### 금지
|
|
214
|
-
```dart
|
|
215
|
-
// @JsonSerializable 없이 수동 fromJson/toJson
|
|
216
|
-
@JsonSerializable() // includeIfNull 누락 → 기본값 true
|
|
217
|
-
class UserBody { ... }
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### 이유
|
|
221
|
-
서버가 null 필드를 bad request 처리하는 API 계약 위배. 코드 생성 불일치.
|
|
222
|
-
|
|
223
|
-
### 대체
|
|
224
|
-
```dart
|
|
225
|
-
@JsonSerializable(includeIfNull: false)
|
|
226
|
-
class UserBody { ... }
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Request Body는 **항상** `includeIfNull: false`.
|
|
230
|
-
|
|
231
|
-
### 검증
|
|
232
|
-
```bash
|
|
233
|
-
grep -rn "@JsonSerializable" integrated_data_layer/lib/2_data_sources/remote/request/body/ | grep -v "includeIfNull: false"
|
|
234
|
-
```
|
|
235
|
-
결과 0개여야 함.
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
## 8. Non-null 남용
|
|
240
|
-
|
|
241
|
-
### 금지
|
|
242
|
-
```dart
|
|
243
|
-
// 서버 응답에 대해 non-null 단정
|
|
244
|
-
class ExampleData {
|
|
245
|
-
final String name; // ✗ 필수 지시 없으면 Nullable
|
|
246
|
-
final int id;
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### 이유
|
|
251
|
-
서버는 언제든 필드를 누락할 수 있음. non-null은 런타임 crash 유발.
|
|
252
|
-
|
|
253
|
-
### 대체
|
|
254
|
-
```dart
|
|
255
|
-
class ExampleData {
|
|
256
|
-
final String? name;
|
|
257
|
-
final int? id;
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**필수 지시가 api-contract.json 에 명시된 필드만 non-null 허용.**
|
|
262
|
-
|
|
263
|
-
---
|
|
264
|
-
|
|
265
|
-
## 9. API 키 / 시크릿 하드코딩
|
|
266
|
-
|
|
267
|
-
### 금지
|
|
268
|
-
```dart
|
|
269
|
-
const apiKey = "sk-1234567890abcdef";
|
|
270
|
-
const Dio().options.headers['Authorization'] = 'Bearer xxx';
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### 이유
|
|
274
|
-
APK 디컴파일로 즉시 노출 — 보안 사고.
|
|
275
|
-
|
|
276
|
-
### 대체
|
|
277
|
-
`--dart-define` 또는 `flutter_dotenv` + `.env` (gitignore).
|
|
278
|
-
런타임에 서버에서 발급받는 토큰 사용.
|
|
279
|
-
|
|
280
|
-
### 검증
|
|
281
|
-
```bash
|
|
282
|
-
grep -rEn "(api[_-]?key|secret|token)\s*=\s*['\"][A-Za-z0-9]{16,}['\"]" lib/ integrated_data_layer/lib/
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
---
|
|
286
|
-
|
|
287
|
-
## 셀프 체크 스크립트
|
|
288
|
-
|
|
289
|
-
Generator는 handoff 전 다음 명령을 모두 실행하고 결과를 sprint-contract.md에 기록한다.
|
|
290
|
-
**`fe_target` 에 따라 1번 룰의 적용 여부가 달라진다.**
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
# fe_target 읽기
|
|
294
|
-
FE_TARGET=$(jq -r '.fe_target // "web"' .harness/actions/pipeline.json 2>/dev/null || echo "web")
|
|
295
|
-
|
|
296
|
-
# 1. 웹 API 직접 참조 — fe_target=web 이 아닐 때만 검사
|
|
297
|
-
if [ "$FE_TARGET" != "web" ]; then
|
|
298
|
-
grep -rn "dart:html\|universal_html\|package:web" lib/ integrated_data_layer/lib/ || echo "OK (FL-01 mobile/desktop)"
|
|
299
|
-
else
|
|
300
|
-
# web 타겟: 가드 없는 사용만 경고 (수동 검토)
|
|
301
|
-
grep -rn "dart:html\|package:web" lib/ | grep -v "kIsWeb\|conditional import" || echo "OK (FL-01 web — manual review)"
|
|
302
|
-
fi
|
|
303
|
-
|
|
304
|
-
# 2. print 남발
|
|
305
|
-
grep -rn "^\s*print(" lib/ integrated_data_layer/lib/ || echo "OK"
|
|
306
|
-
|
|
307
|
-
# 3. 하드코딩 색상 (신규 파일만 — git diff 기반)
|
|
308
|
-
git diff --name-only --diff-filter=A HEAD | grep '\.dart$' | xargs grep -n "Color(0x" 2>/dev/null || echo "OK"
|
|
309
|
-
|
|
310
|
-
# 4. bridges/ 신규 참조
|
|
311
|
-
git diff HEAD | grep "^+" | grep "bridges/" || echo "OK"
|
|
312
|
-
|
|
313
|
-
# 5. JsonSerializable + includeIfNull 확인
|
|
314
|
-
grep -rn "@JsonSerializable" integrated_data_layer/lib/2_data_sources/remote/request/body/ | grep -v "includeIfNull: false" || echo "OK"
|
|
315
|
-
|
|
316
|
-
# 6. 하드코딩 한글 (신규 UI)
|
|
317
|
-
git diff --name-only --diff-filter=AM HEAD | grep 'lib/ui/.*\.dart$' | xargs grep -n "Text('[가-힣]" 2>/dev/null || echo "OK"
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
모두 `OK` 여야 handoff 가능.
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
docmeta:
|
|
3
|
-
id: api-layer-pattern
|
|
4
|
-
title: API Layer Pattern (integrated_data_layer)
|
|
5
|
-
type: output
|
|
6
|
-
createdAt: 2026-04-09T00:00:00Z
|
|
7
|
-
updatedAt: 2026-04-09T00:00:00Z
|
|
8
|
-
source:
|
|
9
|
-
producer: agent
|
|
10
|
-
skillId: harness-generator-frontend-flutter
|
|
11
|
-
inputs:
|
|
12
|
-
- documentId: clue-fe-flutter-api-layer
|
|
13
|
-
uri: ../../../../../moon_web/clue-fe-flutter.skill
|
|
14
|
-
relation: output-from
|
|
15
|
-
sections:
|
|
16
|
-
- sourceRange:
|
|
17
|
-
startLine: 1
|
|
18
|
-
endLine: 187
|
|
19
|
-
targetRange:
|
|
20
|
-
startLine: 32
|
|
21
|
-
endLine: 205
|
|
22
|
-
tags:
|
|
23
|
-
- flutter
|
|
24
|
-
- retrofit
|
|
25
|
-
- json-serializable
|
|
26
|
-
- integrated-data-layer
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
# API Layer Pattern (integrated_data_layer)
|
|
30
|
-
|
|
31
|
-
하네스 Flutter Generator는 `api-contract.json` 을 **Source of Truth** 로 삼아
|
|
32
|
-
`integrated_data_layer` 하위에 Dart 타입을 1:1 생성한다.
|
|
33
|
-
|
|
34
|
-
## 디렉토리 구조
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
integrated_data_layer/
|
|
38
|
-
├── lib/
|
|
39
|
-
│ ├── 1_repositories/ # Repository (비즈니스 로직 래퍼)
|
|
40
|
-
│ │ ├── ac_repository.dart
|
|
41
|
-
│ │ ├── visitor_repository.dart
|
|
42
|
-
│ │ ├── s3_repository.dart
|
|
43
|
-
│ │ ├── oauth_repository.dart
|
|
44
|
-
│ │ └── reservation_repository.dart
|
|
45
|
-
│ ├── 2_data_sources/
|
|
46
|
-
│ │ ├── remote/
|
|
47
|
-
│ │ │ ├── rest_api.dart # 모든 Retrofit 엔드포인트
|
|
48
|
-
│ │ │ ├── rest_provider.dart # Dio/RestClient Provider
|
|
49
|
-
│ │ │ ├── request/body/ # Request Body 클래스
|
|
50
|
-
│ │ │ └── response/
|
|
51
|
-
│ │ │ └── abstract/ # 베이스 응답 (ClueResponseImpl 등)
|
|
52
|
-
│ │ └── local/
|
|
53
|
-
│ └── 3_others/
|
|
54
|
-
│ ├── enum/
|
|
55
|
-
│ └── extension/
|
|
56
|
-
├── test/ # lib/ 미러 구조
|
|
57
|
-
│ ├── 1_repositories/
|
|
58
|
-
│ ├── 2_data_sources/remote/request/body/
|
|
59
|
-
│ └── 2_data_sources/remote/response/
|
|
60
|
-
└── data_layer.dart # 진입점 (dataLayer Provider)
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## 1. api-contract.json → Request Body 변환
|
|
64
|
-
|
|
65
|
-
api-contract.json의 요청 스키마를 `@JsonSerializable(includeIfNull: false)`
|
|
66
|
-
클래스로 1:1 매핑한다. **필수 지시가 없으면 모든 필드 Nullable.**
|
|
67
|
-
|
|
68
|
-
```dart
|
|
69
|
-
// lib/2_data_sources/remote/request/body/example_body.dart
|
|
70
|
-
import 'package:json_annotation/json_annotation.dart';
|
|
71
|
-
|
|
72
|
-
part 'example_body.g.dart';
|
|
73
|
-
|
|
74
|
-
@JsonSerializable(includeIfNull: false) // ★ 필수
|
|
75
|
-
class ExampleBody {
|
|
76
|
-
final String? name; // ★ Nullable 기본
|
|
77
|
-
final int? placeId;
|
|
78
|
-
final String? description;
|
|
79
|
-
|
|
80
|
-
const ExampleBody({
|
|
81
|
-
this.name,
|
|
82
|
-
this.placeId,
|
|
83
|
-
this.description,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
factory ExampleBody.fromJson(Map<String, dynamic> json) =>
|
|
87
|
-
_$ExampleBodyFromJson(json);
|
|
88
|
-
Map<String, dynamic> toJson() => _$ExampleBodyToJson(this);
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## 2. Response 변환
|
|
93
|
-
|
|
94
|
-
응답 베이스 클래스 계층:
|
|
95
|
-
- `ClueResponseImpl<T>` — 단일 데이터 (code, data, errors)
|
|
96
|
-
- `ClueResponseListImpl<T>` — 목록 (code, data, errors, totalCount)
|
|
97
|
-
- `VisitorResponseImpl<T>` — Visitor 서버 전용
|
|
98
|
-
|
|
99
|
-
```dart
|
|
100
|
-
// lib/2_data_sources/remote/response/example_response.dart
|
|
101
|
-
import 'package:integrated_data_layer/2_data_sources/remote/response/abstract/clue/clue_response_impl.dart';
|
|
102
|
-
import 'package:json_annotation/json_annotation.dart';
|
|
103
|
-
|
|
104
|
-
part 'example_response.g.dart';
|
|
105
|
-
|
|
106
|
-
@JsonSerializable(explicitToJson: true)
|
|
107
|
-
class ExampleResponse extends ClueResponseImpl<ExampleResponseData> {
|
|
108
|
-
const ExampleResponse({
|
|
109
|
-
required super.code,
|
|
110
|
-
required super.data,
|
|
111
|
-
required super.errors,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
factory ExampleResponse.fromJson(Map<String, dynamic> json) =>
|
|
115
|
-
_$ExampleResponseFromJson(json);
|
|
116
|
-
Map<String, dynamic> toJson() => _$ExampleResponseToJson(this);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
@JsonSerializable(explicitToJson: true)
|
|
120
|
-
class ExampleResponseData {
|
|
121
|
-
final int? id; // ★ Nullable 기본
|
|
122
|
-
final String? name;
|
|
123
|
-
|
|
124
|
-
const ExampleResponseData({this.id, this.name});
|
|
125
|
-
|
|
126
|
-
factory ExampleResponseData.fromJson(Map<String, dynamic> json) =>
|
|
127
|
-
_$ExampleResponseDataFromJson(json);
|
|
128
|
-
Map<String, dynamic> toJson() => _$ExampleResponseDataToJson(this);
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**기존 응답 재사용 원칙**: api-contract.json 의 서로 다른 엔드포인트가
|
|
133
|
-
동일한 응답 구조를 가지면 새 Response 클래스를 만들지 않는다.
|
|
134
|
-
|
|
135
|
-
## 3. rest_api.dart 엔드포인트 추가
|
|
136
|
-
|
|
137
|
-
```dart
|
|
138
|
-
// rest_api.dart 내부 — RestClient 클래스에 추가
|
|
139
|
-
@GET("/examples/{exampleId}")
|
|
140
|
-
Future<ExampleResponse> getExample(
|
|
141
|
-
@Path("exampleId") int exampleId,
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
@POST("/examples")
|
|
145
|
-
Future<ExampleResponse> createExample(
|
|
146
|
-
@Body() ExampleBody requestBody,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
@PUT("/examples/{exampleId}")
|
|
150
|
-
Future<ExampleResponse> updateExample(
|
|
151
|
-
@Path("exampleId") int exampleId,
|
|
152
|
-
@Body() ExampleBody requestBody,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
@DELETE("/examples/{exampleId}")
|
|
156
|
-
Future<ClueDefaultResponse> deleteExample(
|
|
157
|
-
@Path("exampleId") int exampleId,
|
|
158
|
-
);
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
api-contract.json의 path, method, param 위치를 **그대로** 옮긴다.
|
|
162
|
-
경로 변수 추가/제거 금지 — 계약이 Source of Truth.
|
|
163
|
-
|
|
164
|
-
## 4. Repository 래퍼 추가
|
|
165
|
-
|
|
166
|
-
```dart
|
|
167
|
-
// 1_repositories/ac_repository.dart 내부에 메서드 추가
|
|
168
|
-
Future<ExampleResponse> getExample({required int exampleId}) {
|
|
169
|
-
return _restClient.getExample(exampleId);
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Repository는 **순수 래퍼** — 비즈니스 로직 없이 Retrofit 호출을 전달만 한다.
|
|
174
|
-
도메인 분기, 에러 매핑은 VM 계층에서 수행.
|
|
175
|
-
|
|
176
|
-
## 5. 호출 패턴 (VM에서)
|
|
177
|
-
|
|
178
|
-
```dart
|
|
179
|
-
// VM 내부
|
|
180
|
-
final result = await ref.read(dataLayer).ac.getExample(exampleId: 123);
|
|
181
|
-
if (result.code == 200) {
|
|
182
|
-
// 성공 처리
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## 6. TC 작성 (필수)
|
|
187
|
-
|
|
188
|
-
**모든 Request Body, Response에 왕복 테스트 필수.**
|
|
189
|
-
Evaluator가 test 디렉토리를 점검한다.
|
|
190
|
-
|
|
191
|
-
```dart
|
|
192
|
-
// test/2_data_sources/remote/request/body/example_body_test.dart
|
|
193
|
-
import 'dart:convert';
|
|
194
|
-
import 'package:flutter_test/flutter_test.dart';
|
|
195
|
-
import 'package:integrated_data_layer/2_data_sources/remote/request/body/example_body.dart';
|
|
196
|
-
|
|
197
|
-
void main() {
|
|
198
|
-
group("example body test", () {
|
|
199
|
-
test("/fromJson & toJson", () {
|
|
200
|
-
ExampleBody body1 = const ExampleBody(
|
|
201
|
-
name: 'test',
|
|
202
|
-
placeId: 1,
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
ExampleBody body2 = ExampleBody.fromJson(body1.toJson());
|
|
206
|
-
|
|
207
|
-
expect(body1.name, 'test');
|
|
208
|
-
expect(body1.placeId, 1);
|
|
209
|
-
|
|
210
|
-
var body1Data = jsonEncode(body1.toJson());
|
|
211
|
-
var body2Data = jsonEncode(body2.toJson());
|
|
212
|
-
expect(body1Data == body2Data, true);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("/null fields excluded", () {
|
|
216
|
-
ExampleBody body = const ExampleBody(name: 'test');
|
|
217
|
-
var json = body.toJson();
|
|
218
|
-
expect(json.containsKey('placeId'), false); // includeIfNull: false 검증
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
## 7. 코드 생성
|
|
225
|
-
|
|
226
|
-
모든 Request/Response 파일 추가/수정 후 **반드시** 실행:
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
cd <integrated_data_layer 경로>
|
|
230
|
-
flutter pub run build_runner build --delete-conflicting-outputs
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
생성된 `*.g.dart` 파일은 커밋한다 (CI에서 재생성하지 않음).
|