Update intranet tools and voucher comparison
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user