Update intranet tools and voucher comparison

This commit is contained in:
b17301
2026-06-04 09:00:51 +09:00
parent 2ab74bac88
commit b93289bfa8
55 changed files with 32809 additions and 1063 deletions
+169
View File
@@ -67,6 +67,42 @@
min-width: 110px;
}
.pc-job-status {
display: inline-flex;
align-items: center;
min-height: 32px;
max-width: 360px;
padding: 0 10px;
border: 1px solid #d8e2ec;
border-radius: 999px;
background: #f8fafc;
color: #475569;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pc-job-status[data-state="queued"],
.pc-job-status[data-state="running"] {
border-color: #bae6fd;
background: #f0f9ff;
color: #075985;
}
.pc-job-status[data-state="done"] {
border-color: #bbf7d0;
background: #f0fdf4;
color: #166534;
}
.pc-job-status[data-state="failed"] {
border-color: #fecaca;
background: #fef2f2;
color: #991b1b;
}
.pc-side-panel {
display: grid;
gap: 14px;
@@ -793,6 +829,10 @@
</select>
<button type="submit" class="button-secondary">적용</button>
</form>
<div class="pc-tab-row">
<button type="button" class="button-secondary" id="processCostRebuildCacheBtn">캐시 재계산</button>
<span class="pc-job-status" id="processCostJobStatus" data-state="">작업 상태 확인 전</span>
</div>
</div>
<div class="pc-grid">
@@ -1081,6 +1121,9 @@
const processModalNote = document.getElementById("processFlowModalNote");
const processModalClose = document.getElementById("processFlowModalClose");
const processButtons = document.querySelectorAll("[data-process-modal]");
const rebuildCacheButton = document.getElementById("processCostRebuildCacheBtn");
const jobStatus = document.getElementById("processCostJobStatus");
let jobPollTimer = null;
let selectedStartYear = pageState.selectedStartYear;
let selectedEndYear = pageState.selectedEndYear;
let selectedSource = typeof pageState.source === "string" ? pageState.source : "hanmac";
@@ -1124,6 +1167,112 @@
monthlyRows = Array.isArray(pageState.monthlyRows) ? pageState.monthlyRows : [];
}
async function fetchJson(url, options = {}) {
const response = await fetch(url, {
...options,
cache: "no-store",
credentials: "same-origin",
headers: {
"Accept": "application/json",
"Cache-Control": "no-cache",
"X-Requested-With": "XMLHttpRequest",
...(options.headers || {}),
},
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.error || payload?.ok === false) {
throw new Error(payload?.error || `HTTP ${response.status}`);
}
return payload;
}
function describeJob(job) {
if (!job) return { text: "최근 작업 없음", state: "" };
const status = String(job.status || "");
const source = String((job.params && job.params.source) || "");
const range = job.start_year && job.end_year ? `${job.start_year}~${job.end_year}` : "";
const label = [source ? source.toUpperCase() : "", range].filter(Boolean).join(" ");
const message = job.error_message || job.message || "";
if (status === "queued") return { text: `${label} 대기 중`.trim(), state: status };
if (status === "running") return { text: `${label} 계산 중... ${message}`.trim(), state: status };
if (status === "done") return { text: `${label} 계산 완료`.trim(), state: status };
if (status === "failed") return { text: `${label} 실패: ${message || "오류"}`.trim(), state: status };
return { text: `${label} ${message || status || "작업 상태 확인 전"}`.trim(), state: status };
}
function renderJob(job) {
if (!jobStatus) return;
const view = describeJob(job);
jobStatus.textContent = view.text;
jobStatus.dataset.state = view.state || "";
if (rebuildCacheButton) {
rebuildCacheButton.disabled = view.state === "queued" || view.state === "running";
}
}
async function loadLatestJob() {
const params = new URLSearchParams({
page_key: "process_cost",
job_type: "process_cost_bootstrap",
});
const payload = await fetchJson(`/api/system-jobs/latest?${params.toString()}`);
renderJob(payload.job || null);
return payload.job || null;
}
function pollJob(jobId) {
if (!jobId) return;
if (jobPollTimer) window.clearInterval(jobPollTimer);
jobPollTimer = window.setInterval(async () => {
try {
const payload = await fetchJson(`/api/system-jobs/${encodeURIComponent(jobId)}`);
const job = payload.job || null;
renderJob(job);
if (!job || ["done", "failed", "cancelled"].includes(String(job.status || ""))) {
window.clearInterval(jobPollTimer);
jobPollTimer = null;
if (job && job.status === "done") {
window.setTimeout(() => window.location.reload(), 600);
}
}
} catch (_error) {
// Keep the current page usable while transient locks clear.
}
}, 2500);
}
async function requestCacheRebuild() {
if (!rebuildCacheButton) return;
rebuildCacheButton.disabled = true;
if (jobStatus) {
jobStatus.textContent = "작업 등록 중...";
jobStatus.dataset.state = "queued";
}
try {
const params = new URLSearchParams(window.location.search);
const payload = await fetchJson("/process-cost/api/rebuild-cache", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
source: params.get("source") || selectedSource || "hanmac",
start_year: params.get("start_year") || selectedStartYear || null,
end_year: params.get("end_year") || selectedEndYear || null,
code: params.get("code") || selectedCode || "",
include_related: params.get("include_related") || (includeRelatedEnabled ? "1" : "0"),
active_related: params.get("active_related") || activeRelatedCodes.join(","),
}),
});
renderJob(payload.job || null);
pollJob(payload.job?.id);
} catch (error) {
if (jobStatus) {
jobStatus.textContent = error.message || "작업 등록 실패";
jobStatus.dataset.state = "failed";
}
rebuildCacheButton.disabled = false;
}
}
function buildProcessCostUrl(code, options = {}) {
const params = new URLSearchParams();
params.set("source", selectedSource);
@@ -1539,6 +1688,14 @@
syncYearRangeOptions();
}
if (rebuildCacheButton) {
rebuildCacheButton.addEventListener("click", async () => {
const confirmed = window.confirm("현재 프로젝트 원가 조건의 데이터를 서버에서 다시 계산할까요? 계산 중에도 다른 화면을 사용할 수 있습니다.");
if (!confirmed) return;
await requestCacheRebuild();
});
}
const svg = document.getElementById("processMonthlyChart");
const renderMonthlyChart = () => {
if (!svg || !Array.isArray(monthlyRows) || !monthlyRows.length) {
@@ -1590,6 +1747,18 @@
filterProjectList();
renderRelatedChips();
renderMonthlyChart();
loadLatestJob()
.then((job) => {
if (job && ["queued", "running"].includes(String(job.status || ""))) {
pollJob(job.id);
}
})
.catch(() => {
if (jobStatus) {
jobStatus.textContent = "작업 상태 조회 실패";
jobStatus.dataset.state = "failed";
}
});
})();
})();
</script>