@walwal-harness/cli 2.4.0 → 3.2.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.
@@ -33,26 +33,49 @@ docmeta:
33
33
 
34
34
  각 항목은 **패턴 → 이유 → 대체 방법** 형태.
35
35
 
36
- ## 1. 웹 전용 API 직접 참조
36
+ ## 1. 웹 전용 API 직접 참조 (fe_target 별 차이)
37
37
 
38
- ### 금지
38
+ ### `fe_target = mobile` 또는 `desktop` — 금지
39
39
  ```dart
40
- import 'dart:html';
41
- import 'package:universal_html/html.dart';
40
+ import 'dart:html'; // ✗
41
+ import 'package:universal_html/html.dart'; // ✗
42
+ import 'package:web/web.dart'; // ✗ (가드 없으면)
42
43
  ```
43
44
 
44
45
  ### 이유
45
- Flutter 앱은 크로스 플랫폼 — 모바일 빌드에서 `dart:html` 참조는 즉시 빌드 실패한다.
46
- Web 빌드 전용 코드가 필요하면 조건부 import 패턴을 사용한다.
46
+ Flutter 모바일/데스크톱 빌드에서 `dart:html` 참조는 즉시 빌드 실패한다.
47
+ 가드 없이 import 하면 cross-platform 코드가 깨진다.
47
48
 
48
49
  ### 대체
49
- `package:web` 또는 `kIsWeb` 분기로 플랫폼 체크 후 조건부 import.
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
+ ```
50
71
 
51
72
  ### 검증
52
73
  ```bash
53
- grep -rn "dart:html\|universal_html" lib/ integrated_data_layer/lib/
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 와 함께 쓰는지 인접 라인 확인
54
78
  ```
55
- 결과 0개여야 함.
56
79
 
57
80
  ---
58
81
 
@@ -263,11 +286,20 @@ grep -rEn "(api[_-]?key|secret|token)\s*=\s*['\"][A-Za-z0-9]{16,}['\"]" lib/ int
263
286
 
264
287
  ## 셀프 체크 스크립트
265
288
 
266
- Generator는 handoff 전 다음 명령을 모두 실행하고 결과를 sprint-contract.md에 기록한다:
289
+ Generator는 handoff 전 다음 명령을 모두 실행하고 결과를 sprint-contract.md에 기록한다.
290
+ **`fe_target` 에 따라 1번 룰의 적용 여부가 달라진다.**
267
291
 
268
292
  ```bash
269
- # 1. 웹 API 직접 참조
270
- grep -rn "dart:html\|universal_html" lib/ integrated_data_layer/lib/ || echo "OK"
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
271
303
 
272
304
  # 2. print 남발
273
305
  grep -rn "^\s*print(" lib/ integrated_data_layer/lib/ || echo "OK"
@@ -0,0 +1,273 @@
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 는 빌드 산출물만 보장.
@@ -76,40 +76,87 @@ Planner는 Sprint 1 착수 전 반드시 `fe_stack`을 확정하고 `pipeline.js
76
76
 
77
77
  혼재 프로젝트(Web + Flutter mobile)라면 사용자에게 **현 스프린트의 타깃**을 묻는다 — 파이프라인은 한 번에 하나의 `fe_stack`만 취급한다.
78
78
 
79
+ ## 2.5 fe_target 확정 (Flutter 전용)
80
+
81
+ `fe_stack == "flutter"` 인 경우, **반드시** `fe_target` 도 함께 확정해야 한다 (`web` | `mobile` | `desktop`).
82
+ 이는 Flutter Web vs Mobile/Desktop 에서 사용 가능한 API, 빌드 명령, evaluator 가 달라지기 때문이다.
83
+
84
+ ### 자동 감지 (scan-result.json 의 `tech_stack.fe_target`)
85
+
86
+ | 시그널 | fe_target |
87
+ |--------|-----------|
88
+ | `<flutter_root>/web/index.html` 존재 | `web` |
89
+ | `<flutter_root>/android/` 또는 `ios/` 존재, web/ 없음 | `mobile` |
90
+ | `<flutter_root>/macos/`, `windows/`, `linux/` 존재 (mobile/web 없음) | `desktop` |
91
+ | 없음 | `unknown` |
92
+
93
+ ### 사용자 질문 (불명확하거나 multi-target)
94
+
95
+ ```
96
+ Flutter 프로젝트의 타깃을 확인합니다:
97
+
98
+ (A) Web — 브라우저 (Chrome/Safari/Firefox), 컴파일 결과는 HTML+JS+CSS
99
+ (B) Mobile — Android / iOS 네이티브 빌드
100
+ (C) Desktop — macOS / Windows / Linux 네이티브 빌드
101
+
102
+ 감지된 시그널:
103
+ - web/index.html: [있음/없음]
104
+ - android|ios/: [있음/없음]
105
+ - macos|windows|linux/: [있음/없음]
106
+
107
+ 이번 스프린트의 타깃은? (A/B/C)
108
+ ```
109
+
110
+ ### fe_target → Eval 흐름
111
+
112
+ | fe_target | Generator | Eval-Functional | Eval-Visual |
113
+ |-----------|-----------|----------------|-------------|
114
+ | `web` | `generator-frontend-flutter` | `evaluator-functional` (Playwright!) | `evaluator-visual` (Playwright!) |
115
+ | `mobile` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | SKIP |
116
+ | `desktop` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | SKIP |
117
+
118
+ **핵심**: Flutter Web 의 빌드 결과물(HTML/JS/CSS)은 일반 웹앱과 동일하므로 Playwright 기반
119
+ React 경로의 evaluator 를 그대로 재사용한다. Generator-Frontend-Flutter 의 Self-Verification
120
+ 단계에서 `flutter analyze`/`flutter test`/`flutter build web` 정적 검증이 이미 통과한 상태로 handoff 된다.
121
+
79
122
  ## 3. pipeline.json 갱신
80
123
 
81
- `fe_stack` 확정 후 `pipeline.json`에 반드시 추가:
124
+ `fe_stack` + `fe_target` 확정 후 `pipeline.json`에 반드시 추가:
82
125
 
83
126
  ```json
84
127
  {
85
128
  "pipeline": "FULLSTACK",
86
129
  "planner_mode": "full",
87
130
  "fe_stack": "flutter",
131
+ "fe_target": "web",
88
132
  "agents_active": [
89
133
  "planner",
90
134
  "generator-backend",
91
135
  "generator-frontend-flutter",
92
- "evaluator-functional-flutter"
136
+ "evaluator-functional",
137
+ "evaluator-visual"
93
138
  ],
94
139
  "agents_skipped": [
95
140
  "generator-frontend",
96
- "evaluator-functional",
97
- "evaluator-visual"
141
+ "evaluator-functional-flutter"
98
142
  ],
99
- "evaluator_mode": "flutter-native",
100
- "notes": "Flutter 앱 — Playwright 대신 flutter analyze/test + 정적 anti-pattern 검증"
143
+ "evaluator_mode": "playwright-web",
144
+ "notes": "Flutter Web — 컴파일 결과가 HTML+JS+CSS 이므로 React 경로의 Playwright evaluator 사용. Generator 의 Self-Verification 에서 flutter analyze/test 통과 전제."
101
145
  }
102
146
  ```
103
147
 
104
- ### fe_stack → 파이프라인 매핑
105
-
106
- | pipeline | fe_stack | agents_active 예시 |
107
- |----------|----------|-------------------|
108
- | FULLSTACK | react | planner, generator-backend, generator-frontend, evaluator-functional, evaluator-visual |
109
- | FULLSTACK | flutter | planner, generator-backend, generator-frontend-flutter, evaluator-functional-flutter |
110
- | FE-ONLY | react | planner, generator-frontend, evaluator-functional, evaluator-visual |
111
- | FE-ONLY | flutter | planner, generator-frontend-flutter, evaluator-functional-flutter |
112
- | BE-ONLY | (무관) | planner, generator-backend, evaluator-functional |
148
+ ### fe_stack + fe_target → 파이프라인 매핑
149
+
150
+ | pipeline | fe_stack | fe_target | agents_active 예시 |
151
+ |----------|----------|-----------|-------------------|
152
+ | FULLSTACK | react | (n/a) | planner, generator-backend, generator-frontend, evaluator-functional, evaluator-visual |
153
+ | FULLSTACK | flutter | **web** | planner, generator-backend, generator-frontend-flutter, evaluator-functional, evaluator-visual |
154
+ | FULLSTACK | flutter | mobile | planner, generator-backend, generator-frontend-flutter, evaluator-functional-flutter |
155
+ | FULLSTACK | flutter | desktop | planner, generator-backend, generator-frontend-flutter, evaluator-functional-flutter |
156
+ | FE-ONLY | react | (n/a) | planner, generator-frontend, evaluator-functional, evaluator-visual |
157
+ | FE-ONLY | flutter | **web** | planner, generator-frontend-flutter, evaluator-functional, evaluator-visual |
158
+ | FE-ONLY | flutter | mobile | planner, generator-frontend-flutter, evaluator-functional-flutter |
159
+ | BE-ONLY | (무관) | (n/a) | planner, generator-backend, evaluator-functional |
113
160
 
114
161
  ## 4. Flutter 선택 시 추가 작업
115
162