@walwal-harness/cli 4.0.0-alpha.2 → 4.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -270
- package/assets/templates/config.json +1 -48
- package/assets/templates/gitignore-append.txt +1 -0
- package/bin/init.js +1 -0
- package/package.json +1 -1
- package/scripts/harness-dashboard-v4.sh +58 -81
- package/scripts/harness-next.sh +4 -15
- package/scripts/harness-prompts-v4.sh +106 -0
- package/scripts/harness-queue-manager.sh +59 -5
- package/scripts/harness-session-start.sh +18 -0
- package/scripts/harness-studio-v4.sh +69 -69
- package/scripts/harness-team-worker.sh +136 -123
- package/scripts/harness-user-prompt-submit.sh +31 -1
- package/skills/dispatcher/SKILL.md +7 -2
- package/skills/team-action/SKILL.md +58 -0
- package/skills/team-stop/SKILL.md +31 -0
- package/skills/evaluator-functional-flutter/SKILL.md +0 -206
- package/skills/evaluator-functional-flutter/references/ia-compliance.md +0 -77
- package/skills/evaluator-functional-flutter/references/scoring-rubric.md +0 -132
- package/skills/evaluator-functional-flutter/references/static-check-rules.md +0 -99
- package/skills/generator-frontend-flutter/SKILL.md +0 -173
- package/skills/generator-frontend-flutter/references/anti-patterns.md +0 -320
- package/skills/generator-frontend-flutter/references/api-layer-pattern.md +0 -233
- package/skills/generator-frontend-flutter/references/flutter-web-pattern.md +0 -273
- package/skills/generator-frontend-flutter/references/i18n-pattern.md +0 -102
- package/skills/generator-frontend-flutter/references/riverpod-pattern.md +0 -199
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
docmeta:
|
|
3
|
-
id: api-layer-pattern
|
|
4
|
-
title: API Layer Pattern (integrated_data_layer)
|
|
5
|
-
type: output
|
|
6
|
-
createdAt: 2026-04-09T00:00:00Z
|
|
7
|
-
updatedAt: 2026-04-09T00:00:00Z
|
|
8
|
-
source:
|
|
9
|
-
producer: agent
|
|
10
|
-
skillId: harness-generator-frontend-flutter
|
|
11
|
-
inputs:
|
|
12
|
-
- documentId: clue-fe-flutter-api-layer
|
|
13
|
-
uri: ../../../../../moon_web/clue-fe-flutter.skill
|
|
14
|
-
relation: output-from
|
|
15
|
-
sections:
|
|
16
|
-
- sourceRange:
|
|
17
|
-
startLine: 1
|
|
18
|
-
endLine: 187
|
|
19
|
-
targetRange:
|
|
20
|
-
startLine: 32
|
|
21
|
-
endLine: 205
|
|
22
|
-
tags:
|
|
23
|
-
- flutter
|
|
24
|
-
- retrofit
|
|
25
|
-
- json-serializable
|
|
26
|
-
- integrated-data-layer
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
# API Layer Pattern (integrated_data_layer)
|
|
30
|
-
|
|
31
|
-
하네스 Flutter Generator는 `api-contract.json` 을 **Source of Truth** 로 삼아
|
|
32
|
-
`integrated_data_layer` 하위에 Dart 타입을 1:1 생성한다.
|
|
33
|
-
|
|
34
|
-
## 디렉토리 구조
|
|
35
|
-
|
|
36
|
-
```
|
|
37
|
-
integrated_data_layer/
|
|
38
|
-
├── lib/
|
|
39
|
-
│ ├── 1_repositories/ # Repository (비즈니스 로직 래퍼)
|
|
40
|
-
│ │ ├── ac_repository.dart
|
|
41
|
-
│ │ ├── visitor_repository.dart
|
|
42
|
-
│ │ ├── s3_repository.dart
|
|
43
|
-
│ │ ├── oauth_repository.dart
|
|
44
|
-
│ │ └── reservation_repository.dart
|
|
45
|
-
│ ├── 2_data_sources/
|
|
46
|
-
│ │ ├── remote/
|
|
47
|
-
│ │ │ ├── rest_api.dart # 모든 Retrofit 엔드포인트
|
|
48
|
-
│ │ │ ├── rest_provider.dart # Dio/RestClient Provider
|
|
49
|
-
│ │ │ ├── request/body/ # Request Body 클래스
|
|
50
|
-
│ │ │ └── response/
|
|
51
|
-
│ │ │ └── abstract/ # 베이스 응답 (ClueResponseImpl 등)
|
|
52
|
-
│ │ └── local/
|
|
53
|
-
│ └── 3_others/
|
|
54
|
-
│ ├── enum/
|
|
55
|
-
│ └── extension/
|
|
56
|
-
├── test/ # lib/ 미러 구조
|
|
57
|
-
│ ├── 1_repositories/
|
|
58
|
-
│ ├── 2_data_sources/remote/request/body/
|
|
59
|
-
│ └── 2_data_sources/remote/response/
|
|
60
|
-
└── data_layer.dart # 진입점 (dataLayer Provider)
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
## 1. api-contract.json → Request Body 변환
|
|
64
|
-
|
|
65
|
-
api-contract.json의 요청 스키마를 `@JsonSerializable(includeIfNull: false)`
|
|
66
|
-
클래스로 1:1 매핑한다. **필수 지시가 없으면 모든 필드 Nullable.**
|
|
67
|
-
|
|
68
|
-
```dart
|
|
69
|
-
// lib/2_data_sources/remote/request/body/example_body.dart
|
|
70
|
-
import 'package:json_annotation/json_annotation.dart';
|
|
71
|
-
|
|
72
|
-
part 'example_body.g.dart';
|
|
73
|
-
|
|
74
|
-
@JsonSerializable(includeIfNull: false) // ★ 필수
|
|
75
|
-
class ExampleBody {
|
|
76
|
-
final String? name; // ★ Nullable 기본
|
|
77
|
-
final int? placeId;
|
|
78
|
-
final String? description;
|
|
79
|
-
|
|
80
|
-
const ExampleBody({
|
|
81
|
-
this.name,
|
|
82
|
-
this.placeId,
|
|
83
|
-
this.description,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
factory ExampleBody.fromJson(Map<String, dynamic> json) =>
|
|
87
|
-
_$ExampleBodyFromJson(json);
|
|
88
|
-
Map<String, dynamic> toJson() => _$ExampleBodyToJson(this);
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## 2. Response 변환
|
|
93
|
-
|
|
94
|
-
응답 베이스 클래스 계층:
|
|
95
|
-
- `ClueResponseImpl<T>` — 단일 데이터 (code, data, errors)
|
|
96
|
-
- `ClueResponseListImpl<T>` — 목록 (code, data, errors, totalCount)
|
|
97
|
-
- `VisitorResponseImpl<T>` — Visitor 서버 전용
|
|
98
|
-
|
|
99
|
-
```dart
|
|
100
|
-
// lib/2_data_sources/remote/response/example_response.dart
|
|
101
|
-
import 'package:integrated_data_layer/2_data_sources/remote/response/abstract/clue/clue_response_impl.dart';
|
|
102
|
-
import 'package:json_annotation/json_annotation.dart';
|
|
103
|
-
|
|
104
|
-
part 'example_response.g.dart';
|
|
105
|
-
|
|
106
|
-
@JsonSerializable(explicitToJson: true)
|
|
107
|
-
class ExampleResponse extends ClueResponseImpl<ExampleResponseData> {
|
|
108
|
-
const ExampleResponse({
|
|
109
|
-
required super.code,
|
|
110
|
-
required super.data,
|
|
111
|
-
required super.errors,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
factory ExampleResponse.fromJson(Map<String, dynamic> json) =>
|
|
115
|
-
_$ExampleResponseFromJson(json);
|
|
116
|
-
Map<String, dynamic> toJson() => _$ExampleResponseToJson(this);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
@JsonSerializable(explicitToJson: true)
|
|
120
|
-
class ExampleResponseData {
|
|
121
|
-
final int? id; // ★ Nullable 기본
|
|
122
|
-
final String? name;
|
|
123
|
-
|
|
124
|
-
const ExampleResponseData({this.id, this.name});
|
|
125
|
-
|
|
126
|
-
factory ExampleResponseData.fromJson(Map<String, dynamic> json) =>
|
|
127
|
-
_$ExampleResponseDataFromJson(json);
|
|
128
|
-
Map<String, dynamic> toJson() => _$ExampleResponseDataToJson(this);
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**기존 응답 재사용 원칙**: api-contract.json 의 서로 다른 엔드포인트가
|
|
133
|
-
동일한 응답 구조를 가지면 새 Response 클래스를 만들지 않는다.
|
|
134
|
-
|
|
135
|
-
## 3. rest_api.dart 엔드포인트 추가
|
|
136
|
-
|
|
137
|
-
```dart
|
|
138
|
-
// rest_api.dart 내부 — RestClient 클래스에 추가
|
|
139
|
-
@GET("/examples/{exampleId}")
|
|
140
|
-
Future<ExampleResponse> getExample(
|
|
141
|
-
@Path("exampleId") int exampleId,
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
@POST("/examples")
|
|
145
|
-
Future<ExampleResponse> createExample(
|
|
146
|
-
@Body() ExampleBody requestBody,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
@PUT("/examples/{exampleId}")
|
|
150
|
-
Future<ExampleResponse> updateExample(
|
|
151
|
-
@Path("exampleId") int exampleId,
|
|
152
|
-
@Body() ExampleBody requestBody,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
@DELETE("/examples/{exampleId}")
|
|
156
|
-
Future<ClueDefaultResponse> deleteExample(
|
|
157
|
-
@Path("exampleId") int exampleId,
|
|
158
|
-
);
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
api-contract.json의 path, method, param 위치를 **그대로** 옮긴다.
|
|
162
|
-
경로 변수 추가/제거 금지 — 계약이 Source of Truth.
|
|
163
|
-
|
|
164
|
-
## 4. Repository 래퍼 추가
|
|
165
|
-
|
|
166
|
-
```dart
|
|
167
|
-
// 1_repositories/ac_repository.dart 내부에 메서드 추가
|
|
168
|
-
Future<ExampleResponse> getExample({required int exampleId}) {
|
|
169
|
-
return _restClient.getExample(exampleId);
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
Repository는 **순수 래퍼** — 비즈니스 로직 없이 Retrofit 호출을 전달만 한다.
|
|
174
|
-
도메인 분기, 에러 매핑은 VM 계층에서 수행.
|
|
175
|
-
|
|
176
|
-
## 5. 호출 패턴 (VM에서)
|
|
177
|
-
|
|
178
|
-
```dart
|
|
179
|
-
// VM 내부
|
|
180
|
-
final result = await ref.read(dataLayer).ac.getExample(exampleId: 123);
|
|
181
|
-
if (result.code == 200) {
|
|
182
|
-
// 성공 처리
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## 6. TC 작성 (필수)
|
|
187
|
-
|
|
188
|
-
**모든 Request Body, Response에 왕복 테스트 필수.**
|
|
189
|
-
Evaluator가 test 디렉토리를 점검한다.
|
|
190
|
-
|
|
191
|
-
```dart
|
|
192
|
-
// test/2_data_sources/remote/request/body/example_body_test.dart
|
|
193
|
-
import 'dart:convert';
|
|
194
|
-
import 'package:flutter_test/flutter_test.dart';
|
|
195
|
-
import 'package:integrated_data_layer/2_data_sources/remote/request/body/example_body.dart';
|
|
196
|
-
|
|
197
|
-
void main() {
|
|
198
|
-
group("example body test", () {
|
|
199
|
-
test("/fromJson & toJson", () {
|
|
200
|
-
ExampleBody body1 = const ExampleBody(
|
|
201
|
-
name: 'test',
|
|
202
|
-
placeId: 1,
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
ExampleBody body2 = ExampleBody.fromJson(body1.toJson());
|
|
206
|
-
|
|
207
|
-
expect(body1.name, 'test');
|
|
208
|
-
expect(body1.placeId, 1);
|
|
209
|
-
|
|
210
|
-
var body1Data = jsonEncode(body1.toJson());
|
|
211
|
-
var body2Data = jsonEncode(body2.toJson());
|
|
212
|
-
expect(body1Data == body2Data, true);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("/null fields excluded", () {
|
|
216
|
-
ExampleBody body = const ExampleBody(name: 'test');
|
|
217
|
-
var json = body.toJson();
|
|
218
|
-
expect(json.containsKey('placeId'), false); // includeIfNull: false 검증
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
## 7. 코드 생성
|
|
225
|
-
|
|
226
|
-
모든 Request/Response 파일 추가/수정 후 **반드시** 실행:
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
cd <integrated_data_layer 경로>
|
|
230
|
-
flutter pub run build_runner build --delete-conflicting-outputs
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
생성된 `*.g.dart` 파일은 커밋한다 (CI에서 재생성하지 않음).
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
docmeta:
|
|
3
|
-
id: flutter-web-pattern
|
|
4
|
-
title: Flutter Web 패턴 — fe_target=web 전용
|
|
5
|
-
type: output
|
|
6
|
-
createdAt: 2026-04-09T00:00:00Z
|
|
7
|
-
updatedAt: 2026-04-09T00:00:00Z
|
|
8
|
-
source:
|
|
9
|
-
producer: agent
|
|
10
|
-
skillId: harness-generator-frontend-flutter
|
|
11
|
-
inputs:
|
|
12
|
-
- documentId: harness-generator-frontend-flutter-skill
|
|
13
|
-
uri: ../SKILL.md
|
|
14
|
-
relation: output-from
|
|
15
|
-
sections:
|
|
16
|
-
- sourceRange:
|
|
17
|
-
startLine: 56
|
|
18
|
-
endLine: 90
|
|
19
|
-
targetRange:
|
|
20
|
-
startLine: 21
|
|
21
|
-
endLine: 240
|
|
22
|
-
- documentId: flutter-anti-patterns
|
|
23
|
-
uri: ./anti-patterns.md
|
|
24
|
-
relation: output-from
|
|
25
|
-
sections:
|
|
26
|
-
- sourceRange:
|
|
27
|
-
startLine: 22
|
|
28
|
-
endLine: 75
|
|
29
|
-
targetRange:
|
|
30
|
-
startLine: 90
|
|
31
|
-
endLine: 165
|
|
32
|
-
tags:
|
|
33
|
-
- flutter
|
|
34
|
-
- flutter-web
|
|
35
|
-
- dart-web
|
|
36
|
-
- go-router
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
# Flutter Web 패턴 (`fe_target = web` 전용)
|
|
40
|
-
|
|
41
|
-
`pipeline.json.fe_target == "web"` 일 때만 적용. Mobile/Desktop 타겟에서는 이 문서의 규칙을 무시한다.
|
|
42
|
-
|
|
43
|
-
## 1. 프로젝트 활성화
|
|
44
|
-
|
|
45
|
-
기존 Flutter 프로젝트에 Web 타겟이 없으면:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
flutter config --enable-web
|
|
49
|
-
flutter create --platforms=web .
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
`web/index.html`, `web/manifest.json`, `web/favicon.png`, `web/icons/` 가 생성된다.
|
|
53
|
-
|
|
54
|
-
## 2. 빌드 및 개발 서버
|
|
55
|
-
|
|
56
|
-
| 명령 | 용도 |
|
|
57
|
-
|------|------|
|
|
58
|
-
| `flutter run -d chrome` | 개발 서버 (HMR 포함) |
|
|
59
|
-
| `flutter run -d chrome --web-port 8080` | 포트 고정 |
|
|
60
|
-
| `flutter build web --release` | 프로덕션 빌드 (`build/web/`) |
|
|
61
|
-
| `flutter build web --release --web-renderer canvaskit` | CanvasKit 렌더러 (성능 우선) |
|
|
62
|
-
| `flutter build web --release --web-renderer html` | HTML 렌더러 (호환성 우선, 작은 번들) |
|
|
63
|
-
|
|
64
|
-
> **렌더러 선택**: 모바일 사파리 호환성이 중요하면 `html`, 데스크톱/Chrome 중심이면 `canvaskit` (또는 auto).
|
|
65
|
-
|
|
66
|
-
## 3. 라우팅 — `go_router` 권장
|
|
67
|
-
|
|
68
|
-
URL 동기화 + 브라우저 history 지원을 위해 `go_router` 사용.
|
|
69
|
-
|
|
70
|
-
```yaml
|
|
71
|
-
# pubspec.yaml
|
|
72
|
-
dependencies:
|
|
73
|
-
go_router: ^14.0.0
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
```dart
|
|
77
|
-
// lib/router/app_router.dart
|
|
78
|
-
import 'package:go_router/go_router.dart';
|
|
79
|
-
|
|
80
|
-
final pAppRouterProvider = Provider<GoRouter>((ref) {
|
|
81
|
-
return GoRouter(
|
|
82
|
-
initialLocation: '/',
|
|
83
|
-
routes: [
|
|
84
|
-
GoRoute(path: '/', builder: (ctx, state) => const HomePage()),
|
|
85
|
-
GoRoute(path: '/login', builder: (ctx, state) => const LoginPage()),
|
|
86
|
-
GoRoute(
|
|
87
|
-
path: '/items/:id',
|
|
88
|
-
builder: (ctx, state) => ItemPage(id: state.pathParameters['id']!),
|
|
89
|
-
),
|
|
90
|
-
],
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
```dart
|
|
96
|
-
// main.dart
|
|
97
|
-
class App extends ConsumerWidget {
|
|
98
|
-
@override
|
|
99
|
-
Widget build(BuildContext context, WidgetRef ref) {
|
|
100
|
-
final router = ref.watch(pAppRouterProvider);
|
|
101
|
-
return MaterialApp.router(
|
|
102
|
-
routerConfig: router,
|
|
103
|
-
// ...
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
**Hash routing vs Path routing**:
|
|
110
|
-
- 기본은 hash routing (`/#/login`)
|
|
111
|
-
- Path routing 으로 바꾸려면 `web/index.html` 의 `<base href="/">` 설정 + 서버 fallback (모든 경로 → `index.html`)
|
|
112
|
-
|
|
113
|
-
## 4. JS Interop (필요할 때만)
|
|
114
|
-
|
|
115
|
-
크로스플랫폼 코드를 우선하되, Web 전용 API 가 필요하면 `package:web` + `dart:js_interop` 사용.
|
|
116
|
-
|
|
117
|
-
```dart
|
|
118
|
-
import 'package:web/web.dart' as web;
|
|
119
|
-
import 'dart:js_interop';
|
|
120
|
-
|
|
121
|
-
void copyToClipboard(String text) {
|
|
122
|
-
web.window.navigator.clipboard.writeText(text.toJS);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
String getUserAgent() => web.window.navigator.userAgent;
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Conditional import 패턴** (cross-platform 코드를 web/io 로 분기):
|
|
129
|
-
|
|
130
|
-
```dart
|
|
131
|
-
// platform_helper.dart (interface)
|
|
132
|
-
abstract class PlatformHelper {
|
|
133
|
-
String get platformName;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
PlatformHelper getPlatformHelper() => throw UnimplementedError();
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
```dart
|
|
140
|
-
// platform_helper_web.dart
|
|
141
|
-
import 'package:web/web.dart' as web;
|
|
142
|
-
import 'platform_helper.dart';
|
|
143
|
-
|
|
144
|
-
class WebPlatformHelper implements PlatformHelper {
|
|
145
|
-
@override
|
|
146
|
-
String get platformName => 'web (${web.window.navigator.userAgent})';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
PlatformHelper getPlatformHelper() => WebPlatformHelper();
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
```dart
|
|
153
|
-
// platform_helper_io.dart
|
|
154
|
-
import 'dart:io';
|
|
155
|
-
import 'platform_helper.dart';
|
|
156
|
-
|
|
157
|
-
class IoPlatformHelper implements PlatformHelper {
|
|
158
|
-
@override
|
|
159
|
-
String get platformName => 'io (${Platform.operatingSystem})';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
PlatformHelper getPlatformHelper() => IoPlatformHelper();
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
```dart
|
|
166
|
-
// 사용처
|
|
167
|
-
import 'platform_helper.dart'
|
|
168
|
-
if (dart.library.html) 'platform_helper_web.dart'
|
|
169
|
-
if (dart.library.io) 'platform_helper_io.dart';
|
|
170
|
-
|
|
171
|
-
final helper = getPlatformHelper();
|
|
172
|
-
print(helper.platformName);
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## 5. CORS 와 백엔드 연동
|
|
176
|
-
|
|
177
|
-
Flutter Web 은 브라우저에서 실행되므로 백엔드 API 호출 시 **CORS** 가 적용된다.
|
|
178
|
-
|
|
179
|
-
- 개발 단계: 백엔드 Gateway 의 `Access-Control-Allow-Origin` 에 `http://localhost:8080` (또는 `*`) 추가
|
|
180
|
-
- 프로덕션: 동일 origin 또는 명시적 CORS 화이트리스트
|
|
181
|
-
- API 키/인증 토큰은 **HttpOnly cookie** 또는 메모리 보관 (localStorage 는 XSS 위험)
|
|
182
|
-
|
|
183
|
-
`Dio` 인터셉터로 CSRF 토큰 등을 헤더에 추가:
|
|
184
|
-
|
|
185
|
-
```dart
|
|
186
|
-
final dio = Dio(BaseOptions(
|
|
187
|
-
baseUrl: 'https://api.example.com',
|
|
188
|
-
headers: {'Content-Type': 'application/json'},
|
|
189
|
-
));
|
|
190
|
-
|
|
191
|
-
dio.interceptors.add(InterceptorsWrapper(
|
|
192
|
-
onRequest: (options, handler) {
|
|
193
|
-
final token = ref.read(pAuthProvider).accessToken;
|
|
194
|
-
if (token != null) {
|
|
195
|
-
options.headers['Authorization'] = 'Bearer $token';
|
|
196
|
-
}
|
|
197
|
-
return handler.next(options);
|
|
198
|
-
},
|
|
199
|
-
));
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## 6. SEO / Meta Tags
|
|
203
|
-
|
|
204
|
-
`web/index.html` 의 `<head>` 에 SEO 메타 태그 추가:
|
|
205
|
-
|
|
206
|
-
```html
|
|
207
|
-
<meta name="description" content="Suprema CLUe — Smart Access Control">
|
|
208
|
-
<meta property="og:title" content="CLUe">
|
|
209
|
-
<meta property="og:description" content="...">
|
|
210
|
-
<meta property="og:image" content="/icons/og-image.png">
|
|
211
|
-
<link rel="canonical" href="https://app.example.com">
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
> SPA 의 SEO 한계: Flutter Web 은 client-side rendering 이므로 검색엔진이 동적 콘텐츠를 인덱싱하지 못할 수 있다. SSR 이 필요하면 Next.js + REST API 패턴을 고려.
|
|
215
|
-
|
|
216
|
-
## 7. 자산 최적화
|
|
217
|
-
|
|
218
|
-
- 이미지: `assets/images/` 에 두고 `pubspec.yaml` 의 `flutter.assets` 에 등록
|
|
219
|
-
- 폰트: `web/fonts/` 또는 `assets/fonts/` + `pubspec.yaml` 의 `flutter.fonts`
|
|
220
|
-
- 큰 이미지: webp 사용 권장 (`flutter_image_compress` 또는 사전 변환)
|
|
221
|
-
- Tree-shaking: `flutter build web --tree-shake-icons` 로 사용하지 않는 Material 아이콘 제거
|
|
222
|
-
|
|
223
|
-
## 8. PWA (Progressive Web App)
|
|
224
|
-
|
|
225
|
-
`flutter create --platforms=web` 시 자동 생성되는 `web/manifest.json` 을 채워서 PWA 로 동작:
|
|
226
|
-
|
|
227
|
-
```json
|
|
228
|
-
{
|
|
229
|
-
"name": "CLUe",
|
|
230
|
-
"short_name": "CLUe",
|
|
231
|
-
"start_url": "/",
|
|
232
|
-
"display": "standalone",
|
|
233
|
-
"background_color": "#FFFFFF",
|
|
234
|
-
"theme_color": "#6682FF",
|
|
235
|
-
"icons": [
|
|
236
|
-
{ "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
237
|
-
{ "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }
|
|
238
|
-
]
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Service worker 는 `flutter build web` 시 자동 생성된다 (`flutter_service_worker.js`).
|
|
243
|
-
|
|
244
|
-
## 9. 디버깅 — Chrome DevTools
|
|
245
|
-
|
|
246
|
-
- `flutter run -d chrome` 후 Chrome 자체 DevTools 열기 (F12)
|
|
247
|
-
- Console 에서 `print()` 출력 확인 가능 (단, 프로덕션 빌드에서는 `print` 금지)
|
|
248
|
-
- Source map 활성화: `--web-renderer html --source-maps` (디버그 빌드 기본)
|
|
249
|
-
- Network 탭에서 API 호출 직접 검사 가능 → Playwright 기반 `evaluator-functional` 도 동일하게 동작
|
|
250
|
-
|
|
251
|
-
## 10. Eval 인터페이스
|
|
252
|
-
|
|
253
|
-
`fe_target = web` 인 경우 `harness-next.sh` 가 다음과 같이 자동 라우팅:
|
|
254
|
-
|
|
255
|
-
```
|
|
256
|
-
generator-frontend-flutter (Self-Verification: flutter analyze + flutter test)
|
|
257
|
-
→ evaluator-functional (Playwright MCP — http://localhost:포트 E2E)
|
|
258
|
-
→ evaluator-visual (Playwright MCP — 스크린샷 + 반응형 + 접근성)
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**Generator 의 Self-Verification 단계에서 `flutter build web --release` 가 성공하는지 확인** 후 handoff.
|
|
262
|
-
|
|
263
|
-
## 11. 호스팅 (참고)
|
|
264
|
-
|
|
265
|
-
| 호스팅 | 빌드 산출물 |
|
|
266
|
-
|--------|------------|
|
|
267
|
-
| Vercel | `build/web/` 디렉토리를 정적 호스팅 |
|
|
268
|
-
| Netlify | 동일 |
|
|
269
|
-
| Firebase Hosting | `firebase deploy --only hosting` (firebase.json 의 public 을 `build/web` 으로) |
|
|
270
|
-
| GitHub Pages | `build/web/` 을 gh-pages 브랜치에 푸시 |
|
|
271
|
-
| 자체 서버 | nginx 로 정적 파일 서빙 + SPA fallback (`try_files $uri /index.html`) |
|
|
272
|
-
|
|
273
|
-
호스팅 결정은 Planner / 사용자가 함. Generator 는 빌드 산출물만 보장.
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
docmeta:
|
|
3
|
-
id: i18n-pattern
|
|
4
|
-
title: 다국어 (i18n) ARB 패턴
|
|
5
|
-
type: output
|
|
6
|
-
createdAt: 2026-04-09T00:00:00Z
|
|
7
|
-
updatedAt: 2026-04-09T00:00:00Z
|
|
8
|
-
source:
|
|
9
|
-
producer: agent
|
|
10
|
-
skillId: harness-generator-frontend-flutter
|
|
11
|
-
inputs:
|
|
12
|
-
- documentId: clue-fe-flutter-i18n
|
|
13
|
-
uri: ../../../../../moon_web/clue-fe-flutter.skill
|
|
14
|
-
relation: output-from
|
|
15
|
-
sections:
|
|
16
|
-
- sourceRange:
|
|
17
|
-
startLine: 1
|
|
18
|
-
endLine: 60
|
|
19
|
-
targetRange:
|
|
20
|
-
startLine: 32
|
|
21
|
-
endLine: 110
|
|
22
|
-
tags:
|
|
23
|
-
- flutter
|
|
24
|
-
- i18n
|
|
25
|
-
- arb
|
|
26
|
-
- l10n
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
# 다국어 (i18n) ARB 패턴
|
|
30
|
-
|
|
31
|
-
## ARB 파일 기반 l10n
|
|
32
|
-
|
|
33
|
-
### 파일 경로
|
|
34
|
-
|
|
35
|
-
- 영어: `lib/l10n/app_en.arb`
|
|
36
|
-
- 한국어: `lib/l10n/app_ko.arb`
|
|
37
|
-
- 일본어: `lib/l10n/app_ja.arb`
|
|
38
|
-
|
|
39
|
-
프로젝트에 따라 `assets/strings/en.json` 등 다른 경로를 쓸 수 있다.
|
|
40
|
-
`AGENTS.md` / sprint-contract.md 의 기존 컨벤션을 우선한다.
|
|
41
|
-
|
|
42
|
-
### 작업 순서
|
|
43
|
-
|
|
44
|
-
1. 문자열 언어 판별 (예: "취소" → ko, "Cancel" → en)
|
|
45
|
-
2. 해당 언어 arb 파일에 **키 존재 여부 확인** (중복 방지)
|
|
46
|
-
3. 없으면 모든 언어 파일에 동시 추가 (`app_en.arb`, `app_ko.arb`, `app_ja.arb`)
|
|
47
|
-
4. 코드 치환 → `LocaleAssist().of.키이름`
|
|
48
|
-
|
|
49
|
-
### 사용법
|
|
50
|
-
|
|
51
|
-
```dart
|
|
52
|
-
// 기본 문자열
|
|
53
|
-
Text(
|
|
54
|
-
LocaleAssist().of.cancel,
|
|
55
|
-
style: MyTextStyle.size15.w500.xFF9CA3AF,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
// 문자열 내 스타일 별도 구현 (ClueText 등 프로젝트 커스텀 위젯 사용)
|
|
59
|
-
ClueText(
|
|
60
|
-
"${LocaleAssist().of.all} (${count})",
|
|
61
|
-
style: MyTextStyle.size16.w500,
|
|
62
|
-
targetList: [
|
|
63
|
-
TargetModel(
|
|
64
|
-
text: "(${count})",
|
|
65
|
-
style: MyTextStyle.size16.w500.xFF6682FF,
|
|
66
|
-
),
|
|
67
|
-
],
|
|
68
|
-
)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### ARB 키 네이밍 규칙
|
|
72
|
-
|
|
73
|
-
```json
|
|
74
|
-
{
|
|
75
|
-
"cancel": "취소",
|
|
76
|
-
"confirm": "확인",
|
|
77
|
-
"doorOpen": "문 열기",
|
|
78
|
-
"networkError": "네트워크 오류가 발생했습니다"
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
- **camelCase** 사용
|
|
83
|
-
- 모든 arb 파일(en, ko, ja)에 **동일 키 동시 추가**
|
|
84
|
-
- 기존 키 검색 후 중복 방지 (`grep -rn '"cancel"' lib/l10n/`)
|
|
85
|
-
- 플레이스홀더 필요 시 ICU MessageFormat 사용
|
|
86
|
-
|
|
87
|
-
## 필수 원칙
|
|
88
|
-
|
|
89
|
-
- 영문 전용 표기 지시가 없는 한 **모든 사용자 노출 문자열은 다국어 처리**
|
|
90
|
-
- 하드코딩된 문자열 사용 금지 (`Text('취소')` ✗)
|
|
91
|
-
- 새 페이지 추가 시 관련 문자열 **일괄 등록** — 스프린트 중 누락 방지
|
|
92
|
-
- 모든 arb 파일의 키 집합은 동일해야 한다 (Evaluator가 diff 검사)
|
|
93
|
-
|
|
94
|
-
## Self-Check
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
# 하드코딩된 한글 탐지
|
|
98
|
-
grep -rn "Text('[가-힣]" lib/ui/
|
|
99
|
-
|
|
100
|
-
# 누락 키 탐지 (en 기준)
|
|
101
|
-
python3 -c "import json; en=set(json.load(open('lib/l10n/app_en.arb')).keys()); ko=set(json.load(open('lib/l10n/app_ko.arb')).keys()); print('missing in ko:', en - ko); print('missing in en:', ko - en)"
|
|
102
|
-
```
|