Skip to content

Latest commit

ย 

History

History
417 lines (309 loc) ยท 20.5 KB

File metadata and controls

417 lines (309 loc) ยท 20.5 KB

๋ฐฐํฌยท์šด์˜

์ด ๋ฌธ์„œ๋Š” Minesweeper(์ฑ„์šฉ ์ดํ•ด์ถฉ๋Œ ๊ด€๊ณ„์ž ์ถ”์ถœ ์‹œ์Šคํ…œ)๋ฅผ ํ”„๋กœ๋•์…˜์—์„œ ๊ธฐ๋™ํ•˜๊ณ  ์šด์˜ํ•˜๋Š” ์ ˆ์ฐจ๋ฅผ ๋‹ค๋ฃฌ๋‹ค. ์‹œ์Šคํ…œ์€ ๋‘ ๊ฐœ์˜ ์žฅ๊ธฐ ์‹คํ–‰ ํ”„๋กœ์„ธ์Šค๋กœ ๊ตฌ์„ฑ๋œ๋‹ค.

ํ”„๋กœ์„ธ์Šค ์—ญํ•  ๊ธฐ๋™ ๋ช…๋ น
web Next.js 14 App Router ์„œ๋ฒ„ (์—…๋กœ๋“œ API + UI + ์กฐํšŒ/๊ฒ€์ˆ˜ ํ™”๋ฉด) next start
worker tsx ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์›Œ์ปค. ํ๋ฅผ ํด๋งํ•˜๋ฉฐ 4๋‹จ ํŒŒ์ดํ”„๋ผ์ธ(Ingestโ†’Typeโ†’Extractโ†’Aggregate)์„ ์‹คํ–‰ tsx src/worker/index.ts

๋‘ ํ”„๋กœ์„ธ์Šค๋Š” ๊ฐ™์€ ๋จธ์‹ ์—์„œ ๋™์ผํ•œ DATABASE_URL(embedded libsql ํŒŒ์ผ)๊ณผ UPLOAD_DIR์„ ๊ณต์œ ํ•œ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋‹จ์ผ ํŒŒ์ผ(./data/minesweeper.db)์ด๋ฏ€๋กœ, ๋‘ ํ”„๋กœ์„ธ์Šค๋Š” ๋ฐ˜๋“œ์‹œ ๊ฐ™์€ ํŒŒ์ผ์‹œ์Šคํ…œ(๊ฐ™์€ ํ˜ธ์ŠคํŠธ)์— ์žˆ์–ด์•ผ ํ•œ๋‹ค.

๊ด€๋ จ ๋ฌธ์„œ: ์ถ”์ถœ๊ธฐ ์…‹์—…(extractors) ยท ๋ณด์•ˆ(security) ยท ๊ฐœ๋ฐœ ํ™˜๊ฒฝ(development) ยท ํŒŒ์ดํ”„๋ผ์ธ(pipeline)


1. ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ / ๊ธฐ๋™

1.1 ๋นŒ๋“œ

package.json์˜ ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค(๋ฐœ์ทŒ).

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "worker": "tsx src/worker/index.ts",
  "dev:all": "concurrently -n web,worker -c blue,green \"next dev\" \"tsx watch src/worker/index.ts\"",
  "db:generate": "drizzle-kit generate",
  "db:migrate": "tsx src/db/migrate.ts",
  "typecheck": "tsc --noEmit",
  "test": "vitest run"
}

ํ”„๋กœ๋•์…˜ ์ ˆ์ฐจ:

npm ci                 # lockfile ๊ธฐ์ค€ ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ์„ค์น˜
npm run db:migrate     # ./drizzle ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ DATABASE_URL ์— ์ ์šฉ
npm run build          # next build (RSC/route ์ปดํŒŒ์ผ)

๋นŒ๋“œ ์•ˆ์ „์„ฑ ์ฃผ์˜. src/db/client.ts์˜ getDb()๋Š” lazy singleton์ด๋‹ค. ๋ชจ๋“ˆ์„ import ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋Š” DB ํŒŒ์ผ์„ ์—ด์ง€ ์•Š์œผ๋ฉฐ, ์ฒซ ์‹ค์ œ ์‚ฌ์šฉ ์‹œ์ ์— ์—ฐ๊ฒฐํ•œ๋‹ค.

// src/db/client.ts
let _db: DB | null = null;
export function getDb(): DB {
  if (!_db) _db = createDb(defaultUrl);
  return _db;
}

์ด ์„ค๊ณ„ ๋•๋ถ„์— next build๊ฐ€ ์„œ๋ฒ„ ๋ชจ๋“ˆ์„ ํ‰๊ฐ€ํ•  ๋•Œ ํŒŒ์ผ ํ•ธ๋“ค์„ ์žก์ง€ ์•Š๋Š”๋‹ค. ๋˜ํ•œ next.config.mjs๋Š” ๋„ค์ดํ‹ฐ๋ธŒ/์„œ๋ฒ„ ์ „์šฉ ํŒจํ‚ค์ง€(@libsql/client, libsql, pdfjs-dist, exceljs, adm-zip)๋ฅผ serverComponentsExternalPackages๋กœ ๋นผ์„œ ํด๋ผ์ด์–ธํŠธ/RSC ๋ฒˆ๋“ค์— ๋Œ๋ ค๋“ค์–ด๊ฐ€์ง€ ์•Š๊ฒŒ ํ•œ๋‹ค. ๊ฐ™์€ ํŒŒ์ผ์—์„œ eslint: { ignoreDuringBuilds: true }๋กœ ๋‘์–ด, lint ์„ค์ •์ด ์—†๋Š” ์‹ ์„ ํ•œ ํด๋ก ์—์„œ๋„ next build๊ฐ€ ๋ง‰ํžˆ์ง€ ์•Š๋Š”๋‹ค(ํƒ€์ž… ์•ˆ์ „์„ฑ์€ npm run typecheck๋กœ ๋ณ„๋„ ๊ฐ•์ œ).

1.2 ๊ธฐ๋™ (web + worker)

๋‘ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋™์‹œ์— ๋„์›Œ์•ผ ํ•œ๋‹ค. web๋งŒ ๋„์šฐ๋ฉด ์—…๋กœ๋“œ๋Š” ํ์— ์Œ“์ด์ง€๋งŒ ์˜์›ํžˆ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š๋Š”๋‹ค.

์˜ต์…˜ A โ€” concurrently (๋‹จ์ผ ํ˜ธ์ŠคํŠธ, ๋‹จ์ˆœ ์šด์˜). ์ด๋ฏธ devDependency๋กœ ๋“ค์–ด์žˆ๋Š” concurrently๋ฅผ ํ”„๋กœ๋•์…˜ ๊ธฐ๋™์—๋„ ์“ธ ์ˆ˜ ์žˆ๋‹ค(dev:all์€ dev ์ „์šฉ์ด๋ฏ€๋กœ ํ”„๋กœ๋•์…˜์€ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ณ„๋„ ์กฐํ•ฉ ์‚ฌ์šฉ).

npx concurrently -n web,worker -c blue,green \
  "next start" \
  "tsx src/worker/index.ts"

์˜ต์…˜ B โ€” pm2 (๊ถŒ์žฅ: ์ž๋™ ์žฌ์‹œ์ž‘ยท๋กœ๊ทธยท์ƒ์‹œ ์šด์˜).

์ €์žฅ์†Œ์— ์‹ค์ œ ecosystem.config.cjs๊ฐ€ ํฌํ•จ๋ผ ์žˆ๋‹ค โ€” web(:3100) + worker, envยท๋กœ๊ทธ ๊ฒฝ๋กœ(data/logs/)๊นŒ์ง€ ์ •์˜๋ผ ์žˆ๋‹ค.

npm run build
pm2 start ecosystem.config.cjs   # minesweeper-web(:3100) + minesweeper-worker
pm2 save                         # ํ”„๋กœ์„ธ์Šค ๋ชฉ๋ก ์ €์žฅ(์žฌ์‹œ์ž‘ ๋ณต๊ตฌ)
pm2 startup                      # (์„ ํƒ) ๋ถ€ํŒ… ์ž๋™๊ธฐ๋™ โ€” ์ถœ๋ ฅ๋œ sudo ๋ช…๋ น ์‹คํ–‰ ํ›„ pm2 save
pm2 logs minesweeper-worker
๋ช…๋ น ์„ค๋ช…
pm2 status ํ”„๋กœ์„ธ์Šค ์ƒํƒœ
pm2 restart minesweeper-web ์žฌ์‹œ์ž‘
MINESWEEPER_PORT=<ํฌํŠธ> pm2 restart ecosystem.config.cjs --update-env ํฌํŠธ ๋ณ€๊ฒฝ
  • ๊ธฐ๋ณธ ํฌํŠธ 3100 (PORT / MINESWEEPER_PORT). ๋กœ๊ทธ ํŒŒ์ผ์€ data/logs/.
  • env(DATABASE_URLยทUPLOAD_DIRยทEXTRACTOR_MODEยทVLM_*)๋Š” ecosystem.config.cjs์˜ env ๋ธ”๋ก์— ์ •์˜๋ผ ๋‘ ์•ฑ์ด ๋™์ผํ•˜๊ฒŒ ๋ณธ๋‹ค.
  • ์ถ”์ถœ๊ธฐ ์ „ํ™˜: EXTRACTOR_MODE๋ฅผ stub/hybrid/vlm/ensemble ์ค‘ ํ•˜๋‚˜๋กœ ๋ฐ”๊พผ ๋’ค pm2 restart ecosystem.config.cjs --update-env. (ํ˜„์žฌ ์šด์šฉ๊ฐ’: vlm โ€” ์‹ค๋ฌธ์„œ ์ €์ž๋ธ”๋ก/์ธ์ค€ ํŽ˜์ด์ง€๋Š” LLM์ด๋ผ์•ผ ์ถ”์ถœ๋จ, stub์€ 0๊ฑด)

1.3 ์›Œ์ปค์˜ ๋™์ž‘ ๋ชจ๋ธ

์›Œ์ปค(src/worker/index.ts)๋Š” ๋‹จ์ผ ํ๋ฅผ ๋ฌดํ•œ ํด๋งํ•œ๋‹ค. ํ•œ ํ‹ฑ์— ์ตœ๋Œ€ ํ•œ ๊ฑด์˜ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

// src/worker/index.ts
export async function runWorkerTick(db: DB, extractor?: Extractor): Promise<string | null> {
  const job = await claimNextJob(db);
  if (!job) return null;
  try {
    await processApplicant(db, job.payload.applicantId, extractor);
    await completeJob(db, job.id);
  } catch (err) {
    await failJob(db, job.id, err instanceof Error ? err.message : String(err));
  }
  return job.id;
}

๊ธฐ๋™ ์‹œ ๋กœ๊ทธ๋กœ ํด๋ง ์ฃผ๊ธฐ์™€ ์ถ”์ถœ๊ธฐ ๋ชจ๋“œ๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.

[worker] polling every 2000ms (extractor=stub)
ํ ๋น„์—ˆ๋‚˜? โ”€โ”€noโ”€โ”€โ–ถ claimNextJob โ–ถ processApplicant โ–ถ completeJob/failJob โ–ถ (์ฆ‰์‹œ ๋‹ค์Œ ํ‹ฑ)
   โ”‚
  yes
   โ–ผ
sleep(WORKER_POLL_INTERVAL_MS) โ–ถ ๋‹ค์‹œ ํด๋ง

์ž‘์—…์ด ์žˆ์œผ๋ฉด ๊ณง๋ฐ”๋กœ ๋‹ค์Œ ํ‹ฑ์œผ๋กœ ์ด์–ด์ง€๊ณ , ๋น„์—ˆ์„ ๋•Œ๋งŒ WORKER_POLL_INTERVAL_MS ๋งŒํผ ์‰ฐ๋‹ค. ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋†’์ด๋ ค๋ฉด ์›Œ์ปค ์ธ์Šคํ„ด์Šค๋ฅผ ์ˆ˜ํ‰ ํ™•์žฅํ•˜๋ฉด ๋œ๋‹ค(๊ฐ™์€ DB๋ฅผ ๋ณด๋Š” ์—ฌ๋Ÿฌ ์›Œ์ปค ํ”„๋กœ์„ธ์Šค). claimNextJob์ด ์ž‘์—… ํด๋ ˆ์ž„์„ ๋‹ด๋‹นํ•˜๋ฏ€๋กœ ๋™์ผ ์ž‘์—…์˜ ์ค‘๋ณต ์ฒ˜๋ฆฌ๋ฅผ ๋ฐฉ์ง€ํ•œ๋‹ค โ€” ์ž์„ธํ•œ ํ ์˜๋ฏธ๋ก ์€ pipeline.md ์ฐธ์กฐ.


2. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ „์ฒด ๋ ˆํผ๋Ÿฐ์Šค

๊ฐ’๊ณผ ๊ธฐ๋ณธ๊ฐ’์€ ๋ชจ๋‘ .env.example ๋ฐ ์ฝ”๋“œ์˜ fallback์—์„œ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์˜จ ๊ฒƒ์ด๋‹ค.

๋ณ€์ˆ˜ ๊ธฐ๋ณธ๊ฐ’ ์ฝ๋Š” ๊ณณ ์˜๋ฏธ
DATABASE_URL file:./data/minesweeper.db src/db/client.ts, src/db/migrate.ts embedded libsql DB ์œ„์น˜. file: ์ ‘๋‘ ํŒŒ์ผ ๊ฒฝ๋กœ ๋˜๋Š” :memory:(ํ…Œ์ŠคํŠธ). ๋งˆ์ด๊ทธ๋ ˆ์ดํ„ฐ๋Š” file: ๊ฒฝ๋กœ์˜ ์ƒ์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค.
UPLOAD_DIR ./data/uploads ์—…๋กœ๋“œ/์›๋ณธ/ํฌ๋กญ ์ €์žฅ ์—…๋กœ๋“œ zip, ์ถ”์ถœ๋œ ์›๋ณธ, ํฌ๋กญ ์ด๋ฏธ์ง€๊ฐ€ ์ €์žฅ๋˜๋Š” ๋กœ์ปฌ ๋””๋ ‰ํ„ฐ๋ฆฌ.
EXTRACTOR_MODE stub src/worker/index.ts ๋“ฑ Stage 3 ์ถ”์ถœ๊ธฐ ์„ ํƒ. stub=๊ฒฐ์ •์  ํœด๋ฆฌ์Šคํ‹ฑ(๊ธฐ๋ณธยทGPU ๋ถˆํ•„์š”ยทํ…Œ์ŠคํŠธ์šฉ), hybrid=ํ…์ŠคํŠธ๋Š” stubยท์ด๋ฏธ์ง€๋Š” VLM OCR, vlm=์ „์ˆ˜ ์˜จํ”„๋ ˆ ๋น„์ „/LLM(์šด์šฉ๊ฐ’), ensemble=๋‹ค์ค‘ ๋ชจ๋ธ ํˆฌํ‘œ. extractors.md ์ฐธ์กฐ.
VLM_BASE_URL http://localhost:11434/v1 VLM ์ถ”์ถœ๊ธฐ OpenAI ํ˜ธํ™˜ ์—”๋“œํฌ์ธํŠธ base URL. ๊ธฐ๋ณธ๊ฐ’์€ ๋กœ์ปฌ Ollama๋ฅผ ๊ฐ€๋ฆฌํ‚จ๋‹ค.
VLM_API_KEY ollama VLM ์ถ”์ถœ๊ธฐ OpenAI ํ˜ธํ™˜ API ํ‚ค. Ollama๋Š” ์ž„์˜ ๋ฌธ์ž์—ด ํ—ˆ์šฉ(ollama). vLLM ๋“ฑ์—์„œ๋Š” ์‹ค์ œ ํ† ํฐ ์‚ฌ์šฉ.
VLM_MODEL qwen3.5:9B VLM ์ถ”์ถœ๊ธฐ ์‚ฌ์šฉํ•  ๋ชจ๋ธ ์ด๋ฆ„(์—”๋“œํฌ์ธํŠธ์— ๋ฏธ๋ฆฌ ๋ฐ›์•„๋‘” ๋ชจ๋ธ๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•จ).
VLM_TIMEOUT_MS 120000 VLM ์ถ”์ถœ๊ธฐ VLM ํ˜ธ์ถœ ํƒ€์ž„์•„์›ƒ(ms). ๊ธฐ๋ณธ 120์ดˆ.
WORKER_POLL_INTERVAL_MS 2000 src/worker/index.ts ํ๊ฐ€ ๋น„์—ˆ์„ ๋•Œ ํด๋ง ๊ฐ„๊ฒฉ(ms). Number(process.env.WORKER_POLL_INTERVAL_MS ?? 2000).
MAX_UPLOAD_BYTES 209715200 (200 MB) ์—…๋กœ๋“œ ๊ฐ€๋“œ zip ์—…๋กœ๋“œ ์ตœ๋Œ€ ํฌ๊ธฐ. zip-bomb/DoS ๋ฐฉ์ง€.
MAX_ZIP_ENTRIES 5000 ์—…๋กœ๋“œ ๊ฐ€๋“œ zip ๋‚ด ์ตœ๋Œ€ ํŒŒ์ผ ์ˆ˜.
MAX_ZIP_TOTAL_BYTES 524288000 (500 MB) ์—…๋กœ๋“œ ๊ฐ€๋“œ zip ์••์ถ• ํ•ด์ œ ํ›„ ๋ˆ„์  ์ตœ๋Œ€ ํฌ๊ธฐ.

EXTRACTOR_MODE=stub์ด๋ฉด VLM_* ๋ณ€์ˆ˜๋Š” ๋ฌด์‹œ๋œ๋‹ค(stub์€ GPUยท๋„คํŠธ์›Œํฌ๊ฐ€ ํ•„์š” ์—†์Œ). ์—…๋กœ๋“œ/unzip ๊ฐ€๋“œ 3์ข…์˜ ๋ณด์•ˆ์  ๋ฐฐ๊ฒฝ์€ security.md๋ฅผ ์ฐธ์กฐ.

๊ธฐ๋ณธ .env ์‹œ์ž‘์ :

cp .env.example .env
# ํ•„์š”ํ•œ ๊ฐ’๋งŒ ์ˆ˜์ • (์˜ˆ: ํ”„๋กœ๋•์…˜ ์ ˆ๋Œ€๊ฒฝ๋กœ, EXTRACTOR_MODE=vlm ๋“ฑ)

3. ์˜จํ”„๋ ˆ VLM ์…‹์—… (OpenAI ํ˜ธํ™˜)

EXTRACTOR_MODE=vlm์€ OpenAI ํ˜ธํ™˜ ์ถ”๋ก  ์„œ๋ฒ„๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ์™ธ๋ถ€ ํด๋ผ์šฐ๋“œ๋กœ PII๋ฅผ ๋ณด๋‚ด์ง€ ์•Š๋„๋ก, ์˜จํ”„๋ ˆ/์‚ฌ๋‚ด๋ง์— ์„œ๋ฒ„๋ฅผ ๋‘”๋‹ค. ๋‘˜ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฅธ๋‹ค.

3.1 Ollama (๊ธฐ๋ณธ๊ฐ’๊ณผ ๊ฐ€์žฅ ์ž˜ ๋งž์Œ)

# 1) ์„ค์น˜ ํ›„ ์„œ๋น„์Šค ๊ธฐ๋™ (๊ธฐ๋ณธ ํฌํŠธ 11434, /v1 ๊ฐ€ OpenAI ํ˜ธํ™˜)
ollama serve

# 2) ๋ชจ๋ธ ๋ฐ›๊ธฐ (VLM_MODEL ๊ณผ ๋™์ผํ•œ ํƒœ๊ทธ)
ollama pull qwen3.5:9B

.env:

EXTRACTOR_MODE=vlm
VLM_BASE_URL=http://localhost:11434/v1
VLM_API_KEY=ollama          # Ollama ๋Š” ํ‚ค ๊ฒ€์ฆ ์•ˆ ํ•จ (์ž„์˜ ๋ฌธ์ž์—ด)
VLM_MODEL=qwen3.5:9B
VLM_TIMEOUT_MS=120000

3.2 vLLM (๋‹ค์ค‘ ๋™์‹œ์š”์ฒญยท๊ณ ์„ฑ๋Šฅ)

# OpenAI ํ˜ธํ™˜ ์„œ๋ฒ„๋กœ ๋ชจ๋ธ ์„œ๋น™ (์˜ˆ์‹œ)
python -m vllm.entrypoints.openai.api_server \
  --model <hf-org>/<vlm-model> \
  --port 8000

.env:

EXTRACTOR_MODE=vlm
VLM_BASE_URL=http://localhost:8000/v1
VLM_API_KEY=<your-token>
VLM_MODEL=<served-model-name>   # vLLM ์— ๋“ฑ๋ก๋œ ๋ชจ๋ธ ์ด๋ฆ„๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜
VLM_TIMEOUT_MS=120000

3.3 ์—ฐ๊ฒฐ ํ™•์ธ

curl -s "$VLM_BASE_URL/models" -H "Authorization: Bearer $VLM_API_KEY"

VLM_MODEL์ด ์‘๋‹ต ๋ชฉ๋ก์— ๋ณด์ด๋ฉด ์ •์ƒ์ด๋‹ค. ์ถ”์ถœ๊ธฐ ์ธํ„ฐํŽ˜์ด์Šคยทํ”„๋กฌํ”„ํŠธยทํ”Œ๋ž˜๊ทธ(seal|handwriting|signature|low_confidence|ambiguous|needs_vision) ์ฒ˜๋ฆฌ ๋ฐฉ์‹์€ extractors.md์—์„œ ๋‹ค๋ฃฌ๋‹ค.

์›์น™ ์žฌํ™•์ธ. ์ž๋™์ถ”์ถœ์€ ์ดˆ์•ˆ์ด๋ฉฐ ์ตœ์ข… ํŒ๋‹จ์€ ์‚ฌ๋žŒ์ด ํ•œ๋‹ค. VLM์€ ์ด๋ฆ„์„ ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค โ€” ๋ถˆํ™•์‹คํ•˜๋ฉด ํ”Œ๋ž˜๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ณ  ์‚ฌ๋žŒ ๊ฒ€์ˆ˜๋กœ ๋„˜๊ธด๋‹ค.

3.4 ๋กœ์ปฌ vLLM ์•™์ƒ๋ธ” (EXTRACTOR_MODE=ensemble)

์—ฌ๋Ÿฌ OCR-VLM์„ ๋กœ์ปฌ vLLM์œผ๋กœ ๋„์›Œ ํˆฌํ‘œ(์•™์ƒ๋ธ”)ํ•ฉ๋‹ˆ๋‹ค. ์™ธ๋ถ€ API ์—†์Œ.

# 1) ๋ชจ๋ธ ์บ์‹œ(๋””์Šคํฌ๋งŒ, GPU ๋ถˆํ•„์š”)
bash scripts/download-ocr-models.sh           # MODEL1/2/3 env ๋กœ ๊ต์ฒด ๊ฐ€๋Šฅ

# 2) 3๊ฐœ vLLM OpenAI ์„œ๋ฒ„ ๊ธฐ๋™ (ํฌํŠธ 8010/8011/8012) โ€” ์ž์œ  VRAM ํ•„์š”
bash scripts/serve-ocr.sh                      # VRAM ๋ถ€์กฑ/๊ณต์œ  ์ ์œ  ์‹œ ์ž๋™ abort

# 3) ์•™์ƒ๋ธ”๋กœ ์ „ํ™˜
#   ecosystem.config.cjs(or .env): EXTRACTOR_MODE=ensemble,
#   VLM_ENSEMBLE="http://localhost:8010/v1|MODEL1,http://localhost:8011/v1|MODEL2,http://localhost:8012/v1|MODEL3"
pm2 restart minesweeper-worker --update-env
  • ํ•ฉ์˜=์‹ ๋ขฐ๋„, ๋ถˆ์ผ์น˜=์‚ฌ๋žŒ ๊ฒ€ํ† (ํ•„ํ„ฐ๋ง). VLM_ENSEMBLE_MIN_VOTES๋กœ ์ €๋“ํ‘œ ๋“œ๋กญ ๊ฐ€๋Šฅ(๊ธฐ๋ณธ 1).
  • ๊ถŒ์žฅ ๊ธฐ๋ณธ ๋ชจ๋ธ: Qwen/Qwen2.5-VL-7B-Instruct, OpenGVLab/InternVL3-8B, zai-org/GLM-4.1V-9B-Thinking โ€” vLLM ์ง€์›ยทํ•œ๊ตญ์–ด/๋„์žฅ ์ •ํ™•๋„๋Š” ์‹ค์ œ ์ƒ˜ํ”Œ๋กœ ๊ฒ€์ฆ ํ›„ ํ™•์ •(model-evaluation.md).
  • VRAM: 7โ€“9B VLM ร—3 โ‰ˆ 48โ€“60GB โ†’ A40 2์žฅ(92GB) ์—ฌ์œ  ์‹œ ์ถฉ๋ถ„. ํ•œ ์žฅ์— ๋‹ค ๋ชป ์˜ฌ๋ฆฌ๋ฉด GPU1/2/3, VLM_GPU_UTIL๋กœ ์นด๋“œ๋ณ„ ๋ฐฐ์น˜.

3.5 ๋‹จ์ผ ๊ฒฝ๋Ÿ‰ VLM + ๋„์žฅ/์„œ๋ช… ๊ฐ์ง€ (ํ˜„์žฌ ์šด์šฉ) โ€” DETECT_MARKS

๋™์‹œ 3๋ชจ๋ธ ์•™์ƒ๋ธ”์€ VRAM ๋ถ€๋‹ด์ด ์ปค์„œ, ํ˜„์žฌ๋Š” ๋‹จ์ผ ๊ฒฝ๋Ÿ‰ VLM์œผ๋กœ ์šด์šฉํ•œ๋‹ค.

# GPU1(์—ฌ์œ  ์นด๋“œ)์— Qwen2.5-VL-7B ๋„์šฐ๊ธฐ (๋กœ์ปฌ, --api-key ์ž„์˜๊ฐ’)
CUDA_VISIBLE_DEVICES=1 /data/vllm/env/bin/vllm serve Qwen/Qwen2.5-VL-7B-Instruct \
  --served-model-name Qwen2.5-VL-7B-Instruct --port 8010 --api-key local \
  --gpu-memory-utilization 0.5 --max-model-len 16384 --limit-mm-per-prompt '{"image":4}'

# ์•ฑ ํ™˜๊ฒฝ(ecosystem): EXTRACTOR_MODE=hybrid(ํ…์ŠคํŠธ=stub/์ด๋ฏธ์ง€=VLM OCR) + ๊ฐ์ง€ on
#   DETECT_MARKS=1  VLM_BASE_URL=http://localhost:8010/v1  VLM_API_KEY=local  VLM_MODEL=Qwen2.5-VL-7B-Instruct
pm2 restart ecosystem.config.cjs --update-env
# ๋ผ์ด๋ธŒ ์Šค๋ชจํฌ: npm run detect:smoke   (์ด๋ฆ„ ์ถ”์ถœ + ๋„์žฅ bbox ๊ฐ์ง€)
  • ์ถ”์ถœ ๋ชจ๋“œ: EXTRACTOR_MODE=vlm(ํ˜„์žฌ) โ€” ๋ชจ๋“  ๋ฌธ์„œ๋ฅผ ๋กœ์ปฌ VLM์ด ์ถ”์ถœ. ์‹ค์ œ ๋…ผ๋ฌธ ์ €์ž๋ธ”๋กยทํ•œ๊ตญ์–ด ์ธ์ค€ ํŽ˜์ด์ง€๋Š” LLM์ด๋ผ์•ผ ์ฝํžŒ๋‹ค(๊ฒฐ์ •์  stub์€ ์‹ค๋ฌธ์„œ์—์„œ 0๊ฑด). hybrid=ํ…์ŠคํŠธ๋Š” stubยท์ด๋ฏธ์ง€๋Š” VLM, stub=GPU ๋ฏธ์‚ฌ์šฉ/ํ…Œ์ŠคํŠธ. VLM ๋ฏธ๊ฐ€๋™ ์‹œ ์ถ”์ถœ์€ ๋ฌธ์„œ๋ณ„๋กœ ์‹คํŒจ ๊ฐ•๋“ฑ(๊ฒ€ํ†  ํ”Œ๋ž˜๊ทธ)๋˜๋ฉฐ job์€ ์™„๋ฃŒ๋œ๋‹ค. (ยงextractors.md 3)
  • ๊ฐ์ง€(DETECT_MARKS=1): ๊ด€๋ จ ํŽ˜์ด์ง€ ๋ Œ๋” โ†’ VLM์— ๋„์žฅ/์„œ๋ช… ์œ„์น˜ ์งˆ์˜ โ†’ ํฌ๋กญ + ๊ฒ€ํ†  ํ(ยงextractors.md 3c).
  • GPU1 ์นด๋“œ 1์žฅ์ด ๋น„๋ฉด(์•„๋ž˜ systemd util ์กฐ์ • ์ฐธ๊ณ ) 7B VLM์€ util 0.5(~23GB)๋กœ ๋„‰๋„‰ํžˆ ์˜ฌ๋ผ๊ฐ„๋‹ค.

์žฌ๋ถ€ํŒ… ์œ ์ง€ โ€” systemd ๋“ฑ๋ก (๊ถŒ์žฅ)

์œ„ vllm ํ”„๋กœ์„ธ์Šค๋Š” nohup/์ˆ˜๋™์ด๋ฉด ์žฌ๋ถ€ํŒ… ์‹œ ์‚ฌ๋ผ์ง„๋‹ค. ์ฝ”๋” ์„œ๋น„์Šค์™€ ๋™์ผ ํŒจํ„ด์˜ ์œ ๋‹› ํŒŒ์ผ์„ deploy/vllm-ocr-8010.service๋กœ ์ œ๊ณตํ•œ๋‹ค. ์„ค์น˜(์ตœ์ดˆ 1ํšŒ, sudo ํ•„์š”):

sudo cp deploy/vllm-ocr-8010.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable vllm-ocr-8010.service          # ๋ถ€ํŒ… ์‹œ ์ž๋™ ์‹œ์ž‘
# ํ˜„์žฌ ์ˆ˜๋™ ์‹คํ–‰ ์ค‘์ด๋ฉด systemd ๊ด€๋ฆฌ๋กœ ์ „ํ™˜:
pkill -f 'vllm serve Qwen/Qwen2.5-VL-7B' ; sleep 3
sudo systemctl start vllm-ocr-8010.service
systemctl status vllm-ocr-8010.service --no-pager
curl -s localhost:8010/v1/models -H 'authorization: Bearer local'

์œ ๋‹›์€ GPU1(CUDA_VISIBLE_DEVICES=1)ยทHF_HOME=/home/mooner92/.cache/huggingface(๋ชจ๋ธ ์บ์‹œ ์œ„์น˜)๋ฅผ ์“ด๋‹ค. ๋“ฑ๋ก ์ „ nvidia-smi๋กœ GPU1์ด ์ด ๋ชจ๋ธ์šฉ์œผ๋กœ ๋งž๋Š”์ง€ ํ™•์ธํ•  ๊ฒƒ.

โ„น๏ธ GPU ๋ฉ”๋ชจ๋ฆฌ ํ™•๋ณด ํŒ: ๊ณต์œ  ์ฝ”๋” vLLM์ด --gpu-memory-utilization 0.85๋กœ ์นด๋“œ๋ฅผ ์„ ์ ํ•˜๋ฉด ์—ฌ์œ ๊ฐ€ ๊ฑฐ์˜ ์—†๋‹ค. ์†Œ์œ ์ž๊ฐ€ ํ•ด๋‹น systemd ์œ ๋‹›์˜ util์„ ๋‚ฎ์ถ”๊ฑฐ๋‚˜(์˜ˆ: 0.55) TP=1๋กœ ํ•œ ์นด๋“œ์— ๋ชฐ๋ฉด ๋‹ค๋ฅธ ์นด๋“œ๊ฐ€ ํ†ต์งธ๋กœ ๋นˆ๋‹ค. ํƒ€ ์‚ฌ์šฉ์ž ์„œ๋น„์Šค๋Š” ์ข…๋ฃŒ/๋ณ€๊ฒฝํ•˜์ง€ ๋ง ๊ฒƒ โ€” ๋ณธ์ธ ์†Œ์œ  ์„œ๋น„์Šค๋งŒ ์กฐ์ •.


4. GPU ๊ณ ๋ ค์‚ฌํ•ญ

๊ธฐ๋ณธ ์ถ”์ถœ๊ธฐ(stub)๋Š” GPU๊ฐ€ ์ „ํ˜€ ํ•„์š” ์—†๋‹ค. CI/ํ…Œ์ŠคํŠธ(51 tests)์™€ ๋กœ์ปฌ ๊ฐœ๋ฐœ์€ GPU ์—†์ด ์ „๋ถ€ ๋ˆ๋‹ค. GPU๋Š” EXTRACTOR_MODE=vlm์ผ ๋•Œ๋งŒ ์˜๋ฏธ๊ฐ€ ์žˆ๋‹ค.

  • OCR-VLM์€ ์†Œํ˜•(0.9B๊ธ‰)์œผ๋กœ ์ถฉ๋ถ„. ์ด ์‹œ์Šคํ…œ์˜ Stage 3์€ ํ•™์œ„๋…ผ๋ฌธยท๋Œ€ํ‘œ์—ฐ๊ตฌยท์ €๋„ยทh-index ๋“ฑ ๋ฌธ์„œ์—์„œ ์ด๋ฆ„/์—ญํ•  ํ›„๋ณด๋ฅผ ๋ฝ‘๋Š” OCR+๊ตฌ์กฐํ™” ์ž‘์—…์ด๋ผ, ๋Œ€ํ˜• ๋ฒ”์šฉ LLM์ด ์•„๋‹ˆ๋ผ ์ž‘์€ OCR ํŠนํ™” VLM์œผ๋กœ ์ถฉ๋ถ„ํžˆ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. .env.example์˜ ๊ธฐ๋ณธ ํƒœ๊ทธ(qwen3.5:9B)๋Š” ์‹œ์ž‘์ ์ผ ๋ฟ, ์šด์˜ ๋ถ€ํ•˜์— ๋งž์ถฐ ๋” ์ž‘์€/์ ํ•ฉํ•œ OCR-VLM์œผ๋กœ ๊ต์ฒดํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.
  • VRAM ๊ฐ€์ด๋“œ(๋Œ€๋žต). 0.9B๊ธ‰ ์–‘์žํ™” ๋ชจ๋ธ์€ ๋ณดํ†ต ์ˆ˜ GB VRAM์œผ๋กœ ๋‹จ์ผ GPU์—์„œ ์ถฉ๋ถ„ํžˆ ์„œ๋น™๋œ๋‹ค. 9B๊ธ‰์„ ๊ทธ๋Œ€๋กœ ์“ฐ๋ฉด ์–‘์žํ™” ์—ฌ๋ถ€์— ๋”ฐ๋ผ 8~20 GB๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋™์‹œ์„ฑ. ์›Œ์ปค๋Š” ํ•œ ํ‹ฑ์— 1๊ฑด์„ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ, ๋™์‹œ ์ถ”๋ก  ์š”์ฒญ์€ ๊ณง ๋™์‹œ์— ๊ธฐ๋™ํ•œ ์›Œ์ปค ์ธ์Šคํ„ด์Šค ์ˆ˜์™€ ๊ฐ™๋‹ค. ์—ฌ๋Ÿฌ ์›Œ์ปค๋ฅผ ๋„์šธ ๊ณ„ํš์ด๋ฉด vLLM์ฒ˜๋Ÿผ ๋ฐฐ์น˜/๋™์‹œ์š”์ฒญ์— ๊ฐ•ํ•œ ์„œ๋ฒ„๊ฐ€ ์œ ๋ฆฌํ•˜๋‹ค.
  • ํƒ€์ž„์•„์›ƒ. GPU๊ฐ€ ์•ฝํ•˜๋ฉด VLM_TIMEOUT_MS(๊ธฐ๋ณธ 120000)๋ฅผ ๋Š˜๋ ค์•ผ ํฐ ํŽ˜์ด์ง€์—์„œ ์‹คํŒจํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • CPU ํด๋ฐฑ. Ollama๋Š” GPU๊ฐ€ ์—†์œผ๋ฉด CPU๋กœ๋„ ๋™์ž‘ํ•˜์ง€๋งŒ ๋А๋ฆฌ๋‹ค โ€” ๊ฒ€์ˆ˜ ๋Œ€๊ธฐ์—ด์ด ๊ธธ์–ด์งˆ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ธฐ๋Œ€์น˜๋ฅผ ๋‚ฎ์ถฐ ์žก์•„๋ผ.

5. ๋ฐ์ดํ„ฐ ๋””๋ ‰ํ„ฐ๋ฆฌ ๋ฐฑ์—…

๋ชจ๋“  ์˜์† ์ƒํƒœ๋Š” ./data ํ•œ ๊ณณ์— ๋ชจ์ธ๋‹ค.

data/
โ”œโ”€โ”€ minesweeper.db      # libsql ์ž„๋ฒ ๋””๋“œ DB (applicants/jobs/extractions/people ...)
โ””โ”€โ”€ uploads/            # ์—…๋กœ๋“œ zip, ์ถ”์ถœ๋œ ์›๋ณธ, ํฌ๋กญ ์ด๋ฏธ์ง€ (UPLOAD_DIR)

DB์™€ ์—…๋กœ๋“œ ํŒŒ์ผ์€ ์„œ๋กœ๋ฅผ ์ฐธ์กฐํ•˜๋ฏ€๋กœ(์ถ”์ถœ ๋ ˆ์ฝ”๋“œ๊ฐ€ uploads/์˜ ํฌ๋กญ ๊ฒฝ๋กœ๋ฅผ ๊ฐ€๋ฆฌํ‚ด) ํ•ญ์ƒ ํ•จ๊ป˜ ์ผ๊ด€๋œ ์‹œ์ ์œผ๋กœ ๋ฐฑ์—…ํ•ด์•ผ ํ•œ๋‹ค.

๊ถŒ์žฅ ๋ฐฑ์—… ์ ˆ์ฐจ(์ •ํ•ฉ์„ฑ ์šฐ์„ ):

# 1) ์งง์€ ์ •์ง€๋กœ ์ •ํ•ฉ์„ฑ ๋ณด์žฅ (web + worker ๋ชจ๋‘ ๋ฉˆ์ถค)
pm2 stop minesweeper-web minesweeper-worker

# 2) data ๋””๋ ‰ํ„ฐ๋ฆฌ ํ†ต์งธ ์Šค๋ƒ…์ƒท
tar czf "backup-$(date +%F-%H%M).tar.gz" ./data

# 3) ์žฌ๊ธฐ๋™
pm2 start minesweeper-web minesweeper-worker

๋ฌด์ •์ง€ ๋ฐฑ์—…์ด ํ•„์š”ํ•˜๋ฉด libsql/SQLite ์˜จ๋ผ์ธ ๋ฐฑ์—…(์˜ˆ: VACUUM INTO๋กœ DB ์Šค๋ƒ…์ƒท ์ƒ์„ฑ) ํ›„ uploads/๋ฅผ ๋ณ„๋„ ๋ณต์‚ฌํ•œ๋‹ค. ๋‹จ, ์ด ๊ฒฝ์šฐ DB ์Šค๋ƒ…์ƒท ์‹œ์ ๊ณผ ํŒŒ์ผ ๋ณต์‚ฌ ์‹œ์  ์‚ฌ์ด์˜ ์‹ ๊ทœ ์—…๋กœ๋“œ๊ฐ€ ํ•œ์ชฝ์—๋งŒ ์กด์žฌํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์ •์ง€ ๋ฐฑ์—…์ด ๊ฐ€์žฅ ๋‹จ์ˆœยท์•ˆ์ „ํ•˜๋‹ค.

๋ณต์›์€ ์—ญ์ˆœ์ด๋‹ค: ํ”„๋กœ์„ธ์Šค ์ •์ง€ โ†’ data/ ๊ต์ฒด โ†’ ์žฌ๊ธฐ๋™. DB ์Šคํ‚ค๋งˆ๊ฐ€ ๋ฐ”๋€Œ์—ˆ๋‹ค๋ฉด ๋ณต์› ํ›„ npm run db:migrate๋ฅผ ํ•œ ๋ฒˆ ๋” ๋Œ๋ฆฐ๋‹ค(๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ๋ฉฑ๋“ฑ).


6. ์ด์‹์„ฑ (clone โ†’ npm i โ†’ migrate โ†’ ์‹คํ–‰)

์ƒˆ ํ˜ธ์ŠคํŠธ์— ์˜ฎ๊ธฐ๋Š” ๋ฐ ํ•„์š”ํ•œ ๊ฒƒ์€ ์ฝ”๋“œ์™€ ./data๋ฟ์ด๋‹ค. ์™ธ๋ถ€ ๊ด€๋ฆฌํ˜• DB๊ฐ€ ์—†๊ณ (์ž„๋ฒ ๋””๋“œ libsql ํŒŒ์ผ), ์ถ”์ถœ๊ธฐ๋„ ์˜จํ”„๋ ˆ๋ผ ํด๋ก  ํ›„ ๋„ค ๋‹จ๊ณ„๋ฉด ๋๋‚œ๋‹ค.

git clone <repo> minesweeper && cd minesweeper
npm ci                 # (๋˜๋Š” npm i) ์˜์กด์„ฑ ์„ค์น˜
cp .env.example .env   # ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •
npm run db:migrate     # ์Šคํ‚ค๋งˆ ์ ์šฉ (data/ ๋””๋ ‰ํ„ฐ๋ฆฌ ์ž๋™ ์ƒ์„ฑ)

src/db/migrate.ts๋Š” DATABASE_URL์ด file:์ด๋ฉด ์ƒ์œ„ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•œ๋‹ค.

// src/db/migrate.ts (main)
const url = process.env.DATABASE_URL ?? 'file:./data/minesweeper.db';
if (url.startsWith('file:')) {
  const filePath = url.slice('file:'.length);
  if (filePath && filePath !== ':memory:') {
    mkdirSync(dirname(filePath) || '.', { recursive: true });
  }
}

๊ทธ ๋‹ค์Œ ๋นŒ๋“œยท๊ธฐ๋™:

npm run build
npx concurrently -n web,worker -c blue,green "next start" "tsx src/worker/index.ts"
# ๋˜๋Š” pm2 (ยง1.2 ์˜ต์…˜ B)

์ƒˆ ํ˜ธ์ŠคํŠธ๋กœ ๋ฐ์ดํ„ฐ๊นŒ์ง€ ๊ทธ๋Œ€๋กœ ์ด์ „ํ•˜๋ ค๋ฉด, ์œ„ ์ ˆ์ฐจ์—์„œ npm run db:migrate ๋Œ€์‹  ๊ธฐ์กด ./data ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋ณต์‚ฌํ•ด ๋„ฃ์œผ๋ฉด ๋œ๋‹ค(ํ•„์š” ์‹œ ๊ทธ ํ›„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ 1ํšŒ).

๊ฒ€์ฆ(์ด์‹ ์งํ›„ ๊ถŒ์žฅ):

npm run typecheck      # tsc --noEmit  โ†’ 0 errors
npm test               # vitest run    โ†’ 51 tests
npm run build          # next build    โ†’ 0 errors

7. Cloudflare Tunnel (์™ธ๋ถ€ ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ)

PM2๋กœ ๋„์šด ๋กœ์ปฌ ์„œ๋น„์Šค(http://localhost:3100)๋ฅผ cloudflared๋กœ ๋„๋ฉ”์ธ์— ์—ฐ๊ฒฐํ•œ๋‹ค.

๋ช…๋ช… ํ„ฐ๋„ (๋„๋ฉ”์ธ + ์ƒ์‹œ):

cloudflared tunnel login                              # CF ๊ณ„์ • ์ธ์ฆ(๋ธŒ๋ผ์šฐ์ €)
cloudflared tunnel create minesweeper                 # ํ„ฐ๋„ + ์ž๊ฒฉ์ฆ๋ช… ํŒŒ์ผ ์ƒ์„ฑ
cloudflared tunnel route dns minesweeper <๋„๋ฉ”์ธ>      # ์˜ˆ: minesweeper.example.com

~/.cloudflared/config.yml:

tunnel: minesweeper
credentials-file: /home/<user>/.cloudflared/<TUNNEL-UUID>.json
ingress:
  - hostname: <๋„๋ฉ”์ธ>
    service: http://localhost:3100
  - service: http_status:404
cloudflared tunnel run minesweeper        # ์‹คํ–‰
sudo cloudflared service install           # (์„ ํƒ) systemd ์ƒ์‹œ ๋“ฑ๋ก

*๋น ๋ฅธ ํ…Œ์ŠคํŠธ (๋„๋ฉ”์ธยท๋กœ๊ทธ์ธ ๋ถˆํ•„์š”, ์ž„์‹œ .trycloudflare.com URL):

cloudflared tunnel --url http://localhost:3100

โš ๏ธ ์ธ์ฆ ๊ฒฝ๊ณ„ ํ•„์ˆ˜. ์ด ์•ฑ์€ Phase 1์ด๋ผ ์ž์ฒด ์ธ์ฆ์ด ์—†๋‹ค(ยงsecurity.md). ํ„ฐ๋„ hostname์„ ๊ทธ๋Œ€๋กœ ๊ณต๊ฐœํ•˜๋ฉด ์ง€์›์ž PII๊ฐ€ ๋…ธ์ถœ๋˜๋ฏ€๋กœ, ๋ฐ˜๋“œ์‹œ Cloudflare Access(Zero Trust) ์ •์ฑ…์„ ๊ทธ hostname์— ์ ์šฉํ•ด ์ด๋ฉ”์ผ/์กฐ์ง ๋‹จ์œ„๋กœ ์ ‘๊ทผ์„ ์ œํ•œํ•˜๋ผ. ํฌํŠธ(3100)๋Š” ์™ธ๋ถ€์— ์ง์ ‘ ๊ฐœ๋ฐฉํ•˜์ง€ ๋ง๊ณ  ํ„ฐ๋„ ๊ฒฝ์œ ๋กœ๋งŒ ๋…ธ์ถœํ•œ๋‹ค.

โš ๏ธ ์—…๋กœ๋“œ ํฌ๊ธฐ ํ•œ๋„ (Cloudflare 100MB)

Cloudflare ๋ฌด๋ฃŒ/ํ”„๋กœ ํ”Œ๋žœ์€ ์š”์ฒญ ๋ณธ๋ฌธ์„ ์•ฝ 100MB๋กœ ์ œํ•œํ•œ๋‹ค. 100MB๋ฅผ ๋„˜๋Š” ์••์ถ•ํŒŒ์ผ์€ edge์—์„œ ์ฐจ๋‹จ๋˜์–ด ์„œ๋ฒ„์— ๋„๋‹ฌํ•˜์ง€ ๋ชปํ•œ๋‹ค(์„œ๋ฒ„ ๋กœ๊ทธยท์—…๋กœ๋“œ ๋””๋ ‰ํ„ฐ๋ฆฌ์— ํ”์ ์ด ๋‚จ์ง€ ์•Š์Œ โ†’ "์—…๋กœ๋“œ ์‹คํŒจ"๋กœ๋งŒ ๋ณด์ž„). ์„œ๋ฒ„ ์ž์ฒด ํ•œ๋„๋Š” MAX_UPLOAD_BYTES(๊ธฐ๋ณธ 200MB)๋‹ค.

  • ํ•ด๊ฒฐ 1 โ€” ๋‚ด๋ถ€๋ง ์ง์ ‘ ์—…๋กœ๋“œ(๊ถŒ์žฅ): ๊ฐ™์€ ๋ง์—์„œ http://<์„œ๋ฒ„-LAN-IP>:3100(์˜ˆ: http://192.168.1.105:3100)์œผ๋กœ ์ ‘์†ํ•ด ์—…๋กœ๋“œํ•˜๋ฉด Cloudflare๋ฅผ ์šฐํšŒํ•œ๋‹ค(์„œ๋ฒ„ 200MB๊นŒ์ง€). next start๋Š” 0.0.0.0:3100์— ๋ฐ”์ธ๋”ฉ๋˜์–ด LAN์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅ.
  • ํ•ด๊ฒฐ 2 โ€” zip ๋ถ„ํ• : ์นดํ…Œ๊ณ ๋ฆฌ ๋‹จ์œ„๋กœ ๋‚˜๋ˆ  100MB ๋ฏธ๋งŒ์œผ๋กœ ์—…๋กœ๋“œ.
  • ํ•ด๊ฒฐ 3 โ€” ํ•œ๋„ ์ƒํ–ฅ: Cloudflare Business(200MB)/Enterprise๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ(์ธํ”„๋ผ ๊ฒฐ์ •).

UploadForm์€ ๊ณต๊ฐœ ๋„๋ฉ”์ธ์—์„œ 100MB ์ดˆ๊ณผ ํŒŒ์ผ์„ ์—…๋กœ๋“œ ์ „์— ๊ฐ์ง€ํ•ด ์œ„ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šด๋‹ค.

๋ณด์•ˆ ๊ฒฝ๊ณ (์žฌ๊ฒŒ์‹œ). Phase 1์—๋Š” ๋‚ด์žฅ ์ธ์ฆ์ด ์—†๋‹ค. ๋ฆฌ์†Œ์Šค๋Š” id๋กœ ์ฃผ์†Œํ™”๋˜๋ฏ€๋กœ ์„œ๋ฒ„์— ๋„๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ˆ„๊ตฌ๋‚˜ ์ง€์›์ž PII๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋‹ค. ๋ฐ˜๋“œ์‹œ ์‚ฌ๋‚ด๋ง ๋˜๋Š” ์ธ์ฆ์„ ๊ฐ•์ œํ•˜๋Š” ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ๋’ค์— ๋‘๊ณ , ๊ณต๊ฐœ ์ธํ„ฐ๋„ท์— ๋…ธ์ถœํ•˜์ง€ ๋งˆ๋ผ. ์ž์„ธํ•œ ์œ„ํ˜‘ ๋ชจ๋ธ๊ณผ ์™„ํ™”์ฑ…์€ security.md ์ฐธ์กฐ. ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ(ํ…Œ์ŠคํŠธยท์‹œ๋“œยท๋กœ์ปฌ ์‹คํ–‰)๋Š” development.md์— ์žˆ๋‹ค.