chore: bootstrap development environment
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
.git
|
||||
.env
|
||||
.env.*
|
||||
data/*
|
||||
npm-debug.log*
|
||||
@@ -0,0 +1,5 @@
|
||||
HOST=0.0.0.0
|
||||
PORT=3000
|
||||
ORIGIN=http://localhost:3000
|
||||
DATABASE_URL=./data/holodream.db
|
||||
WATCH_USE_POLLING=false
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
build
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
data/*
|
||||
!data/.gitkeep
|
||||
npm-debug.log*
|
||||
.DS_Store
|
||||
*.tsbuildinfo
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:22-bookworm-slim AS base
|
||||
WORKDIR /app
|
||||
ENV CI=true
|
||||
|
||||
FROM base AS deps
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM base AS dev
|
||||
ENV NODE_ENV=development
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
EXPOSE 5173
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
FROM base AS build
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
RUN npm prune --omit=dev
|
||||
|
||||
FROM node:22-bookworm-slim AS runtime
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=3000 \
|
||||
DATABASE_URL=/app/data/holodream.db
|
||||
COPY --from=build /app/build ./build
|
||||
COPY --from=build /app/drizzle ./drizzle
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
COPY docker/entrypoint.sh /usr/local/bin/holodream-entrypoint
|
||||
RUN chmod +x /usr/local/bin/holodream-entrypoint \
|
||||
&& mkdir -p /app/data \
|
||||
&& chown -R node:node /app
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=5 \
|
||||
CMD node -e "fetch('http://127.0.0.1:3000/api/health').then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1))"
|
||||
ENTRYPOINT ["holodream-entrypoint"]
|
||||
CMD ["node", "build"]
|
||||
@@ -0,0 +1,87 @@
|
||||
# Holodream Tools
|
||||
|
||||
홀로드림스 덱 최적화와 스킬 로스 분석을 통합할 웹사이트의 개발 기반입니다. 현재 단계에는 게임 기능을 포함하지 않습니다.
|
||||
|
||||
## 구성
|
||||
|
||||
- SvelteKit + TypeScript + `@sveltejs/adapter-node`
|
||||
- SQLite + Drizzle ORM
|
||||
- Web Worker를 위한 독립 모듈 경계
|
||||
- Docker Compose 개발 환경(HMR)
|
||||
- 멀티 스테이지 production 이미지
|
||||
- Docker named volume 기반 SQLite 영속 저장소
|
||||
|
||||
## Docker 개발 실행
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
- 웹: <http://localhost:5173>
|
||||
- 상태 확인: <http://localhost:5173/api/health>
|
||||
|
||||
소스 파일을 수정하면 Windows의 Docker Desktop에서도 polling 기반 HMR이 동작합니다.
|
||||
|
||||
종료:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
`docker compose down`은 SQLite 볼륨을 보존합니다. 데이터까지 지우려는 경우에만 `docker compose down --volumes`를 사용하세요.
|
||||
|
||||
## production 실행
|
||||
|
||||
```bash
|
||||
docker compose -f compose.prod.yaml up --build -d
|
||||
```
|
||||
|
||||
- 웹: <http://localhost:3000>
|
||||
- 상태 확인: <http://localhost:3000/api/health>
|
||||
|
||||
종료:
|
||||
|
||||
```bash
|
||||
docker compose -f compose.prod.yaml down
|
||||
```
|
||||
|
||||
## 호스트에서 실행
|
||||
|
||||
Node.js 22.12 이상이 필요합니다.
|
||||
|
||||
```powershell
|
||||
Copy-Item .env.example .env
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
기본 로컬 DB 경로는 `./data/holodream.db`입니다.
|
||||
|
||||
## 검증 명령
|
||||
|
||||
```bash
|
||||
npm run check
|
||||
npm run build
|
||||
```
|
||||
|
||||
스키마를 변경한 뒤에는 migration을 생성합니다.
|
||||
|
||||
```bash
|
||||
npm run db:generate
|
||||
```
|
||||
|
||||
생성된 migration은 서버가 시작될 때 자동 적용됩니다. 호스트에서 수동 적용하려면 `npm run db:migrate`를 사용합니다.
|
||||
|
||||
## 디렉터리 경계
|
||||
|
||||
```text
|
||||
src/
|
||||
├─ lib/
|
||||
│ ├─ engine/ # UI 프레임워크에 의존하지 않는 순수 계산 로직
|
||||
│ ├─ models/ # UI·엔진·서버가 공유하는 도메인 타입
|
||||
│ ├─ server/db/ # SQLite/Drizzle 서버 전용 모듈
|
||||
│ └─ workers/ # 무거운 최적화 작업의 Web Worker 진입점
|
||||
└─ routes/ # SvelteKit 화면과 API
|
||||
```
|
||||
|
||||
의존성 방향은 `UI → Engine → Models`를 유지합니다. 계산 엔진은 Svelte나 서버 모듈을 import하지 않습니다.
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: runtime
|
||||
container_name: holodream-tools
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
PORT: 3000
|
||||
ORIGIN: ${ORIGIN:-http://localhost:3000}
|
||||
DATABASE_URL: /app/data/holodream.db
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- sqlite_data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/health').then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1))"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
start_period: 10s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
sqlite_data:
|
||||
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
target: dev
|
||||
container_name: holodream-tools-dev
|
||||
working_dir: /app
|
||||
command: npm run dev -- --host 0.0.0.0
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
DATABASE_URL: /app/data/holodream.db
|
||||
WATCH_USE_POLLING: "true"
|
||||
ports:
|
||||
- "5173:5173"
|
||||
volumes:
|
||||
- .:/app
|
||||
- node_modules:/app/node_modules
|
||||
- sqlite_data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:5173/api/health').then((response) => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1))"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
start_period: 15s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
node_modules:
|
||||
sqlite_data:
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# A named volume keeps its original ownership when reused between services.
|
||||
# Normalize it at startup, then run the application as the unprivileged node user.
|
||||
mkdir -p /app/data
|
||||
chown -R node:node /app/data
|
||||
|
||||
exec setpriv --reuid=node --regid=node --init-groups "$@"
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
dialect: 'sqlite',
|
||||
schema: './src/lib/server/db/schema.ts',
|
||||
out: './drizzle',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL ?? './data/holodream.db'
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE `app_metadata` (
|
||||
`key` text PRIMARY KEY NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`updated_at` integer NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "1033356a-590a-4b7a-b521-4cff12d33126",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"app_metadata": {
|
||||
"name": "app_metadata",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1786677379788,
|
||||
"tag": "0000_cloudy_praxagora",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
+3727
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "holodream-tools",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0 --port 5173",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 0.0.0.0",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"start": "node build",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-node": "^5.5.7",
|
||||
"@sveltejs/kit": "^2.70.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||
"@types/better-sqlite3": "^9.6.0",
|
||||
"@types/node": "^26.2.0",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"svelte": "^5.56.8",
|
||||
"svelte-check": "^4.7.4",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.1.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"drizzle-orm": "^0.45.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
# Calculation engine
|
||||
|
||||
기존 스킬 로스 계산기의 계산 로직을 UI와 분리해 둘 위치입니다.
|
||||
|
||||
- Svelte 컴포넌트나 SvelteKit 서버 모듈을 import하지 않습니다.
|
||||
- 브라우저, Web Worker, Node.js, 단위 테스트에서 공유할 수 있는 순수 TypeScript로 작성합니다.
|
||||
- 공용 데이터 타입은 `src/lib/models`에서 가져옵니다.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Shared models
|
||||
|
||||
UI, 계산 엔진, 서버가 함께 사용하는 도메인 타입과 검증 규칙을 둡니다. 특정 실행 환경에 의존하는 코드는 포함하지 않습니다.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Server-only modules
|
||||
|
||||
SQLite/Drizzle 저장소와 SvelteKit 서버에서만 사용하는 모듈을 둡니다. 이 디렉터리의 코드는 브라우저나 계산 엔진에서 import하지 않습니다.
|
||||
@@ -0,0 +1,24 @@
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import Database from 'better-sqlite3';
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
||||
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
const databaseUrl = env.DATABASE_URL ?? './data/holodream.db';
|
||||
|
||||
if (databaseUrl !== ':memory:') {
|
||||
mkdirSync(dirname(databaseUrl), { recursive: true });
|
||||
}
|
||||
|
||||
const sqlite = new Database(databaseUrl);
|
||||
sqlite.pragma('journal_mode = WAL');
|
||||
sqlite.pragma('foreign_keys = ON');
|
||||
|
||||
export const db = drizzle(sqlite);
|
||||
migrate(db, { migrationsFolder: './drizzle' });
|
||||
|
||||
export function checkDatabase(): void {
|
||||
db.run(sql`select 1`);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
// Infrastructure-only table. Game and user schemas will be added in later phases.
|
||||
export const appMetadata = sqliteTable('app_metadata', {
|
||||
key: text('key').primaryKey(),
|
||||
value: text('value').notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull()
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
# Web Workers
|
||||
|
||||
덱 조합 탐색처럼 UI를 막을 수 있는 계산의 Web Worker 진입점을 둡니다. 실제 최적화 로직은 `src/lib/engine`을 호출하도록 구성합니다.
|
||||
@@ -0,0 +1,47 @@
|
||||
<svelte:head>
|
||||
<title>Holodream Tools</title>
|
||||
<meta name="description" content="홀로드림스 통합 덱 최적화 도구" />
|
||||
</svelte:head>
|
||||
|
||||
<main>
|
||||
<section class="panel">
|
||||
<p class="eyebrow">Development Environment</p>
|
||||
<h1>Holodream Tools</h1>
|
||||
<p>통합 덱 최적화 사이트의 기본 개발 환경이 준비되었습니다.</p>
|
||||
|
||||
<div class="status">
|
||||
<span class="dot"></span>
|
||||
<span>SvelteKit 서버가 실행 중입니다.</span>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
:global(*) { box-sizing: border-box; }
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #e5e7eb;
|
||||
background: #0b1020;
|
||||
}
|
||||
main {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.panel {
|
||||
width: min(680px, 100%);
|
||||
padding: 36px;
|
||||
border: 1px solid #27324a;
|
||||
border-radius: 18px;
|
||||
background: #111827;
|
||||
}
|
||||
.eyebrow { color: #93c5fd; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; font-size: 12px; }
|
||||
h1 { margin: 8px 0 12px; font-size: clamp(32px, 6vw, 52px); }
|
||||
p { color: #aeb8c8; line-height: 1.65; }
|
||||
.status { margin-top: 28px; display: flex; align-items: center; gap: 10px; color: #d1fae5; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 999px; background: #34d399; box-shadow: 0 0 16px #34d399; }
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { checkDatabase } from '$lib/server/db/client';
|
||||
|
||||
export function GET() {
|
||||
const checkedAt = new Date().toISOString();
|
||||
|
||||
try {
|
||||
checkDatabase();
|
||||
|
||||
return json({
|
||||
ok: true,
|
||||
service: 'holodream-tools',
|
||||
database: 'ok',
|
||||
checkedAt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Health check failed', error);
|
||||
|
||||
return json(
|
||||
{
|
||||
ok: false,
|
||||
service: 'holodream-tools',
|
||||
database: 'unavailable',
|
||||
checkedAt
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import adapter from '@sveltejs/adapter-node';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
out: 'build'
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
watch: {
|
||||
usePolling: process.env.WATCH_USE_POLLING === 'true'
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user