chore: snapshot current intranet work
This commit is contained in:
+169
-99
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user