chore: snapshot current intranet work

This commit is contained in:
b17301
2026-04-24 09:09:39 +09:00
parent a2affa2d45
commit fb11139da4
23 changed files with 16075 additions and 115 deletions
+169 -99
View File
@@ -155,6 +155,9 @@
.search-layout {
display: grid;
gap: 16px;
overflow: visible;
position: relative;
z-index: 90;
}
.analysis-shell {
@@ -168,7 +171,7 @@
gap: 12px;
align-items: start;
position: relative;
z-index: 5200;
z-index: 120;
}
.analysis-main {
@@ -181,16 +184,6 @@
display: none;
}
.toolbar-field {
background: rgba(255, 255, 255, 0.95);
border: 1px solid var(--line);
border-radius: 16px;
padding: 14px;
position: relative;
overflow: visible;
z-index: 5300;
}
.toolbar-head {
display: flex;
justify-content: space-between;
@@ -208,7 +201,7 @@
flex: 1;
}
.toolbar-field h3 {
.analysis-toolbar h3 {
font-size: 16px;
margin-bottom: 0;
}
@@ -299,7 +292,7 @@
position: relative;
display: grid;
gap: 8px;
z-index: 5400;
z-index: 180;
}
.project-search-dropdown {
@@ -308,7 +301,7 @@
top: calc(100% + 8px);
left: 0;
right: 0;
z-index: 9999;
z-index: 4000;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(255, 255, 255, 0.98);
@@ -917,7 +910,7 @@
border-radius: 10px;
background: #fff;
font: inherit;
color: #111827;
color: var(--ink);
resize: vertical;
}
@@ -2315,68 +2308,66 @@
<div class="analysis-shell">
<div class="analysis-main">
<section class="analysis-toolbar">
<div class="toolbar-field">
<div class="toolbar-head">
<div class="toolbar-title-group">
<h3>프로젝트 검색</h3>
<div class="project-quick-link-bar" id="projectQuickLinkBar"></div>
</div>
<div class="toolbar-head-actions">
<button type="button" id="saveProjectPageState" class="button-icon button-secondary" title="페이지 입력사항 저장" aria-label="페이지 입력사항 저장">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 4h10l4 4v12H6z"></path>
<path d="M9 4v6h6V4"></path>
<path d="M9 20v-6h6v6"></path>
</svg>
<span class="sr-only">페이지 입력사항 저장</span>
</button>
<button type="button" id="openRelatedProjectToolbar" class="button-icon button-secondary" title="연관 프로젝트 검색" aria-label="연관 프로젝트 검색">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M10.8 13.2l2.4-2.4"></path>
<path d="M7.5 14.7l-1.6 1.6a3 3 0 1 1-4.2-4.2l3-3a3 3 0 0 1 4.2 0"></path>
<path d="M16.5 9.3l1.6-1.6a3 3 0 1 1 4.2 4.2l-3 3a3 3 0 0 1-4.2 0"></path>
</svg>
<span class="sr-only">연관 프로젝트 검색</span>
</button>
<button type="button" id="toggleProjectAnalysis" class="button-icon button-secondary toggle-analysis-button" title="프로젝트 상세 열기/숨기기" aria-label="프로젝트 상세 열기/숨기기">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 9l6 6 6-6"></path>
</svg>
<span class="sr-only">프로젝트 상세 열기/숨기기</span>
</button>
<button type="button" id="openProjectModal" class="button-icon" title="사업현황 추가/수정" aria-label="사업현황 추가/수정">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5v14"></path>
<path d="M5 12h14"></path>
</svg>
<span class="sr-only">사업현황 추가/수정</span>
</button>
<div class="toolbar-head">
<div class="toolbar-title-group">
<h3>프로젝트 검색</h3>
<div class="project-quick-link-bar" id="projectQuickLinkBar"></div>
</div>
<div class="toolbar-head-actions">
<button type="button" id="saveProjectPageState" class="button-icon button-secondary" title="페이지 입력사항 저장" aria-label="페이지 입력사항 저장">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 4h10l4 4v12H6z"></path>
<path d="M9 4v6h6V4"></path>
<path d="M9 20v-6h6v6"></path>
</svg>
<span class="sr-only">페이지 입력사항 저장</span>
</button>
<button type="button" id="openRelatedProjectToolbar" class="button-icon button-secondary" title="연관 프로젝트 검색" aria-label="연관 프로젝트 검색">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M10.8 13.2l2.4-2.4"></path>
<path d="M7.5 14.7l-1.6 1.6a3 3 0 1 1-4.2-4.2l3-3a3 3 0 0 1 4.2 0"></path>
<path d="M16.5 9.3l1.6-1.6a3 3 0 1 1 4.2 4.2l-3 3a3 3 0 0 1-4.2 0"></path>
</svg>
<span class="sr-only">연관 프로젝트 검색</span>
</button>
<button type="button" id="toggleProjectAnalysis" class="button-icon button-secondary toggle-analysis-button" title="프로젝트 상세 열기/숨기기" aria-label="프로젝트 상세 열기/숨기기">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 9l6 6 6-6"></path>
</svg>
<span class="sr-only">프로젝트 상세 열기/숨기기</span>
</button>
<button type="button" id="openProjectModal" class="button-icon" title="사업현황 추가/수정" aria-label="사업현황 추가/수정">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5v14"></path>
<path d="M5 12h14"></path>
</svg>
<span class="sr-only">사업현황 추가/수정</span>
</button>
</div>
</div>
<div class="project-search-controls">
<div class="project-search-field">
<input id="projectCostSearch" type="text" placeholder="지원부서코드 또는 사업명을 입력하세요." autocomplete="off">
<div class="project-search-dropdown" id="projectSearchDropdown">
<div class="explorer-list" id="projectExplorerList"></div>
<div class="project-search-empty" id="projectExplorerEmpty"></div>
</div>
</div>
<div class="project-search-controls">
<div class="project-search-field">
<input id="projectCostSearch" type="text" placeholder="지원부서코드 또는 사업명을 입력하세요." autocomplete="off">
<div class="project-search-dropdown" id="projectSearchDropdown">
<div class="explorer-list" id="projectExplorerList"></div>
<div class="project-search-empty" id="projectExplorerEmpty"></div>
</div>
</div>
<div class="field">
<select id="projectCostYearFilter" aria-label="연도 필터">
<option value="">전체 연도</option>
{% for year in project_year_options %}
<option value="{{ year }}">{{ year }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<select id="projectContractFilter" aria-label="계약 상태 필터">
<option value="">전체 상태</option>
<option value="contracted">계약 진행</option>
<option value="uncontracted">미계약</option>
<option value="uncontracted_cost">미계약 비용 발생</option>
</select>
</div>
<div class="field">
<select id="projectCostYearFilter" aria-label="연도 필터">
<option value="">전체 연도</option>
{% for year in project_year_options %}
<option value="{{ year }}">{{ year }}</option>
{% endfor %}
</select>
</div>
<div class="field">
<select id="projectContractFilter" aria-label="계약 상태 필터">
<option value="">전체 상태</option>
<option value="contracted">계약 진행</option>
<option value="uncontracted">미계약</option>
<option value="uncontracted_cost">미계약 비용 발생</option>
</select>
</div>
</div>
</section>
@@ -5674,6 +5665,9 @@
if (group === "labor_joint") {
return entryGroup === "labor_joint";
}
if (group === "overhead") {
return entryGroup === "overhead" || entryGroup === "cost_plan";
}
return entryGroup === group;
})
: [];
@@ -5853,6 +5847,8 @@
const overheadPlanned = getExecBudgetGroupEntries(item, "cost_plan", "계정별비용계획");
const laborActual = getActualGroupEntries(item, "labor", "실투입 인건비");
const laborJointActual = getActualGroupEntries(item, "labor_joint", "실투입 인건비(합사)");
const outsourceInputActual = getActualGroupEntries(item, "outsource", "실투입 외주비");
const overheadInputActual = getActualGroupEntries(item, "overhead", "실투입 제경비");
const asActual = getActualGroupEntries(item, "as", "실투입 A/S비");
const sgaInputActual = getActualGroupEntries(item, "sga", "실투입 판관비");
const costEntries = aggregateCodes.flatMap((code) => getCostBreakdownEntries(code).map((entry) => ({
@@ -5863,7 +5859,9 @@
account_name: String(entry.account_name || "").trim(),
support_dept_code: code,
})));
const outsourceActual = costEntries
const outsourceActual = [
...outsourceInputActual,
...costEntries
.filter(isDesignOutsourceEntry)
.map((entry) => ({
...entry,
@@ -5875,8 +5873,11 @@
project_code: item.support_dept_code,
project_name: item.support_dept_name,
},
}));
const overheadActual = costEntries
})),
];
const overheadActual = [
...overheadInputActual,
...costEntries
.filter((entry) => !isDesignOutsourceEntry(entry))
.map((entry) => ({
...entry,
@@ -5888,7 +5889,8 @@
project_code: item.support_dept_code,
project_name: item.support_dept_name,
},
}));
})),
];
return {
collection: {
planned: revenuePlanned,
@@ -5918,6 +5920,18 @@
};
}
function getActualExecutionCostFromDetails(details) {
if (!details) return 0;
return (
sumEntryAmounts(details.labor?.actual)
+ sumEntryAmounts(details.labor?.actual_joint)
+ sumEntryAmounts(details.outsource?.actual)
+ sumEntryAmounts(details.overhead?.actual)
+ sumEntryAmounts(details.as?.actual)
+ sumEntryAmounts(details.sga?.actual)
);
}
function renderAnalysisHero(item) {
const editUrl = getProjectEditUrl(item.support_dept_code);
return `
@@ -6462,16 +6476,8 @@
function renderAnalysisMetrics(item) {
const details = buildComparisonDetails(item);
const actualExecutionCost =
Number(item.total_expense || 0)
|| (
sumEntryAmounts(details.labor.actual)
+ sumEntryAmounts(details.labor.actual_joint)
+ sumEntryAmounts(details.outsource.actual)
+ sumEntryAmounts(details.overhead.actual)
+ sumEntryAmounts(details.as.actual)
+ sumEntryAmounts(details.sga.actual)
);
const actualExecutionCost = getActualExecutionCostFromDetails(details);
const operatingBalance = Number(item.collection_amount || 0) - actualExecutionCost;
return `
<h4>핵심 비교 지표</h4>
<div class="analysis-kpis">
@@ -6489,7 +6495,7 @@
</div>
<div class="analysis-kpi">
<span>영업수지</span>
<strong>${formatDisplayAmount(item.operating_balance)}</strong>
<strong>${formatDisplayAmount(operatingBalance)}</strong>
</div>
<div class="analysis-kpi">
<span>과업수행계획비용</span>
@@ -6706,11 +6712,76 @@
});
}
function getProjectStatusYearRange(item) {
const years = [
Number(item?.latest_year || 0),
Number(String(item?.project_start_date || "").slice(0, 4) || 0),
Number(String(item?.project_end_date || "").slice(0, 4) || 0),
getProjectCodeFullYear(item?.support_dept_code),
].filter(Boolean);
if (!years.length) {
return { min_year: 0, max_year: 0 };
}
return { min_year: Math.min(...years), max_year: Math.max(...years) };
}
function projectStatusMatchesYear(item, yearFilterValue) {
if (!yearFilterValue) return true;
const targetYear = Number(yearFilterValue || 0);
if (!targetYear) return true;
const range = getProjectStatusYearRange(item);
if (range.min_year && range.max_year) {
return range.min_year <= targetYear && targetYear <= range.max_year;
}
return false;
}
function mergeProjectExplorerRows(costRows, statusRows, yearFilterValue = "") {
const merged = new Map();
aggregateProjectCostRows(costRows).forEach((item) => {
merged.set(item.support_dept_code, { ...item });
});
statusRows.forEach((statusItem) => {
const code = String(statusItem?.support_dept_code || "").trim();
if (!code || !projectStatusMatchesYear(statusItem, yearFilterValue)) return;
const yearRange = getProjectStatusYearRange(statusItem);
const current = merged.get(code) || {
support_dept_code: code,
support_dept_name: statusItem.support_dept_name || code,
revenue_amount: 0,
expense_amount: 0,
min_year: yearRange.min_year,
max_year: yearRange.max_year,
matched_years: [],
};
const statusRevenue = Number(statusItem.total_revenue || statusItem.collection_amount || 0);
const statusExpense = Number(statusItem.total_cost || 0) + Number(statusItem.total_sga || 0);
current.support_dept_name = statusItem.support_dept_name || current.support_dept_name || code;
current.revenue_amount = Number(current.revenue_amount || 0) || statusRevenue;
current.expense_amount = Number(current.expense_amount || 0) || statusExpense;
current.min_year = Math.min(
...[current.min_year, yearRange.min_year].map(Number).filter(Boolean),
) || current.min_year || yearRange.min_year || 0;
current.max_year = Math.max(
...[current.max_year, yearRange.max_year].map(Number).filter(Boolean),
) || current.max_year || yearRange.max_year || 0;
current._hasProjectStatus = true;
current._searchText = `${code} ${current.support_dept_name || ""}`.toLowerCase();
merged.set(code, current);
});
return [...merged.values()]
.map((item) => ({
...item,
_searchText: `${item.support_dept_code || ""} ${item.support_dept_name || ""}`.toLowerCase(),
}))
.sort((a, b) => {
if ((b.max_year || 0) !== (a.max_year || 0)) return (b.max_year || 0) - (a.max_year || 0);
return String(a.support_dept_code).localeCompare(String(b.support_dept_code));
});
}
const aggregatedProjectCostDatasetCache = new Map();
const aggregatedAllProjectCostRows = aggregateProjectCostRows(projectCostRows).map((item) => ({
...item,
_searchText: `${item.support_dept_code || ""} ${item.support_dept_name || ""}`.toLowerCase(),
}));
const aggregatedAllProjectCostRows = mergeProjectExplorerRows(projectCostRows, projectStatusRows);
const aggregatedAllProjectCostMap = new Map(
aggregatedAllProjectCostRows.map((item) => [item.support_dept_code, item]),
);
@@ -6722,12 +6793,11 @@
}
const dataset = !yearFilterValue
? aggregatedAllProjectCostRows
: aggregateProjectCostRows(
: mergeProjectExplorerRows(
projectCostRows.filter((item) => String(item.year) === String(yearFilterValue)),
).map((item) => ({
...item,
_searchText: `${item.support_dept_code || ""} ${item.support_dept_name || ""}`.toLowerCase(),
}));
projectStatusRows,
yearFilterValue,
);
aggregatedProjectCostDatasetCache.set(cacheKey, dataset);
return dataset;
}