2026-10-26 11:02:58 +00:00
'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' );
2026-10-26 11:56:38 +00:00
// [Ariweb Suite 변경] rev: 저장할 때마다 1씩 오르는 개정 번호. 여러 사용자가 같은 프로젝트를 편집할 때 화면이 알고 있던 rev 와 다르면 조용히 덮어쓰지 않고 409 로 알린다(server.js PATCH·PUT /files, Ariweb Suite §9.3).
2026-10-26 11:02:58 +00:00
function save ( site ) {
recomputeTotals ( site );
2026-10-26 11:56:38 +00:00
site . rev = ( Number ( site . rev ) || 0 ) + 1 ;
2026-10-26 11:02:58 +00:00
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 };