@walwal-harness/cli 2.4.0 → 2.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.md +46 -22
- package/assets/templates/config.json +23 -4
- package/package.json +1 -1
- package/scripts/harness-next.sh +12 -5
- package/scripts/lib/harness-render-progress.sh +8 -3
- package/scripts/scan-project.sh +33 -3
- package/skills/dispatcher/references/pipeline-definitions.md +20 -9
- package/skills/evaluator-functional-flutter/SKILL.md +10 -3
- package/skills/generator-frontend-flutter/SKILL.md +36 -2
- package/skills/generator-frontend-flutter/references/anti-patterns.md +44 -12
- package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +273 -0
- package/skills/planner/references/fe-stack-detection.md +62 -15
package/README.md
CHANGED
|
@@ -155,17 +155,18 @@ Brainstormer? → Planner → Generator-BE → Evaluator-Func(API-only)
|
|
|
155
155
|
|
|
156
156
|
> `Brainstormer?` = Dispatcher가 사용자에게 확인 후 조건부 실행. 명확한 PRD/OpenAPI가 있으면 생략 가능.
|
|
157
157
|
|
|
158
|
-
### Flutter 프로젝트 자동 감지
|
|
158
|
+
### Flutter 프로젝트 자동 감지 (Web / Mobile / Desktop)
|
|
159
159
|
|
|
160
|
-
`pubspec.yaml` + `flutter:` 키가 감지되면 FE 에이전트가 **자동
|
|
160
|
+
`pubspec.yaml` + `flutter:` 키가 감지되면 FE 에이전트가 **자동 치환**됩니다. **Flutter Web** 은 컴파일 결과가 HTML+JS+CSS 이므로 React 와 동일하게 Playwright 기반 evaluator 를 그대로 사용하고, **Mobile/Desktop** 만 정적 분석 evaluator 로 교체됩니다.
|
|
161
161
|
|
|
162
|
-
|
|
|
163
|
-
|
|
164
|
-
| `generator-frontend` | `
|
|
165
|
-
| `evaluator-functional
|
|
166
|
-
| `
|
|
162
|
+
| fe_stack | fe_target | Generator | Eval-Functional | Eval-Visual |
|
|
163
|
+
|----------|-----------|-----------|----------------|-------------|
|
|
164
|
+
| react | (n/a) | `generator-frontend` | `evaluator-functional` (Playwright) | `evaluator-visual` |
|
|
165
|
+
| **flutter** | **web** | `generator-frontend-flutter` | **`evaluator-functional`** (Playwright!) | **`evaluator-visual`** (Playwright!) |
|
|
166
|
+
| **flutter** | mobile | `generator-frontend-flutter` | `evaluator-functional-flutter` (flutter analyze/test) | **SKIP** |
|
|
167
|
+
| **flutter** | desktop | `generator-frontend-flutter` | `evaluator-functional-flutter` (flutter analyze/test) | **SKIP** |
|
|
167
168
|
|
|
168
|
-
치환은 `pipeline.json.fe_stack`
|
|
169
|
+
**감지 방식**: `scan-project.sh` 가 Flutter 프로젝트의 `web/index.html`, `android/`, `ios/`, `macos/`, `windows/`, `linux/` 디렉터리 존재 여부로 `fe_target` 을 자동 판정. 멀티 타겟 또는 모호한 경우 Planner 가 사용자에게 확인합니다. 치환은 `pipeline.json.fe_stack + fe_target` 조합에 의해 `harness-next.sh` 가 자동 처리합니다.
|
|
169
170
|
|
|
170
171
|
---
|
|
171
172
|
|
|
@@ -269,20 +270,24 @@ Evaluator는 기능 테스트 전에 AGENTS.md의 IA-MAP과 실제 폴더 구조
|
|
|
269
270
|
| Evaluator-Visual | `harness-evaluator-visual` — 스크린샷 기반 디자인/접근성/반응형 검증 |
|
|
270
271
|
| 레퍼런스 | Vercel Best Practices, Design System Rules, AI Forbidden Patterns, Component Patterns |
|
|
271
272
|
|
|
272
|
-
### Flutter (Dart)
|
|
273
|
+
### Flutter (Dart) — Web / Mobile / Desktop
|
|
273
274
|
|
|
274
|
-
| 항목 |
|
|
275
|
-
|
|
276
|
-
| Generator | `harness-generator-frontend-flutter`
|
|
277
|
-
|
|
|
278
|
-
|
|
|
279
|
-
| 레퍼런스 | API Layer Pattern, Riverpod Pattern, i18n Pattern, Anti-Patterns
|
|
275
|
+
| 항목 | Web (`fe_target=web`) | Mobile/Desktop (`fe_target=mobile`/`desktop`) |
|
|
276
|
+
|------|----------------------|---------------------------------------------|
|
|
277
|
+
| Generator | `harness-generator-frontend-flutter` (Web 가이드 활성) | `harness-generator-frontend-flutter` (Mobile/Desktop 가이드) |
|
|
278
|
+
| Eval-Func | **`harness-evaluator-functional`** (Playwright MCP) | `harness-evaluator-functional-flutter` (`flutter analyze` + `flutter test` + build_runner drift + FL-01~FL-08 정적 검증) |
|
|
279
|
+
| Eval-Visual | **`harness-evaluator-visual`** (Playwright 스크린샷/반응형/접근성) | **SKIP** (브라우저 없음) |
|
|
280
|
+
| 레퍼런스 공통 | API Layer Pattern, Riverpod Pattern, i18n Pattern, Anti-Patterns | 동일 |
|
|
281
|
+
| 레퍼런스 추가 | **Flutter Web Pattern** (`dart:html`/`package:web` 허용, go_router, CORS, PWA, 빌드/호스팅) | — |
|
|
282
|
+
| dart:html / package:web | **허용** (가능하면 conditional import 권장) | **금지** (모바일/데스크톱 빌드 실패 유발) |
|
|
283
|
+
| 빌드 명령 | `flutter build web --release` / `flutter run -d chrome` | `flutter build apk` / `flutter build ios` / `flutter build macos` 등 |
|
|
280
284
|
|
|
281
|
-
### FE 스택 감지
|
|
285
|
+
### FE 스택 + 타겟 감지
|
|
282
286
|
|
|
283
|
-
1. `scan-project.sh`가 `pubspec.yaml` + `flutter:` 키를
|
|
284
|
-
2.
|
|
285
|
-
3.
|
|
287
|
+
1. `scan-project.sh`가 `pubspec.yaml` + `flutter:` 키를 탐지하여 `fe_stack = "flutter"` 설정
|
|
288
|
+
2. 같은 스캔이 `web/index.html`, `android/`, `ios/`, `macos/`/`windows/`/`linux/` 존재 여부로 `fe_target` 자동 판정 (`web` / `mobile` / `desktop`)
|
|
289
|
+
3. Planner 가 모호한 경우(멀티 타겟, unknown) 사용자에게 단 한 번 확인하고 `pipeline.json.fe_target` 확정
|
|
290
|
+
4. `harness-next.sh`가 `fe_stack + fe_target` 조합에 따라 FE 에이전트를 자동 치환 (Agent Bar에도 반영)
|
|
286
291
|
|
|
287
292
|
---
|
|
288
293
|
|
|
@@ -340,7 +345,7 @@ Evaluator-Functional / Evaluator-Visual이 브라우저 테스트를 수행하
|
|
|
340
345
|
}
|
|
341
346
|
```
|
|
342
347
|
|
|
343
|
-
> Flutter
|
|
348
|
+
> **Flutter Web (`fe_target=web`) 프로젝트도 Playwright 가 필요합니다** — 컴파일 결과가 일반 웹앱과 동일하므로 React 와 같은 evaluator 를 재사용합니다. **Flutter Mobile/Desktop (`fe_target=mobile`/`desktop`) 프로젝트만** Playwright 없이 `flutter analyze` + `flutter test` 기반으로 동작합니다.
|
|
344
349
|
|
|
345
350
|
---
|
|
346
351
|
|
|
@@ -384,10 +389,29 @@ grep "flutter:" pubspec.yaml
|
|
|
384
389
|
|
|
385
390
|
# 재스캔
|
|
386
391
|
bash scripts/scan-project.sh .
|
|
387
|
-
# pipeline.json의 fe_stack 값 확인
|
|
388
|
-
cat .harness/actions/pipeline.json | jq '.fe_stack'
|
|
392
|
+
# pipeline.json의 fe_stack + fe_target 값 확인
|
|
393
|
+
cat .harness/actions/pipeline.json | jq '.fe_stack, .fe_target'
|
|
389
394
|
```
|
|
390
395
|
|
|
396
|
+
### Flutter Web 프로젝트인데 evaluator-functional-flutter (정적 분석) 가 실행돼요
|
|
397
|
+
|
|
398
|
+
`fe_target` 이 `web` 으로 감지되지 않아서 발생합니다. 다음 두 가지를 확인:
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
# 1) Flutter 프로젝트 루트에 web/index.html 이 존재하는가?
|
|
402
|
+
ls -la web/index.html
|
|
403
|
+
# 없으면 Flutter Web 활성화:
|
|
404
|
+
flutter config --enable-web
|
|
405
|
+
flutter create --platforms=web .
|
|
406
|
+
|
|
407
|
+
# 2) pipeline.json 의 fe_target 값 확인
|
|
408
|
+
jq '.fe_target' .harness/actions/pipeline.json
|
|
409
|
+
# "mobile" 또는 "unknown" 이면 수동 수정 또는 재스캔
|
|
410
|
+
bash scripts/scan-project.sh .
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`fe_target = web` 으로 확정되면 Playwright 기반 `evaluator-functional` + `evaluator-visual` 이 자동 사용됩니다.
|
|
414
|
+
|
|
391
415
|
### Auto-routing을 끄고 싶어요
|
|
392
416
|
|
|
393
417
|
```json
|
|
@@ -87,11 +87,30 @@
|
|
|
87
87
|
"BE-ONLY": ["planner", "generator-backend", "evaluator-functional:api-only"]
|
|
88
88
|
},
|
|
89
89
|
"fe_stack_substitution": {
|
|
90
|
-
"comment": "pipeline.json.fe_stack
|
|
90
|
+
"comment": "pipeline.json.fe_stack + fe_target 에 따라 FE 에이전트 치환. Flutter Web 은 React 와 동일한 Playwright 기반 evaluator 를 사용한다.",
|
|
91
91
|
"flutter": {
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
|
|
92
|
+
"_doc": "fe_target = web | mobile | desktop. by_target 으로 분기.",
|
|
93
|
+
"by_target": {
|
|
94
|
+
"web": {
|
|
95
|
+
"_doc": "Flutter Web — 컴파일 결과가 HTML+JS+CSS 이므로 Playwright evaluator 사용 가능",
|
|
96
|
+
"generator-frontend": "generator-frontend-flutter",
|
|
97
|
+
"evaluator-functional": "evaluator-functional",
|
|
98
|
+
"evaluator-visual": "evaluator-visual"
|
|
99
|
+
},
|
|
100
|
+
"mobile": {
|
|
101
|
+
"_doc": "Flutter Mobile (Android/iOS) — 브라우저 없음, 정적 분석 evaluator 사용",
|
|
102
|
+
"generator-frontend": "generator-frontend-flutter",
|
|
103
|
+
"evaluator-functional": "evaluator-functional-flutter",
|
|
104
|
+
"evaluator-visual": "__skip__"
|
|
105
|
+
},
|
|
106
|
+
"desktop": {
|
|
107
|
+
"_doc": "Flutter Desktop (macOS/Windows/Linux) — 브라우저 없음, 정적 분석 evaluator 사용",
|
|
108
|
+
"generator-frontend": "generator-frontend-flutter",
|
|
109
|
+
"evaluator-functional": "evaluator-functional-flutter",
|
|
110
|
+
"evaluator-visual": "__skip__"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"_default_target": "mobile"
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
116
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@walwal-harness/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "Production harness for AI agent engineering — Planner, Generator(BE/FE), Evaluator(Func/Visual), optional Brainstormer (requirements refinement). Supports React and Flutter FE stacks.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"walwal-harness": "bin/init.js"
|
package/scripts/harness-next.sh
CHANGED
|
@@ -41,17 +41,24 @@ next_agent=$(jq -r '.next_agent // "null"' "$PROGRESS")
|
|
|
41
41
|
retry_count=$(jq -r '.sprint.retry_count // 0' "$PROGRESS")
|
|
42
42
|
max_retries=$(jq -r '.flow.max_retries_per_sprint // 10' "$CONFIG" 2>/dev/null || echo 10)
|
|
43
43
|
|
|
44
|
-
# fe_stack 치환 (Flutter 지원) — pipeline.json 에서 읽음
|
|
44
|
+
# fe_stack + fe_target 치환 (Flutter Web/Mobile/Desktop 지원) — pipeline.json 에서 읽음
|
|
45
45
|
fe_stack="react"
|
|
46
|
+
fe_target="web"
|
|
46
47
|
if [ -f "$PIPELINE_JSON" ]; then
|
|
47
48
|
fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
|
|
49
|
+
fe_target=$(jq -r '.fe_target // empty' "$PIPELINE_JSON" 2>/dev/null || true)
|
|
50
|
+
if [ -z "$fe_target" ]; then
|
|
51
|
+
# pipeline.json 에 fe_target 미지정 시 config.json 의 _default_target 사용
|
|
52
|
+
fe_target=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}._default_target // \"web\"" "$CONFIG" 2>/dev/null || echo "web")
|
|
53
|
+
fi
|
|
48
54
|
fi
|
|
49
55
|
|
|
50
56
|
# ─────────────────────────────────────────
|
|
51
|
-
# fe_stack 치환 헬퍼
|
|
52
|
-
# pipeline_selection.pipelines에서 읽은 에이전트명을 fe_stack에 따라 치환
|
|
57
|
+
# fe_stack + fe_target 치환 헬퍼
|
|
58
|
+
# pipeline_selection.pipelines 에서 읽은 에이전트명을 fe_stack/fe_target 에 따라 치환
|
|
53
59
|
# - react: 그대로
|
|
54
|
-
# - flutter: generator-frontend → generator-frontend-flutter
|
|
60
|
+
# - flutter+web: generator-frontend → generator-frontend-flutter 만 치환, eval 은 그대로 (Playwright 사용 가능)
|
|
61
|
+
# - flutter+mobile/desktop: eval 도 정적 분석용으로 치환, evaluator-visual 은 __skip__
|
|
55
62
|
# ─────────────────────────────────────────
|
|
56
63
|
substitute_fe_stack() {
|
|
57
64
|
local agent="$1"
|
|
@@ -60,7 +67,7 @@ substitute_fe_stack() {
|
|
|
60
67
|
return
|
|
61
68
|
fi
|
|
62
69
|
local sub
|
|
63
|
-
sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.
|
|
70
|
+
sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}.by_target[\"${fe_target}\"][\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
|
|
64
71
|
echo "$sub"
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -286,12 +286,17 @@ render_agent_bar() {
|
|
|
286
286
|
|
|
287
287
|
if [ ! -f "$PROGRESS" ] || [ ! -f "$CONFIG" ]; then return 1; fi
|
|
288
288
|
|
|
289
|
-
local pipeline current_agent fe_stack
|
|
289
|
+
local pipeline current_agent fe_stack fe_target
|
|
290
290
|
pipeline=$(jq -r '.pipeline // "unknown"' "$PROGRESS")
|
|
291
291
|
current_agent=$(jq -r '.current_agent // "none"' "$PROGRESS")
|
|
292
292
|
fe_stack="react"
|
|
293
|
+
fe_target="web"
|
|
293
294
|
if [ -f "$PIPELINE_JSON" ]; then
|
|
294
295
|
fe_stack=$(jq -r '.fe_stack // "react"' "$PIPELINE_JSON" 2>/dev/null || echo "react")
|
|
296
|
+
fe_target=$(jq -r '.fe_target // empty' "$PIPELINE_JSON" 2>/dev/null || true)
|
|
297
|
+
if [ -z "$fe_target" ]; then
|
|
298
|
+
fe_target=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.${fe_stack}._default_target // \"web\"" "$CONFIG" 2>/dev/null || echo "web")
|
|
299
|
+
fi
|
|
295
300
|
fi
|
|
296
301
|
|
|
297
302
|
local completed_agents
|
|
@@ -305,10 +310,10 @@ render_agent_bar() {
|
|
|
305
310
|
while IFS= read -r agent; do
|
|
306
311
|
agent=$(echo "$agent" | sed 's/:.*//') # strip mode suffix like :light, :api-only
|
|
307
312
|
|
|
308
|
-
# fe_stack 치환 적용
|
|
313
|
+
# fe_stack + fe_target 치환 적용
|
|
309
314
|
if [ "$fe_stack" = "flutter" ]; then
|
|
310
315
|
local sub
|
|
311
|
-
sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter[\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
|
|
316
|
+
sub=$(jq -r ".flow.pipeline_selection.fe_stack_substitution.flutter.by_target[\"${fe_target}\"][\"${agent}\"] // \"${agent}\"" "$CONFIG" 2>/dev/null)
|
|
312
317
|
if [ "$sub" = "__skip__" ]; then continue; fi
|
|
313
318
|
agent="$sub"
|
|
314
319
|
fi
|
package/scripts/scan-project.sh
CHANGED
|
@@ -62,11 +62,15 @@ fi
|
|
|
62
62
|
|
|
63
63
|
# Frontend
|
|
64
64
|
# Flutter 우선 감지 — pubspec.yaml 존재하면 Flutter 프로젝트로 판정 (TECH_LANG도 보정)
|
|
65
|
-
FE_STACK="react"
|
|
65
|
+
FE_STACK="react" # react | flutter (기본값은 react 계열)
|
|
66
|
+
FE_TARGET="web" # web | mobile | desktop (Flutter 의 컴파일 타겟; React 는 항상 web)
|
|
67
|
+
FLUTTER_ROOT="" # Flutter 프로젝트 루트 경로 (web/mobile/desktop 감지용)
|
|
68
|
+
|
|
66
69
|
if [ -f "${PROJECT_ROOT}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/pubspec.yaml" 2>/dev/null; then
|
|
67
70
|
TECH_FRONTEND="flutter"
|
|
68
71
|
TECH_LANG="dart"
|
|
69
72
|
FE_STACK="flutter"
|
|
73
|
+
FLUTTER_ROOT="${PROJECT_ROOT}"
|
|
70
74
|
elif [ -f "${PROJECT_ROOT}/next.config.js" ] || [ -f "${PROJECT_ROOT}/next.config.ts" ] || [ -f "${PROJECT_ROOT}/next.config.mjs" ]; then
|
|
71
75
|
TECH_FRONTEND="nextjs"
|
|
72
76
|
elif [ -f "${PROJECT_ROOT}/vite.config.ts" ] || [ -f "${PROJECT_ROOT}/vite.config.js" ]; then
|
|
@@ -91,16 +95,41 @@ fi
|
|
|
91
95
|
# Flutter 서브디렉토리 감지 (monorepo 또는 서브 프로젝트 케이스)
|
|
92
96
|
if [ "$TECH_FRONTEND" = "unknown" ]; then
|
|
93
97
|
# 대표적인 Flutter 서브폴더 이름을 얕게 탐색
|
|
94
|
-
for d in apps/mobile mobile clue_mobile_app flutter_app; do
|
|
98
|
+
for d in apps/mobile apps/web mobile clue_mobile_app flutter_app; do
|
|
95
99
|
if [ -f "${PROJECT_ROOT}/${d}/pubspec.yaml" ] && grep -q "flutter:" "${PROJECT_ROOT}/${d}/pubspec.yaml" 2>/dev/null; then
|
|
96
100
|
TECH_FRONTEND="flutter"
|
|
97
101
|
TECH_LANG="dart"
|
|
98
102
|
FE_STACK="flutter"
|
|
103
|
+
FLUTTER_ROOT="${PROJECT_ROOT}/${d}"
|
|
99
104
|
break
|
|
100
105
|
fi
|
|
101
106
|
done
|
|
102
107
|
fi
|
|
103
108
|
|
|
109
|
+
# Flutter fe_target 감지 (web / mobile / desktop)
|
|
110
|
+
# - web/index.html 존재 → web
|
|
111
|
+
# - android/ 또는 ios/ 존재 + web/ 없음 → mobile
|
|
112
|
+
# - macos/ 또는 windows/ 또는 linux/ 존재 + 위 둘 없음 → desktop
|
|
113
|
+
# - 동시 존재 (멀티 타겟) → web 우선 (사용자가 Planner에서 변경 가능)
|
|
114
|
+
if [ "$FE_STACK" = "flutter" ] && [ -n "$FLUTTER_ROOT" ]; then
|
|
115
|
+
HAS_WEB=false
|
|
116
|
+
HAS_MOBILE=false
|
|
117
|
+
HAS_DESKTOP=false
|
|
118
|
+
[ -f "${FLUTTER_ROOT}/web/index.html" ] && HAS_WEB=true
|
|
119
|
+
{ [ -d "${FLUTTER_ROOT}/android" ] || [ -d "${FLUTTER_ROOT}/ios" ]; } && HAS_MOBILE=true
|
|
120
|
+
{ [ -d "${FLUTTER_ROOT}/macos" ] || [ -d "${FLUTTER_ROOT}/windows" ] || [ -d "${FLUTTER_ROOT}/linux" ]; } && HAS_DESKTOP=true
|
|
121
|
+
|
|
122
|
+
if [ "$HAS_WEB" = "true" ]; then
|
|
123
|
+
FE_TARGET="web"
|
|
124
|
+
elif [ "$HAS_MOBILE" = "true" ]; then
|
|
125
|
+
FE_TARGET="mobile"
|
|
126
|
+
elif [ "$HAS_DESKTOP" = "true" ]; then
|
|
127
|
+
FE_TARGET="desktop"
|
|
128
|
+
else
|
|
129
|
+
FE_TARGET="unknown"
|
|
130
|
+
fi
|
|
131
|
+
fi
|
|
132
|
+
|
|
104
133
|
# Database
|
|
105
134
|
if grep -rq "typeorm\|prisma\|sequelize\|knex" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
106
135
|
if grep -q "pg\|postgres" "${PROJECT_ROOT}/package.json" 2>/dev/null; then
|
|
@@ -225,6 +254,7 @@ cat > "$OUTPUT" << JSONEOF
|
|
|
225
254
|
"backend": "${TECH_BACKEND}",
|
|
226
255
|
"frontend": "${TECH_FRONTEND}",
|
|
227
256
|
"fe_stack": "${FE_STACK}",
|
|
257
|
+
"fe_target": "${FE_TARGET}",
|
|
228
258
|
"database": "${TECH_DB}",
|
|
229
259
|
"monorepo": "${TECH_MONOREPO}",
|
|
230
260
|
"language": "${TECH_LANG}"
|
|
@@ -282,7 +312,7 @@ echo "=== Scan Complete ==="
|
|
|
282
312
|
echo "Output: ${OUTPUT}"
|
|
283
313
|
echo ""
|
|
284
314
|
echo "--- Summary ---"
|
|
285
|
-
echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} (fe_stack=${FE_STACK}) / ${TECH_DB}"
|
|
315
|
+
echo "Tech Stack: ${TECH_BACKEND} / ${TECH_FRONTEND} (fe_stack=${FE_STACK}, fe_target=${FE_TARGET}) / ${TECH_DB}"
|
|
286
316
|
echo "Monorepo: ${TECH_MONOREPO}"
|
|
287
317
|
echo "OpenAPI: ${OPENAPI}"
|
|
288
318
|
echo "Git: ${GIT_INIT} (${GIT_COMMITS} commits, branch: ${GIT_BRANCH})"
|
|
@@ -31,20 +31,31 @@ docmeta:
|
|
|
31
31
|
> **FE Stack 차원**: 모든 FE 관련 파이프라인은 `fe_stack` 필드로 React/Flutter를 구분한다.
|
|
32
32
|
> Planner가 scan-result.json(`tech_stack.fe_stack`) 또는 사용자 질문으로 확정한다.
|
|
33
33
|
|
|
34
|
-
## fe_stack 스위치 매트릭스
|
|
34
|
+
## fe_stack + fe_target 스위치 매트릭스
|
|
35
35
|
|
|
36
|
-
| fe_stack | FE Generator |
|
|
37
|
-
|
|
38
|
-
| `react` | `generator-frontend` | `evaluator-functional` (Playwright
|
|
39
|
-
| `flutter`| `generator-frontend-flutter` |
|
|
36
|
+
| fe_stack | fe_target | FE Generator | Eval-Functional | Eval-Visual |
|
|
37
|
+
|----------|-----------|--------------|----------------|-------------|
|
|
38
|
+
| `react` | (n/a) | `generator-frontend` | `evaluator-functional` (Playwright) | `evaluator-visual` |
|
|
39
|
+
| `flutter`| **`web`** | `generator-frontend-flutter` | **`evaluator-functional`** (Playwright!) | **`evaluator-visual`** (Playwright!) |
|
|
40
|
+
| `flutter`| `mobile` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | **SKIP** |
|
|
41
|
+
| `flutter`| `desktop` | `generator-frontend-flutter` | `evaluator-functional-flutter` (정적 분석) | **SKIP** |
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
> **Flutter Web**: 컴파일 결과가 HTML+JS+CSS 이므로 React 경로의 Playwright evaluator 가 정상 동작.
|
|
44
|
+
> Generator-Frontend-Flutter 의 Self-Verification 에서 `flutter analyze` / `flutter test` /
|
|
45
|
+
> `flutter build web --release` 가 이미 통과한 상태로 handoff 됨이 전제.
|
|
46
|
+
|
|
47
|
+
Dispatcher는 `next_agent`를 설정할 때 `pipeline.json` 의 `fe_stack` + `fe_target` 두 값을 보고 치환한다:
|
|
42
48
|
|
|
43
49
|
```
|
|
44
50
|
if pipeline.json.fe_stack == "flutter":
|
|
45
|
-
"generator-frontend"
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
"generator-frontend" → "generator-frontend-flutter" (모든 fe_target 공통)
|
|
52
|
+
|
|
53
|
+
if fe_target == "web":
|
|
54
|
+
"evaluator-functional" → "evaluator-functional" (그대로, Playwright 사용)
|
|
55
|
+
"evaluator-visual" → "evaluator-visual" (그대로)
|
|
56
|
+
elif fe_target in ("mobile", "desktop"):
|
|
57
|
+
"evaluator-functional" → "evaluator-functional-flutter"
|
|
58
|
+
"evaluator-visual" → __skip__
|
|
48
59
|
```
|
|
49
60
|
|
|
50
61
|
## FE-ONLY
|
|
@@ -6,9 +6,16 @@ disable-model-invocation: true
|
|
|
6
6
|
|
|
7
7
|
# Evaluator-Functional-Flutter — Dart/Flutter 정적·동적 검증
|
|
8
8
|
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
9
|
+
> **이 에이전트는 `fe_target = mobile` 또는 `desktop` 일 때만 사용된다.**
|
|
10
|
+
>
|
|
11
|
+
> Flutter Mobile/Desktop 앱은 브라우저가 아니므로 Playwright MCP(`browser_*`)를 쓸 수 없다.
|
|
12
|
+
> 이 에이전트는 `flutter analyze`, `flutter test`, `dart format`, 정적 grep 검증, 생성 파일
|
|
13
|
+
> 일관성으로 평가한다. 실기기/시뮬레이터 UI 검증은 사람 QA 또는 별도 파이프라인으로 위임.
|
|
14
|
+
>
|
|
15
|
+
> **`fe_target = web` 인 Flutter Web 프로젝트는 이 에이전트가 아니라 일반 `evaluator-functional`
|
|
16
|
+
> 과 `evaluator-visual` 이 Playwright 로 검증한다** (컴파일 결과가 HTML+JS+CSS 이므로).
|
|
17
|
+
> 이 경우 Generator-Frontend-Flutter 의 Self-Verification 단계에서 `flutter analyze` /
|
|
18
|
+
> `flutter test` / `flutter build web --release` 가 이미 통과했음이 전제된다.
|
|
12
19
|
|
|
13
20
|
## Session Boundary Protocol
|
|
14
21
|
|
|
@@ -33,7 +33,10 @@ disable-model-invocation: true
|
|
|
33
33
|
4. `pwd` + `.harness/progress.json` + `git log --oneline -20`
|
|
34
34
|
5. `.harness/actions/api-contract.json` 읽기 — **서버 API 계약이 Source of Truth**
|
|
35
35
|
6. `.harness/actions/feature-list.json` — `layer: "frontend"` 필터
|
|
36
|
-
7.
|
|
36
|
+
7. `.harness/actions/pipeline.json` 의 **`fe_target`** 확인 (`web` | `mobile` | `desktop`)
|
|
37
|
+
- 이 값에 따라 허용되는 API, 빌드 명령, 안티패턴이 달라진다 (아래 "fe_target 분기" 섹션 참조)
|
|
38
|
+
8. `pubspec.yaml` 확인 — Flutter 버전, Riverpod/Retrofit/json_serializable 의존성 존재 확인
|
|
39
|
+
- `fe_target = web` 인 경우 `flutter config --enable-web` 활성화 + `web/index.html` 존재 확인
|
|
37
40
|
|
|
38
41
|
## AGENTS.md — 읽기 전용
|
|
39
42
|
|
|
@@ -60,7 +63,38 @@ Backend 코드, `.harness/`, `AGENTS.md` 수정 금지.
|
|
|
60
63
|
| [API Layer Pattern](references/api-layer-pattern.md) | integrated_data_layer 구조, Request/Response, Retrofit | API 연동 시 |
|
|
61
64
|
| [Riverpod Pattern](references/riverpod-pattern.md) | Page+VM 쌍, NotifierProvider, family 패턴 | 페이지/위젯 구현 시 |
|
|
62
65
|
| [i18n Pattern](references/i18n-pattern.md) | ARB 파일, LocaleAssist, 키 네이밍 | 문자열 추가 시 |
|
|
63
|
-
| [Anti-Patterns](references/anti-patterns.md) | 금지 API, 하드코딩, bridges/, StatefulWidget 직접 호출 | 구현 완료 후 셀프 체크 |
|
|
66
|
+
| [Anti-Patterns](references/anti-patterns.md) | 금지 API, 하드코딩, bridges/, StatefulWidget 직접 호출 (fe_target 별 차이 포함) | 구현 완료 후 셀프 체크 |
|
|
67
|
+
| [Flutter Web Pattern](references/flutter-web-pattern.md) | Web 전용 — `dart:html`/`package:web` 허용, 라우팅, 빌드, hosting | `fe_target = web` 일 때만 |
|
|
68
|
+
|
|
69
|
+
## fe_target 분기
|
|
70
|
+
|
|
71
|
+
`pipeline.json.fe_target` 에 따라 적용되는 규칙이 다르다:
|
|
72
|
+
|
|
73
|
+
### `fe_target = web` (Flutter Web)
|
|
74
|
+
|
|
75
|
+
- **빌드**: `flutter build web --release` / 개발 서버: `flutter run -d chrome`
|
|
76
|
+
- **HTML/JS 인터롭 허용**: `dart:html`, `package:web`, `dart:js_interop` 사용 가능
|
|
77
|
+
- 단, 가능하면 cross-platform 코드를 우선하고 web 전용 코드는 `if (kIsWeb)` 가드 또는 conditional import
|
|
78
|
+
- **라우팅**: `go_router` 권장 (URL 동기화). `Navigator 1.0` 의 `MaterialApp.routes` 도 OK 지만 hash routing 주의
|
|
79
|
+
- **자산**: `web/index.html`, `web/manifest.json`, `web/icons/` 관리. SEO 가 필요하면 `<meta>` 태그 명시
|
|
80
|
+
- **CORS**: 백엔드 API 가 `localhost:포트` 에서 다른 origin 일 수 있으므로 CORS 설정 확인
|
|
81
|
+
- **Eval 흐름**: Playwright 기반 `evaluator-functional` + `evaluator-visual` 이 정상 작동 (브라우저 E2E + 시각 검증)
|
|
82
|
+
- 상세 → [Flutter Web Pattern](references/flutter-web-pattern.md)
|
|
83
|
+
|
|
84
|
+
### `fe_target = mobile` (Android / iOS)
|
|
85
|
+
|
|
86
|
+
- **빌드**: `flutter build apk` / `flutter build ios` / 개발: `flutter run -d <device>`
|
|
87
|
+
- **HTML/JS 인터롭 금지**: `dart:html`, `package:web`, `universal_html` 직접 참조 → 빌드 실패
|
|
88
|
+
- **플랫폼 채널**: 네이티브 기능 사용 시 `MethodChannel` 또는 검증된 plugin 사용
|
|
89
|
+
- **권한**: `AndroidManifest.xml` / `Info.plist` 에 권한 명시
|
|
90
|
+
- **Eval 흐름**: 정적 분석 기반 `evaluator-functional-flutter` 사용 (Playwright 불가)
|
|
91
|
+
|
|
92
|
+
### `fe_target = desktop` (macOS / Windows / Linux)
|
|
93
|
+
|
|
94
|
+
- **빌드**: `flutter build macos` / `flutter build windows` / `flutter build linux`
|
|
95
|
+
- **HTML/JS 인터롭 금지**: 모바일과 동일
|
|
96
|
+
- **윈도우 관리**: `window_manager` 등 플러그인으로 윈도우 크기/위치 제어
|
|
97
|
+
- **Eval 흐름**: 정적 분석 기반 `evaluator-functional-flutter` 사용
|
|
64
98
|
|
|
65
99
|
## 핵심 규칙
|
|
66
100
|
|
|
@@ -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
|
|
46
|
-
|
|
46
|
+
Flutter 모바일/데스크톱 빌드에서 `dart:html` 참조는 즉시 빌드 실패한다.
|
|
47
|
+
가드 없이 import 하면 cross-platform 코드가 깨진다.
|
|
47
48
|
|
|
48
49
|
### 대체
|
|
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
|
+
```
|
|
50
71
|
|
|
51
72
|
### 검증
|
|
52
73
|
```bash
|
|
53
|
-
|
|
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
|
-
#
|
|
270
|
-
|
|
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
|
|
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": "
|
|
100
|
-
"notes": "Flutter
|
|
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-
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
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
|
|