@gaonjs/cli 0.13.2 → 0.15.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/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +14 -1
- package/dist/dev/console.d.ts +1 -1
- package/dist/dev/console.js +1 -0
- package/dist/dev/frontend-build.d.ts +14 -0
- package/dist/dev/frontend-build.js +56 -0
- package/dist/dev/health.d.ts +72 -0
- package/dist/dev/health.js +154 -0
- package/dist/dev/index.d.ts +2 -0
- package/dist/dev/index.js +2 -0
- package/dist/doctor.d.ts +12 -0
- package/dist/doctor.js +50 -24
- package/dist/generate.js +5 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -2
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +9 -0
- package/dist/templates/auth/registration.controller.ts.tpl +1 -1
- package/dist/templates/auth/session.controller.ts.tpl +2 -2
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/agents/data.md.tpl +5 -0
- package/dist/templates/project/agents/frontend.md.tpl +20 -0
- package/dist/templates/project/apps/web/composables/useGaonHealth.ts.tpl +79 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +93 -17
- package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +403 -20
- package/dist/templates/project/package.json.tpl +1 -0
- package/package.json +6 -6
- package/dist/templates/auth/app.ts.tpl +0 -29
- package/dist/templates/auth/server.ts.tpl +0 -14
|
@@ -151,6 +151,24 @@ Nuxt 식 자동 import 는 넣지 않는다. 모든 컴포넌트·컴포저블
|
|
|
151
151
|
import 한다. `gaon doctor` 의 **no-auto-import** 검사가 자동 import
|
|
152
152
|
설정을 잡는다.
|
|
153
153
|
|
|
154
|
+
### 7. 랜딩·라이브 헬스 (결정 69 · 70)
|
|
155
|
+
|
|
156
|
+
`gaon new` 첫 화면(`apps/web/pages/Home/Index.vue`)은 라이브 상태 랜딩이다 — 다크
|
|
157
|
+
헤더 레이아웃(`layouts/Default.vue`) + 실 상태 카드(WEB·DATABASE·HUB·DOCTOR) + 동적
|
|
158
|
+
다음 단계 + 실 `routes.ts` 코드 블록. 상태는 **하드코딩하지 않는다** — dev 전용
|
|
159
|
+
엔드포인트 `/_gaon/health` 를 컴포저블(`composables/useGaonHealth.ts`)로 읽어 바인딩한다.
|
|
160
|
+
이 엔드포인트는 `gaon dev` 에서만 등록되고(운영 빌드엔 없음), 404 면 컴포저블이
|
|
161
|
+
`available=false` 로 우아하게 degrade 한다.
|
|
162
|
+
|
|
163
|
+
- **스타일은 SFC `<style scoped>` + CSS 변수** — 스캐폴드에 Tailwind 를 넣지
|
|
164
|
+
않는다(의존성 경량 · 결정 69). 유틸 클래스 대신 scoped CSS 로 캡슐화.
|
|
165
|
+
- **auth 링크는 수동(결정 70)** — `gaon g auth` 는 auth 페이지·라우트만 신설하고
|
|
166
|
+
랜딩·레이아웃 nav 를 편집하지 않는다. 헤더에 로그인 링크를 두려면 `Default.vue`
|
|
167
|
+
의 nav 에 `<a href="/session/new">로그인</a>` 을 직접 추가한다(Rails 관례).
|
|
168
|
+
- **auth 페이지 경로 = `pages/Auth/`(PascalCase)** — `gaon g auth` 는 `Auth/Login.vue`
|
|
169
|
+
·`Auth/Signup.vue` 를 내고 컨트롤러는 `this.render('Auth/Login')` 로 부른다.
|
|
170
|
+
소문자 `auth/` 는 doctor page-filename 이 잡는다(결정 32·46).
|
|
171
|
+
|
|
154
172
|
## 정본 예시
|
|
155
173
|
|
|
156
174
|
```vue
|
|
@@ -203,4 +221,6 @@ async function runSearch(q: string) {
|
|
|
203
221
|
| 결정 25 (E-5) | 컴포저블·레이아웃 관례 · 프론트 로직 배치 3규칙 · 자동 import 금지 |
|
|
204
222
|
| 결정 37 | bigint PK 컨트롤러 `String()` 정규화 |
|
|
205
223
|
| 결정 46 | doctor page-filename(페이지 PascalCase)·model/column 검사 3종 |
|
|
224
|
+
| 결정 69 | 랜딩 정본(라이브 헬스 카드 · 다크 헤더 레이아웃 · Tailwind 미편입 · scoped CSS) |
|
|
225
|
+
| 결정 70 | auth 통합 = 수동(`gaon g auth` 는 랜딩·nav 를 안 건드림 · Rails 관례) |
|
|
206
226
|
| E-3 §C | 타입드 `api()` 클라이언트 (routes.d.ts 브리지 재사용) |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// useGaonHealth — 앱 전용 컴포저블(errata E-5 §2.1).
|
|
2
|
+
// gaon dev 가 띄운 서버의 dev 전용 진단 엔드포인트(/_gaon/health)를 읽어
|
|
3
|
+
// 랜딩(Home/Index.vue)의 라이브 상태 카드에 바인딩한다.
|
|
4
|
+
//
|
|
5
|
+
// dev-only: 이 엔드포인트는 gaon dev(= gaon serve --dev)에서만 등록된다.
|
|
6
|
+
// 운영 빌드에는 없다 — 404 면 available=false 로 우아하게 degrade하고(카드가
|
|
7
|
+
// "개발 서버에서만" 로 표시), 에러를 던지지 않는다.
|
|
8
|
+
import { ref, onMounted } from 'vue'
|
|
9
|
+
|
|
10
|
+
// 서버 응답(GaonHealth) 의 앱 쪽 미러. 서버 타입을 import 하지 않고(앱은 프레임웍
|
|
11
|
+
// 내부 타입에 의존하지 않는다) 랜딩이 쓰는 필드만 선언한다.
|
|
12
|
+
export interface HealthWeb {
|
|
13
|
+
host: string
|
|
14
|
+
port: number
|
|
15
|
+
node: string
|
|
16
|
+
gaonjs: string
|
|
17
|
+
apps: { name: string; prefix: string }[]
|
|
18
|
+
}
|
|
19
|
+
export interface HealthDatabase {
|
|
20
|
+
configured: boolean
|
|
21
|
+
connected?: boolean
|
|
22
|
+
adapter?: string
|
|
23
|
+
tables?: number
|
|
24
|
+
tableNames?: string[]
|
|
25
|
+
migrations?: number
|
|
26
|
+
error?: string
|
|
27
|
+
}
|
|
28
|
+
export interface HealthHub {
|
|
29
|
+
configured: boolean
|
|
30
|
+
connected?: boolean
|
|
31
|
+
streams?: { stream: string; waiting: number | null }[]
|
|
32
|
+
error?: string
|
|
33
|
+
}
|
|
34
|
+
export interface HealthDoctor {
|
|
35
|
+
checks: number
|
|
36
|
+
passed: number
|
|
37
|
+
warnings: number
|
|
38
|
+
errors: number
|
|
39
|
+
level: 'pass' | 'warn' | 'error'
|
|
40
|
+
fatal?: string
|
|
41
|
+
}
|
|
42
|
+
export interface GaonHealth {
|
|
43
|
+
ok: boolean
|
|
44
|
+
env: string
|
|
45
|
+
web: HealthWeb
|
|
46
|
+
database: HealthDatabase
|
|
47
|
+
hub: HealthHub
|
|
48
|
+
doctor: HealthDoctor
|
|
49
|
+
routes: { path: string; source: string } | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useGaonHealth() {
|
|
53
|
+
const health = ref<GaonHealth | null>(null)
|
|
54
|
+
const loading = ref(true)
|
|
55
|
+
// 엔드포인트가 있는가(= dev 서버인가). 운영 빌드에선 false 로 남는다.
|
|
56
|
+
const available = ref(false)
|
|
57
|
+
|
|
58
|
+
async function load(): Promise<void> {
|
|
59
|
+
loading.value = true
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch('/_gaon/health', { headers: { Accept: 'application/json' } })
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
available.value = false
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
health.value = (await res.json()) as GaonHealth
|
|
67
|
+
available.value = true
|
|
68
|
+
} catch {
|
|
69
|
+
// 네트워크·파싱 실패 = 진단 불가. 랜딩은 정적 안내로 degrade.
|
|
70
|
+
available.value = false
|
|
71
|
+
} finally {
|
|
72
|
+
loading.value = false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
onMounted(load)
|
|
77
|
+
|
|
78
|
+
return { health, loading, available, reload: load }
|
|
79
|
+
}
|
|
@@ -5,39 +5,115 @@
|
|
|
5
5
|
//
|
|
6
6
|
// 레이아웃은 shared 에 두지 않는다(E-5 §2.3) — 앱마다 레이아웃이 다른 것이
|
|
7
7
|
// 정상이고, 공용 조각(로고·푸터 등) 만 shared/components 로 뽑는다.
|
|
8
|
+
//
|
|
9
|
+
// 다크 헤더 + 라이트 본문(결정 69). 버전은 스캐폴드 시점 gaonjs 버전이 박힌다.
|
|
10
|
+
//
|
|
11
|
+
// 결정 70(auth = 수동): gaon g auth 는 이 nav 를 건드리지 않는다. 로그인 링크를
|
|
12
|
+
// 헤더에 두려면 아래 nav 에 <a href="/session/new">로그인</a> 을 직접 추가한다.
|
|
13
|
+
|
|
14
|
+
// package.json 의 gaonjs 의존 범위(예: ^0.9.2)에서 캐럿·틸드를 벗겨 표기.
|
|
15
|
+
const version = '{{GAONJS_VERSION}}'.replace(/^[\^~]/, '')
|
|
8
16
|
</script>
|
|
9
17
|
|
|
10
18
|
<template>
|
|
11
|
-
<div class="layout">
|
|
12
|
-
<header class="
|
|
13
|
-
<
|
|
19
|
+
<div class="gaon-layout">
|
|
20
|
+
<header class="gaon-header">
|
|
21
|
+
<a class="gaon-brand" href="/">
|
|
22
|
+
<span class="gaon-brand-mark">가온</span>
|
|
23
|
+
<span class="gaon-brand-word">GAONJS</span>
|
|
24
|
+
<span class="gaon-brand-ver">v{{ version }}</span>
|
|
25
|
+
</a>
|
|
26
|
+
<nav class="gaon-nav">
|
|
27
|
+
<a href="https://gaonjs.dev" target="_blank" rel="noreferrer">문서</a>
|
|
28
|
+
<a href="https://github.com/gaonjs" target="_blank" rel="noreferrer">GitHub</a>
|
|
29
|
+
</nav>
|
|
14
30
|
</header>
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
<
|
|
31
|
+
|
|
32
|
+
<main class="gaon-main">
|
|
33
|
+
<slot />
|
|
34
|
+
</main>
|
|
35
|
+
|
|
36
|
+
<footer class="gaon-footer">
|
|
37
|
+
<small>{{PROJECT_NAME}} · Powered by
|
|
38
|
+
<a href="https://gaonjs.dev" target="_blank" rel="noreferrer">Gaon</a></small>
|
|
18
39
|
</footer>
|
|
19
40
|
</div>
|
|
20
41
|
</template>
|
|
21
42
|
|
|
22
43
|
<style scoped>
|
|
23
|
-
.layout {
|
|
44
|
+
.gaon-layout {
|
|
24
45
|
min-height: 100vh;
|
|
25
46
|
display: flex;
|
|
26
47
|
flex-direction: column;
|
|
48
|
+
background: #ffffff;
|
|
49
|
+
color: #1b1f24;
|
|
50
|
+
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* 다크 헤더 — 본문 테마와 무관하게 브랜드 바는 어둡게. */
|
|
54
|
+
.gaon-header {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
gap: 1rem;
|
|
59
|
+
padding: 0.85rem 1.5rem;
|
|
60
|
+
background: #0d1117;
|
|
61
|
+
color: #e6edf3;
|
|
62
|
+
border-bottom: 1px solid #21262d;
|
|
63
|
+
}
|
|
64
|
+
.gaon-brand {
|
|
65
|
+
display: inline-flex;
|
|
66
|
+
align-items: baseline;
|
|
67
|
+
gap: 0.55rem;
|
|
68
|
+
text-decoration: none;
|
|
69
|
+
color: inherit;
|
|
27
70
|
}
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
padding:
|
|
31
|
-
|
|
32
|
-
|
|
71
|
+
.gaon-brand-mark {
|
|
72
|
+
display: inline-block;
|
|
73
|
+
padding: 0.15rem 0.45rem;
|
|
74
|
+
border-radius: 6px;
|
|
75
|
+
background: linear-gradient(135deg, #4f8cff, #7c5cff);
|
|
76
|
+
color: #fff;
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
font-size: 0.8rem;
|
|
79
|
+
letter-spacing: 0.02em;
|
|
33
80
|
}
|
|
34
|
-
.
|
|
35
|
-
|
|
81
|
+
.gaon-brand-word {
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
letter-spacing: 0.08em;
|
|
84
|
+
font-size: 0.95rem;
|
|
36
85
|
}
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
86
|
+
.gaon-brand-ver {
|
|
87
|
+
font-size: 0.72rem;
|
|
88
|
+
color: #8b949e;
|
|
89
|
+
font-variant-numeric: tabular-nums;
|
|
90
|
+
}
|
|
91
|
+
.gaon-nav {
|
|
92
|
+
display: flex;
|
|
93
|
+
gap: 1.1rem;
|
|
94
|
+
font-size: 0.85rem;
|
|
95
|
+
}
|
|
96
|
+
.gaon-nav a {
|
|
97
|
+
color: #c9d1d9;
|
|
98
|
+
text-decoration: none;
|
|
99
|
+
}
|
|
100
|
+
.gaon-nav a:hover {
|
|
101
|
+
color: #fff;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.gaon-main {
|
|
105
|
+
flex: 1;
|
|
106
|
+
width: 100%;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.gaon-footer {
|
|
110
|
+
padding: 1.25rem 1.5rem;
|
|
40
111
|
text-align: center;
|
|
41
112
|
color: #6a737d;
|
|
113
|
+
border-top: 1px solid #eaecef;
|
|
114
|
+
}
|
|
115
|
+
.gaon-footer a {
|
|
116
|
+
color: #4f8cff;
|
|
117
|
+
text-decoration: none;
|
|
42
118
|
}
|
|
43
119
|
</style>
|
|
@@ -1,36 +1,419 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
2
3
|
import { pageProps } from 'gaonjs/vue'
|
|
4
|
+
import { useGaonHealth, type HealthDoctor } from '../../composables/useGaonHealth.js'
|
|
3
5
|
|
|
4
6
|
// home#index 의 render props — Serialized<> 로 넘어온다(§6.2).
|
|
5
7
|
// 라우트 키는 .gaon/routes.d.ts 가 유효한 값을 알려준다.
|
|
6
8
|
const props = pageProps<'web:home#index'>()
|
|
9
|
+
|
|
10
|
+
// 라이브 상태(dev 전용 /_gaon/health). 운영 빌드에선 available=false 로 degrade.
|
|
11
|
+
const { health, loading, available } = useGaonHealth()
|
|
12
|
+
|
|
13
|
+
type CardStatus = 'ok' | 'warn' | 'error' | 'idle'
|
|
14
|
+
interface Card {
|
|
15
|
+
readonly key: string
|
|
16
|
+
readonly title: string
|
|
17
|
+
readonly status: CardStatus
|
|
18
|
+
readonly headline: string
|
|
19
|
+
readonly lines: readonly string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function doctorNote(d: HealthDoctor | undefined): string {
|
|
23
|
+
if (!d) return '—'
|
|
24
|
+
if (d.fatal) return `검사 불가 (${d.fatal})`
|
|
25
|
+
return `${d.passed} 통과 · ${d.warnings} 경고 · ${d.errors} 오류`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cards = computed<Card[]>(() => {
|
|
29
|
+
const h = health.value
|
|
30
|
+
const web = h?.web
|
|
31
|
+
const db = h?.database
|
|
32
|
+
const hub = h?.hub
|
|
33
|
+
const doc = h?.doctor
|
|
34
|
+
|
|
35
|
+
const webCard: Card = {
|
|
36
|
+
key: 'web',
|
|
37
|
+
title: 'WEB',
|
|
38
|
+
status: available.value ? 'ok' : 'idle',
|
|
39
|
+
headline: web ? `:${web.port}` : '대기',
|
|
40
|
+
lines: web
|
|
41
|
+
? [`Node ${web.node}`, `gaonjs v${web.gaonjs}`, `앱 ${web.apps.map((a) => a.name).join(', ')}`]
|
|
42
|
+
: ['개발 서버에서만'],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dbCard: Card = {
|
|
46
|
+
key: 'database',
|
|
47
|
+
title: 'DATABASE',
|
|
48
|
+
status: !db || !db.configured ? 'idle' : db.connected ? 'ok' : 'error',
|
|
49
|
+
headline: !db || !db.configured ? '미설정' : db.connected ? '연결됨' : '미연결',
|
|
50
|
+
lines:
|
|
51
|
+
db && db.configured
|
|
52
|
+
? db.connected
|
|
53
|
+
? [`${db.adapter ?? 'postgres'}`, `테이블 ${db.tables ?? 0}개`, `마이그레이션 ${db.migrations ?? 0}건`]
|
|
54
|
+
: [db.error ?? '연결 실패', '.env DATABASE_URL 확인']
|
|
55
|
+
: ['.env 에 DATABASE_URL', 'gaon.config.ts 에서 켜기'],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hubCard: Card = {
|
|
59
|
+
key: 'hub',
|
|
60
|
+
title: 'HUB',
|
|
61
|
+
status: !hub || !hub.configured ? 'idle' : hub.connected ? 'ok' : 'error',
|
|
62
|
+
headline: !hub || !hub.configured ? '미설정' : hub.connected ? '연결됨' : '미연결',
|
|
63
|
+
lines:
|
|
64
|
+
hub && hub.configured
|
|
65
|
+
? hub.connected
|
|
66
|
+
? (hub.streams ?? []).map((s) => `${s.stream}: ${s.waiting === null ? '대기 없음' : `${s.waiting} 대기`}`)
|
|
67
|
+
: [hub.error ?? 'NATS 미연결']
|
|
68
|
+
: ['.env 에 NATS_URL', '실시간·비동기 백본'],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const doctorCard: Card = {
|
|
72
|
+
key: 'doctor',
|
|
73
|
+
title: 'DOCTOR',
|
|
74
|
+
status: !doc ? 'idle' : doc.level === 'pass' ? 'ok' : doc.level === 'warn' ? 'warn' : 'error',
|
|
75
|
+
headline: !doc
|
|
76
|
+
? '대기'
|
|
77
|
+
: doc.fatal
|
|
78
|
+
? '검사 불가'
|
|
79
|
+
: doc.errors > 0
|
|
80
|
+
? `오류 ${doc.errors}`
|
|
81
|
+
: doc.warnings > 0
|
|
82
|
+
? `경고 ${doc.warnings}`
|
|
83
|
+
: '통과',
|
|
84
|
+
lines: [doctorNote(doc), 'gaon check 로 재실행'],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [webCard, dbCard, hubCard, doctorCard]
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// 다음 단계 — 라이브 상태에서 완료 여부를 파생한다.
|
|
91
|
+
interface Step {
|
|
92
|
+
readonly label: string
|
|
93
|
+
readonly cmd: string
|
|
94
|
+
readonly done: boolean
|
|
95
|
+
readonly note: string
|
|
96
|
+
}
|
|
97
|
+
const steps = computed<Step[]>(() => {
|
|
98
|
+
const h = health.value
|
|
99
|
+
const db = h?.database
|
|
100
|
+
const configured = !!(db && db.configured)
|
|
101
|
+
const connected = !!(db && db.configured && db.connected)
|
|
102
|
+
const tables = configured ? db!.tables ?? 0 : 0
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
label: '개발 스택 부팅',
|
|
106
|
+
cmd: 'gaon dev',
|
|
107
|
+
done: available.value,
|
|
108
|
+
note: 'Docker · .gaon 타입 브리지 · 서버 · watch',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: '데이터베이스 연결',
|
|
112
|
+
cmd: configured ? 'gaon db migrate' : '.env: DATABASE_URL',
|
|
113
|
+
done: connected,
|
|
114
|
+
note: configured ? (connected ? '연결됨' : '설정됨 · 미연결') : '미설정',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: '첫 모델 만들기',
|
|
118
|
+
cmd: 'gaon g model Post',
|
|
119
|
+
done: tables > 0,
|
|
120
|
+
note: tables > 0 ? `테이블 ${tables}개` : '스키마 + 모델 (E-4)',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
label: '인증 배터리',
|
|
124
|
+
cmd: 'gaon g auth',
|
|
125
|
+
done: false,
|
|
126
|
+
note: '가입 · 로그인 · 세션 (수동 통합 · 결정 70)',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: '검사 통과',
|
|
130
|
+
cmd: 'gaon check',
|
|
131
|
+
done: h?.doctor.level === 'pass',
|
|
132
|
+
note: doctorNote(h?.doctor),
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// routes.ts 코드 블록 — 라이브 원문을 줄 단위로 나누고 주석만 가볍게 색을 준다.
|
|
138
|
+
// v-html 없이(텍스트 보간만) 안전하게 그린다. 의존성 없는 최소 하이라이트.
|
|
139
|
+
interface CodeLine {
|
|
140
|
+
readonly code: string
|
|
141
|
+
readonly comment: string
|
|
142
|
+
}
|
|
143
|
+
function splitComment(line: string): CodeLine {
|
|
144
|
+
const m = line.match(/\s\/\/.*$/)
|
|
145
|
+
if (m && m.index !== undefined) return { code: line.slice(0, m.index), comment: line.slice(m.index) }
|
|
146
|
+
if (line.trimStart().startsWith('//')) return { code: '', comment: line }
|
|
147
|
+
return { code: line, comment: '' }
|
|
148
|
+
}
|
|
149
|
+
const routesPath = computed(() => health.value?.routes?.path ?? 'apps/web/routes.ts')
|
|
150
|
+
const routeLines = computed<CodeLine[]>(() => {
|
|
151
|
+
const src = health.value?.routes?.source
|
|
152
|
+
if (!src) return []
|
|
153
|
+
return src.replace(/\n+$/, '').split('\n').map(splitComment)
|
|
154
|
+
})
|
|
7
155
|
</script>
|
|
8
156
|
|
|
9
157
|
<template>
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
158
|
+
<div class="landing">
|
|
159
|
+
<section class="hero">
|
|
160
|
+
<p class="eyebrow">{{ props.title }}</p>
|
|
161
|
+
<h1>가온에 올라탔습니다.</h1>
|
|
162
|
+
<p class="lede">
|
|
163
|
+
이 화면은 <code>apps/web/pages/Home/Index.vue</code> 입니다.
|
|
164
|
+
<code>routes.ts</code> 의 <code>r.get('/', 'home#index')</code> 가 여기로 연결했습니다.
|
|
165
|
+
</p>
|
|
166
|
+
<p class="lede sub">
|
|
167
|
+
아래 카드는 지금 이 서버의 <strong>실제 상태</strong>입니다 —
|
|
168
|
+
하드코딩이 아니라 <code>/_gaon/health</code> 를 읽어 그립니다.
|
|
169
|
+
<a :href="props.docs" target="_blank" rel="noreferrer">{{ props.docs }}</a>
|
|
170
|
+
</p>
|
|
171
|
+
</section>
|
|
172
|
+
|
|
173
|
+
<section class="cards" aria-label="프레임웍 상태">
|
|
174
|
+
<article v-for="c in cards" :key="c.key" class="card" :class="`is-${c.status}`">
|
|
175
|
+
<header class="card-top">
|
|
176
|
+
<span class="dot" :class="`dot-${c.status}`" aria-hidden="true"></span>
|
|
177
|
+
<span class="card-title">{{ c.title }}</span>
|
|
178
|
+
</header>
|
|
179
|
+
<p class="card-headline">{{ c.headline }}</p>
|
|
180
|
+
<ul class="card-lines">
|
|
181
|
+
<li v-for="(ln, i) in c.lines" :key="i">{{ ln }}</li>
|
|
182
|
+
</ul>
|
|
183
|
+
</article>
|
|
184
|
+
</section>
|
|
185
|
+
|
|
186
|
+
<p v-if="loading" class="hint">상태 확인 중…</p>
|
|
187
|
+
<p v-else-if="!available" class="hint">
|
|
188
|
+
라이브 상태는 <code>gaon dev</code> 개발 서버에서만 보입니다(운영 빌드엔 진단 엔드포인트가 없습니다).
|
|
15
189
|
</p>
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
190
|
+
|
|
191
|
+
<section class="next">
|
|
192
|
+
<h2>다음 단계</h2>
|
|
193
|
+
<ul class="steps">
|
|
194
|
+
<li v-for="s in steps" :key="s.label" :class="{ done: s.done }">
|
|
195
|
+
<span class="check" aria-hidden="true">{{ s.done ? '✓' : '○' }}</span>
|
|
196
|
+
<div class="step-body">
|
|
197
|
+
<div class="step-head">
|
|
198
|
+
<span class="step-label">{{ s.label }}</span>
|
|
199
|
+
<code class="step-cmd">{{ s.cmd }}</code>
|
|
200
|
+
</div>
|
|
201
|
+
<span class="step-note">{{ s.note }}</span>
|
|
202
|
+
</div>
|
|
203
|
+
</li>
|
|
204
|
+
</ul>
|
|
205
|
+
</section>
|
|
206
|
+
|
|
207
|
+
<section v-if="routeLines.length" class="code">
|
|
208
|
+
<div class="code-head">{{ routesPath }}</div>
|
|
209
|
+
<pre class="code-body"><code><span v-for="(ln, i) in routeLines" :key="i" class="code-line"><span class="c-code">{{ ln.code }}</span><span class="c-comment">{{ ln.comment }}</span>{{ '\n' }}</span></code></pre>
|
|
210
|
+
</section>
|
|
211
|
+
</div>
|
|
26
212
|
</template>
|
|
27
213
|
|
|
28
214
|
<style scoped>
|
|
29
|
-
.
|
|
30
|
-
max-width:
|
|
31
|
-
margin:
|
|
32
|
-
padding:
|
|
33
|
-
|
|
215
|
+
.landing {
|
|
216
|
+
max-width: 880px;
|
|
217
|
+
margin: 0 auto;
|
|
218
|
+
padding: 3rem 1.25rem 4rem;
|
|
219
|
+
color: #1b1f24;
|
|
34
220
|
line-height: 1.6;
|
|
35
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
|
+
}
|
|
36
419
|
</style>
|