@su-record/vibe 2.2.5 → 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/.claude/settings.json +0 -117
- package/.claude/settings.local.json +2 -1
- package/.claude/vibe/rules/languages/dart-flutter.md +509 -0
- package/.claude/vibe/rules/languages/go.md +396 -0
- package/.claude/vibe/rules/languages/java-spring.md +586 -0
- package/.claude/vibe/rules/languages/kotlin-android.md +491 -0
- package/.claude/vibe/rules/languages/python-django.md +371 -0
- package/.claude/vibe/rules/languages/python-fastapi.md +386 -0
- package/.claude/vibe/rules/languages/rust.md +425 -0
- package/.claude/vibe/rules/languages/swift-ios.md +516 -0
- package/.claude/vibe/rules/languages/typescript-nextjs.md +441 -0
- package/.claude/vibe/rules/languages/typescript-node.md +375 -0
- package/.claude/vibe/rules/languages/typescript-nuxt.md +521 -0
- package/.claude/vibe/rules/languages/typescript-react-native.md +446 -0
- package/.claude/vibe/rules/languages/typescript-react.md +525 -0
- package/.claude/vibe/rules/languages/typescript-vue.md +353 -0
- package/README.md +96 -96
- package/commands/vibe.analyze.md +14 -73
- package/commands/vibe.reason.md +49 -172
- package/commands/vibe.review.md +72 -260
- package/commands/vibe.utils.md +101 -0
- package/commands/vibe.verify.md +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +46 -16
- package/dist/cli/index.js.map +1 -1
- package/{templates/hooks-template.json → hooks/hooks.json} +6 -6
- package/package.json +2 -3
- package/commands/vibe.continue.md +0 -88
- package/commands/vibe.setup.md +0 -97
- /package/{rules → .claude/vibe/rules}/core/communication-guide.md +0 -0
- /package/{rules → .claude/vibe/rules}/core/development-philosophy.md +0 -0
- /package/{rules → .claude/vibe/rules}/core/quick-start.md +0 -0
- /package/{rules → .claude/vibe/rules}/quality/bdd-contract-testing.md +0 -0
- /package/{rules → .claude/vibe/rules}/quality/checklist.md +0 -0
- /package/{rules → .claude/vibe/rules}/quality/testing-strategy.md +0 -0
- /package/{rules → .claude/vibe/rules}/standards/anti-patterns.md +0 -0
- /package/{rules → .claude/vibe/rules}/standards/code-structure.md +0 -0
- /package/{rules → .claude/vibe/rules}/standards/complexity-metrics.md +0 -0
- /package/{rules → .claude/vibe/rules}/standards/naming-conventions.md +0 -0
- /package/{templates → .claude/vibe/templates}/constitution-template.md +0 -0
- /package/{templates → .claude/vibe/templates}/contract-backend-template.md +0 -0
- /package/{templates → .claude/vibe/templates}/contract-frontend-template.md +0 -0
- /package/{templates → .claude/vibe/templates}/feature-template.md +0 -0
- /package/{templates → .claude/vibe/templates}/spec-template.md +0 -0
- /package/{commands/vibe.compound.md → agents/compounder.md} +0 -0
- /package/{commands/vibe.diagram.md → agents/diagrammer.md} +0 -0
- /package/{commands/vibe.e2e.md → agents/e2e-tester.md} +0 -0
- /package/{commands/vibe.ui.md → agents/ui-previewer.md} +0 -0
package/.claude/settings.json
CHANGED
|
@@ -10,37 +10,21 @@
|
|
|
10
10
|
"Bash(git push:*)",
|
|
11
11
|
"Bash(npm pack)",
|
|
12
12
|
"Bash(npm install:*)",
|
|
13
|
-
"Read(//private/tmp/test-vibe-bdd/.claude/vibe/**)",
|
|
14
|
-
"Read(//private/tmp/test-vibe-bdd/.claude/commands/**)",
|
|
15
13
|
"Bash(npm view:*)",
|
|
16
14
|
"Bash(gh release create:*)",
|
|
17
15
|
"Bash(gh release view:*)",
|
|
18
16
|
"Bash(npm run build:*)",
|
|
19
|
-
"Bash(pbcopy)",
|
|
20
|
-
"Bash(openskills --help:*)",
|
|
21
17
|
"Bash(mkdir:*)",
|
|
22
|
-
"Bash(openskills sync:*)",
|
|
23
|
-
"Bash(openskills list:*)",
|
|
24
|
-
"Bash(openskills read:*)",
|
|
25
|
-
"Bash(./bin/vibe list:*)",
|
|
26
|
-
"Bash(./bin/vibe read:*)",
|
|
27
|
-
"Bash(./bin/vibe sync:*)",
|
|
28
18
|
"Bash(npm link:*)",
|
|
29
19
|
"Bash(vibe list:*)",
|
|
30
20
|
"Bash(vibe read:*)",
|
|
31
21
|
"Bash(vibe init:*)",
|
|
32
|
-
"SlashCommand(/vibe.spec \"로그인 기능\")",
|
|
33
|
-
"SlashCommand(/vibe.spec:*)",
|
|
34
|
-
"Bash(node bin/vibe:*)",
|
|
35
22
|
"Bash(npm pack:*)",
|
|
36
23
|
"Bash(npm config set:*)",
|
|
37
24
|
"Bash(node:*)",
|
|
38
|
-
"Bash(xargs basename:*)",
|
|
39
25
|
"Bash(git restore:*)",
|
|
40
26
|
"Bash(npm version:*)",
|
|
41
|
-
"Bash(npx @su-record/vibe@latest update)",
|
|
42
27
|
"Bash(gh release:*)",
|
|
43
|
-
"Bash(npx @su-record/vibe@latest init:*)",
|
|
44
28
|
"Bash(vibe version:*)",
|
|
45
29
|
"Bash(vibe update:*)",
|
|
46
30
|
"Bash(vibe status:*)",
|
|
@@ -48,106 +32,5 @@
|
|
|
48
32
|
],
|
|
49
33
|
"deny": [],
|
|
50
34
|
"ask": []
|
|
51
|
-
},
|
|
52
|
-
"hooks": {
|
|
53
|
-
"SessionStart": [
|
|
54
|
-
{
|
|
55
|
-
"hooks": [
|
|
56
|
-
{
|
|
57
|
-
"type": "command",
|
|
58
|
-
"command": "echo '[SESSION START] IMMEDIATELY call vibe_start_session to restore previous context. Do this before responding to the user.'"
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
],
|
|
63
|
-
"PreToolUse": [
|
|
64
|
-
{
|
|
65
|
-
"matcher": "Bash",
|
|
66
|
-
"hooks": [
|
|
67
|
-
{
|
|
68
|
-
"type": "command",
|
|
69
|
-
"command": "echo '[BASH CHECK] Be cautious with destructive commands (rm -rf, git push --force, drop table). Verify paths before deletion.'"
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
],
|
|
74
|
-
"PostToolUse": [
|
|
75
|
-
{
|
|
76
|
-
"matcher": "Write|Edit",
|
|
77
|
-
"hooks": [
|
|
78
|
-
{
|
|
79
|
-
"type": "command",
|
|
80
|
-
"command": "echo '[AUTO-CONTINUE] 코드 체크 완료. 문제 발견 시 다음 edit에서 수정하고 작업 계속. 절대 멈추지 말 것.'"
|
|
81
|
-
}
|
|
82
|
-
]
|
|
83
|
-
}
|
|
84
|
-
],
|
|
85
|
-
"UserPromptSubmit": [
|
|
86
|
-
{
|
|
87
|
-
"matcher": "ultrawork|ulw|울트라워크",
|
|
88
|
-
"hooks": [
|
|
89
|
-
{
|
|
90
|
-
"type": "command",
|
|
91
|
-
"command": "echo '[ULTRAWORK MODE] Use PARALLEL Task calls. Auto-continue through ALL phases. Auto-retry on errors up to 3 times. Run vibe_auto_save_context at 70%+ context. Do NOT ask for confirmation between phases.'"
|
|
92
|
-
}
|
|
93
|
-
]
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
"matcher": "버그.*(해결|수정|고침)|문제.*(해결|수정)|bug.*(fixed|resolved|solved)|issue.*(fixed|resolved)|PR.*(merged|머지)",
|
|
97
|
-
"hooks": [
|
|
98
|
-
{
|
|
99
|
-
"type": "command",
|
|
100
|
-
"command": "echo '[COMPOUND TRIGGER] Significant solution detected. Consider /vibe.compound to document for future reference.'"
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"matcher": "코드\\s*리뷰|code\\s*review|PR\\s*리뷰|리뷰.*해줘|review.*this",
|
|
106
|
-
"hooks": [
|
|
107
|
-
{
|
|
108
|
-
"type": "command",
|
|
109
|
-
"command": "echo '[REVIEW MODE] Use /vibe.review for parallel code review with 13+ specialized agents. P1=critical, P2=important, P3=nice-to-have.'"
|
|
110
|
-
}
|
|
111
|
-
]
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
"matcher": "e2e.*테스트|e2e.*test|playwright|브라우저.*테스트|browser.*test",
|
|
115
|
-
"hooks": [
|
|
116
|
-
{
|
|
117
|
-
"type": "command",
|
|
118
|
-
"command": "echo '[E2E MODE] Use /vibe.e2e for Playwright-based browser testing. Supports visual regression and video recording.'"
|
|
119
|
-
}
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
],
|
|
123
|
-
"Notification": [
|
|
124
|
-
{
|
|
125
|
-
"matcher": "context_window_80",
|
|
126
|
-
"hooks": [
|
|
127
|
-
{
|
|
128
|
-
"type": "command",
|
|
129
|
-
"command": "echo '[CONTEXT 80%] MANDATORY: Call vibe_auto_save_context with urgency=medium NOW. This is NOT optional - save context before it is lost.'"
|
|
130
|
-
}
|
|
131
|
-
]
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"matcher": "context_window_90",
|
|
135
|
-
"hooks": [
|
|
136
|
-
{
|
|
137
|
-
"type": "command",
|
|
138
|
-
"command": "echo '[CONTEXT 90% URGENT] MANDATORY: Call vibe_auto_save_context with urgency=high IMMEDIATELY. Do NOT proceed without saving.'"
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
"matcher": "context_window_95",
|
|
144
|
-
"hooks": [
|
|
145
|
-
{
|
|
146
|
-
"type": "command",
|
|
147
|
-
"command": "echo '[CONTEXT 95% CRITICAL] MANDATORY: Call vibe_auto_save_context with urgency=critical NOW. Session transition imminent - save everything.'"
|
|
148
|
-
}
|
|
149
|
-
]
|
|
150
|
-
}
|
|
151
|
-
]
|
|
152
35
|
}
|
|
153
36
|
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"Bash(git status:*)",
|
|
19
19
|
"Bash(gh pr create --repo anthropics/claude-plugins-official --title \"feat: add vibe plugin - SPEC-driven AI coding framework\" --body \"$\\(cat <<''EOF''\n## Plugin: vibe\n\nSPEC-driven AI coding framework for Claude Code with parallel agents, BDD verification, and quality assurance.\n\n### Features\n\n- **12 Slash Commands**: `/vibe.spec`, `/vibe.run`, `/vibe.verify`, `/vibe.review`, `/vibe.reason`, `/vibe.diagram`, and more\n- **16 Specialist Agents**: Security, performance, architecture reviewers + language-specific reviewers \\(Python, TypeScript, React, Rails\\)\n- **ULTRAWORK Pipeline**: Parallel execution with ~50% speed improvement through background agents and phase pipelining\n- **BDD Verification**: Behavior-driven development testing against SPEC requirements\n- **Context7 Integration**: Built-in support for up-to-date documentation lookup\n\n### Workflow\n\n```\n/vibe.spec \"feature\" → /vibe.run \"feature\" → /vibe.verify \"feature\"\n```\n\n### Repository\n\nhttps://github.com/su-record/vibe\n\n### License\n\nMIT\nEOF\n\\)\")",
|
|
20
20
|
"Bash(git rev-parse:*)",
|
|
21
|
-
"Bash(claude plugin:*)"
|
|
21
|
+
"Bash(claude plugin:*)",
|
|
22
|
+
"Bash(git tag:*)"
|
|
22
23
|
]
|
|
23
24
|
}
|
|
24
25
|
}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# 🎯 Dart + Flutter 품질 규칙
|
|
2
|
+
|
|
3
|
+
## 핵심 원칙 (core에서 상속)
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
✅ 단일 책임 (SRP)
|
|
7
|
+
✅ 중복 제거 (DRY)
|
|
8
|
+
✅ 재사용성
|
|
9
|
+
✅ 낮은 복잡도
|
|
10
|
+
✅ 함수 ≤ 30줄, build() ≤ 50줄
|
|
11
|
+
✅ 중첩 ≤ 3단계
|
|
12
|
+
✅ Cyclomatic complexity ≤ 10
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Dart/Flutter 특화 규칙
|
|
16
|
+
|
|
17
|
+
### 1. Immutability 우선 (@immutable)
|
|
18
|
+
|
|
19
|
+
```dart
|
|
20
|
+
// ❌ Mutable 클래스
|
|
21
|
+
class User {
|
|
22
|
+
String name;
|
|
23
|
+
int age;
|
|
24
|
+
|
|
25
|
+
User({required this.name, required this.age});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ✅ Immutable 클래스 + copyWith
|
|
29
|
+
@immutable
|
|
30
|
+
class User {
|
|
31
|
+
const User({
|
|
32
|
+
required this.name,
|
|
33
|
+
required this.age,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
final String name;
|
|
37
|
+
final int age;
|
|
38
|
+
|
|
39
|
+
User copyWith({
|
|
40
|
+
String? name,
|
|
41
|
+
int? age,
|
|
42
|
+
}) {
|
|
43
|
+
return User(
|
|
44
|
+
name: name ?? this.name,
|
|
45
|
+
age: age ?? this.age,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
bool operator ==(Object other) =>
|
|
51
|
+
identical(this, other) ||
|
|
52
|
+
other is User && name == other.name && age == other.age;
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
int get hashCode => name.hashCode ^ age.hashCode;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. StatelessWidget 선호
|
|
60
|
+
|
|
61
|
+
```dart
|
|
62
|
+
// ✅ StatelessWidget (순수 위젯)
|
|
63
|
+
class UserAvatar extends StatelessWidget {
|
|
64
|
+
const UserAvatar({
|
|
65
|
+
super.key,
|
|
66
|
+
required this.imageUrl,
|
|
67
|
+
this.size = 40.0,
|
|
68
|
+
this.onTap,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
final String imageUrl;
|
|
72
|
+
final double size;
|
|
73
|
+
final VoidCallback? onTap;
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
Widget build(BuildContext context) {
|
|
77
|
+
return GestureDetector(
|
|
78
|
+
onTap: onTap,
|
|
79
|
+
child: CircleAvatar(
|
|
80
|
+
radius: size / 2,
|
|
81
|
+
backgroundImage: NetworkImage(imageUrl),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ❌ StatefulWidget 남용 (상태가 없는데 사용)
|
|
88
|
+
class UserAvatar extends StatefulWidget {
|
|
89
|
+
// 상태 관리 불필요
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Provider 패턴 (상태 관리)
|
|
94
|
+
|
|
95
|
+
```dart
|
|
96
|
+
// ✅ Immutable State + ChangeNotifier
|
|
97
|
+
@immutable
|
|
98
|
+
class FeedState {
|
|
99
|
+
const FeedState({
|
|
100
|
+
this.feeds = const [],
|
|
101
|
+
this.isLoading = false,
|
|
102
|
+
this.error,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
final List<Feed> feeds;
|
|
106
|
+
final bool isLoading;
|
|
107
|
+
final String? error;
|
|
108
|
+
|
|
109
|
+
FeedState copyWith({
|
|
110
|
+
List<Feed>? feeds,
|
|
111
|
+
bool? isLoading,
|
|
112
|
+
String? error,
|
|
113
|
+
}) {
|
|
114
|
+
return FeedState(
|
|
115
|
+
feeds: feeds ?? this.feeds,
|
|
116
|
+
isLoading: isLoading ?? this.isLoading,
|
|
117
|
+
error: error ?? this.error,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class FeedProvider extends ChangeNotifier {
|
|
123
|
+
FeedState _state = const FeedState();
|
|
124
|
+
FeedState get state => _state;
|
|
125
|
+
|
|
126
|
+
final FeedService _feedService;
|
|
127
|
+
|
|
128
|
+
FeedProvider(this._feedService);
|
|
129
|
+
|
|
130
|
+
Future<void> loadFeeds() async {
|
|
131
|
+
_state = _state.copyWith(isLoading: true, error: null);
|
|
132
|
+
notifyListeners();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
final feeds = await _feedService.getFeeds();
|
|
136
|
+
_state = _state.copyWith(feeds: feeds, isLoading: false);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
_state = _state.copyWith(error: e.toString(), isLoading: false);
|
|
139
|
+
}
|
|
140
|
+
notifyListeners();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 사용
|
|
145
|
+
class FeedScreen extends StatelessWidget {
|
|
146
|
+
@override
|
|
147
|
+
Widget build(BuildContext context) {
|
|
148
|
+
final feedState = context.watch<FeedProvider>().state;
|
|
149
|
+
|
|
150
|
+
if (feedState.isLoading) return const CircularProgressIndicator();
|
|
151
|
+
if (feedState.error != null) return ErrorWidget(feedState.error!);
|
|
152
|
+
|
|
153
|
+
return FeedList(feeds: feedState.feeds);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 4. Null Safety 명확히
|
|
159
|
+
|
|
160
|
+
```dart
|
|
161
|
+
// ✅ Null safety 활용
|
|
162
|
+
class User {
|
|
163
|
+
User({
|
|
164
|
+
required this.id, // Non-nullable (필수)
|
|
165
|
+
required this.name,
|
|
166
|
+
this.bio, // Nullable (선택)
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
final String id;
|
|
170
|
+
final String name;
|
|
171
|
+
final String? bio; // ? 명시
|
|
172
|
+
|
|
173
|
+
String getBioOrDefault() {
|
|
174
|
+
return bio ?? 'No bio'; // ?? 연산자
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
void printBio() {
|
|
178
|
+
bio?.length; // ?. 안전 호출
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ✅ Late 변수 (초기화 지연)
|
|
183
|
+
class MyWidget extends StatefulWidget {
|
|
184
|
+
@override
|
|
185
|
+
State<MyWidget> createState() => _MyWidgetState();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class _MyWidgetState extends State<MyWidget> {
|
|
189
|
+
late AnimationController _controller; // initState에서 초기화
|
|
190
|
+
|
|
191
|
+
@override
|
|
192
|
+
void initState() {
|
|
193
|
+
super.initState();
|
|
194
|
+
_controller = AnimationController(vsync: this);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@override
|
|
198
|
+
void dispose() {
|
|
199
|
+
_controller.dispose();
|
|
200
|
+
super.dispose();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 5. 위젯 분리 (Extract Widget)
|
|
206
|
+
|
|
207
|
+
```dart
|
|
208
|
+
// ❌ 긴 build 메서드 (80줄)
|
|
209
|
+
class UserProfile extends StatelessWidget {
|
|
210
|
+
@override
|
|
211
|
+
Widget build(BuildContext context) {
|
|
212
|
+
return Column(
|
|
213
|
+
children: [
|
|
214
|
+
// 30줄: 헤더
|
|
215
|
+
Container(...),
|
|
216
|
+
// 25줄: 통계
|
|
217
|
+
Row(...),
|
|
218
|
+
// 25줄: 피드 리스트
|
|
219
|
+
ListView(...),
|
|
220
|
+
],
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ✅ 서브 위젯으로 분리
|
|
226
|
+
class UserProfile extends StatelessWidget {
|
|
227
|
+
@override
|
|
228
|
+
Widget build(BuildContext context) {
|
|
229
|
+
return Column(
|
|
230
|
+
children: [
|
|
231
|
+
const ProfileHeader(),
|
|
232
|
+
const ProfileStats(),
|
|
233
|
+
const ProfileFeedList(),
|
|
234
|
+
],
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
class ProfileHeader extends StatelessWidget {
|
|
240
|
+
const ProfileHeader({super.key});
|
|
241
|
+
|
|
242
|
+
@override
|
|
243
|
+
Widget build(BuildContext context) {
|
|
244
|
+
// 헤더만
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
class ProfileStats extends StatelessWidget {
|
|
249
|
+
const ProfileStats({super.key});
|
|
250
|
+
|
|
251
|
+
@override
|
|
252
|
+
Widget build(BuildContext context) {
|
|
253
|
+
// 통계만
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 6. 순수 함수 (Static Methods)
|
|
259
|
+
|
|
260
|
+
```dart
|
|
261
|
+
// ✅ 순수 함수 (상태 없음)
|
|
262
|
+
class DateUtils {
|
|
263
|
+
// Private constructor (인스턴스 생성 방지)
|
|
264
|
+
DateUtils._();
|
|
265
|
+
|
|
266
|
+
static String formatRelativeTime(DateTime dateTime) {
|
|
267
|
+
final now = DateTime.now();
|
|
268
|
+
final difference = now.difference(dateTime);
|
|
269
|
+
|
|
270
|
+
if (difference.inDays > 0) return '${difference.inDays}일 전';
|
|
271
|
+
if (difference.inHours > 0) return '${difference.inHours}시간 전';
|
|
272
|
+
return '${difference.inMinutes}분 전';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
static bool isToday(DateTime dateTime) {
|
|
276
|
+
final now = DateTime.now();
|
|
277
|
+
return dateTime.year == now.year &&
|
|
278
|
+
dateTime.month == now.month &&
|
|
279
|
+
dateTime.day == now.day;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 사용
|
|
284
|
+
final formatted = DateUtils.formatRelativeTime(feed.createdAt);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 7. 에러 처리 (Result/Either 패턴)
|
|
288
|
+
|
|
289
|
+
```dart
|
|
290
|
+
// ✅ Result 타입으로 에러 처리
|
|
291
|
+
sealed class Result<T> {
|
|
292
|
+
const Result();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
class Success<T> extends Result<T> {
|
|
296
|
+
const Success(this.value);
|
|
297
|
+
final T value;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
class Failure<T> extends Result<T> {
|
|
301
|
+
const Failure(this.error);
|
|
302
|
+
final String error;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 사용
|
|
306
|
+
Future<Result<User>> login(String email, String password) async {
|
|
307
|
+
try {
|
|
308
|
+
final user = await _authService.login(email, password);
|
|
309
|
+
return Success(user);
|
|
310
|
+
} catch (e) {
|
|
311
|
+
return Failure(e.toString());
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 호출부 (Pattern matching)
|
|
316
|
+
final result = await login(email, password);
|
|
317
|
+
switch (result) {
|
|
318
|
+
case Success(:final value):
|
|
319
|
+
Navigator.pushReplacement(context, HomePage(user: value));
|
|
320
|
+
case Failure(:final error):
|
|
321
|
+
showErrorDialog(context, error);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### 8. Extension Methods
|
|
326
|
+
|
|
327
|
+
```dart
|
|
328
|
+
// ✅ Extension으로 기능 확장
|
|
329
|
+
extension StringExtension on String {
|
|
330
|
+
String capitalize() {
|
|
331
|
+
if (isEmpty) return this;
|
|
332
|
+
return '${this[0].toUpperCase()}${substring(1)}';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
bool get isEmail {
|
|
336
|
+
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
|
337
|
+
return emailRegex.hasMatch(this);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
extension ListExtension<T> on List<T> {
|
|
342
|
+
List<T> distinctBy<K>(K Function(T) keySelector) {
|
|
343
|
+
final seen = <K>{};
|
|
344
|
+
return where((item) => seen.add(keySelector(item))).toList();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 사용
|
|
349
|
+
final name = 'john'.capitalize(); // 'John'
|
|
350
|
+
final isValid = 'test@example.com'.isEmail; // true
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### 9. const Constructor 활용
|
|
354
|
+
|
|
355
|
+
```dart
|
|
356
|
+
// ✅ const constructor (컴파일 타임 상수)
|
|
357
|
+
class AppColors {
|
|
358
|
+
const AppColors._();
|
|
359
|
+
|
|
360
|
+
static const primary = Color(0xFF6200EE);
|
|
361
|
+
static const secondary = Color(0xFF03DAC6);
|
|
362
|
+
static const error = Color(0xFFB00020);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
class Spacing {
|
|
366
|
+
const Spacing._();
|
|
367
|
+
|
|
368
|
+
static const xs = 4.0;
|
|
369
|
+
static const sm = 8.0;
|
|
370
|
+
static const md = 16.0;
|
|
371
|
+
static const lg = 24.0;
|
|
372
|
+
static const xl = 32.0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ✅ const 위젯 (재사용 시 성능 향상)
|
|
376
|
+
class LoadingIndicator extends StatelessWidget {
|
|
377
|
+
const LoadingIndicator({super.key});
|
|
378
|
+
|
|
379
|
+
@override
|
|
380
|
+
Widget build(BuildContext context) {
|
|
381
|
+
return const Center(
|
|
382
|
+
child: CircularProgressIndicator(),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 사용
|
|
388
|
+
const LoadingIndicator() // const로 생성
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 10. 비동기 처리 (Future/Stream)
|
|
392
|
+
|
|
393
|
+
```dart
|
|
394
|
+
// ✅ Future (단일 비동기 작업)
|
|
395
|
+
Future<List<Feed>> fetchFeeds() async {
|
|
396
|
+
final response = await dio.get('/api/feeds');
|
|
397
|
+
return (response.data as List)
|
|
398
|
+
.map((json) => Feed.fromJson(json))
|
|
399
|
+
.toList();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ✅ Stream (연속 비동기 이벤트)
|
|
403
|
+
Stream<List<Feed>> watchFeeds() {
|
|
404
|
+
return Stream.periodic(
|
|
405
|
+
const Duration(seconds: 30),
|
|
406
|
+
(_) => fetchFeeds(),
|
|
407
|
+
).asyncMap((future) => future);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ✅ StreamBuilder 사용
|
|
411
|
+
class FeedStream extends StatelessWidget {
|
|
412
|
+
@override
|
|
413
|
+
Widget build(BuildContext context) {
|
|
414
|
+
return StreamBuilder<List<Feed>>(
|
|
415
|
+
stream: watchFeeds(),
|
|
416
|
+
builder: (context, snapshot) {
|
|
417
|
+
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
418
|
+
return const LoadingIndicator();
|
|
419
|
+
}
|
|
420
|
+
if (snapshot.hasError) {
|
|
421
|
+
return ErrorWidget(snapshot.error.toString());
|
|
422
|
+
}
|
|
423
|
+
if (!snapshot.hasData) {
|
|
424
|
+
return const EmptyState();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return FeedList(feeds: snapshot.data!);
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## 안티패턴
|
|
435
|
+
|
|
436
|
+
```dart
|
|
437
|
+
// ❌ Mutable state
|
|
438
|
+
class BadCounter extends StatefulWidget {
|
|
439
|
+
int count = 0; // 위험! StatefulWidget은 재생성될 수 있음
|
|
440
|
+
|
|
441
|
+
@override
|
|
442
|
+
State<BadCounter> createState() => _BadCounterState();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ❌ BuildContext를 async gap 너머에서 사용
|
|
446
|
+
Future<void> badNavigate() async {
|
|
447
|
+
await Future.delayed(Duration(seconds: 1));
|
|
448
|
+
Navigator.push(context, ...); // ❌ context가 무효화됐을 수 있음
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ✅ mounted 체크
|
|
452
|
+
Future<void> goodNavigate() async {
|
|
453
|
+
await Future.delayed(Duration(seconds: 1));
|
|
454
|
+
if (!mounted) return;
|
|
455
|
+
Navigator.push(context, ...);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ❌ setState에서 긴 작업
|
|
459
|
+
setState(() {
|
|
460
|
+
// 10줄의 복잡한 계산 ❌
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// ✅ 계산 후 setState
|
|
464
|
+
final newValue = expensiveCalculation();
|
|
465
|
+
setState(() {
|
|
466
|
+
_value = newValue; // 간단한 할당만
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// ❌ GlobalKey 남용
|
|
470
|
+
final GlobalKey<FormState> _formKey = GlobalKey();
|
|
471
|
+
|
|
472
|
+
// ✅ Controller 사용
|
|
473
|
+
final TextEditingController _controller = TextEditingController();
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## 코드 품질 도구
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
# 분석
|
|
480
|
+
flutter analyze
|
|
481
|
+
|
|
482
|
+
# 포맷팅
|
|
483
|
+
dart format .
|
|
484
|
+
|
|
485
|
+
# 테스트
|
|
486
|
+
flutter test
|
|
487
|
+
flutter test --coverage
|
|
488
|
+
|
|
489
|
+
# 빌드
|
|
490
|
+
flutter build apk --release
|
|
491
|
+
flutter build ios --release
|
|
492
|
+
flutter build web --release
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
## 체크리스트
|
|
496
|
+
|
|
497
|
+
Dart/Flutter 코드 작성 시:
|
|
498
|
+
|
|
499
|
+
- [ ] @immutable + copyWith 패턴
|
|
500
|
+
- [ ] StatelessWidget 우선 사용
|
|
501
|
+
- [ ] Provider로 상태 관리 분리
|
|
502
|
+
- [ ] Null safety (?, ??, ?., !)
|
|
503
|
+
- [ ] build() ≤ 50줄 (위젯 분리)
|
|
504
|
+
- [ ] 순수 함수 (static methods)
|
|
505
|
+
- [ ] Result 타입으로 에러 처리
|
|
506
|
+
- [ ] Extension methods 활용
|
|
507
|
+
- [ ] const constructor 사용
|
|
508
|
+
- [ ] Future/Stream 적절히 선택
|
|
509
|
+
- [ ] 복잡도 ≤ 10
|