Update intranet tools and voucher comparison
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
(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;
|
||||
})();
|
||||
Reference in New Issue
Block a user