@walwal-harness/cli 4.0.0-alpha.2 → 4.0.0-alpha.21

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.
@@ -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에서 재생성하지 않음).
@@ -1,273 +0,0 @@
1
- ---
2
- docmeta:
3
- id: flutter-web-pattern
4
- title: Flutter Web 패턴 — fe_target=web 전용
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: harness-generator-frontend-flutter-skill
13
- uri: ../SKILL.md
14
- relation: output-from
15
- sections:
16
- - sourceRange:
17
- startLine: 56
18
- endLine: 90
19
- targetRange:
20
- startLine: 21
21
- endLine: 240
22
- - documentId: flutter-anti-patterns
23
- uri: ./anti-patterns.md
24
- relation: output-from
25
- sections:
26
- - sourceRange:
27
- startLine: 22
28
- endLine: 75
29
- targetRange:
30
- startLine: 90
31
- endLine: 165
32
- tags:
33
- - flutter
34
- - flutter-web
35
- - dart-web
36
- - go-router
37
- ---
38
-
39
- # Flutter Web 패턴 (`fe_target = web` 전용)
40
-
41
- `pipeline.json.fe_target == "web"` 일 때만 적용. Mobile/Desktop 타겟에서는 이 문서의 규칙을 무시한다.
42
-
43
- ## 1. 프로젝트 활성화
44
-
45
- 기존 Flutter 프로젝트에 Web 타겟이 없으면:
46
-
47
- ```bash
48
- flutter config --enable-web
49
- flutter create --platforms=web .
50
- ```
51
-
52
- `web/index.html`, `web/manifest.json`, `web/favicon.png`, `web/icons/` 가 생성된다.
53
-
54
- ## 2. 빌드 및 개발 서버
55
-
56
- | 명령 | 용도 |
57
- |------|------|
58
- | `flutter run -d chrome` | 개발 서버 (HMR 포함) |
59
- | `flutter run -d chrome --web-port 8080` | 포트 고정 |
60
- | `flutter build web --release` | 프로덕션 빌드 (`build/web/`) |
61
- | `flutter build web --release --web-renderer canvaskit` | CanvasKit 렌더러 (성능 우선) |
62
- | `flutter build web --release --web-renderer html` | HTML 렌더러 (호환성 우선, 작은 번들) |
63
-
64
- > **렌더러 선택**: 모바일 사파리 호환성이 중요하면 `html`, 데스크톱/Chrome 중심이면 `canvaskit` (또는 auto).
65
-
66
- ## 3. 라우팅 — `go_router` 권장
67
-
68
- URL 동기화 + 브라우저 history 지원을 위해 `go_router` 사용.
69
-
70
- ```yaml
71
- # pubspec.yaml
72
- dependencies:
73
- go_router: ^14.0.0
74
- ```
75
-
76
- ```dart
77
- // lib/router/app_router.dart
78
- import 'package:go_router/go_router.dart';
79
-
80
- final pAppRouterProvider = Provider<GoRouter>((ref) {
81
- return GoRouter(
82
- initialLocation: '/',
83
- routes: [
84
- GoRoute(path: '/', builder: (ctx, state) => const HomePage()),
85
- GoRoute(path: '/login', builder: (ctx, state) => const LoginPage()),
86
- GoRoute(
87
- path: '/items/:id',
88
- builder: (ctx, state) => ItemPage(id: state.pathParameters['id']!),
89
- ),
90
- ],
91
- );
92
- });
93
- ```
94
-
95
- ```dart
96
- // main.dart
97
- class App extends ConsumerWidget {
98
- @override
99
- Widget build(BuildContext context, WidgetRef ref) {
100
- final router = ref.watch(pAppRouterProvider);
101
- return MaterialApp.router(
102
- routerConfig: router,
103
- // ...
104
- );
105
- }
106
- }
107
- ```
108
-
109
- **Hash routing vs Path routing**:
110
- - 기본은 hash routing (`/#/login`)
111
- - Path routing 으로 바꾸려면 `web/index.html` 의 `<base href="/">` 설정 + 서버 fallback (모든 경로 → `index.html`)
112
-
113
- ## 4. JS Interop (필요할 때만)
114
-
115
- 크로스플랫폼 코드를 우선하되, Web 전용 API 가 필요하면 `package:web` + `dart:js_interop` 사용.
116
-
117
- ```dart
118
- import 'package:web/web.dart' as web;
119
- import 'dart:js_interop';
120
-
121
- void copyToClipboard(String text) {
122
- web.window.navigator.clipboard.writeText(text.toJS);
123
- }
124
-
125
- String getUserAgent() => web.window.navigator.userAgent;
126
- ```
127
-
128
- **Conditional import 패턴** (cross-platform 코드를 web/io 로 분기):
129
-
130
- ```dart
131
- // platform_helper.dart (interface)
132
- abstract class PlatformHelper {
133
- String get platformName;
134
- }
135
-
136
- PlatformHelper getPlatformHelper() => throw UnimplementedError();
137
- ```
138
-
139
- ```dart
140
- // platform_helper_web.dart
141
- import 'package:web/web.dart' as web;
142
- import 'platform_helper.dart';
143
-
144
- class WebPlatformHelper implements PlatformHelper {
145
- @override
146
- String get platformName => 'web (${web.window.navigator.userAgent})';
147
- }
148
-
149
- PlatformHelper getPlatformHelper() => WebPlatformHelper();
150
- ```
151
-
152
- ```dart
153
- // platform_helper_io.dart
154
- import 'dart:io';
155
- import 'platform_helper.dart';
156
-
157
- class IoPlatformHelper implements PlatformHelper {
158
- @override
159
- String get platformName => 'io (${Platform.operatingSystem})';
160
- }
161
-
162
- PlatformHelper getPlatformHelper() => IoPlatformHelper();
163
- ```
164
-
165
- ```dart
166
- // 사용처
167
- import 'platform_helper.dart'
168
- if (dart.library.html) 'platform_helper_web.dart'
169
- if (dart.library.io) 'platform_helper_io.dart';
170
-
171
- final helper = getPlatformHelper();
172
- print(helper.platformName);
173
- ```
174
-
175
- ## 5. CORS 와 백엔드 연동
176
-
177
- Flutter Web 은 브라우저에서 실행되므로 백엔드 API 호출 시 **CORS** 가 적용된다.
178
-
179
- - 개발 단계: 백엔드 Gateway 의 `Access-Control-Allow-Origin` 에 `http://localhost:8080` (또는 `*`) 추가
180
- - 프로덕션: 동일 origin 또는 명시적 CORS 화이트리스트
181
- - API 키/인증 토큰은 **HttpOnly cookie** 또는 메모리 보관 (localStorage 는 XSS 위험)
182
-
183
- `Dio` 인터셉터로 CSRF 토큰 등을 헤더에 추가:
184
-
185
- ```dart
186
- final dio = Dio(BaseOptions(
187
- baseUrl: 'https://api.example.com',
188
- headers: {'Content-Type': 'application/json'},
189
- ));
190
-
191
- dio.interceptors.add(InterceptorsWrapper(
192
- onRequest: (options, handler) {
193
- final token = ref.read(pAuthProvider).accessToken;
194
- if (token != null) {
195
- options.headers['Authorization'] = 'Bearer $token';
196
- }
197
- return handler.next(options);
198
- },
199
- ));
200
- ```
201
-
202
- ## 6. SEO / Meta Tags
203
-
204
- `web/index.html` 의 `<head>` 에 SEO 메타 태그 추가:
205
-
206
- ```html
207
- <meta name="description" content="Suprema CLUe — Smart Access Control">
208
- <meta property="og:title" content="CLUe">
209
- <meta property="og:description" content="...">
210
- <meta property="og:image" content="/icons/og-image.png">
211
- <link rel="canonical" href="https://app.example.com">
212
- ```
213
-
214
- > SPA 의 SEO 한계: Flutter Web 은 client-side rendering 이므로 검색엔진이 동적 콘텐츠를 인덱싱하지 못할 수 있다. SSR 이 필요하면 Next.js + REST API 패턴을 고려.
215
-
216
- ## 7. 자산 최적화
217
-
218
- - 이미지: `assets/images/` 에 두고 `pubspec.yaml` 의 `flutter.assets` 에 등록
219
- - 폰트: `web/fonts/` 또는 `assets/fonts/` + `pubspec.yaml` 의 `flutter.fonts`
220
- - 큰 이미지: webp 사용 권장 (`flutter_image_compress` 또는 사전 변환)
221
- - Tree-shaking: `flutter build web --tree-shake-icons` 로 사용하지 않는 Material 아이콘 제거
222
-
223
- ## 8. PWA (Progressive Web App)
224
-
225
- `flutter create --platforms=web` 시 자동 생성되는 `web/manifest.json` 을 채워서 PWA 로 동작:
226
-
227
- ```json
228
- {
229
- "name": "CLUe",
230
- "short_name": "CLUe",
231
- "start_url": "/",
232
- "display": "standalone",
233
- "background_color": "#FFFFFF",
234
- "theme_color": "#6682FF",
235
- "icons": [
236
- { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" },
237
- { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }
238
- ]
239
- }
240
- ```
241
-
242
- Service worker 는 `flutter build web` 시 자동 생성된다 (`flutter_service_worker.js`).
243
-
244
- ## 9. 디버깅 — Chrome DevTools
245
-
246
- - `flutter run -d chrome` 후 Chrome 자체 DevTools 열기 (F12)
247
- - Console 에서 `print()` 출력 확인 가능 (단, 프로덕션 빌드에서는 `print` 금지)
248
- - Source map 활성화: `--web-renderer html --source-maps` (디버그 빌드 기본)
249
- - Network 탭에서 API 호출 직접 검사 가능 → Playwright 기반 `evaluator-functional` 도 동일하게 동작
250
-
251
- ## 10. Eval 인터페이스
252
-
253
- `fe_target = web` 인 경우 `harness-next.sh` 가 다음과 같이 자동 라우팅:
254
-
255
- ```
256
- generator-frontend-flutter (Self-Verification: flutter analyze + flutter test)
257
- → evaluator-functional (Playwright MCP — http://localhost:포트 E2E)
258
- → evaluator-visual (Playwright MCP — 스크린샷 + 반응형 + 접근성)
259
- ```
260
-
261
- **Generator 의 Self-Verification 단계에서 `flutter build web --release` 가 성공하는지 확인** 후 handoff.
262
-
263
- ## 11. 호스팅 (참고)
264
-
265
- | 호스팅 | 빌드 산출물 |
266
- |--------|------------|
267
- | Vercel | `build/web/` 디렉토리를 정적 호스팅 |
268
- | Netlify | 동일 |
269
- | Firebase Hosting | `firebase deploy --only hosting` (firebase.json 의 public 을 `build/web` 으로) |
270
- | GitHub Pages | `build/web/` 을 gh-pages 브랜치에 푸시 |
271
- | 자체 서버 | nginx 로 정적 파일 서빙 + SPA fallback (`try_files $uri /index.html`) |
272
-
273
- 호스팅 결정은 Planner / 사용자가 함. Generator 는 빌드 산출물만 보장.
@@ -1,102 +0,0 @@
1
- ---
2
- docmeta:
3
- id: i18n-pattern
4
- title: 다국어 (i18n) ARB 패턴
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-i18n
13
- uri: ../../../../../moon_web/clue-fe-flutter.skill
14
- relation: output-from
15
- sections:
16
- - sourceRange:
17
- startLine: 1
18
- endLine: 60
19
- targetRange:
20
- startLine: 32
21
- endLine: 110
22
- tags:
23
- - flutter
24
- - i18n
25
- - arb
26
- - l10n
27
- ---
28
-
29
- # 다국어 (i18n) ARB 패턴
30
-
31
- ## ARB 파일 기반 l10n
32
-
33
- ### 파일 경로
34
-
35
- - 영어: `lib/l10n/app_en.arb`
36
- - 한국어: `lib/l10n/app_ko.arb`
37
- - 일본어: `lib/l10n/app_ja.arb`
38
-
39
- 프로젝트에 따라 `assets/strings/en.json` 등 다른 경로를 쓸 수 있다.
40
- `AGENTS.md` / sprint-contract.md 의 기존 컨벤션을 우선한다.
41
-
42
- ### 작업 순서
43
-
44
- 1. 문자열 언어 판별 (예: "취소" → ko, "Cancel" → en)
45
- 2. 해당 언어 arb 파일에 **키 존재 여부 확인** (중복 방지)
46
- 3. 없으면 모든 언어 파일에 동시 추가 (`app_en.arb`, `app_ko.arb`, `app_ja.arb`)
47
- 4. 코드 치환 → `LocaleAssist().of.키이름`
48
-
49
- ### 사용법
50
-
51
- ```dart
52
- // 기본 문자열
53
- Text(
54
- LocaleAssist().of.cancel,
55
- style: MyTextStyle.size15.w500.xFF9CA3AF,
56
- )
57
-
58
- // 문자열 내 스타일 별도 구현 (ClueText 등 프로젝트 커스텀 위젯 사용)
59
- ClueText(
60
- "${LocaleAssist().of.all} (${count})",
61
- style: MyTextStyle.size16.w500,
62
- targetList: [
63
- TargetModel(
64
- text: "(${count})",
65
- style: MyTextStyle.size16.w500.xFF6682FF,
66
- ),
67
- ],
68
- )
69
- ```
70
-
71
- ### ARB 키 네이밍 규칙
72
-
73
- ```json
74
- {
75
- "cancel": "취소",
76
- "confirm": "확인",
77
- "doorOpen": "문 열기",
78
- "networkError": "네트워크 오류가 발생했습니다"
79
- }
80
- ```
81
-
82
- - **camelCase** 사용
83
- - 모든 arb 파일(en, ko, ja)에 **동일 키 동시 추가**
84
- - 기존 키 검색 후 중복 방지 (`grep -rn '"cancel"' lib/l10n/`)
85
- - 플레이스홀더 필요 시 ICU MessageFormat 사용
86
-
87
- ## 필수 원칙
88
-
89
- - 영문 전용 표기 지시가 없는 한 **모든 사용자 노출 문자열은 다국어 처리**
90
- - 하드코딩된 문자열 사용 금지 (`Text('취소')` ✗)
91
- - 새 페이지 추가 시 관련 문자열 **일괄 등록** — 스프린트 중 누락 방지
92
- - 모든 arb 파일의 키 집합은 동일해야 한다 (Evaluator가 diff 검사)
93
-
94
- ## Self-Check
95
-
96
- ```bash
97
- # 하드코딩된 한글 탐지
98
- grep -rn "Text('[가-힣]" lib/ui/
99
-
100
- # 누락 키 탐지 (en 기준)
101
- python3 -c "import json; en=set(json.load(open('lib/l10n/app_en.arb')).keys()); ko=set(json.load(open('lib/l10n/app_ko.arb')).keys()); print('missing in ko:', en - ko); print('missing in en:', ko - en)"
102
- ```