chore: bootstrap development environment

This commit is contained in:
2026-08-14 12:24:46 +09:00
commit c3a3ff5e17
28 changed files with 4239 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules
.svelte-kit
build
.git
.env
.env.*
data/*
npm-debug.log*
+5
View File
@@ -0,0 +1,5 @@
HOST=0.0.0.0
PORT=3000
ORIGIN=http://localhost:3000
DATABASE_URL=./data/holodream.db
WATCH_USE_POLLING=false
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+11
View File
@@ -0,0 +1,11 @@
node_modules
.svelte-kit
build
.env
.env.*
!.env.example
data/*
!data/.gitkeep
npm-debug.log*
.DS_Store
*.tsbuildinfo
+46
View File
@@ -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"]
+87
View File
@@ -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하지 않습니다.
+26
View File
@@ -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:
+29
View File
@@ -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:
+1
View File
@@ -0,0 +1 @@
+9
View File
@@ -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 "$@"
+10
View File
@@ -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'
}
});
+5
View File
@@ -0,0 +1,5 @@
CREATE TABLE `app_metadata` (
`key` text PRIMARY KEY NOT NULL,
`value` text NOT NULL,
`updated_at` integer NOT NULL
);
+49
View File
@@ -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": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1786677379788,
"tag": "0000_cloudy_praxagora",
"breakpoints": true
}
]
}
+3727
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -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"
}
}
+12
View File
@@ -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>
+7
View File
@@ -0,0 +1,7 @@
# Calculation engine
기존 스킬 로스 계산기의 계산 로직을 UI와 분리해 둘 위치입니다.
- Svelte 컴포넌트나 SvelteKit 서버 모듈을 import하지 않습니다.
- 브라우저, Web Worker, Node.js, 단위 테스트에서 공유할 수 있는 순수 TypeScript로 작성합니다.
- 공용 데이터 타입은 `src/lib/models`에서 가져옵니다.
+3
View File
@@ -0,0 +1,3 @@
# Shared models
UI, 계산 엔진, 서버가 함께 사용하는 도메인 타입과 검증 규칙을 둡니다. 특정 실행 환경에 의존하는 코드는 포함하지 않습니다.
+3
View File
@@ -0,0 +1,3 @@
# Server-only modules
SQLite/Drizzle 저장소와 SvelteKit 서버에서만 사용하는 모듈을 둡니다. 이 디렉터리의 코드는 브라우저나 계산 엔진에서 import하지 않습니다.
+24
View File
@@ -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`);
}
+8
View File
@@ -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()
});
+3
View File
@@ -0,0 +1,3 @@
# Web Workers
덱 조합 탐색처럼 UI를 막을 수 있는 계산의 Web Worker 진입점을 둡니다. 실제 최적화 로직은 `src/lib/engine`을 호출하도록 구성합니다.
+47
View File
@@ -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>
+29
View File
@@ -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 }
);
}
}
+14
View File
@@ -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;
+14
View File
@@ -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"
}
}
+13
View File
@@ -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'
}
}
});