Files
HM_project_Viewer_Board/static/hm-biz-process/flow_data_store.js
T

442 lines
14 KiB
JavaScript

(function () {
function createFlowDataStore(options) {
const {
config,
getFlowModelPayload,
getStepMeta,
getEntityMeta = () => ({}),
setStepMeta,
setEntityMeta = () => {},
applyFlowModelObject,
migrateStepMetaKeys = () => false,
setSyncStatus = () => {},
onRemoteSaved = () => {}
} = options;
const {
apiFlowDataUrl,
stepMetaKey,
flowModelKey,
entityMetaKey,
bootstrapMarkerKey,
bootstrapMarkerValue,
idbDbName,
idbStoreName,
defaultJsonCandidates = []
} = config;
let idbOpenPromise = null;
let remoteSaveTimer = null;
let remoteSaveInFlight = false;
let remoteSaveQueued = false;
function buildNoCacheUrl(url) {
const target = String(url || '').trim();
if (!target) return target;
const joiner = target.includes('?') ? '&' : '?';
return `${target}${joiner}_ts=${Date.now()}`;
}
function openIdb() {
if (!('indexedDB' in window)) return Promise.resolve(null);
if (idbOpenPromise) return idbOpenPromise;
idbOpenPromise = new Promise((resolve) => {
try {
const req = indexedDB.open(idbDbName, 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(idbStoreName)) {
db.createObjectStore(idbStoreName, { keyPath: 'k' });
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => resolve(null);
} catch (error) {
resolve(null);
}
});
return idbOpenPromise;
}
async function idbSet(key, value) {
const db = await openIdb();
if (!db) return false;
return new Promise((resolve) => {
try {
const tx = db.transaction(idbStoreName, 'readwrite');
tx.objectStore(idbStoreName).put({ k: key, v: value });
tx.oncomplete = () => resolve(true);
tx.onerror = () => resolve(false);
} catch (error) {
resolve(false);
}
});
}
async function idbGet(key) {
const db = await openIdb();
if (!db) return null;
return new Promise((resolve) => {
try {
const tx = db.transaction(idbStoreName, 'readonly');
const req = tx.objectStore(idbStoreName).get(key);
req.onsuccess = () => resolve(req.result ? req.result.v : null);
req.onerror = () => resolve(null);
} catch (error) {
resolve(null);
}
});
}
function persistMarker() {
try {
localStorage.setItem(bootstrapMarkerKey, bootstrapMarkerValue);
} catch (error) {
// ignore localStorage failures
}
idbSet(bootstrapMarkerKey, bootstrapMarkerValue);
}
function persistLocalSnapshot(
flowModelPayload = getFlowModelPayload(),
stepMetaPayload = getStepMeta(),
entityMetaPayload = getEntityMeta()
) {
try {
localStorage.setItem(flowModelKey, JSON.stringify(flowModelPayload));
} catch (error) {
// localStorage quota exceeded; IndexedDB fallback handles persistence
}
try {
localStorage.setItem(stepMetaKey, JSON.stringify(stepMetaPayload));
} catch (error) {
// localStorage quota exceeded; IndexedDB fallback handles persistence
}
try {
localStorage.setItem(entityMetaKey, JSON.stringify(entityMetaPayload));
} catch (error) {
// localStorage quota exceeded; IndexedDB fallback handles persistence
}
idbSet(flowModelKey, flowModelPayload);
idbSet(stepMetaKey, stepMetaPayload);
idbSet(entityMetaKey, entityMetaPayload);
}
function buildCurrentPayload() {
return {
version: 1,
exportedAt: new Date().toISOString(),
flowModel: getFlowModelPayload(),
stepMeta: getStepMeta(),
entityMeta: getEntityMeta()
};
}
function extractImportPayload(parsed) {
if (!parsed || typeof parsed !== 'object') return null;
const flowModel = (parsed.flowModel && typeof parsed.flowModel === 'object')
? parsed.flowModel
: ((Array.isArray(parsed.mainSteps) || (parsed.subFlow && typeof parsed.subFlow === 'object'))
? parsed
: null);
if (!flowModel || typeof flowModel !== 'object') return null;
return {
flowModel,
importedMeta: (parsed.stepMeta && typeof parsed.stepMeta === 'object') ? parsed.stepMeta : {},
entityMeta: (parsed.entityMeta && typeof parsed.entityMeta === 'object') ? parsed.entityMeta : {}
};
}
function applyImportedData(flowModel, importedMeta, entityMeta, persistRemote = true) {
applyFlowModelObject(flowModel);
setStepMeta((importedMeta && typeof importedMeta === 'object') ? importedMeta : {});
setEntityMeta((entityMeta && typeof entityMeta === 'object') ? entityMeta : {});
migrateStepMetaKeys();
persistLocalSnapshot(getFlowModelPayload(), getStepMeta(), getEntityMeta());
if (persistRemote) scheduleRemoteSave();
return true;
}
async function persistRemoteData() {
if (remoteSaveInFlight) {
remoteSaveQueued = true;
return;
}
remoteSaveInFlight = true;
setSyncStatus('saving', 'DB 저장 중');
try {
const res = await fetch(buildNoCacheUrl(apiFlowDataUrl), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
Pragma: 'no-cache'
},
body: JSON.stringify(buildCurrentPayload())
});
if (!res.ok) {
console.warn('DB save failed', res.status);
setSyncStatus('error', 'DB 저장 실패');
} else {
setSyncStatus('ready', 'DB 저장됨');
onRemoteSaved();
}
} catch (error) {
console.warn('DB save failed', error);
setSyncStatus('error', 'DB 저장 실패');
} finally {
remoteSaveInFlight = false;
if (remoteSaveQueued) {
remoteSaveQueued = false;
persistRemoteData();
}
}
}
function scheduleRemoteSave() {
if (remoteSaveTimer) clearTimeout(remoteSaveTimer);
remoteSaveTimer = window.setTimeout(() => {
remoteSaveTimer = null;
persistRemoteData();
}, 250);
}
function loadStepMeta() {
try {
const raw = localStorage.getItem(stepMetaKey);
if (!raw) return;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return;
setStepMeta(parsed);
migrateStepMetaKeys();
} catch (error) {
setStepMeta({});
}
}
function loadFlowModel() {
try {
const raw = localStorage.getItem(flowModelKey);
if (!raw) return;
const parsed = JSON.parse(raw);
applyFlowModelObject(parsed);
} catch (error) {
// ignore malformed saved model
}
}
async function loadFromIdbFallback() {
const hasLocalFlow = !!localStorage.getItem(flowModelKey);
const hasLocalMeta = !!localStorage.getItem(stepMetaKey);
const hasLocalEntityMeta = !!localStorage.getItem(entityMetaKey);
if (!hasLocalFlow) {
const flow = await idbGet(flowModelKey);
if (flow && typeof flow === 'object') applyFlowModelObject(flow);
}
if (!hasLocalMeta) {
const meta = await idbGet(stepMetaKey);
if (meta && typeof meta === 'object') {
setStepMeta(meta);
migrateStepMetaKeys();
}
}
if (!hasLocalEntityMeta) {
const entityMeta = await idbGet(entityMetaKey);
if (entityMeta && typeof entityMeta === 'object') {
setEntityMeta(entityMeta);
}
}
if (localStorage.getItem(bootstrapMarkerKey) !== bootstrapMarkerValue) {
const marker = await idbGet(bootstrapMarkerKey);
if (marker === bootstrapMarkerValue) {
try {
localStorage.setItem(bootstrapMarkerKey, bootstrapMarkerValue);
} catch (error) {
// ignore localStorage failures
}
}
}
}
async function loadRemoteData() {
setSyncStatus('loading', 'DB 불러오는 중');
try {
const res = await fetch(buildNoCacheUrl(apiFlowDataUrl), {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache'
}
});
if (!res.ok) {
if (res.status === 404) return false;
throw new Error(`HTTP ${res.status}`);
}
const parsed = await res.json();
const extracted = extractImportPayload(parsed);
if (!extracted) return false;
applyImportedData(extracted.flowModel, extracted.importedMeta, extracted.entityMeta, false);
persistMarker();
setSyncStatus('ready', 'DB 연결됨');
return true;
} catch (error) {
console.warn('DB load failed', error);
const loadedFromJson = await loadFromDefaultJsonCandidates(false);
if (loadedFromJson) {
setSyncStatus('ready', 'Gitea 데이터 불러옴');
return true;
}
return false;
}
}
async function loadFromDefaultJsonCandidates(persistRemote = true) {
for (const path of defaultJsonCandidates) {
try {
let parsed = null;
try {
const res = await fetch(path, { cache: 'no-store' });
if (res.ok) parsed = await res.json();
} catch (error) {
// fall through to XHR
}
if (!parsed) {
parsed = await new Promise((resolve) => {
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', path, true);
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || (xhr.status === 0 && xhr.responseText)) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (error) {
resolve(null);
}
} else {
resolve(null);
}
};
xhr.send();
} catch (error) {
resolve(null);
}
});
}
if (!parsed) continue;
const extracted = extractImportPayload(parsed);
if (!extracted) continue;
applyImportedData(
extracted.flowModel,
extracted.importedMeta,
extracted.entityMeta,
persistRemote
);
persistMarker();
return true;
} catch (error) {
// ignore and try next candidate
}
}
return false;
}
async function bootstrapDefaultJsonIfEmpty() {
let hasMarker = localStorage.getItem(bootstrapMarkerKey) === bootstrapMarkerValue;
if (!hasMarker) {
const marker = await idbGet(bootstrapMarkerKey);
hasMarker = marker === bootstrapMarkerValue;
}
if (hasMarker) return false;
try {
const res = await fetch(buildNoCacheUrl(apiFlowDataUrl), {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache'
}
});
if (res.ok) {
const parsed = await res.json();
const extracted = extractImportPayload(parsed);
if (extracted) {
applyImportedData(extracted.flowModel, extracted.importedMeta, extracted.entityMeta, true);
persistMarker();
setSyncStatus('ready', '기본 데이터 적재됨');
return true;
}
}
} catch (error) {
// fallback to local json seed
}
const loadedFromJson = await loadFromDefaultJsonCandidates(true);
if (loadedFromJson) {
setSyncStatus('ready', '기본 데이터 적재됨');
return true;
}
return false;
}
function saveStepMeta() {
persistLocalSnapshot(getFlowModelPayload(), getStepMeta(), getEntityMeta());
scheduleRemoteSave();
}
function saveFlowModel() {
persistLocalSnapshot(getFlowModelPayload(), getStepMeta(), getEntityMeta());
scheduleRemoteSave();
}
function exportAllData() {
const payload = buildCurrentPayload();
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
const day = new Date().toISOString().slice(0, 10);
anchor.href = url;
anchor.download = `flow-data-${day}.json`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}
function importAllData(rawText) {
let parsed;
try {
parsed = JSON.parse(rawText);
} catch (error) {
return { ok: false, message: 'JSON 파일 형식이 올바르지 않습니다.' };
}
if (!parsed || typeof parsed !== 'object') {
return { ok: false, message: '가져올 데이터 형식이 아닙니다.' };
}
const extracted = extractImportPayload(parsed);
if (!extracted) {
return { ok: false, message: 'flowModel 데이터가 없습니다.' };
}
applyImportedData(extracted.flowModel, extracted.importedMeta, extracted.entityMeta, true);
persistMarker();
return { ok: true };
}
return {
persistMarker,
saveStepMeta,
saveFlowModel,
applyImportedData,
exportAllData,
importAllData,
loadFlowModel,
loadStepMeta,
loadFromIdbFallback,
loadRemoteData,
bootstrapDefaultJsonIfEmpty
};
}
window.createFlowDataStore = createFlowDataStore;
})();