@gaonjs/cli 0.15.0 → 0.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/g.js +8 -0
- package/dist/commands/new.js +5 -0
- package/dist/dev/health.d.ts +4 -1
- package/dist/dev/health.js +5 -3
- package/dist/doctor/method-override.d.ts +5 -0
- package/dist/doctor/method-override.js +75 -0
- package/dist/doctor/route-registration.d.ts +5 -0
- package/dist/doctor/route-registration.js +77 -0
- package/dist/doctor/static-collision.d.ts +5 -0
- package/dist/doctor/static-collision.js +84 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor/types.js +3 -3
- package/dist/doctor/ui-kit-wiring.d.ts +5 -0
- package/dist/doctor/ui-kit-wiring.js +93 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +19 -2
- package/dist/generate.js +8 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +19 -5
- package/dist/scaffold/app-wiring.d.ts +12 -0
- package/dist/scaffold/app-wiring.js +68 -0
- package/dist/serve.d.ts +10 -0
- package/dist/serve.js +90 -2
- package/dist/templates/auth/Dashboard.vue.tpl +21 -5
- package/dist/templates/auth/Login.vue.tpl +37 -8
- package/dist/templates/auth/Signup.vue.tpl +40 -9
- package/dist/templates/project/.dockerignore.tpl +12 -0
- package/dist/templates/project/AGENTS.md.tpl +10 -5
- package/dist/templates/project/CLAUDE.md.tpl +4 -3
- package/dist/templates/project/Dockerfile.tpl +30 -0
- package/dist/templates/project/agents/async.md.tpl +19 -7
- package/dist/templates/project/agents/data.md.tpl +37 -15
- package/dist/templates/project/agents/frontend.md.tpl +81 -4
- package/dist/templates/project/agents/realtime.md.tpl +24 -17
- package/dist/templates/project/agents/security.md.tpl +11 -2
- package/dist/templates/project/agents/web.md.tpl +5 -0
- package/dist/templates/project/apps/web/composables/useGaonHealth.ts.tpl +2 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +19 -94
- package/dist/templates/project/apps/web/main.ts.tpl +4 -0
- package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +68 -251
- package/dist/templates/project/apps/web/static/robots.txt.tpl +4 -0
- package/dist/templates/project/apps/web/style.css.tpl +66 -0
- package/dist/templates/project/compose.prod.yaml.tpl +98 -0
- package/dist/templates/project/package.json.tpl +4 -0
- package/dist/templates/project/postcss.config.js.tpl +15 -0
- package/dist/templates/project/tailwind.config.ts.tpl +56 -0
- package/dist/templates/ui-kit/Alert.vue.tpl +23 -0
- package/dist/templates/ui-kit/AlertDescription.vue.tpl +9 -0
- package/dist/templates/ui-kit/AlertTitle.vue.tpl +9 -0
- package/dist/templates/ui-kit/Badge.vue.tpl +25 -0
- package/dist/templates/ui-kit/Button.vue.tpl +39 -0
- package/dist/templates/ui-kit/Card.vue.tpl +10 -0
- package/dist/templates/ui-kit/CardContent.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardDescription.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardFooter.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardHeader.vue.tpl +9 -0
- package/dist/templates/ui-kit/CardTitle.vue.tpl +9 -0
- package/dist/templates/ui-kit/Dialog.vue.tpl +68 -0
- package/dist/templates/ui-kit/Form.vue.tpl +13 -0
- package/dist/templates/ui-kit/FormField.vue.tpl +16 -0
- package/dist/templates/ui-kit/FormMessage.vue.tpl +9 -0
- package/dist/templates/ui-kit/Input.vue.tpl +22 -0
- package/dist/templates/ui-kit/Label.vue.tpl +9 -0
- package/dist/templates/ui-kit/Sheet.vue.tpl +73 -0
- package/dist/templates/ui-kit/utils.ts.tpl +26 -0
- package/dist/uikit.d.ts +28 -0
- package/dist/uikit.js +138 -0
- package/dist/work.js +2 -0
- package/package.json +6 -6
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { pageProps } from 'gaonjs/vue'
|
|
4
4
|
import { useGaonHealth, type HealthDoctor } from '../../composables/useGaonHealth.js'
|
|
5
|
+
import Card from '../../components/ui/Card.vue'
|
|
6
|
+
import Badge from '../../components/ui/Badge.vue'
|
|
5
7
|
|
|
6
8
|
// home#index 의 render props — Serialized<> 로 넘어온다(§6.2).
|
|
7
9
|
// 라우트 키는 .gaon/routes.d.ts 가 유효한 값을 알려준다.
|
|
@@ -11,7 +13,7 @@ const props = pageProps<'web:home#index'>()
|
|
|
11
13
|
const { health, loading, available } = useGaonHealth()
|
|
12
14
|
|
|
13
15
|
type CardStatus = 'ok' | 'warn' | 'error' | 'idle'
|
|
14
|
-
interface
|
|
16
|
+
interface StatusCard {
|
|
15
17
|
readonly key: string
|
|
16
18
|
readonly title: string
|
|
17
19
|
readonly status: CardStatus
|
|
@@ -25,14 +27,14 @@ function doctorNote(d: HealthDoctor | undefined): string {
|
|
|
25
27
|
return `${d.passed} 통과 · ${d.warnings} 경고 · ${d.errors} 오류`
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
const cards = computed<
|
|
30
|
+
const cards = computed<StatusCard[]>(() => {
|
|
29
31
|
const h = health.value
|
|
30
32
|
const web = h?.web
|
|
31
33
|
const db = h?.database
|
|
32
34
|
const hub = h?.hub
|
|
33
35
|
const doc = h?.doctor
|
|
34
36
|
|
|
35
|
-
const webCard:
|
|
37
|
+
const webCard: StatusCard = {
|
|
36
38
|
key: 'web',
|
|
37
39
|
title: 'WEB',
|
|
38
40
|
status: available.value ? 'ok' : 'idle',
|
|
@@ -42,7 +44,7 @@ const cards = computed<Card[]>(() => {
|
|
|
42
44
|
: ['개발 서버에서만'],
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
const dbCard:
|
|
47
|
+
const dbCard: StatusCard = {
|
|
46
48
|
key: 'database',
|
|
47
49
|
title: 'DATABASE',
|
|
48
50
|
status: !db || !db.configured ? 'idle' : db.connected ? 'ok' : 'error',
|
|
@@ -55,7 +57,7 @@ const cards = computed<Card[]>(() => {
|
|
|
55
57
|
: ['.env 에 DATABASE_URL', 'gaon.config.ts 에서 켜기'],
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
const hubCard:
|
|
60
|
+
const hubCard: StatusCard = {
|
|
59
61
|
key: 'hub',
|
|
60
62
|
title: 'HUB',
|
|
61
63
|
status: !hub || !hub.configured ? 'idle' : hub.connected ? 'ok' : 'error',
|
|
@@ -63,12 +65,16 @@ const cards = computed<Card[]>(() => {
|
|
|
63
65
|
lines:
|
|
64
66
|
hub && hub.configured
|
|
65
67
|
? hub.connected
|
|
66
|
-
?
|
|
68
|
+
? [
|
|
69
|
+
// jobs = 스트림 대기 수 · channels = 프레즌스 실 접속자 수(결정 71).
|
|
70
|
+
...(hub.streams ?? []).map((s) => `${s.stream}: ${s.waiting === null ? '대기 없음' : `${s.waiting} 대기`}`),
|
|
71
|
+
`채널 접속 ${(hub.channels ?? []).reduce((n, c) => n + c.members, 0)}명`,
|
|
72
|
+
]
|
|
67
73
|
: [hub.error ?? 'NATS 미연결']
|
|
68
74
|
: ['.env 에 NATS_URL', '실시간·비동기 백본'],
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
const doctorCard:
|
|
77
|
+
const doctorCard: StatusCard = {
|
|
72
78
|
key: 'doctor',
|
|
73
79
|
title: 'DOCTOR',
|
|
74
80
|
status: !doc ? 'idle' : doc.level === 'pass' ? 'ok' : doc.level === 'warn' ? 'warn' : 'error',
|
|
@@ -87,6 +93,14 @@ const cards = computed<Card[]>(() => {
|
|
|
87
93
|
return [webCard, dbCard, hubCard, doctorCard]
|
|
88
94
|
})
|
|
89
95
|
|
|
96
|
+
// 상태 → 점 색(Tailwind). idle 은 은은하게, 나머지는 신호색.
|
|
97
|
+
const DOT: Record<CardStatus, string> = {
|
|
98
|
+
ok: 'bg-green-500',
|
|
99
|
+
warn: 'bg-yellow-500',
|
|
100
|
+
error: 'bg-red-500',
|
|
101
|
+
idle: 'bg-muted-foreground/40',
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
// 다음 단계 — 라이브 상태에서 완료 여부를 파생한다.
|
|
91
105
|
interface Step {
|
|
92
106
|
readonly label: string
|
|
@@ -155,265 +169,68 @@ const routeLines = computed<CodeLine[]>(() => {
|
|
|
155
169
|
</script>
|
|
156
170
|
|
|
157
171
|
<template>
|
|
158
|
-
<div class="
|
|
159
|
-
<section class="
|
|
160
|
-
<p class="
|
|
161
|
-
<h1>가온에 올라탔습니다.</h1>
|
|
162
|
-
<p class="
|
|
163
|
-
이 화면은 <code>apps/web/pages/Home/Index.vue</code> 입니다.
|
|
164
|
-
<code>routes.ts</code> 의 <code>r.get('/', 'home#index')</code> 가 여기로 연결했습니다.
|
|
172
|
+
<div class="mx-auto max-w-[880px] px-5 pb-16 pt-12 leading-relaxed">
|
|
173
|
+
<section class="mb-10">
|
|
174
|
+
<p class="mb-1.5 text-xs font-semibold uppercase tracking-[0.08em] text-[#7c5cff]">{{ props.title }}</p>
|
|
175
|
+
<h1 class="mb-3.5 text-[clamp(2rem,5vw,2.9rem)] font-bold leading-[1.15] tracking-tight">가온에 올라탔습니다.</h1>
|
|
176
|
+
<p class="my-1.5 text-[1.02rem] text-foreground/80">
|
|
177
|
+
이 화면은 <code class="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.86em]">apps/web/pages/Home/Index.vue</code> 입니다.
|
|
178
|
+
<code class="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.86em]">routes.ts</code> 의 <code class="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.86em]">r.get('/', 'home#index')</code> 가 여기로 연결했습니다.
|
|
165
179
|
</p>
|
|
166
|
-
<p class="
|
|
167
|
-
아래 카드는 지금 이 서버의 <strong>실제 상태</strong>입니다 —
|
|
168
|
-
하드코딩이 아니라 <code>/_gaon/health</code> 를 읽어 그립니다.
|
|
169
|
-
<a :href="props.docs" target="_blank" rel="noreferrer">{{ props.docs }}</a>
|
|
180
|
+
<p class="my-1.5 text-[0.95rem] text-muted-foreground">
|
|
181
|
+
아래 카드는 지금 이 서버의 <strong class="font-semibold text-foreground">실제 상태</strong>입니다 —
|
|
182
|
+
하드코딩이 아니라 <code class="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.86em]">/_gaon/health</code> 를 읽어 그립니다.
|
|
183
|
+
<a :href="props.docs" target="_blank" rel="noreferrer" class="text-[#4f8cff] hover:underline">{{ props.docs }}</a>
|
|
170
184
|
</p>
|
|
171
185
|
</section>
|
|
172
186
|
|
|
173
|
-
<section class="
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
<span class="
|
|
177
|
-
<span class="
|
|
178
|
-
</
|
|
179
|
-
<p class="
|
|
180
|
-
<ul class="
|
|
181
|
-
<li v-for="(ln, i) in c.lines" :key="i">{{ ln }}</li>
|
|
187
|
+
<section class="mb-4 grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-4" aria-label="프레임웍 상태">
|
|
188
|
+
<Card v-for="c in cards" :key="c.key" class="card p-[1.15rem]">
|
|
189
|
+
<div class="mb-2.5 flex items-center gap-2">
|
|
190
|
+
<span class="h-2.5 w-2.5 rounded-full" :class="DOT[c.status]" aria-hidden="true"></span>
|
|
191
|
+
<span class="text-[0.74rem] font-bold tracking-[0.08em] text-muted-foreground">{{ c.title }}</span>
|
|
192
|
+
</div>
|
|
193
|
+
<p class="mb-2 text-[1.35rem] font-bold tabular-nums tracking-tight">{{ c.headline }}</p>
|
|
194
|
+
<ul class="m-0 list-none p-0 text-[0.82rem] text-muted-foreground">
|
|
195
|
+
<li v-for="(ln, i) in c.lines" :key="i" class="break-words py-[0.08rem]">{{ ln }}</li>
|
|
182
196
|
</ul>
|
|
183
|
-
</
|
|
197
|
+
</Card>
|
|
184
198
|
</section>
|
|
185
199
|
|
|
186
|
-
<p v-if="loading" class="
|
|
187
|
-
<p v-else-if="!available" class="
|
|
188
|
-
라이브 상태는 <code>gaon dev</code> 개발 서버에서만 보입니다(운영 빌드엔 진단 엔드포인트가 없습니다).
|
|
200
|
+
<p v-if="loading" class="my-1 mb-6 text-sm text-muted-foreground">상태 확인 중…</p>
|
|
201
|
+
<p v-else-if="!available" class="my-1 mb-6 text-sm text-muted-foreground">
|
|
202
|
+
라이브 상태는 <code class="rounded bg-muted px-1.5 py-0.5 font-mono text-[0.86em]">gaon dev</code> 개발 서버에서만 보입니다(운영 빌드엔 진단 엔드포인트가 없습니다).
|
|
189
203
|
</p>
|
|
190
204
|
|
|
191
|
-
<section class="
|
|
192
|
-
<h2>다음 단계</h2>
|
|
193
|
-
<ul class="
|
|
194
|
-
<li
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
205
|
+
<section class="my-10">
|
|
206
|
+
<h2 class="mb-4 text-[1.15rem] font-semibold">다음 단계</h2>
|
|
207
|
+
<ul class="flex list-none flex-col gap-1.5 p-0">
|
|
208
|
+
<li
|
|
209
|
+
v-for="s in steps"
|
|
210
|
+
:key="s.label"
|
|
211
|
+
class="flex items-start gap-3 rounded-[10px] border px-3.5 py-2.5"
|
|
212
|
+
:class="s.done ? 'border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950/30' : 'border-border bg-muted/30'"
|
|
213
|
+
>
|
|
214
|
+
<span class="font-bold leading-6" :class="s.done ? 'text-green-600' : 'text-muted-foreground/60'" aria-hidden="true">{{ s.done ? '✓' : '○' }}</span>
|
|
215
|
+
<div class="min-w-0 flex-1">
|
|
216
|
+
<div class="flex flex-wrap items-center gap-2.5">
|
|
217
|
+
<span class="font-semibold">{{ s.label }}</span>
|
|
218
|
+
<code class="rounded bg-[#0d1117] px-1.5 py-0.5 font-mono text-[0.86em] text-[#e6edf3]">{{ s.cmd }}</code>
|
|
200
219
|
</div>
|
|
201
|
-
<span class="
|
|
220
|
+
<span class="mt-0.5 block text-xs text-muted-foreground">{{ s.note }}</span>
|
|
202
221
|
</div>
|
|
203
222
|
</li>
|
|
204
223
|
</ul>
|
|
205
224
|
</section>
|
|
206
225
|
|
|
207
|
-
<section v-if="routeLines.length" class="
|
|
208
|
-
<div class="
|
|
209
|
-
<pre class="
|
|
226
|
+
<section v-if="routeLines.length" class="overflow-hidden rounded-xl border">
|
|
227
|
+
<div class="border-b bg-muted px-4 py-2 font-mono text-xs text-muted-foreground">{{ routesPath }}</div>
|
|
228
|
+
<pre class="m-0 overflow-x-auto bg-[#0d1117] px-[1.15rem] py-4 text-[0.82rem] leading-[1.65] text-[#c9d1d9]"><code><span v-for="(ln, i) in routeLines" :key="i" class="block"><span>{{ ln.code }}</span><span class="text-[#8b949e]">{{ ln.comment }}</span>{{ '\n' }}</span></code></pre>
|
|
210
229
|
</section>
|
|
230
|
+
|
|
231
|
+
<p class="mt-8 text-center text-xs text-muted-foreground">
|
|
232
|
+
<Badge variant="secondary">UI 킷</Badge>
|
|
233
|
+
이 화면·카드·다음 단계는 gaon g ui-kit 로 심은 컴포넌트로 그렸습니다 — apps/web/components/ui/ 에서 소유·수정하세요.
|
|
234
|
+
</p>
|
|
211
235
|
</div>
|
|
212
236
|
</template>
|
|
213
|
-
|
|
214
|
-
<style scoped>
|
|
215
|
-
.landing {
|
|
216
|
-
max-width: 880px;
|
|
217
|
-
margin: 0 auto;
|
|
218
|
-
padding: 3rem 1.25rem 4rem;
|
|
219
|
-
color: #1b1f24;
|
|
220
|
-
line-height: 1.6;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.hero {
|
|
224
|
-
margin-bottom: 2.5rem;
|
|
225
|
-
}
|
|
226
|
-
.eyebrow {
|
|
227
|
-
margin: 0 0 0.4rem;
|
|
228
|
-
font-size: 0.8rem;
|
|
229
|
-
font-weight: 600;
|
|
230
|
-
letter-spacing: 0.08em;
|
|
231
|
-
text-transform: uppercase;
|
|
232
|
-
color: #7c5cff;
|
|
233
|
-
}
|
|
234
|
-
.hero h1 {
|
|
235
|
-
margin: 0 0 0.9rem;
|
|
236
|
-
font-size: clamp(2rem, 5vw, 2.9rem);
|
|
237
|
-
line-height: 1.15;
|
|
238
|
-
letter-spacing: -0.02em;
|
|
239
|
-
}
|
|
240
|
-
.lede {
|
|
241
|
-
margin: 0.35rem 0;
|
|
242
|
-
color: #3a424c;
|
|
243
|
-
font-size: 1.02rem;
|
|
244
|
-
}
|
|
245
|
-
.lede.sub {
|
|
246
|
-
color: #57606a;
|
|
247
|
-
font-size: 0.95rem;
|
|
248
|
-
}
|
|
249
|
-
.lede a {
|
|
250
|
-
color: #4f8cff;
|
|
251
|
-
}
|
|
252
|
-
code {
|
|
253
|
-
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
|
254
|
-
font-size: 0.86em;
|
|
255
|
-
background: #f2f4f7;
|
|
256
|
-
padding: 0.1rem 0.35rem;
|
|
257
|
-
border-radius: 5px;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.cards {
|
|
261
|
-
display: grid;
|
|
262
|
-
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
263
|
-
gap: 1rem;
|
|
264
|
-
margin-bottom: 1rem;
|
|
265
|
-
}
|
|
266
|
-
.card {
|
|
267
|
-
border: 1px solid #e4e7eb;
|
|
268
|
-
border-radius: 12px;
|
|
269
|
-
padding: 1.1rem 1.15rem;
|
|
270
|
-
background: #fff;
|
|
271
|
-
box-shadow: 0 1px 2px rgba(16, 22, 26, 0.04);
|
|
272
|
-
}
|
|
273
|
-
.card-top {
|
|
274
|
-
display: flex;
|
|
275
|
-
align-items: center;
|
|
276
|
-
gap: 0.5rem;
|
|
277
|
-
margin-bottom: 0.6rem;
|
|
278
|
-
}
|
|
279
|
-
.card-title {
|
|
280
|
-
font-size: 0.74rem;
|
|
281
|
-
font-weight: 700;
|
|
282
|
-
letter-spacing: 0.08em;
|
|
283
|
-
color: #6a737d;
|
|
284
|
-
}
|
|
285
|
-
.dot {
|
|
286
|
-
width: 9px;
|
|
287
|
-
height: 9px;
|
|
288
|
-
border-radius: 50%;
|
|
289
|
-
background: #c2c8cf;
|
|
290
|
-
}
|
|
291
|
-
.dot-ok {
|
|
292
|
-
background: #2da44e;
|
|
293
|
-
box-shadow: 0 0 0 3px rgba(45, 164, 78, 0.15);
|
|
294
|
-
}
|
|
295
|
-
.dot-warn {
|
|
296
|
-
background: #d4a72c;
|
|
297
|
-
box-shadow: 0 0 0 3px rgba(212, 167, 44, 0.15);
|
|
298
|
-
}
|
|
299
|
-
.dot-error {
|
|
300
|
-
background: #cf222e;
|
|
301
|
-
box-shadow: 0 0 0 3px rgba(207, 34, 46, 0.15);
|
|
302
|
-
}
|
|
303
|
-
.card-headline {
|
|
304
|
-
margin: 0 0 0.5rem;
|
|
305
|
-
font-size: 1.35rem;
|
|
306
|
-
font-weight: 700;
|
|
307
|
-
letter-spacing: -0.01em;
|
|
308
|
-
font-variant-numeric: tabular-nums;
|
|
309
|
-
}
|
|
310
|
-
.card-lines {
|
|
311
|
-
margin: 0;
|
|
312
|
-
padding: 0;
|
|
313
|
-
list-style: none;
|
|
314
|
-
font-size: 0.82rem;
|
|
315
|
-
color: #57606a;
|
|
316
|
-
}
|
|
317
|
-
.card-lines li {
|
|
318
|
-
padding: 0.08rem 0;
|
|
319
|
-
overflow-wrap: anywhere;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
.hint {
|
|
323
|
-
margin: 0.25rem 0 1.5rem;
|
|
324
|
-
font-size: 0.85rem;
|
|
325
|
-
color: #6a737d;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
.next {
|
|
329
|
-
margin: 2.5rem 0;
|
|
330
|
-
}
|
|
331
|
-
.next h2 {
|
|
332
|
-
font-size: 1.15rem;
|
|
333
|
-
margin: 0 0 1rem;
|
|
334
|
-
}
|
|
335
|
-
.steps {
|
|
336
|
-
list-style: none;
|
|
337
|
-
margin: 0;
|
|
338
|
-
padding: 0;
|
|
339
|
-
display: flex;
|
|
340
|
-
flex-direction: column;
|
|
341
|
-
gap: 0.35rem;
|
|
342
|
-
}
|
|
343
|
-
.steps li {
|
|
344
|
-
display: flex;
|
|
345
|
-
gap: 0.75rem;
|
|
346
|
-
align-items: flex-start;
|
|
347
|
-
padding: 0.65rem 0.85rem;
|
|
348
|
-
border: 1px solid #eaecef;
|
|
349
|
-
border-radius: 10px;
|
|
350
|
-
background: #fbfcfd;
|
|
351
|
-
}
|
|
352
|
-
.steps li.done {
|
|
353
|
-
border-color: #cbe7d3;
|
|
354
|
-
background: #f3faf5;
|
|
355
|
-
}
|
|
356
|
-
.check {
|
|
357
|
-
color: #b0b7bf;
|
|
358
|
-
font-weight: 700;
|
|
359
|
-
line-height: 1.5;
|
|
360
|
-
}
|
|
361
|
-
.steps li.done .check {
|
|
362
|
-
color: #2da44e;
|
|
363
|
-
}
|
|
364
|
-
.step-body {
|
|
365
|
-
flex: 1;
|
|
366
|
-
min-width: 0;
|
|
367
|
-
}
|
|
368
|
-
.step-head {
|
|
369
|
-
display: flex;
|
|
370
|
-
align-items: center;
|
|
371
|
-
gap: 0.6rem;
|
|
372
|
-
flex-wrap: wrap;
|
|
373
|
-
}
|
|
374
|
-
.step-label {
|
|
375
|
-
font-weight: 600;
|
|
376
|
-
}
|
|
377
|
-
.step-cmd {
|
|
378
|
-
background: #0d1117;
|
|
379
|
-
color: #e6edf3;
|
|
380
|
-
}
|
|
381
|
-
.step-note {
|
|
382
|
-
display: block;
|
|
383
|
-
font-size: 0.8rem;
|
|
384
|
-
color: #6a737d;
|
|
385
|
-
margin-top: 0.15rem;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
.code {
|
|
389
|
-
border: 1px solid #e4e7eb;
|
|
390
|
-
border-radius: 12px;
|
|
391
|
-
overflow: hidden;
|
|
392
|
-
}
|
|
393
|
-
.code-head {
|
|
394
|
-
padding: 0.55rem 1rem;
|
|
395
|
-
background: #f6f8fa;
|
|
396
|
-
border-bottom: 1px solid #e4e7eb;
|
|
397
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
398
|
-
font-size: 0.78rem;
|
|
399
|
-
color: #57606a;
|
|
400
|
-
}
|
|
401
|
-
.code-body {
|
|
402
|
-
margin: 0;
|
|
403
|
-
padding: 1rem 1.15rem;
|
|
404
|
-
overflow-x: auto;
|
|
405
|
-
background: #0d1117;
|
|
406
|
-
color: #c9d1d9;
|
|
407
|
-
font-size: 0.82rem;
|
|
408
|
-
line-height: 1.65;
|
|
409
|
-
}
|
|
410
|
-
.code-body code {
|
|
411
|
-
background: none;
|
|
412
|
-
padding: 0;
|
|
413
|
-
font-size: inherit;
|
|
414
|
-
color: inherit;
|
|
415
|
-
}
|
|
416
|
-
.c-comment {
|
|
417
|
-
color: #8b949e;
|
|
418
|
-
}
|
|
419
|
-
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* apps/web/style.css — 앱 전역 스타일 (결정 74).
|
|
2
|
+
*
|
|
3
|
+
* main.ts 가 이 파일을 import 해 번들에 싣는다. @tailwind 3줄이 Tailwind 의
|
|
4
|
+
* base/components/utilities 레이어를 펼친다(PostCSS · postcss.config.js).
|
|
5
|
+
*
|
|
6
|
+
* :root/.dark 의 CSS 변수 = 디자인 토큰(shadcn 관례). UI 킷 컴포넌트는 이
|
|
7
|
+
* 토큰(bg-primary·text-muted-foreground …)만 참조하므로, 브랜드 색을 바꾸려면
|
|
8
|
+
* 여기 한 곳만 고친다. hsl 채널 값으로 두는 이유는 tailwind.config.ts 가
|
|
9
|
+
* hsl(var(--token)) 로 감싸 투명도 유틸(bg-primary/50)까지 동작하게 하기 위함이다. */
|
|
10
|
+
@tailwind base;
|
|
11
|
+
@tailwind components;
|
|
12
|
+
@tailwind utilities;
|
|
13
|
+
|
|
14
|
+
@layer base {
|
|
15
|
+
:root {
|
|
16
|
+
--background: 0 0% 100%;
|
|
17
|
+
--foreground: 240 10% 3.9%;
|
|
18
|
+
--card: 0 0% 100%;
|
|
19
|
+
--card-foreground: 240 10% 3.9%;
|
|
20
|
+
--primary: 240 5.9% 10%;
|
|
21
|
+
--primary-foreground: 0 0% 98%;
|
|
22
|
+
--secondary: 240 4.8% 95.9%;
|
|
23
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
24
|
+
--muted: 240 4.8% 95.9%;
|
|
25
|
+
--muted-foreground: 240 3.8% 46.1%;
|
|
26
|
+
--accent: 240 4.8% 95.9%;
|
|
27
|
+
--accent-foreground: 240 5.9% 10%;
|
|
28
|
+
--destructive: 0 84.2% 60.2%;
|
|
29
|
+
--destructive-foreground: 0 0% 98%;
|
|
30
|
+
--border: 240 5.9% 90%;
|
|
31
|
+
--input: 240 5.9% 90%;
|
|
32
|
+
--ring: 240 5.9% 10%;
|
|
33
|
+
--radius: 0.5rem;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.dark {
|
|
37
|
+
--background: 240 10% 3.9%;
|
|
38
|
+
--foreground: 0 0% 98%;
|
|
39
|
+
--card: 240 10% 3.9%;
|
|
40
|
+
--card-foreground: 0 0% 98%;
|
|
41
|
+
--primary: 0 0% 98%;
|
|
42
|
+
--primary-foreground: 240 5.9% 10%;
|
|
43
|
+
--secondary: 240 3.7% 15.9%;
|
|
44
|
+
--secondary-foreground: 0 0% 98%;
|
|
45
|
+
--muted: 240 3.7% 15.9%;
|
|
46
|
+
--muted-foreground: 240 5% 64.9%;
|
|
47
|
+
--accent: 240 3.7% 15.9%;
|
|
48
|
+
--accent-foreground: 0 0% 98%;
|
|
49
|
+
--destructive: 0 62.8% 30.6%;
|
|
50
|
+
--destructive-foreground: 0 0% 98%;
|
|
51
|
+
--border: 240 3.7% 15.9%;
|
|
52
|
+
--input: 240 3.7% 15.9%;
|
|
53
|
+
--ring: 240 4.9% 83.9%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
* {
|
|
57
|
+
border-color: hsl(var(--border));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
body {
|
|
61
|
+
background-color: hsl(var(--background));
|
|
62
|
+
color: hsl(var(--foreground));
|
|
63
|
+
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
64
|
+
-webkit-font-smoothing: antialiased;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# {{PROJECT_NAME}} 운영 스택 — 웹·워커·허브 3종 프로세스 + 실 인프라.
|
|
2
|
+
#
|
|
3
|
+
# 개발 스택(docker-compose.yaml)과 달리 앱 이미지(Dockerfile)를 빌드해 serve·work·
|
|
4
|
+
# hub 를 별도 서비스로 띄운다(운영 프로세스 3종 · CLAUDE.md §2 · 결정 60). 인프라는
|
|
5
|
+
# 실 pg·redis·nats(목업 금지 §9). 시크릿은 env 로 주입한다 — 이 파일에 값을 박지 말 것.
|
|
6
|
+
#
|
|
7
|
+
# 기동: docker compose -f compose.prod.yaml up -d --build
|
|
8
|
+
# 정지: docker compose -f compose.prod.yaml down
|
|
9
|
+
name: {{PROJECT_NAME}}-prod
|
|
10
|
+
|
|
11
|
+
x-app-env: &app-env
|
|
12
|
+
DATABASE_URL: postgres://{{PROJECT_NAME}}:${DB_PASSWORD:?set DB_PASSWORD}@postgres:5432/{{PROJECT_NAME}}
|
|
13
|
+
REDIS_URL: redis://redis:6379
|
|
14
|
+
NATS_URL: nats://nats:4222
|
|
15
|
+
COOKIE_SECRET: ${COOKIE_SECRET:?set COOKIE_SECRET (32+ chars)}
|
|
16
|
+
NODE_ENV: production
|
|
17
|
+
|
|
18
|
+
services:
|
|
19
|
+
# 웹 프로세스(gaon serve). node:cluster 워커 수는 WEB_CONCURRENCY 로(기본 1).
|
|
20
|
+
web:
|
|
21
|
+
build: .
|
|
22
|
+
command: ["pnpm", "serve"]
|
|
23
|
+
environment:
|
|
24
|
+
<<: *app-env
|
|
25
|
+
PORT: "3000"
|
|
26
|
+
WEB_CONCURRENCY: "${WEB_CONCURRENCY:-1}"
|
|
27
|
+
ports:
|
|
28
|
+
- "3000:3000"
|
|
29
|
+
depends_on:
|
|
30
|
+
postgres: { condition: service_healthy }
|
|
31
|
+
redis: { condition: service_healthy }
|
|
32
|
+
nats: { condition: service_healthy }
|
|
33
|
+
restart: unless-stopped
|
|
34
|
+
|
|
35
|
+
# 워커 프로세스(gaon work) — 잡·리스너·스케줄러·아웃박스 릴레이.
|
|
36
|
+
worker:
|
|
37
|
+
build: .
|
|
38
|
+
command: ["pnpm", "work"]
|
|
39
|
+
environment: *app-env
|
|
40
|
+
depends_on:
|
|
41
|
+
postgres: { condition: service_healthy }
|
|
42
|
+
nats: { condition: service_healthy }
|
|
43
|
+
restart: unless-stopped
|
|
44
|
+
|
|
45
|
+
# 허브 프로세스(gaon hub) — 리더만 bind, 프레즌스 단일 권위(결정 60 · §7).
|
|
46
|
+
hub:
|
|
47
|
+
build: .
|
|
48
|
+
command: ["pnpm", "hub"]
|
|
49
|
+
environment:
|
|
50
|
+
<<: *app-env
|
|
51
|
+
GAON_HUB_PORT: "4001"
|
|
52
|
+
GAON_HUB_ADVERTISE: "hub:4001"
|
|
53
|
+
depends_on:
|
|
54
|
+
nats: { condition: service_healthy }
|
|
55
|
+
restart: unless-stopped
|
|
56
|
+
|
|
57
|
+
postgres:
|
|
58
|
+
image: postgres:16-alpine
|
|
59
|
+
environment:
|
|
60
|
+
POSTGRES_USER: {{PROJECT_NAME}}
|
|
61
|
+
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD}
|
|
62
|
+
POSTGRES_DB: {{PROJECT_NAME}}
|
|
63
|
+
volumes:
|
|
64
|
+
- pgdata:/var/lib/postgresql/data
|
|
65
|
+
healthcheck:
|
|
66
|
+
test: ["CMD-SHELL", "pg_isready -U {{PROJECT_NAME}} -d {{PROJECT_NAME}}"]
|
|
67
|
+
interval: 5s
|
|
68
|
+
timeout: 3s
|
|
69
|
+
retries: 30
|
|
70
|
+
restart: unless-stopped
|
|
71
|
+
|
|
72
|
+
redis:
|
|
73
|
+
image: redis:7-alpine
|
|
74
|
+
volumes:
|
|
75
|
+
- redisdata:/data
|
|
76
|
+
healthcheck:
|
|
77
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
78
|
+
interval: 5s
|
|
79
|
+
timeout: 3s
|
|
80
|
+
retries: 30
|
|
81
|
+
restart: unless-stopped
|
|
82
|
+
|
|
83
|
+
nats:
|
|
84
|
+
image: nats:2.10-alpine
|
|
85
|
+
command: ["-js", "-sd", "/data", "-m", "8222"]
|
|
86
|
+
volumes:
|
|
87
|
+
- natsdata:/data
|
|
88
|
+
healthcheck:
|
|
89
|
+
test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:8222/healthz"]
|
|
90
|
+
interval: 5s
|
|
91
|
+
timeout: 3s
|
|
92
|
+
retries: 30
|
|
93
|
+
restart: unless-stopped
|
|
94
|
+
|
|
95
|
+
volumes:
|
|
96
|
+
pgdata:
|
|
97
|
+
redisdata:
|
|
98
|
+
natsdata:
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@10.27.0",
|
|
6
7
|
"engines": {
|
|
7
8
|
"node": ">=22"
|
|
8
9
|
},
|
|
@@ -24,6 +25,9 @@
|
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/node": "^22.0.0",
|
|
26
27
|
"@vitejs/plugin-vue": "^6.0.0",
|
|
28
|
+
"autoprefixer": "^10.4.0",
|
|
29
|
+
"postcss": "^8.4.0",
|
|
30
|
+
"tailwindcss": "^3.4.0",
|
|
27
31
|
"typescript": "^5.9.0",
|
|
28
32
|
"vite": "^7.0.0",
|
|
29
33
|
"vue-tsc": "^3.3.0",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// postcss.config.js — Vite 가 CSS 를 처리할 때 타는 PostCSS 파이프라인 (결정 74).
|
|
2
|
+
//
|
|
3
|
+
// Vite 는 프로젝트 루트에서 이 파일을 자동으로 찾아 style.css 의 @tailwind
|
|
4
|
+
// 지시문을 실 유틸 클래스로 펼치고(tailwindcss), 벤더 프리픽스를 붙인다
|
|
5
|
+
// (autoprefixer). gaon dev(Vite 미들웨어)·vite build 가 같은 설정을 공유한다.
|
|
6
|
+
//
|
|
7
|
+
// PostCSS 설정은 .js 가 표준이다 — Vite 의 postcss-load-config 가 .ts 를 바로
|
|
8
|
+
// 로드하지 못한다. 프레임웍의 "TS 전용" 관례는 앱/도메인 소스에 대한 것이고,
|
|
9
|
+
// 툴 설정 파일은 각 도구가 요구하는 형식을 따른다.
|
|
10
|
+
export default {
|
|
11
|
+
plugins: {
|
|
12
|
+
tailwindcss: {},
|
|
13
|
+
autoprefixer: {},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// tailwind.config.ts — Tailwind CSS 설정 (결정 74).
|
|
2
|
+
//
|
|
3
|
+
// content 는 유틸 클래스가 등장하는 모든 소스를 가리킨다. 앱 페이지·컴포넌트와
|
|
4
|
+
// shared UI 를 훑는다(자동 import 금지 관례와 무관 — 여기선 클래스 문자열 스캔).
|
|
5
|
+
// darkMode: 'class' — <html class="dark"> 로 다크 테마를 켠다(토큰은 style.css).
|
|
6
|
+
//
|
|
7
|
+
// 색 토큰은 style.css 의 CSS 변수(hsl 채널)를 가리킨다(shadcn 관례). 컴포넌트는
|
|
8
|
+
// bg-primary·text-muted-foreground 처럼 의미 토큰만 쓰고, 실제 색은 style.css 의
|
|
9
|
+
// :root/.dark 한 곳에서 바꾼다 — 라이트/다크가 한 지점에서 갈린다.
|
|
10
|
+
import type { Config } from 'tailwindcss'
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
darkMode: 'class',
|
|
14
|
+
content: ['./apps/**/*.{vue,ts}', './shared/**/*.{vue,ts}', './apps/**/index.html'],
|
|
15
|
+
theme: {
|
|
16
|
+
extend: {
|
|
17
|
+
colors: {
|
|
18
|
+
border: 'hsl(var(--border))',
|
|
19
|
+
input: 'hsl(var(--input))',
|
|
20
|
+
ring: 'hsl(var(--ring))',
|
|
21
|
+
background: 'hsl(var(--background))',
|
|
22
|
+
foreground: 'hsl(var(--foreground))',
|
|
23
|
+
primary: {
|
|
24
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
25
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
26
|
+
},
|
|
27
|
+
secondary: {
|
|
28
|
+
DEFAULT: 'hsl(var(--secondary))',
|
|
29
|
+
foreground: 'hsl(var(--secondary-foreground))',
|
|
30
|
+
},
|
|
31
|
+
destructive: {
|
|
32
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
33
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
34
|
+
},
|
|
35
|
+
muted: {
|
|
36
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
37
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
38
|
+
},
|
|
39
|
+
accent: {
|
|
40
|
+
DEFAULT: 'hsl(var(--accent))',
|
|
41
|
+
foreground: 'hsl(var(--accent-foreground))',
|
|
42
|
+
},
|
|
43
|
+
card: {
|
|
44
|
+
DEFAULT: 'hsl(var(--card))',
|
|
45
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
borderRadius: {
|
|
49
|
+
lg: 'var(--radius)',
|
|
50
|
+
md: 'calc(var(--radius) - 2px)',
|
|
51
|
+
sm: 'calc(var(--radius) - 4px)',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
plugins: [],
|
|
56
|
+
} satisfies Config
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// UI 킷 · Alert (결정 75). 인라인 알림. variant='destructive' 로 오류 톤.
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { cn } from '../../lib/utils.js'
|
|
5
|
+
|
|
6
|
+
type Variant = 'default' | 'destructive'
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(defineProps<{ variant?: Variant }>(), { variant: 'default' })
|
|
9
|
+
|
|
10
|
+
const VARIANTS: Record<Variant, string> = {
|
|
11
|
+
default: 'bg-background text-foreground',
|
|
12
|
+
destructive: 'border-destructive/50 text-destructive',
|
|
13
|
+
}
|
|
14
|
+
const classes = computed(() =>
|
|
15
|
+
cn('relative w-full rounded-lg border p-4', VARIANTS[props.variant]),
|
|
16
|
+
)
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div role="alert" :class="classes">
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|