@walwal-harness/cli 2.0.0 → 2.3.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/assets/templates/config.json +41 -1
- package/bin/init.js +93 -7
- package/package.json +5 -2
- package/scripts/harness-next.sh +36 -1
- package/scripts/harness-user-prompt-submit.sh +106 -0
- package/scripts/lib/harness-render-progress.sh +15 -1
- package/scripts/scan-project.sh +24 -2
- package/skills/brainstorming/SKILL.md +200 -0
- package/skills/brainstorming/references/attribution.md +109 -0
- package/skills/brainstorming/references/spec-document-reviewer-prompt.md +49 -0
- package/skills/brainstorming/references/visual-companion.md +287 -0
- package/skills/brainstorming/scripts/frame-template.html +214 -0
- package/skills/brainstorming/scripts/helper.js +88 -0
- package/skills/brainstorming/scripts/server.cjs +354 -0
- package/skills/brainstorming/scripts/start-server.sh +148 -0
- package/skills/brainstorming/scripts/stop-server.sh +56 -0
- package/skills/dispatcher/SKILL.md +114 -2
- package/skills/dispatcher/references/pipeline-definitions.md +53 -5
- package/skills/evaluator-functional-flutter/SKILL.md +198 -0
- package/skills/evaluator-functional-flutter/references/ia-compliance.md +77 -0
- package/skills/evaluator-functional-flutter/references/scoring-rubric.md +132 -0
- package/skills/evaluator-functional-flutter/references/static-check-rules.md +99 -0
- package/skills/generator-frontend-flutter/SKILL.md +138 -0
- package/skills/generator-frontend-flutter/references/anti-patterns.md +288 -0
- package/skills/generator-frontend-flutter/references/api-layer-pattern.md +233 -0
- package/skills/generator-frontend-flutter/references/i18n-pattern.md +102 -0
- package/skills/generator-frontend-flutter/references/riverpod-pattern.md +199 -0
- package/skills/planner/SKILL.md +23 -1
- package/skills/planner/references/fe-stack-detection.md +131 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
docmeta:
|
|
3
|
+
id: scoring-rubric
|
|
4
|
+
title: Flutter Functional Evaluation 스코어링 루브릭
|
|
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-evaluator-functional-flutter
|
|
11
|
+
inputs:
|
|
12
|
+
- documentId: react-evaluator-scoring-rubric
|
|
13
|
+
uri: ../../evaluator-functional/references/scoring-rubric.md
|
|
14
|
+
relation: output-from
|
|
15
|
+
sections:
|
|
16
|
+
- sourceRange:
|
|
17
|
+
startLine: 1
|
|
18
|
+
endLine: 53
|
|
19
|
+
targetRange:
|
|
20
|
+
startLine: 32
|
|
21
|
+
endLine: 160
|
|
22
|
+
tags:
|
|
23
|
+
- evaluator
|
|
24
|
+
- flutter
|
|
25
|
+
- scoring
|
|
26
|
+
- rubric
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Flutter Functional Evaluation 스코어링 루브릭
|
|
30
|
+
|
|
31
|
+
## 차원별 채점
|
|
32
|
+
|
|
33
|
+
| 차원 | 가중치 | 하드 임계값 | 측정 방법 |
|
|
34
|
+
|------|--------|------------|----------|
|
|
35
|
+
| Static Analysis | 25% | warning/error 0개 | `flutter analyze --no-fatal-infos` |
|
|
36
|
+
| Test Pass Rate | 25% | 100% | `flutter test` 모든 스위트 |
|
|
37
|
+
| API Contract 준수 | 25% | 100% | rest_api.dart vs api-contract.json 대조 — 불일치 즉시 FAIL |
|
|
38
|
+
| Anti-Pattern 청결 | 15% | 위반 0건 | static-check-rules.md 의 FL-01 ~ FL-08 |
|
|
39
|
+
| Contract Criteria 충족률 | 10% | 80% | sprint-contract.md FE 기준 통과 수 / 전체 |
|
|
40
|
+
|
|
41
|
+
**어떤 차원이든 하드 임계값 미달 → 스프린트 FAIL**
|
|
42
|
+
|
|
43
|
+
## 차원별 점수 계산
|
|
44
|
+
|
|
45
|
+
### Static Analysis (25점)
|
|
46
|
+
|
|
47
|
+
| 결과 | 점수 |
|
|
48
|
+
|------|------|
|
|
49
|
+
| error 0, warning 0 | 25 |
|
|
50
|
+
| error 0, warning 1~2 | 0 (하드 임계값 미달 → FAIL) |
|
|
51
|
+
| error 1+ | 0 (즉시 FAIL) |
|
|
52
|
+
|
|
53
|
+
info 수준은 점수에 반영하지 않지만 보고서에 카운트 기록.
|
|
54
|
+
|
|
55
|
+
### Test Pass Rate (25점)
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
score = 25 * (passed / total)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `total == 0` (테스트 존재하지 않음) → **0점 + FAIL** (Coverage gate)
|
|
62
|
+
- `fromJson/toJson` 왕복 테스트 누락 (이번 스프린트 추가분) → **FAIL**
|
|
63
|
+
|
|
64
|
+
### API Contract 준수 (25점)
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
score = 25 * (matched_endpoints / total_endpoints)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
불일치 허용 없음 — 1개라도 불일치면 즉시 FAIL (하드 임계값 100%).
|
|
71
|
+
|
|
72
|
+
매칭 체크:
|
|
73
|
+
- method (GET/POST/PUT/DELETE)
|
|
74
|
+
- path (변수명 포함)
|
|
75
|
+
- path param → `@Path(...)` 매핑
|
|
76
|
+
- body → `@Body() XxxBody`
|
|
77
|
+
- response 타입 → 계약 스키마의 code/data/errors 구조
|
|
78
|
+
|
|
79
|
+
### Anti-Pattern 청결 (15점)
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
score = 15 * (passed_rules / total_rules)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- 8개 룰 중 1개라도 FAIL → **0점 + 스프린트 FAIL**
|
|
86
|
+
- 위반 0건이면 15점
|
|
87
|
+
|
|
88
|
+
### Contract Criteria 충족률 (10점)
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
score = 10 * (passed_criteria / total_criteria)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- 80% 미달 → FAIL
|
|
95
|
+
- 개별 기준의 실패 사유를 "Failures Detail"에 기록
|
|
96
|
+
|
|
97
|
+
## Total
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
total = static_analysis + test_pass + api_contract + anti_pattern + contract_criteria
|
|
101
|
+
verdict = PASS (if all hard thresholds met AND total >= 80) else FAIL
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## failure_location 라우팅
|
|
105
|
+
|
|
106
|
+
Flutter 앱은 항상 FE 재작업이므로:
|
|
107
|
+
|
|
108
|
+
| location | 재작업 대상 |
|
|
109
|
+
|----------|-----------|
|
|
110
|
+
| `frontend` (기본) | `generator-frontend-flutter` |
|
|
111
|
+
|
|
112
|
+
예외: API Contract 불일치가 **서버 측 계약 오류**로 확인된 경우 → Planner에게 계약 수정 요청 필요.
|
|
113
|
+
이 경우 evaluation에 `## Change Request` 섹션 추가.
|
|
114
|
+
|
|
115
|
+
## evaluation-functional.md 헤더
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
# Flutter Functional Evaluation: Sprint [N]
|
|
119
|
+
|
|
120
|
+
## Date: [YYYY-MM-DD]
|
|
121
|
+
## Verdict: PASS / FAIL
|
|
122
|
+
## Attempt: [N] / 10
|
|
123
|
+
## Stack: flutter
|
|
124
|
+
|
|
125
|
+
## Total Score: [N] / 100
|
|
126
|
+
| Dimension | Score | Threshold | Status |
|
|
127
|
+
| Static Analysis | X/25 | warning=0 | PASS/FAIL |
|
|
128
|
+
| Test Pass Rate | X/25 | 100% | PASS/FAIL |
|
|
129
|
+
| API Contract | X/25 | 100% | PASS/FAIL |
|
|
130
|
+
| Anti-Pattern | X/15 | 0 violations | PASS/FAIL |
|
|
131
|
+
| Contract Criteria | X/10 | 80% | PASS/FAIL |
|
|
132
|
+
```
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
docmeta:
|
|
3
|
+
id: static-check-rules
|
|
4
|
+
title: Flutter 정적 검증 룰 (Anti-Pattern Gate)
|
|
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-evaluator-functional-flutter
|
|
11
|
+
inputs:
|
|
12
|
+
- documentId: flutter-anti-patterns
|
|
13
|
+
uri: ../../generator-frontend-flutter/references/anti-patterns.md
|
|
14
|
+
relation: output-from
|
|
15
|
+
sections:
|
|
16
|
+
- sourceRange:
|
|
17
|
+
startLine: 1
|
|
18
|
+
endLine: 280
|
|
19
|
+
targetRange:
|
|
20
|
+
startLine: 32
|
|
21
|
+
endLine: 180
|
|
22
|
+
tags:
|
|
23
|
+
- evaluator
|
|
24
|
+
- flutter
|
|
25
|
+
- static-check
|
|
26
|
+
- anti-pattern
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Flutter 정적 검증 룰
|
|
30
|
+
|
|
31
|
+
Generator의 `anti-patterns.md` 를 Source of Truth 로 삼아 정적 검증을 수행한다.
|
|
32
|
+
Generator가 신규 룰을 추가하면 Evaluator는 자동으로 그 룰을 따르게 된다 — **두 문서 간 drift 금지.**
|
|
33
|
+
|
|
34
|
+
## 실행 방법
|
|
35
|
+
|
|
36
|
+
**Evaluator는 반드시** `skills/generator-frontend-flutter/references/anti-patterns.md` 의
|
|
37
|
+
"셀프 체크 스크립트" 섹션을 **파일로 읽어서 그대로 실행**한다. 하네스의 단일 진실 원칙.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# anti-patterns.md 에서 bash 블록만 추출해서 실행
|
|
41
|
+
awk '/^```bash$/,/^```$/' skills/generator-frontend-flutter/references/anti-patterns.md
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 룰 요약 (Evaluator가 결과를 표로 정리)
|
|
45
|
+
|
|
46
|
+
| Rule ID | 설명 | 명령 | Fail 조건 |
|
|
47
|
+
|---------|------|------|----------|
|
|
48
|
+
| FL-01 | 웹 API 직접 참조 | `grep -rn "dart:html\|universal_html" lib/ integrated_data_layer/lib/` | 매치 1+ |
|
|
49
|
+
| FL-02 | print/console 남발 | `grep -rn "^\s*print(" lib/ integrated_data_layer/lib/` | 매치 1+ |
|
|
50
|
+
| FL-03 | 하드코딩 색상 (신규) | `git diff --name-only --diff-filter=A HEAD~N..HEAD \| grep '\.dart$' \| xargs grep -n "Color(0x"` | 매치 1+ (`ColorManager` 참조 제외) |
|
|
51
|
+
| FL-04 | StatefulWidget 직접 API 호출 | 수동 감사 — `_page.dart` 에 `dataLayer\|DataLayer` 호출이 있는데 짝 `_page_vm.dart` 없음 | 패턴 발견 |
|
|
52
|
+
| FL-05 | bridges/ 신규 참조 | `git diff HEAD~N..HEAD \| grep "^+.*bridges/"` | 매치 1+ |
|
|
53
|
+
| FL-06 | 하드코딩 한글 (신규 UI) | `git diff --name-only --diff-filter=AM HEAD~N..HEAD \| grep 'lib/ui/.*\.dart$' \| xargs grep -n "Text('[가-힣]"` | 매치 1+ |
|
|
54
|
+
| FL-07 | JsonSerializable includeIfNull 누락 | `grep -rn "@JsonSerializable" integrated_data_layer/lib/2_data_sources/remote/request/body/ \| grep -v "includeIfNull: false"` | 매치 1+ |
|
|
55
|
+
| FL-08 | API 키/시크릿 하드코딩 | `grep -rEn "(api[_-]?key\|secret\|token)\s*=\s*['\"][A-Za-z0-9]{16,}['\"]" lib/ integrated_data_layer/lib/` | 매치 1+ |
|
|
56
|
+
|
|
57
|
+
> `HEAD~N` 의 N은 sprint_commits 수 — `git log --format=%h HEAD~sprint_start..HEAD` 로 결정.
|
|
58
|
+
|
|
59
|
+
## FL-04 수동 감사 방법
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 1. 신규/수정된 page 파일 목록
|
|
63
|
+
PAGES=$(git diff --name-only HEAD~N..HEAD | grep '_page\.dart$')
|
|
64
|
+
|
|
65
|
+
# 2. 각 page에서 API 호출 존재 여부
|
|
66
|
+
for p in $PAGES; do
|
|
67
|
+
if grep -q "dataLayer\|DataLayer" "$p"; then
|
|
68
|
+
vm="${p%_page.dart}_page_vm.dart"
|
|
69
|
+
if [ -f "$vm" ] && grep -q "dataLayer\|DataLayer" "$vm"; then
|
|
70
|
+
echo "OK (VM handles API): $p"
|
|
71
|
+
else
|
|
72
|
+
echo "FAIL (page calls API without VM): $p"
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
done
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
VM이 없고 페이지가 직접 호출하면 **FL-04 FAIL**.
|
|
79
|
+
|
|
80
|
+
## 결과 집계
|
|
81
|
+
|
|
82
|
+
```markdown
|
|
83
|
+
## Step 4: Anti-Pattern
|
|
84
|
+
|
|
85
|
+
| Rule | Status | Count | Files |
|
|
86
|
+
|------|--------|-------|-------|
|
|
87
|
+
| FL-01 dart:html | PASS/FAIL | 0 | - |
|
|
88
|
+
| FL-02 print | PASS/FAIL | 0 | - |
|
|
89
|
+
| FL-03 color hardcode | PASS/FAIL | 0 | - |
|
|
90
|
+
| FL-04 stateful API call | PASS/FAIL | 0 | - |
|
|
91
|
+
| FL-05 bridges/ new | PASS/FAIL | 0 | - |
|
|
92
|
+
| FL-06 ko hardcode | PASS/FAIL | 0 | - |
|
|
93
|
+
| FL-07 includeIfNull missing | PASS/FAIL | 0 | - |
|
|
94
|
+
| FL-08 secret hardcode | PASS/FAIL | 0 | - |
|
|
95
|
+
|
|
96
|
+
Overall: PASS / FAIL
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**어떤 룰이든 FAIL 1건 → Step 4 전체 FAIL → 스프린트 FAIL** (하드 임계값: 위반 0건).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: harness-generator-frontend-flutter
|
|
3
|
+
description: "하네스 Flutter Frontend Generator. Flutter(Dart) + Riverpod + integrated_data_layer(Retrofit) 기반 모바일 앱을 구현한다. ARB 다국어, JsonSerializable, NotifierProvider 패턴 준수. api-contract.json이 Source of Truth — UI 추론 금지."
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Generator-Frontend-Flutter — Dart + Riverpod + integrated_data_layer
|
|
8
|
+
|
|
9
|
+
## Session Boundary Protocol
|
|
10
|
+
|
|
11
|
+
### On Start
|
|
12
|
+
1. `.harness/progress.json` 읽기 — `next_agent`가 `"generator-frontend-flutter"`인지 확인
|
|
13
|
+
2. `.harness/actions/pipeline.json`의 `fe_stack == "flutter"` 재확인 — 아니면 즉시 STOP + 사용자에게 불일치 보고
|
|
14
|
+
3. progress.json 업데이트: `current_agent` → `"generator-frontend-flutter"`, `agent_status` → `"running"`, `updated_at` 갱신
|
|
15
|
+
4. `failure` 필드 확인 — retry인 경우 평가 문서의 실패 사유 우선 읽기
|
|
16
|
+
|
|
17
|
+
### On Complete
|
|
18
|
+
1. progress.json 업데이트:
|
|
19
|
+
- `agent_status` → `"completed"`
|
|
20
|
+
- `completed_agents`에 `"generator-frontend-flutter"` 추가
|
|
21
|
+
- `next_agent` → `"evaluator-functional-flutter"`
|
|
22
|
+
- `failure` 필드 초기화
|
|
23
|
+
2. `feature-list.json`의 해당 feature `passes`에 `"generator-frontend-flutter"` 추가
|
|
24
|
+
3. `.harness/progress.log`에 요약 추가
|
|
25
|
+
4. **STOP. 다음 에이전트를 직접 호출하지 않는다.**
|
|
26
|
+
5. 출력: `"✓ Generator-Frontend-Flutter 완료. bash scripts/harness-next.sh 실행하여 다음 단계 확인."`
|
|
27
|
+
|
|
28
|
+
## Startup
|
|
29
|
+
|
|
30
|
+
1. `AGENTS.md` 읽기 — IA-MAP, 권한 확인 (Flutter 소유 경로)
|
|
31
|
+
2. `.harness/gotchas/generator-frontend-flutter.md` 읽기 (없으면 skip) — **과거 실수 반복 금지**
|
|
32
|
+
3. `pwd` + `.harness/progress.json` + `git log --oneline -20`
|
|
33
|
+
4. `.harness/actions/api-contract.json` 읽기 — **서버 API 계약이 Source of Truth**
|
|
34
|
+
5. `.harness/actions/feature-list.json` — `layer: "frontend"` 필터
|
|
35
|
+
6. `pubspec.yaml` 확인 — Flutter 버전, Riverpod/Retrofit/json_serializable 의존성 존재 확인
|
|
36
|
+
|
|
37
|
+
## AGENTS.md — 읽기 전용
|
|
38
|
+
|
|
39
|
+
`[FE]` + `→ Generator-Frontend-Flutter` 소유 경로만 쓰기 가능. 일반적으로:
|
|
40
|
+
- `lib/ui/pages/`, `lib/ui/component/`, `lib/l10n/`
|
|
41
|
+
- `integrated_data_layer/lib/`, `integrated_data_layer/test/`
|
|
42
|
+
- `assets/strings/`
|
|
43
|
+
|
|
44
|
+
Backend 코드, `.harness/`, `AGENTS.md` 수정 금지.
|
|
45
|
+
|
|
46
|
+
## Sprint Workflow
|
|
47
|
+
|
|
48
|
+
1. **Sprint Contract FE 섹션 추가** — 페이지, VM, API 연동, 성공 기준
|
|
49
|
+
2. **api-contract.json → Retrofit/JsonSerializable 변환**
|
|
50
|
+
3. **구현** — 아래 4개 레퍼런스를 반드시 참조
|
|
51
|
+
4. **코드 생성**: `flutter pub run build_runner build --delete-conflicting-outputs`
|
|
52
|
+
5. **Self-Verification** — `flutter analyze` + `flutter test` 통과
|
|
53
|
+
6. **Handoff** → Evaluator-Functional-Flutter
|
|
54
|
+
|
|
55
|
+
## 개발론 레퍼런스 (점진적 로딩)
|
|
56
|
+
|
|
57
|
+
| 문서 | 내용 | 언제 로드 |
|
|
58
|
+
|------|------|----------|
|
|
59
|
+
| [API Layer Pattern](references/api-layer-pattern.md) | integrated_data_layer 구조, Request/Response, Retrofit | API 연동 시 |
|
|
60
|
+
| [Riverpod Pattern](references/riverpod-pattern.md) | Page+VM 쌍, NotifierProvider, family 패턴 | 페이지/위젯 구현 시 |
|
|
61
|
+
| [i18n Pattern](references/i18n-pattern.md) | ARB 파일, LocaleAssist, 키 네이밍 | 문자열 추가 시 |
|
|
62
|
+
| [Anti-Patterns](references/anti-patterns.md) | 금지 API, 하드코딩, bridges/, StatefulWidget 직접 호출 | 구현 완료 후 셀프 체크 |
|
|
63
|
+
|
|
64
|
+
## 핵심 규칙
|
|
65
|
+
|
|
66
|
+
### api-contract.json → Dart 변환 규칙
|
|
67
|
+
|
|
68
|
+
- 각 엔드포인트 → `rest_api.dart`에 Retrofit 어노테이션 (`@GET`, `@POST`, `@PUT`, `@DELETE`)
|
|
69
|
+
- Request body → `2_data_sources/remote/request/body/xxx_body.dart` (`@JsonSerializable(includeIfNull: false)`)
|
|
70
|
+
- Response → `2_data_sources/remote/response/xxx_response.dart` (`ClueResponseImpl<T>` 상속 or 재사용)
|
|
71
|
+
- **필수 지시가 없으면 모든 필드는 Nullable** — 서버가 언제든 필드를 누락할 수 있음
|
|
72
|
+
- **기존 응답 타입 재사용 우선** — 동일 구조면 새 클래스 생성 금지
|
|
73
|
+
- Repository 래퍼 메서드 추가 → `1_repositories/xxx_repository.dart`
|
|
74
|
+
|
|
75
|
+
### UI / 상태관리
|
|
76
|
+
|
|
77
|
+
- 모든 페이지 = `xxx_page.dart` + `xxx_page_vm.dart` 쌍
|
|
78
|
+
- Page는 `ConsumerStatefulWidget` 또는 `ConsumerWidget`
|
|
79
|
+
- VM은 `NotifierProvider<Notifier, State>` (다중 인스턴스는 `.family`)
|
|
80
|
+
- State는 `Equatable` + `copyWith` 불변성
|
|
81
|
+
- `ref.watch` (build 내) / `ref.read` (이벤트 핸들러, initState)
|
|
82
|
+
- API 호출은 반드시 VM 안에서 `ref.read(dataLayer).xxx.method()`
|
|
83
|
+
- Provider 네이밍: `p` + PascalCase + `Provider` (예: `pHomePageProvider`)
|
|
84
|
+
|
|
85
|
+
### 다국어 (i18n)
|
|
86
|
+
|
|
87
|
+
- 영문 전용 지시가 없는 한 **모든 사용자 노출 문자열은 ARB 경유 필수**
|
|
88
|
+
- ARB: `lib/l10n/app_en.arb`, `app_ko.arb`, `app_ja.arb`
|
|
89
|
+
- 코드 접근: `LocaleAssist().of.키이름`
|
|
90
|
+
- 키 네이밍: camelCase (예: `doorOpen`, `networkError`)
|
|
91
|
+
- 모든 언어 파일에 동일 키 동시 추가
|
|
92
|
+
|
|
93
|
+
### 코드 생성
|
|
94
|
+
|
|
95
|
+
Request Body / Response / rest_api.dart 수정 후 **반드시** 실행:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cd <integrated_data_layer 경로>
|
|
99
|
+
flutter pub run build_runner build --delete-conflicting-outputs
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Self-Verification (Handoff 전)
|
|
103
|
+
|
|
104
|
+
1. `flutter analyze` → 경고 0개 (info 수준은 허용, warning/error 금지)
|
|
105
|
+
2. `flutter test` → 100% 통과
|
|
106
|
+
3. integrated_data_layer의 새 Request Body / Response에 `fromJson`/`toJson` 왕복 테스트 존재
|
|
107
|
+
4. `grep -rn 'dart:html\|universal_html\|print(\|console\.log' lib/ integrated_data_layer/lib/` → 0개
|
|
108
|
+
5. `grep -rn "bridges/" lib/` → 신규 코드 내 참조 0개
|
|
109
|
+
6. 하드코딩 색상(`Color(0xFF...`) → `ColorManager` 경유 확인
|
|
110
|
+
7. 새 페이지/위젯은 `StatefulWidget`에서 직접 API 호출하지 않는지 확인 (VM/Provider 경유)
|
|
111
|
+
|
|
112
|
+
## 금지 사항
|
|
113
|
+
|
|
114
|
+
- Backend 코드 수정, 서버 API 경로 임의 변경
|
|
115
|
+
- api-contract.json에 없는 엔드포인트 호출/추가
|
|
116
|
+
- `dart:html`, `universal_html` 직접 참조
|
|
117
|
+
- `print()` / `debugPrint` 남발 — `logger` 사용
|
|
118
|
+
- 하드코딩 색상 → `ColorManager` 강제
|
|
119
|
+
- `StatefulWidget` 내 직접 API 호출 → VM/Provider 경유
|
|
120
|
+
- `bridges/` 신규 개발 — BLOC 기반 레거시, 유지보수만 허용
|
|
121
|
+
- AI 추측으로 서버 응답 구조를 만들지 말 것 (계약이 Source of Truth)
|
|
122
|
+
- API 키/시크릿 하드코딩
|
|
123
|
+
|
|
124
|
+
## 명령어
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
flutter pub get # 의존성
|
|
128
|
+
flutter run # 실행
|
|
129
|
+
flutter test # 테스트
|
|
130
|
+
flutter analyze # 정적 분석
|
|
131
|
+
flutter pub run build_runner build --delete-conflicting-outputs # 코드 생성
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## After Completion
|
|
135
|
+
|
|
136
|
+
1. sprint-contract.md의 FE 섹션에 완료 항목 체크
|
|
137
|
+
2. Self-Verification 체크리스트 결과 요약
|
|
138
|
+
3. Session Boundary Protocol On Complete 실행
|
|
@@ -0,0 +1,288 @@
|
|
|
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 직접 참조
|
|
37
|
+
|
|
38
|
+
### 금지
|
|
39
|
+
```dart
|
|
40
|
+
import 'dart:html';
|
|
41
|
+
import 'package:universal_html/html.dart';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 이유
|
|
45
|
+
Flutter 앱은 크로스 플랫폼 — 모바일 빌드에서 `dart:html` 참조는 즉시 빌드 실패한다.
|
|
46
|
+
Web 빌드 전용 코드가 필요하면 조건부 import 패턴을 사용한다.
|
|
47
|
+
|
|
48
|
+
### 대체
|
|
49
|
+
`package:web` 또는 `kIsWeb` 분기로 플랫폼 체크 후 조건부 import.
|
|
50
|
+
|
|
51
|
+
### 검증
|
|
52
|
+
```bash
|
|
53
|
+
grep -rn "dart:html\|universal_html" lib/ integrated_data_layer/lib/
|
|
54
|
+
```
|
|
55
|
+
결과 0개여야 함.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 2. print / console 남발
|
|
60
|
+
|
|
61
|
+
### 금지
|
|
62
|
+
```dart
|
|
63
|
+
print("user clicked");
|
|
64
|
+
debugPrint("response: $data");
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 이유
|
|
68
|
+
프로덕션 빌드에 로그가 섞이면 성능/보안 문제. 통일된 로거가 없으면 분석 불가.
|
|
69
|
+
|
|
70
|
+
### 대체
|
|
71
|
+
프로젝트의 `logger` 모듈 사용 (`Logger().d()`, `.i()`, `.w()`, `.e()`).
|
|
72
|
+
디버그 전용 출력도 `kDebugMode` 가드 필수.
|
|
73
|
+
|
|
74
|
+
### 검증
|
|
75
|
+
```bash
|
|
76
|
+
grep -rn "^\s*print(" lib/ integrated_data_layer/lib/
|
|
77
|
+
grep -rn "console\.log" lib/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 3. 하드코딩 색상
|
|
83
|
+
|
|
84
|
+
### 금지
|
|
85
|
+
```dart
|
|
86
|
+
Container(color: Color(0xFF6682FF))
|
|
87
|
+
Text('Hello', style: TextStyle(color: Colors.blue))
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 이유
|
|
91
|
+
디자인 시스템 붕괴 + 다크모드/브랜딩 변경 시 전역 수정 불가.
|
|
92
|
+
|
|
93
|
+
### 대체
|
|
94
|
+
```dart
|
|
95
|
+
Container(color: ColorManager.primary)
|
|
96
|
+
Text('Hello', style: TextStyle(color: ColorManager.textPrimary))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`ColorManager` (또는 프로젝트의 Design Token) 경유 필수.
|
|
100
|
+
|
|
101
|
+
### 검증
|
|
102
|
+
```bash
|
|
103
|
+
grep -rn "Color(0x" lib/ui/ | grep -v "ColorManager\|color_manager"
|
|
104
|
+
```
|
|
105
|
+
신규 파일에서 결과 0개여야 함 (레거시 파일은 예외).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 4. StatefulWidget 내 직접 API 호출
|
|
110
|
+
|
|
111
|
+
### 금지
|
|
112
|
+
```dart
|
|
113
|
+
class _MyPageState extends State<MyPage> {
|
|
114
|
+
@override
|
|
115
|
+
void initState() {
|
|
116
|
+
super.initState();
|
|
117
|
+
DataLayer.instance.ac.getExample(exampleId: 1).then((res) { // ✗
|
|
118
|
+
setState(() { _data = res; });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 이유
|
|
125
|
+
테스트 불가능, 상태 공유 불가, UI와 비즈니스 로직 결합.
|
|
126
|
+
|
|
127
|
+
### 대체
|
|
128
|
+
VM (`NotifierProvider`) 에 API 호출을 옮기고 Page는 `ConsumerWidget` 으로.
|
|
129
|
+
상세 → [riverpod-pattern.md](./riverpod-pattern.md)
|
|
130
|
+
|
|
131
|
+
### 검증
|
|
132
|
+
`grep` 으로 자동 감지 어려움 — Evaluator가 신규 `*_page.dart` 파일에서
|
|
133
|
+
`dataLayer\|DataLayer` 호출 패턴을 확인하고, 파트너 `*_page_vm.dart` 파일에
|
|
134
|
+
동일 호출이 있는지 대조한다.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 5. bridges/ 신규 사용
|
|
139
|
+
|
|
140
|
+
### 금지
|
|
141
|
+
```dart
|
|
142
|
+
import 'package:clue_mobile_app/bridges/xxx.dart';
|
|
143
|
+
```
|
|
144
|
+
신규 코드에서 참조 금지.
|
|
145
|
+
|
|
146
|
+
### 이유
|
|
147
|
+
BLOC 기반 레거시 — Riverpod 마이그레이션 방침에 따라 유지보수만 허용.
|
|
148
|
+
|
|
149
|
+
### 대체
|
|
150
|
+
신규 상태관리는 `NotifierProvider` + `Notifier<State>`.
|
|
151
|
+
|
|
152
|
+
### 검증
|
|
153
|
+
신규/수정 파일의 git diff에서 `bridges/` import 추가 여부 확인.
|
|
154
|
+
```bash
|
|
155
|
+
git diff --name-only HEAD~1 | xargs grep -l "bridges/" 2>/dev/null
|
|
156
|
+
```
|
|
157
|
+
새로 추가된 줄이 있으면 FAIL.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 6. 하드코딩 문자열 (다국어 미처리)
|
|
162
|
+
|
|
163
|
+
### 금지
|
|
164
|
+
```dart
|
|
165
|
+
Text('취소')
|
|
166
|
+
Text('로그인 실패')
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 이유
|
|
170
|
+
i18n 원칙 위반 — 다국어 전환 시 즉시 깨짐.
|
|
171
|
+
|
|
172
|
+
### 대체
|
|
173
|
+
```dart
|
|
174
|
+
Text(LocaleAssist().of.cancel)
|
|
175
|
+
Text(LocaleAssist().of.loginFailed)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
상세 → [i18n-pattern.md](./i18n-pattern.md)
|
|
179
|
+
|
|
180
|
+
### 검증
|
|
181
|
+
```bash
|
|
182
|
+
grep -rn "Text('[가-힣ぁ-んァ-ヶ一-龯]" lib/ui/
|
|
183
|
+
```
|
|
184
|
+
신규 파일에서 결과 0개여야 함.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 7. JsonSerializable 누락 / includeIfNull 잘못 설정
|
|
189
|
+
|
|
190
|
+
### 금지
|
|
191
|
+
```dart
|
|
192
|
+
// @JsonSerializable 없이 수동 fromJson/toJson
|
|
193
|
+
@JsonSerializable() // includeIfNull 누락 → 기본값 true
|
|
194
|
+
class UserBody { ... }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### 이유
|
|
198
|
+
서버가 null 필드를 bad request 처리하는 API 계약 위배. 코드 생성 불일치.
|
|
199
|
+
|
|
200
|
+
### 대체
|
|
201
|
+
```dart
|
|
202
|
+
@JsonSerializable(includeIfNull: false)
|
|
203
|
+
class UserBody { ... }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Request Body는 **항상** `includeIfNull: false`.
|
|
207
|
+
|
|
208
|
+
### 검증
|
|
209
|
+
```bash
|
|
210
|
+
grep -rn "@JsonSerializable" integrated_data_layer/lib/2_data_sources/remote/request/body/ | grep -v "includeIfNull: false"
|
|
211
|
+
```
|
|
212
|
+
결과 0개여야 함.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 8. Non-null 남용
|
|
217
|
+
|
|
218
|
+
### 금지
|
|
219
|
+
```dart
|
|
220
|
+
// 서버 응답에 대해 non-null 단정
|
|
221
|
+
class ExampleData {
|
|
222
|
+
final String name; // ✗ 필수 지시 없으면 Nullable
|
|
223
|
+
final int id;
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 이유
|
|
228
|
+
서버는 언제든 필드를 누락할 수 있음. non-null은 런타임 crash 유발.
|
|
229
|
+
|
|
230
|
+
### 대체
|
|
231
|
+
```dart
|
|
232
|
+
class ExampleData {
|
|
233
|
+
final String? name;
|
|
234
|
+
final int? id;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**필수 지시가 api-contract.json 에 명시된 필드만 non-null 허용.**
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 9. API 키 / 시크릿 하드코딩
|
|
243
|
+
|
|
244
|
+
### 금지
|
|
245
|
+
```dart
|
|
246
|
+
const apiKey = "sk-1234567890abcdef";
|
|
247
|
+
const Dio().options.headers['Authorization'] = 'Bearer xxx';
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 이유
|
|
251
|
+
APK 디컴파일로 즉시 노출 — 보안 사고.
|
|
252
|
+
|
|
253
|
+
### 대체
|
|
254
|
+
`--dart-define` 또는 `flutter_dotenv` + `.env` (gitignore).
|
|
255
|
+
런타임에 서버에서 발급받는 토큰 사용.
|
|
256
|
+
|
|
257
|
+
### 검증
|
|
258
|
+
```bash
|
|
259
|
+
grep -rEn "(api[_-]?key|secret|token)\s*=\s*['\"][A-Za-z0-9]{16,}['\"]" lib/ integrated_data_layer/lib/
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 셀프 체크 스크립트
|
|
265
|
+
|
|
266
|
+
Generator는 handoff 전 다음 명령을 모두 실행하고 결과를 sprint-contract.md에 기록한다:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
# 1. 웹 API 직접 참조
|
|
270
|
+
grep -rn "dart:html\|universal_html" lib/ integrated_data_layer/lib/ || echo "OK"
|
|
271
|
+
|
|
272
|
+
# 2. print 남발
|
|
273
|
+
grep -rn "^\s*print(" lib/ integrated_data_layer/lib/ || echo "OK"
|
|
274
|
+
|
|
275
|
+
# 3. 하드코딩 색상 (신규 파일만 — git diff 기반)
|
|
276
|
+
git diff --name-only --diff-filter=A HEAD | grep '\.dart$' | xargs grep -n "Color(0x" 2>/dev/null || echo "OK"
|
|
277
|
+
|
|
278
|
+
# 4. bridges/ 신규 참조
|
|
279
|
+
git diff HEAD | grep "^+" | grep "bridges/" || echo "OK"
|
|
280
|
+
|
|
281
|
+
# 5. JsonSerializable + includeIfNull 확인
|
|
282
|
+
grep -rn "@JsonSerializable" integrated_data_layer/lib/2_data_sources/remote/request/body/ | grep -v "includeIfNull: false" || echo "OK"
|
|
283
|
+
|
|
284
|
+
# 6. 하드코딩 한글 (신규 UI)
|
|
285
|
+
git diff --name-only --diff-filter=AM HEAD | grep 'lib/ui/.*\.dart$' | xargs grep -n "Text('[가-힣]" 2>/dev/null || echo "OK"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
모두 `OK` 여야 handoff 가능.
|