κ³ κ°, κ³μ½, μ ν, νμ¨μ΄(νλ‘ν μ½), AS μ΄λ ₯μ ν΅ν© κ΄λ¦¬νλ μ¬λ΄ CMS μμ€ν μ λλ€.
| λΆλ₯ | κΈ°μ |
|---|---|
| νλ μμν¬ | SvelteKit 2 + Svelte 5 |
| μΈμ΄ | TypeScript |
| λ°μ΄ν°λ² μ΄μ€ | SQLite (better-sqlite3) |
| ORM | Drizzle ORM |
| μΈμ¦ | JWT (jose λΌμ΄λΈλ¬λ¦¬, HS256) |
| UI | shadcn-svelte (bits-ui κΈ°λ°), Tailwind CSS 4, Tabler Icons |
| ν μ΄λΈ | TanStack Table |
| μ°¨νΈ | LayerChart (D3 κΈ°λ°) |
| ν μ€νΈ | Vitest (λ¨μ/ν΅ν©), Playwright (E2E) |
| CI/CD | GitHub Actions (ν μ€νΈ β Docker λΉλ β ghcr.io λ°°ν¬) |
| λ°°ν¬ | Docker (Node.js 20 Alpine) |
src/
βββ hooks.server.ts # μΈμ¦ λ―Έλ€μ¨μ΄ (JWT κ²μ¦, HTTPS 리λ€μ΄λ νΈ)
βββ service-worker.ts # PWA μλΉμ€ μ컀
βββ lib/
β βββ server/
β β βββ db/
β β β βββ index.ts # DB μ°κ²° (WAL λͺ¨λ, FK νμ±ν)
β β β βββ schema.ts # Drizzle ν
μ΄λΈ μ€ν€λ§ μ μ
β β βββ auth.ts # JWT ν ν° μμ±/κ²μ¦
β β βββ file-storage.ts # νμΌ μ
λ‘λ/λ€μ΄λ‘λ/μμ
β β βββ crud-helpers.ts # κ³΅ν΅ CRUD ν¬νΌ (soft delete, νμΌ μ²λ¦¬)
β βββ components/
β βββ clients-table.svelte # κ³ κ° κ΄λ¦¬ ν
μ΄λΈ
β βββ contracts-table.svelte # κ³μ½ κ΄λ¦¬ ν
μ΄λΈ
β βββ products-table.svelte # μ ν κ΄λ¦¬ ν
μ΄λΈ
β βββ firmware-table.svelte # νμ¨μ΄ κ΄λ¦¬ ν
μ΄λΈ
β βββ as-table.svelte # AS(μ§νμ€) ν
μ΄λΈ
β βββ completed-as-table.svelte # AS(μλ£) ν
μ΄λΈ
β βββ incomplete-as-table.svelte # AS(λ―Έμλ£) ν
μ΄λΈ
β βββ pre-sales-table.svelte # μ¬μ μμ
ν
μ΄λΈ
β βββ home-contract-dialog.svelte # ν κ³μ½ μμΈ λ€μ΄μΌλ‘κ·Έ
β βββ home-as-dialog.svelte # ν AS μμΈ λ€μ΄μΌλ‘κ·Έ
β βββ data-table/utils.ts # μ νλ²νΈ/μ΄λ©μΌ ν¬λ§·ν
Β·κ²μ¦ μ νΈ
β βββ ui/
β βββ file-upload-field.svelte # μ¬μ¬μ© νμΌ μ
λ‘λ μ»΄ν¬λνΈ
β βββ contact-section.svelte # μ¬μ¬μ© λ΄λΉμ μ 보 νΌ
β βββ ... # shadcn-svelte κΈ°λ° UI μ»΄ν¬λνΈ
βββ routes/
β βββ (auth)/login/ # λ‘κ·ΈμΈ νμ΄μ§
β βββ (app)/
β β βββ +page.* # ν (λμ보λ)
β β βββ clients/ # κ³ κ° κ΄λ¦¬
β β βββ contracts/ # κ³μ½ κ΄λ¦¬
β β β βββ contract-helpers.ts # κ³μ½ μ μ© ν¬νΌ (λ΄λΉμΒ·νμ λ°μ΄ν° μ²λ¦¬)
β β βββ products/ # μ ν κ΄λ¦¬
β β βββ firmware/ # νμ¨μ΄(νλ‘ν μ½) κ΄λ¦¬
β β βββ as/ # AS κ΄λ¦¬
β β βββ settings/ # μ€μ (λΉλ°λ²νΈ λ³κ²½)
β β βββ logout/ # λ‘κ·Έμμ
β βββ api/ # REST API μλν¬μΈνΈ
β β βββ clients/ # κ³ κ° API
β β βββ contracts/[id]/ # κ³μ½ μμΈ API
β β βββ products/ # μ ν API
β β βββ firmware/ # νμ¨μ΄ API
β β βββ as/ # AS API
β β βββ files/ # νμΌ λ€μ΄λ‘λ API
β β βββ settings/ # μ€μ API
β βββ healthcheck/ # ν¬μ€μ²΄ν¬ μλν¬μΈνΈ
e2e/ # Playwright E2E ν
μ€νΈ
βββ auth.test.ts # μΈμ¦ νλ¦ (λ‘κ·ΈμΈ/λ‘κ·Έμμ)
βββ navigation.test.ts # νμ΄μ§ λ€λΉκ²μ΄μ
βββ products.test.ts # μ ν CRUD
βββ clients.test.ts # κ³ κ° CRUD
βββ settings.test.ts # μ€μ νμ΄μ§
drizzle/
βββ 0000_*.sql ~ 0011_*.sql # SQL λ§μ΄κ·Έλ μ΄μ
νμΌ
βββ meta/ # Drizzle λ§μ΄κ·Έλ μ΄μ
λ©νλ°μ΄ν°
scripts/
βββ build-docker-image.sh # Docker μ΄λ―Έμ§ λΉλ μ€ν¬λ¦½νΈ
βββ docker-entrypoint.sh # Docker μνΈλ¦¬ν¬μΈνΈ (λ§μ΄κ·Έλ μ΄μ
β μ± μμ)
| ν μ΄λΈ | μ€λͺ |
|---|---|
clients |
κ³ κ°μ¬ (μ¬μ μλ±λ‘λ²νΈ, μ£Όμ, λ΄λΉμ λ±) |
contracts |
κ³μ½ (κ³ κ°Β·λ°μ£Όμ² μ°κ²°, κΈμ‘, κΈ°κ°, μ€μΉμ 보 λ±) |
products |
μ ν (κ΄λ¦¬μ½λ, λ¨κ°, λ²μ ) |
protocols |
νμ¨μ΄/νλ‘ν μ½ (λ²μ , νμΌ) |
files |
νμΌ μ μ₯μ (FILE_LIST_IDλ‘ μν°ν°μ μ°κ²°) |
repeaters |
μ€κ³κΈ° ID (κ³μ½μ μ’ μ) |
install_products |
μ€μΉμ ν (κ³μ½μ μ’ μ, μ νΒ·νλ‘ν μ½ μ°Έμ‘°) |
rooms |
κ°μ€ ID (κ³μ½μ μ’ μ) |
as_records |
AS μ΄λ ₯ (μμ²/λμ λ΄μ©, λΉμ©, μλ£ μ¬λΆ) |
product_inventory |
μ ν μ μΆκ³ κΈ°λ‘ |
passwords |
λ‘κ·ΈμΈ λΉλ°λ²νΈ (scrypt ν΄μ) |
settings |
μμ€ν μ€μ (key-value) |
λͺ¨λ μ£Όμ μν°ν°λ deleted_at 컬λΌμ μ¬μ©ν μννΈ μμ λ₯Ό μ μ©ν©λλ€. λ°μ΄ν°λ₯Ό 물리μ μΌλ‘ μμ νμ§ μκ³ μμ μκ°μ κΈ°λ‘νλ©°, μ‘°ν μ deleted_at IS NULL 쑰건μΌλ‘ νν°λ§ν©λλ€.
νμΌμ FILE_LIST_ID(UUID)λ₯Ό ν΅ν΄ μν°ν°μ μ°κ²°λ©λλ€. νλμ FILE_LIST_IDμ μ¬λ¬ νμΌμ΄ μ°κ²°λ μ μμΌλ©°, 물리 νμΌμ uploads/ λλ ν 리μ μ μ₯λ©λλ€.
- λ¨μΌ λΉλ°λ²νΈ λ°©μ (μ¬λ΄ μμ€ν μ©)
- JWT ν ν° (HS256, 7μΌ λ§λ£)μ μΏ ν€μ μ μ₯
hooks.server.tsμμ λͺ¨λ μμ²μ λν΄ μΈμ¦ κ²μ¦
hooks.server.tsμ handleErrorμμ μλ² μλ¬λ₯Ό νμμ€ν¬ν, κ²½λ‘μ ν¨κ» λ‘κΉ
ν©λλ€. 404λ μ μΈνλ©°, κ°λ° νκ²½μμλ μμΈ μλ¬ λ©μμ§λ₯Ό, νλ‘λμ
μμλ μΌλ° λ©μμ§λ₯Ό ν΄λΌμ΄μΈνΈμ λ°νν©λλ€.
νλ‘λμ
νκ²½μμ HTTP μ μ μ HTTPS μλ²μ /healthcheckλ‘ νλ‘λΈλ₯Ό λ³΄λ΄ μλ΅μ΄ μμ λλ§ λ¦¬λ€μ΄λ νΈν©λλ€. HTTPSκ° λ€μ΄λμμ λλ HTTPλ‘ μ μ μ μ κ°λ₯ν©λλ€.
- Node.js 20 μ΄μ
- npm
# μμ‘΄μ± μ€μΉ
npm install
# νκ²½λ³μ μ€μ
cp .env.example .env
# .env νμΌμ μ΄μ΄ κ°μ μ€μ :
# DATABASE_URL=local.db # SQLite νμΌ κ²½λ‘
# JWT_SECRET=your-secret-key # JWT μλͺ
ν€ (32μ μ΄μ κΆμ₯)
# PRODUCTION_DOMAIN= # νλ‘λμ
λλ©μΈ (HTTPS 리λ€μ΄λ νΈμ©, κ°λ° μ λΉμλ )
# κ°λ° μλ² μ€ν
npm run devκ°λ° μλ²κ° http://localhost:5173μμ μ€νλ©λλ€.
μ²μ μ€ν μ passwords ν
μ΄λΈμ΄ λΉμ΄μμΌλ―λ‘ λ‘κ·ΈμΈν μ μμ΅λλ€. μλ λ°©λ² μ€ νλλ‘ λΉλ°λ²νΈλ₯Ό μ€μ νμΈμ:
- μ€μ νμ΄μ§ μ¬μ©: μ΄λ―Έ λ‘κ·ΈμΈλ μνμμ
/settingsμμ λΉλ°λ²νΈλ₯Ό λ³κ²½ν μ μμ΅λλ€. - DB μ§μ μ κ·Ό: κ°λ° νκ²½μμ SQLite DBμ μ§μ scrypt ν΄μ λΉλ°λ²νΈλ₯Ό μ½μ ν©λλ€.
# νλ‘λμ
λΉλ
npm run build
# λΉλ κ²°κ³Ό μ€ν
npm run start./scripts/build-docker-image.sh
# λλ
docker build -t cms:latest .docker run --rm -p 3000:3000 \
-v ./data:/app/data \
-v ./uploads:/app/uploads \
-e DATABASE_URL=/app/data/sqlite.db \
-e JWT_SECRET=your-secret-key \
cms:latest| 컨ν μ΄λ κ²½λ‘ | μ©λ |
|---|---|
/app/data |
SQLite λ°μ΄ν°λ² μ΄μ€ νμΌ |
/app/uploads |
μ λ‘λλ νμΌ μ μ₯μ |
Docker 컨ν
μ΄λ μμ μ docker-entrypoint.shκ° μλμΌλ‘ Drizzle λ§μ΄κ·Έλ μ΄μ
μ μ€νν λ€ μ±μ μμν©λλ€.
Container Managerμμ μμ λμΌν μ€μ μΌλ‘ 컨ν μ΄λλ₯Ό μμ±ν©λλ€. λ³Όλ₯¨ λ§μ΄νΈ κ²½λ‘λ₯Ό NASμ 곡μ ν΄λμ λ§κ² μ‘°μ νμΈμ.
| λ³μ | νμ | μ€λͺ | κΈ°λ³Έκ° |
|---|---|---|---|
DATABASE_URL |
O | SQLite νμΌ κ²½λ‘ | - |
JWT_SECRET |
O | JWT μλͺ ν€ | - |
PRODUCTION_DOMAIN |
X | νλ‘λμ λλ©μΈ (HTTPS 리λ€μ΄λ νΈμ©) | '' |
BODY_SIZE_LIMIT |
X | μ λ‘λ νμΌ ν¬κΈ° μ ν (λ°μ΄νΈ) | 524288 |
PORT |
X | μλ² ν¬νΈ | 3000 |
| λͺ λ Ή | μ€λͺ |
|---|---|
npm run dev |
κ°λ° μλ² μ€ν |
npm run build |
νλ‘λμ λΉλ |
npm run start |
λΉλλ μ± μ€ν |
npm run check |
Svelte/TypeScript νμ κ²μ¬ |
npm run lint |
ESLint + Prettier κ²μ¬ |
npm run format |
Prettier μλ ν¬λ§· |
npm run test |
Vitest λ¨μ/ν΅ν© ν μ€νΈ |
npm run test:e2e |
Playwright E2E ν μ€νΈ |
npm run test:all |
λ¨μ + E2E μ 체 ν μ€νΈ |
npm run db:generate |
μ€ν€λ§ λ³κ²½μμ λ§μ΄κ·Έλ μ΄μ SQL μμ± |
npm run db:migrate |
λ§μ΄κ·Έλ μ΄μ μ μ© |
npm run db:push |
μ€ν€λ§λ₯Ό DBμ μ§μ νΈμ (κ°λ°μ©) |
npm run db:studio |
Drizzle Studio (DB λΈλΌμ°μ ) |
μλ² μ νΈλ¦¬ν°, API λΌμ°νΈ, νμ΄μ§ μ‘μ , ν΄λΌμ΄μΈνΈ μ νΈλ¦¬ν° λ± 201κ° ν μ€νΈλ₯Ό ν¬ν¨ν©λλ€.
npm testμ£Όμ ν μ€νΈ νμΌ:
src/lib/server/*.test.tsβ μΈμ¦, λΉλ°λ²νΈ, νμΌ μ μ₯, CRUD ν¬νΌsrc/routes/**/server.test.tsβ API μλν¬μΈνΈ λ° νμ΄μ§ μ‘μ src/lib/components/data-table/*.test.tsβ νΌ/νμΌ μ νΈλ¦¬ν°src/hooks.server.test.tsβ μΈμ¦ λ―Έλ€μ¨μ΄
Playwrightλ‘ λΈλΌμ°μ κΈ°λ° E2E ν μ€νΈλ₯Ό μ€νν©λλ€. μ€ν μ μ±μ λΉλνκ³ ν리뷰 μλ²λ₯Ό μλ μμν©λλ€.
# Playwright λΈλΌμ°μ μ€μΉ (μ΅μ΄ 1ν)
npx playwright install chromium
# E2E ν
μ€νΈ μ€ν
npm run test:e2eGitHub Actionsλ‘ main λΈλμΉμ push μ μλ μ€νλ©λλ€.
- ν
μ€νΈ β
npm testλ‘ λ¨μ/ν΅ν© ν μ€νΈ μ€ν - λΉλ λ° λ°°ν¬ β ν
μ€νΈ ν΅κ³Ό μ Docker μ΄λ―Έμ§ λΉλ ν
ghcr.ioμ νΈμ
μν¬νλ‘ νμΌ: .github/workflows/docker-publish.yml
Drizzle ORMμ λ§μ΄κ·Έλ μ΄μ μμ€ν μ μ¬μ©ν©λλ€.
# μ€ν€λ§ λ³κ²½ ν λ§μ΄κ·Έλ μ΄μ
νμΌ μμ±
npm run db:generate
# λ§μ΄κ·Έλ μ΄μ
μ μ© (κ°λ° νκ²½)
npm run db:migrate
# Drizzle Studio (DB λΈλΌμ°μ )
npm run db:studioDocker νκ²½μμλ 컨ν μ΄λ μμ μ μλμΌλ‘ λ§μ΄κ·Έλ μ΄μ μ΄ μ€νλ©λλ€.