Refactor data loading and query projections

This commit is contained in:
b17301
2026-05-18 21:01:46 +09:00
parent 49a7a45070
commit 2ab74bac88
11 changed files with 6218 additions and 968 deletions
+177 -10
View File
@@ -3666,20 +3666,23 @@
const serverFocusCode = {{ project_focus_code | tojson }};
const projectEditCache = new Map();
let modalOriginalEdit = null;
const revenueMix = {{ project_revenue_mix | tojson }};
let revenueMix = [];
const revenueMixMode = {{ ("monthly" if selected_year else "yearly") | tojson }};
const projectCostRows = {{ project_cost_by_year | tojson }};
let projectCostRows = [];
const projectMonthlyCostRows = {{ project_monthly_cost_rows | tojson }};
const projectStatusRows = {{ project_status_rows | tojson }};
let projectStatusRows = [];
const projectComparisonNotes = {{ project_comparison_notes | tojson }};
const projectAnalysisSettings = {{ project_analysis_settings | tojson }};
const persistedProjectPageState = {{ project_page_state | tojson }};
const persistedProjectRelatedLinks = {{ project_related_links | tojson }};
const persistedUncontractedCategoryOverrides = {{ project_uncontracted_classifications | tojson }};
const projectPageSessionId = window.clientSessionId || "";
const projectStatusMap = Object.fromEntries(projectStatusRows.map((item) => [item.support_dept_code, item]));
const projectAccountBreakdowns = {{ project_account_breakdowns | tojson }};
const projectCostMap = Object.fromEntries(projectCostRows.map((item) => [item.support_dept_code, item]));
let projectStatusMap = {};
let projectAccountBreakdowns = {};
let projectAccountBreakdownsLoaded = false;
let projectAccountBreakdownsLoadedCodes = new Set();
let projectAccountBreakdownsPromise = null;
let projectCostMap = {};
const currencyFormatter = new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 0 });
window.__projectRelatedSelections = new Map();
const persistedUncontractedFilterState = {
@@ -4586,6 +4589,7 @@
} else {
projectStatusRows.push(projectRow);
}
refreshProjectDerivedCaches();
}
function createCollectionRow(row = {}) {
@@ -6126,6 +6130,126 @@
return "";
}
function buildProjectBreakdownRequestCodes(baseCode = "") {
const normalizedBaseCode = String(baseCode || selectedCode || "").trim();
if (!normalizedBaseCode) {
return [];
}
const codeSet = new Set([normalizedBaseCode]);
getRelatedCodes(normalizedBaseCode).forEach((code) => {
const normalizedCode = String(code || "").trim();
if (normalizedCode) {
codeSet.add(normalizedCode);
}
});
return [...codeSet];
}
function hasProjectBreakdownDataForCodes(codes = []) {
const requestedCodes = Array.isArray(codes)
? codes.map((code) => String(code || "").trim()).filter(Boolean)
: [];
if (!requestedCodes.length) {
return projectAccountBreakdownsLoaded;
}
return requestedCodes.every((code) => projectAccountBreakdownsLoadedCodes.has(code));
}
async function loadProjectAccountBreakdowns(force = false, codes = []) {
const requestedCodes = Array.isArray(codes)
? codes.map((code) => String(code || "").trim()).filter(Boolean)
: [];
const needsFullLoad = !requestedCodes.length;
if (!force) {
if (needsFullLoad && projectAccountBreakdownsLoaded) {
return projectAccountBreakdowns;
}
if (!needsFullLoad && requestedCodes.every((code) => projectAccountBreakdownsLoadedCodes.has(code))) {
return projectAccountBreakdowns;
}
if (projectAccountBreakdownsPromise) {
await projectAccountBreakdownsPromise;
if (needsFullLoad && projectAccountBreakdownsLoaded) {
return projectAccountBreakdowns;
}
if (!needsFullLoad && requestedCodes.every((code) => projectAccountBreakdownsLoadedCodes.has(code))) {
return projectAccountBreakdowns;
}
}
}
const codesToLoad = force || needsFullLoad
? requestedCodes
: requestedCodes.filter((code) => !projectAccountBreakdownsLoadedCodes.has(code));
const queryParams = new URLSearchParams();
if (selectedYear) {
queryParams.set("year", String(selectedYear));
}
if (codesToLoad.length) {
queryParams.set("codes", codesToLoad.join(","));
}
const query = queryParams.toString() ? `?${queryParams.toString()}` : "";
projectAccountBreakdownsPromise = fetch(`/projects/account-breakdowns${query}`, {
method: "GET",
credentials: "same-origin",
headers: {
"Accept": "application/json",
},
}).then(async (response) => {
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.error) {
throw new Error(payload?.error || `HTTP ${response.status}`);
}
const nextPayload = payload || {};
if (needsFullLoad) {
projectAccountBreakdowns = nextPayload;
projectAccountBreakdownsLoaded = true;
projectAccountBreakdownsLoadedCodes = new Set(Object.keys(nextPayload || {}));
} else {
projectAccountBreakdowns = {
...projectAccountBreakdowns,
...nextPayload,
};
codesToLoad.forEach((code) => {
projectAccountBreakdownsLoadedCodes.add(code);
});
}
return projectAccountBreakdowns;
}).catch((error) => {
console.error("프로젝트 계정 분해 조회 에러", error);
if (needsFullLoad) {
projectAccountBreakdowns = {};
projectAccountBreakdownsLoaded = false;
projectAccountBreakdownsLoadedCodes = new Set();
}
throw error;
}).finally(() => {
projectAccountBreakdownsPromise = null;
});
return projectAccountBreakdownsPromise;
}
async function loadProjectBootstrapData(force = false) {
if (!force && projectStatusRows.length && projectCostRows.length) {
return;
}
const query = selectedYear ? `?year=${encodeURIComponent(String(selectedYear))}` : "";
const response = await fetch(`/projects/bootstrap-data${query}`, {
method: "GET",
credentials: "same-origin",
headers: {
"Accept": "application/json",
},
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload?.error) {
throw new Error(payload?.error || `HTTP ${response.status}`);
}
revenueMix = Array.isArray(payload?.revenue_mix) ? payload.revenue_mix : [];
projectCostRows = Array.isArray(payload?.project_cost_by_year) ? payload.project_cost_by_year : [];
projectStatusRows = Array.isArray(payload?.project_status_rows) ? payload.project_status_rows : [];
refreshProjectDerivedCaches();
}
function getBreakdownList(code, kind) {
const item = projectAccountBreakdowns?.[code] || {};
return Array.isArray(item?.[kind]) ? item[kind] : [];
@@ -7934,10 +8058,18 @@
}
const aggregatedProjectCostDatasetCache = new Map();
const aggregatedAllProjectCostRows = mergeProjectExplorerRows(projectCostRows, projectStatusRows);
const aggregatedAllProjectCostMap = new Map(
aggregatedAllProjectCostRows.map((item) => [item.support_dept_code, item]),
);
let aggregatedAllProjectCostRows = [];
let aggregatedAllProjectCostMap = new Map();
function refreshProjectDerivedCaches() {
projectStatusMap = Object.fromEntries(projectStatusRows.map((item) => [item.support_dept_code, item]));
projectCostMap = Object.fromEntries(projectCostRows.map((item) => [item.support_dept_code, item]));
aggregatedProjectCostDatasetCache.clear();
aggregatedAllProjectCostRows = mergeProjectExplorerRows(projectCostRows, projectStatusRows);
aggregatedAllProjectCostMap = new Map(
aggregatedAllProjectCostRows.map((item) => [item.support_dept_code, item]),
);
}
function getAggregatedProjectCostDataset(yearFilterValue) {
const cacheKey = String(yearFilterValue || "");
@@ -8950,6 +9082,36 @@
scheduleStandaloneUncontractedDashboardRender();
return;
}
const requestedBreakdownCodes = buildProjectBreakdownRequestCodes(item.support_dept_code);
if (!hasProjectBreakdownDataForCodes(requestedBreakdownCodes)) {
relatedBarBox.innerHTML = "";
heroBox.innerHTML = renderAnalysisHero(item);
metricsBox.innerHTML = renderAnalysisMetrics(item);
comparisonBox.innerHTML = `
<div class="detail-block">
<p class="stacked-note">프로젝트 세부 집계 데이터를 불러오는 중입니다...</p>
</div>
`;
notesBox.innerHTML = `
<div class="detail-block">
<p class="stacked-note">프로젝트 세부 집계 데이터를 준비한 뒤 표시합니다.</p>
</div>
`;
loadProjectAccountBreakdowns(false, requestedBreakdownCodes).then(() => {
const latestItem = getAnalysisItem(item.support_dept_code);
if (latestItem && String(currentSelectedProjectCode || "") === String(item.support_dept_code || "")) {
renderAnalysis(latestItem);
}
}).catch(() => {
comparisonBox.innerHTML = `
<div class="detail-block">
<p class="stacked-note">프로젝트 세부 집계 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.</p>
</div>
`;
});
scheduleStandaloneUncontractedDashboardRender();
return;
}
const baseCode = String(item.support_dept_code || "").trim();
const allRelatedItems = getRelatedItems(item.support_dept_code, { includeInactive: true });
const relatedItems = getRelatedItems(item.support_dept_code);
@@ -9502,6 +9664,11 @@
(async () => {
await loadProjectPageStateFromServer();
await loadProjectQuickLinksFromServer();
try {
await loadProjectBootstrapData();
} catch (error) {
console.error("프로젝트 부트스트랩 데이터 조회 에러", error);
}
if (serverEditCode) {
selectedCode = serverEditCode;
currentSelectedProjectCode = serverEditCode;