'use strict'; // 사이트 메타데이터/명령 이력/토큰 사용량을 data/sites/.json 에 저장하는 단순 JSON 저장소. const fs = require('fs'); const path = require('path'); const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data', 'sites'); fs.mkdirSync(DATA_DIR, { recursive: true }); const ID_RE = /^[a-f0-9]{32}$/; function fileOf(id) { return path.join(DATA_DIR, `${id}.json`); } function emptyTotals() { return { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, thinking: 0, total: 0, costUsd: 0, chargeKrw: 0, commands: 0 }; } function recomputeTotals(site) { const t = emptyTotals(); for (const c of site.commands) { if (!c.usage) continue; t.input += c.usage.input; t.output += c.usage.output; t.cacheCreate += c.usage.cacheCreate; t.cacheRead += c.usage.cacheRead; t.thinking += c.usage.thinking || 0; t.total += c.usage.total; t.costUsd += c.costUsd || 0; t.chargeKrw += c.chargeKrw || 0; t.commands += 1; } site.totals = t; return site; } // v1.6.31 (요구 10): 원자적 쓰기(임시 파일 + fsync + rename) + 직전 정상본 .bak 보존. 읽기 실패는 손상으로 기록해 관리자 운영 복구 화면에 보인다(빈 값으로 숨기지 않음) const js = require('./jsonstore'); function save(site) { recomputeTotals(site); site.updatedAt = new Date().toISOString(); js.writeJsonAtomic(fileOf(site.id), JSON.stringify(site, null, 2), { mode: 0o600, keepBackup: true }); return site; } /** 사이트 기록 읽기. 없으면 null. 깨졌으면 .bak 으로 복구를 시도하고 손상 목록에 남긴다(복구도 안 되면 null — 화면에는 "손상" 으로 보인다) */ function load(id) { if (!ID_RE.test(id)) return null; return js.readJson(fileOf(id), null, {}); } /** 손상된 사이트 파일 목록 (운영 복구 화면) */ function health() { return js.health().filter((h) => h.file.startsWith(DATA_DIR)); } function list(ownerId) { return fs.readdirSync(DATA_DIR) .filter((f) => f.endsWith('.json')) .map((f) => load(f.slice(0, -5))) .filter(Boolean) .filter((s) => !ownerId || s.ownerId === ownerId) .sort((a, b) => (b.createdAt || '').localeCompare(a.createdAt || '')) // v1.2.0: 상단 사이트 선택 상자가 "생성 중 / 완성 / 가져옴·복사" 로 묶어 보여 주도록 mode·plan 단계도 함께 준다 .map((s) => ({ id: s.id, title: s.title, createdAt: s.createdAt, status: s.status, totals: s.totals, commands: s.commands.length, mode: s.mode || null, stage: (s.plan && s.plan.stage) || null })); } function create(fields) { const site = { id: fields.id, createdAt: new Date().toISOString(), ownerId: fields.ownerId || null, ownerName: fields.ownerName || '', title: fields.title, purpose: fields.purpose, content: fields.content, design: fields.design, extra: fields.extra, git: { seq: 0, changes: [] }, sessionId: fields.sessionId, status: 'idle', commands: [], totals: emptyTotals(), }; return save(site); } module.exports = { health, ID_RE, create, save, load, list, DATA_DIR };