@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.
@@ -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
- ```
@@ -1,199 +0,0 @@
1
- ---
2
- docmeta:
3
- id: riverpod-pattern
4
- title: Riverpod 상태관리 패턴 (Page + VM)
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-riverpod
13
- uri: ../../../../../moon_web/clue-fe-flutter.skill
14
- relation: output-from
15
- sections:
16
- - sourceRange:
17
- startLine: 1
18
- endLine: 149
19
- targetRange:
20
- startLine: 32
21
- endLine: 180
22
- tags:
23
- - flutter
24
- - riverpod
25
- - state-management
26
- - notifier-provider
27
- ---
28
-
29
- # Riverpod 상태관리 패턴 (Page + VM)
30
-
31
- ## 기본 구조: Page + VM 쌍
32
-
33
- 모든 화면은 `xxx_page.dart` + `xxx_page_vm.dart` 쌍으로 구성.
34
- 이는 테스트 가능성과 UI/로직 분리를 위한 강제 규약.
35
-
36
- ### VM (ViewModel)
37
-
38
- ```dart
39
- // example_page_vm.dart
40
- import 'package:equatable/equatable.dart';
41
- import 'package:flutter_riverpod/flutter_riverpod.dart';
42
- import 'package:integrated_data_layer/data_layer.dart';
43
-
44
- final pExampleProvider =
45
- NotifierProvider<ExampleNotifier, ExampleState>(ExampleNotifier.new);
46
-
47
- class ExampleNotifier extends Notifier<ExampleState> {
48
- @override
49
- ExampleState build() {
50
- return const ExampleState();
51
- }
52
-
53
- Future<void> loadData() async {
54
- state = state.copyWith(isLoading: true);
55
- try {
56
- final result = await ref.read(dataLayer).ac.getExample(exampleId: 1);
57
- if (result.code == 200) {
58
- state = state.copyWith(
59
- isLoading: false,
60
- data: result.data,
61
- );
62
- } else {
63
- state = state.copyWith(
64
- isLoading: false,
65
- error: 'code=${result.code}',
66
- );
67
- }
68
- } catch (e) {
69
- state = state.copyWith(isLoading: false, error: e.toString());
70
- }
71
- }
72
- }
73
-
74
- class ExampleState extends Equatable {
75
- final bool isLoading;
76
- final dynamic data;
77
- final String? error;
78
-
79
- const ExampleState({
80
- this.isLoading = false,
81
- this.data,
82
- this.error,
83
- });
84
-
85
- ExampleState copyWith({
86
- bool? isLoading,
87
- dynamic data,
88
- String? error,
89
- }) {
90
- return ExampleState(
91
- isLoading: isLoading ?? this.isLoading,
92
- data: data ?? this.data,
93
- error: error ?? this.error,
94
- );
95
- }
96
-
97
- @override
98
- List<Object?> get props => [isLoading, data, error];
99
- }
100
- ```
101
-
102
- ### Page
103
-
104
- ```dart
105
- // example_page.dart
106
- import 'package:flutter/material.dart';
107
- import 'package:flutter_riverpod/flutter_riverpod.dart';
108
- import 'example_page_vm.dart';
109
-
110
- class ExamplePage extends ConsumerStatefulWidget {
111
- const ExamplePage({super.key});
112
-
113
- @override
114
- ConsumerState<ExamplePage> createState() => _ExamplePageState();
115
- }
116
-
117
- class _ExamplePageState extends ConsumerState<ExamplePage> {
118
- @override
119
- void initState() {
120
- super.initState();
121
- // 초기 데이터 로드는 addPostFrameCallback 으로
122
- WidgetsBinding.instance.addPostFrameCallback((_) {
123
- ref.read(pExampleProvider.notifier).loadData();
124
- });
125
- }
126
-
127
- @override
128
- Widget build(BuildContext context) {
129
- final state = ref.watch(pExampleProvider); // watch로 상태 구독
130
-
131
- if (state.isLoading) {
132
- return const Center(child: CircularProgressIndicator());
133
- }
134
- if (state.error != null) {
135
- return Center(child: Text(state.error!));
136
- }
137
- // ... 정상 상태 렌더링
138
- return const SizedBox.shrink();
139
- }
140
- }
141
- ```
142
-
143
- ## Family 패턴 (다중 자식 컴포넌트)
144
-
145
- 리스트 아이템처럼 동일 타입 인스턴스가 여러 개 필요할 때:
146
-
147
- ```dart
148
- // relay_tile_vm.dart
149
- final pRelayItemProvider = NotifierProvider.family<
150
- RelayItemNotifier,
151
- RelayItemState,
152
- ({int ioId, String topic})>(
153
- () => RelayItemNotifier(),
154
- );
155
-
156
- class RelayItemNotifier
157
- extends FamilyNotifier<RelayItemState, ({int ioId, String topic})> {
158
- @override
159
- RelayItemState build(({int ioId, String topic}) arg) {
160
- ref.onDispose(() {
161
- // 정리 로직 (스트림 구독 해제 등)
162
- });
163
- return RelayItemState(ioId: arg.ioId, topic: arg.topic);
164
- }
165
-
166
- Future<void> doAction() async {
167
- final result = await ref.read(dataLayer).ac.someMethod(id: state.ioId);
168
- state = state.copyWith(/* ... */);
169
- }
170
- }
171
- ```
172
-
173
- 사용:
174
- ```dart
175
- // Page/Widget에서
176
- final itemState = ref.watch(
177
- pRelayItemProvider((ioId: item.ioId, topic: item.topic)),
178
- );
179
- ```
180
-
181
- ## 핵심 규칙
182
-
183
- | 규칙 | 설명 |
184
- |------|------|
185
- | `ref.watch` | UI rebuild이 필요한 곳 (build 메서드 내) |
186
- | `ref.read` | 일회성 호출 (이벤트 핸들러, initState 콜백) |
187
- | `dataLayer` 접근 | VM 내에서 `ref.read(dataLayer).repository.method()` |
188
- | State 불변성 | Equatable 상속 + copyWith 패턴 |
189
- | Provider 네이밍 | `p` 접두사 + PascalCase + Provider (예: `pHomePageProvider`) |
190
- | 초기 로드 | `addPostFrameCallback` 으로 첫 프레임 이후 호출 |
191
- | 에러/로딩 | State에 `isLoading`, `error` 필수 포함 (UI 3가지 상태 처리) |
192
-
193
- ## 금지
194
-
195
- - `bridges/` 사용 금지 — BLOC 기반 레거시
196
- - StatefulWidget 내 직접 API 호출 — 반드시 VM 경유
197
- - `setState` 로 API 응답 상태 관리 — Riverpod 상태로 대체
198
- - `ref.read`를 build 메서드 내에서 사용 (→ `ref.watch`)
199
- - VM 내 UI 의존 코드 (Navigator, ScaffoldMessenger 등) — UI 콜백으로 분리