window.HANWHA_FORMS = [ { "formId": "frmConsolidation", "title": "집계 실행", "route": "consolidation", "authority": "OPERATOR", "messages": [ { "code": "RUN_HINT", "level": "INFO", "text": "업로드가 ACCEPTED 상태인 파일만 집계 대상에 포함됩니다." } ] }, { "formId": "frmLogin", "title": "로그인", "route": "login", "authority": "PUBLIC", "messages": [ { "code": "LOGIN_HINT", "level": "INFO", "text": "기본 계정 admin/operator/viewer, 비밀번호 demo1234" } ] }, { "formId": "frmMasterData", "title": "기준정보 관리", "route": "master", "authority": "VIEWER", "messages": [ { "code": "MASTER_LOAD", "level": "INFO", "text": "기준정보, 계정, 환율, 지분율을 확인합니다." } ] }, { "formId": "frmReportsOps", "title": "리포트/운영", "route": "reports", "authority": "VIEWER", "messages": [ { "code": "REPORT_HINT", "level": "INFO", "text": "batch가 생성한 Excel/PDF를 내려받고 최근 로그를 확인합니다." } ] }, { "formId": "frmUploadValidation", "title": "업로드/검증", "route": "uploads", "authority": "OPERATOR", "messages": [ { "code": "UPLOAD_HINT", "level": "INFO", "text": "invalid 샘플로 오류 시나리오를 확인한 뒤 valid 샘플을 재업로드합니다." } ] } ]; const state = { currentRoute: "login", session: null, master: null, uploads: null, runs: null, reports: null, selectedBatchId: null }; const formMap = new Map(window.HANWHA_FORMS.map((form) => [form.route, form])); async function api(path, options = {}) { const response = await fetch(path, { credentials: "same-origin", headers: { ...(options.body instanceof FormData ? {} : { "Content-Type": "application/json" }), ...(options.headers || {}) }, ...options }); if (!response.ok) { const payload = await response.json().catch(() => ({ message: response.statusText })); throw new Error(payload.message || "요청 처리에 실패했습니다."); } const contentType = response.headers.get("content-type") || ""; if (contentType.includes("application/json")) { return response.json(); } return response.blob(); } function formatValue(value) { if (value === null || value === undefined || value === "") { return "-"; } return value; } function table(columns, rows, options = {}) { const body = rows.length ? rows .map( (row) => `${columns .map((column) => { const value = options.render ? options.render(column, row[column.id], row) : row[column.id]; return `${formatValue(value)}`; }) .join("")}` ) .join("") : `데이터가 없습니다.`; return `
${columns .map((column) => ``) .join("")}${body}
${column.text}
`; } function pill(value) { return `${value}`; } function renderNav() { return window.HANWHA_FORMS.map((form) => { const active = state.currentRoute === form.route ? "active" : ""; const locked = form.authority !== "PUBLIC" && !state.session; return ``; }).join(""); } function heroContent() { const form = formMap.get(state.currentRoute); const note = form?.messages?.[0]?.text || "Spec driven preview"; return `

${form.title}

${note}

`; } function renderLogin() { return ` ${heroContent()}

세션 로그인

기본 계정: admin/operator/viewer / demo1234
`; } function renderMaster() { const datasets = state.master?.datasets || {}; const entities = datasets.entities || []; const accounts = datasets.accounts || []; const fxRates = datasets.fxRates || []; const ownerships = datasets.ownerships || []; return ` ${heroContent()}
법인 수
${entities.length}
계정 수
${accounts.length}
환율 수
${fxRates.length}
지분율 수
${ownerships.length}

법인정보

${table([{ id: "entityCode", text: "법인코드" }, { id: "entityName", text: "법인명" }, { id: "baseCurrency", text: "통화" }], entities)}

계정코드

${table([{ id: "accountCode", text: "계정" }, { id: "accountName", text: "계정명" }, { id: "accountCategory", text: "분류" }, { id: "internalTradeYn", text: "내부거래" }], accounts)}

환율

${table([{ id: "fiscalPeriod", text: "회계기간" }, { id: "currencyCode", text: "통화" }, { id: "rateToKrw", text: "환산율" }], fxRates)}

지분율

${table([{ id: "parentEntityCode", text: "모법인" }, { id: "childEntityCode", text: "자법인" }, { id: "ownershipRatio", text: "지분율" }], ownerships)}
`; } function renderUploads() { const datasets = state.uploads?.datasets || {}; const batches = datasets.uploadBatches || []; const issues = datasets.validationIssues || []; return ` ${heroContent()}

업로드

오류 샘플 정상 TB 정상 Forecast

업로드 이력

${table( [ { id: "id", text: "배치ID" }, { id: "templateCode", text: "템플릿" }, { id: "fiscalPeriod", text: "회계기간" }, { id: "statusCode", text: "상태" }, { id: "originalFilename", text: "파일명" }, { id: "rowCount", text: "건수" }, { id: "errorCount", text: "오류" }, { id: "uploadedAt", text: "업로드시각" } ], batches, { render(column, value) { if (column.id === "statusCode") { return pill(value); } return value; } } )}

오류내역

${table( [ { id: "batchId", text: "배치ID" }, { id: "rowNumber", text: "행" }, { id: "issueCode", text: "오류코드" }, { id: "issueMessage", text: "오류메시지" }, { id: "severityCode", text: "등급" } ], issues, { render(column, value) { if (column.id === "severityCode") { return pill(value); } return value; } } )}
`; } function renderConsolidation() { const runs = state.runs?.datasets?.runs || []; return ` ${heroContent()}

집계 실행

집계 이력

${table( [ { id: "id", text: "실행ID" }, { id: "fiscalPeriod", text: "회계기간" }, { id: "statusCode", text: "상태" }, { id: "requestedBy", text: "요청자" }, { id: "requestedAt", text: "요청시각" }, { id: "finishedAt", text: "완료시각" }, { id: "summaryMessage", text: "요약" } ], runs, { render(column, value) { if (column.id === "statusCode") { return pill(value); } return value; } } )}
`; } function renderReports() { const datasets = state.reports?.datasets || {}; const artifacts = datasets.artifacts || []; const logs = datasets.jobLogs || []; return ` ${heroContent()}
산출물 수
${artifacts.length}
최근 로그 수
${logs.length}
세션 사용자
${state.session?.fullName || "-"}

리포트 산출물

${table( [ { id: "id", text: "산출물ID" }, { id: "runId", text: "실행ID" }, { id: "artifactType", text: "형식" }, { id: "downloadName", text: "파일명" }, { id: "createdAt", text: "생성시각" } ], artifacts, { render(column, value, row) { if (column.id === "downloadName") { return `${value}`; } return value; } } )}

최근 배치 로그

${table( [ { id: "id", text: "로그ID" }, { id: "jobType", text: "작업유형" }, { id: "referenceId", text: "참조ID" }, { id: "logLevel", text: "레벨" }, { id: "logMessage", text: "메시지" }, { id: "createdAt", text: "생성시각" } ], logs, { render(column, value) { if (column.id === "logLevel") { return pill(value); } return value; } } )}
`; } function shellContent(content) { return `
${content}
`; } function render() { let content = ""; switch (state.currentRoute) { case "login": content = renderLogin(); break; case "master": content = renderMaster(); break; case "uploads": content = renderUploads(); break; case "consolidation": content = renderConsolidation(); break; case "reports": content = renderReports(); break; default: content = "
정의되지 않은 화면입니다.
"; } document.getElementById("app").innerHTML = shellContent(content); bindEvents(); } async function loadSession() { try { state.session = await api("/api/auth/me"); } catch (error) { state.session = null; } } async function loadMaster() { if (!state.session) return; state.master = await api("/api/tx/master/reference"); } async function loadUploads() { if (!state.session) return; state.uploads = await api("/api/tx/uploads/overview"); } async function loadRuns() { if (!state.session) return; state.runs = await api("/api/tx/consolidations/overview"); } async function loadReports() { if (!state.session) return; state.reports = await api("/api/tx/reports/overview"); } async function refreshAll() { await loadSession(); if (state.session) { await Promise.all([loadMaster(), loadUploads(), loadRuns(), loadReports()]); } else { state.master = null; state.uploads = null; state.runs = null; state.reports = null; } render(); } function bindEvents() { document.querySelectorAll("[data-route]").forEach((element) => { element.addEventListener("click", async () => { state.currentRoute = element.dataset.route; if (state.currentRoute === "master") await loadMaster(); if (state.currentRoute === "uploads") await loadUploads(); if (state.currentRoute === "consolidation") await loadRuns(); if (state.currentRoute === "reports") await loadReports(); render(); }); }); const loginButton = document.querySelector("[data-action='login']"); if (loginButton) { loginButton.addEventListener("click", async () => { const username = document.getElementById("login-username").value; const password = document.getElementById("login-password").value; await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); state.currentRoute = "master"; await refreshAll(); }); } const logoutButton = document.querySelector("[data-action='logout']"); if (logoutButton) { logoutButton.addEventListener("click", async () => { await api("/api/auth/logout", { method: "POST" }); state.currentRoute = "login"; await refreshAll(); }); } const uploadButton = document.querySelector("[data-action='upload']"); if (uploadButton) { uploadButton.addEventListener("click", async () => { const templateCode = document.getElementById("upload-template").value; const fiscalPeriod = document.getElementById("upload-period").value; const file = document.getElementById("upload-file").files[0]; if (!file) { alert("업로드할 파일을 선택하세요."); return; } const formData = new FormData(); formData.append("templateCode", templateCode); formData.append("fiscalPeriod", fiscalPeriod); formData.append("file", file); await api("/api/uploads", { method: "POST", body: formData }); await loadUploads(); render(); }); } const reloadUploadsButton = document.querySelector("[data-action='reload-uploads']"); if (reloadUploadsButton) { reloadUploadsButton.addEventListener("click", async () => { await loadUploads(); render(); }); } const runButton = document.querySelector("[data-action='request-run']"); if (runButton) { runButton.addEventListener("click", async () => { const fiscalPeriod = document.getElementById("run-period").value; await api("/api/consolidations/runs", { method: "POST", body: JSON.stringify({ fiscalPeriod, reportCurrency: "KRW" }) }); await loadRuns(); render(); }); } const reloadRunsButton = document.querySelector("[data-action='reload-runs']"); if (reloadRunsButton) { reloadRunsButton.addEventListener("click", async () => { await loadRuns(); await loadReports(); render(); }); } } refreshAll().catch((error) => { console.error(error); document.getElementById("app").innerHTML = `

초기화 실패

${error.message}

`; });