Files
AriWeb-Suite/modules/webmaker/lib/store.js
T

74 lines
3.0 KiB
JavaScript

'use strict';
// 사이트 메타데이터/명령 이력/토큰 사용량을 data/sites/<id>.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 };