Fix project comparison consistency and labor input handling

This commit is contained in:
b17301
2026-04-29 20:30:33 +09:00
parent 3bf48c42db
commit 7afd2680d3
5 changed files with 2157 additions and 188 deletions
BIN
View File
Binary file not shown.
+1062 -52
View File
File diff suppressed because it is too large Load Diff
+67 -1
View File
@@ -390,6 +390,72 @@
stroke: #a93a3a; stroke: #a93a3a;
} }
.modal-backdrop {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(20, 33, 47, 0.5);
z-index: 10000;
isolation: isolate;
}
.modal-backdrop.open,
.modal-backdrop.is-open {
display: flex;
}
.modal-card,
.related-modal-card {
position: relative;
z-index: 1;
background: #ffffff;
border: 1px solid var(--line);
box-shadow: 0 24px 64px rgba(21, 24, 29, 0.18);
}
.related-modal-card {
width: min(680px, 100%);
max-height: min(70vh, 760px);
overflow: hidden;
border-radius: 16px;
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
}
.related-modal-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.related-modal-head--singleline {
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
}
.related-modal-title {
margin: 0;
font-size: 16px;
font-weight: 800;
line-height: 1.2;
}
.related-modal-body {
display: grid;
gap: 10px;
overflow: auto;
min-height: 0;
overscroll-behavior: contain;
}
.sr-only { .sr-only {
position: absolute; position: absolute;
width: 1px; width: 1px;
@@ -799,7 +865,7 @@
} }
function hasOpenModal() { function hasOpenModal() {
return Boolean(document.querySelector(".modal-backdrop.open")); return Boolean(document.querySelector(".modal-backdrop.open, .modal-backdrop.is-open"));
} }
function shouldDelayRefresh() { function shouldDelayRefresh() {
+120 -16
View File
@@ -333,6 +333,10 @@
min-width: 0; min-width: 0;
} }
.pc-selected-card[hidden] {
display: none;
}
.pc-selected-head { .pc-selected-head {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -452,6 +456,16 @@
min-height: 32px; min-height: 32px;
} }
.pc-chip-link {
color: inherit;
text-decoration: none;
font-weight: 700;
}
.pc-chip-link:hover {
text-decoration: underline;
}
.pc-chip button { .pc-chip button {
border: none; border: none;
background: transparent; background: transparent;
@@ -463,6 +477,33 @@
cursor: pointer; cursor: pointer;
} }
.pc-related-include {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
min-width: 18px;
height: 18px;
border-radius: 999px;
border: 1px solid rgba(217, 221, 227, 0.9);
background: #fff;
color: var(--muted);
font-size: 12px;
font-weight: 800;
padding: 0;
line-height: 1;
}
.pc-related-include.is-active {
color: #1f6f3d;
background: #eefbf3;
}
.pc-related-include:disabled {
opacity: 0.42;
cursor: default;
}
.pc-related-form { .pc-related-form {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto auto; grid-template-columns: minmax(0, 1fr) auto auto;
@@ -686,12 +727,12 @@
<div class="pc-toolbar"> <div class="pc-toolbar">
<div class="pc-tab-row"> <div class="pc-tab-row">
<div class="pc-source-tabs"> <div class="pc-source-tabs">
<a href="/process-cost?source=hanmac{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}{% if process_cost_include_related %}&include_related=1{% endif %}" class="{% if process_cost_source == 'hanmac' %}active{% endif %}">Hanmac ERP</a> <a href="/process-cost?source=hanmac{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}{% if process_cost_include_related %}&include_related=1&active_related={% if process_cost_active_related_codes %}{{ process_cost_active_related_codes|join(',') }}{% elif process_cost_related_codes %}{{ process_cost_related_codes|join(',') }}{% else %}-{% endif %}{% endif %}" class="{% if process_cost_source == 'hanmac' %}active{% endif %}">Hanmac ERP</a>
<a href="/process-cost?source=wehago{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}{% if process_cost_include_related %}&include_related=1{% endif %}" class="{% if process_cost_source == 'wehago' %}active{% endif %}">WEHAGO</a> <a href="/process-cost?source=wehago{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}{% if process_cost_include_related %}&include_related=1&active_related={% if process_cost_active_related_codes %}{{ process_cost_active_related_codes|join(',') }}{% elif process_cost_related_codes %}{{ process_cost_related_codes|join(',') }}{% else %}-{% endif %}{% endif %}" class="{% if process_cost_source == 'wehago' %}active{% endif %}">WEHAGO</a>
</div> </div>
<div class="pc-source-tabs"> <div class="pc-source-tabs">
<a href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}" class="{% if not process_cost_include_related %}active{% endif %}">연계 미반영</a> <a href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}" class="{% if not process_cost_include_related %}active{% endif %}">연계 미반영</a>
<a href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}&include_related=1" class="{% if process_cost_include_related %}active{% endif %}">연계 반영</a> <a href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}{% if process_cost_selected_code %}&code={{ process_cost_selected_code }}{% endif %}&include_related=1&active_related={% if process_cost_active_related_codes %}{{ process_cost_active_related_codes|join(',') }}{% elif process_cost_related_codes %}{{ process_cost_related_codes|join(',') }}{% else %}-{% endif %}" class="{% if process_cost_include_related %}active{% endif %}">연계 반영</a>
</div> </div>
</div> </div>
<form method="get" action="/process-cost" class="pc-filter"> <form method="get" action="/process-cost" class="pc-filter">
@@ -699,6 +740,7 @@
<input type="hidden" name="code" value="{{ process_cost_selected_code }}"> <input type="hidden" name="code" value="{{ process_cost_selected_code }}">
{% if process_cost_include_related %} {% if process_cost_include_related %}
<input type="hidden" name="include_related" value="1"> <input type="hidden" name="include_related" value="1">
<input type="hidden" name="active_related" value="{% if process_cost_active_related_codes %}{{ process_cost_active_related_codes|join(',') }}{% else %}-{% endif %}">
{% endif %} {% endif %}
<select name="start_year" aria-label="시작 연도 선택" id="processCostStartYear"> <select name="start_year" aria-label="시작 연도 선택" id="processCostStartYear">
{% if not process_cost_years %} {% if not process_cost_years %}
@@ -758,7 +800,7 @@
<div class="pc-project-dropdown" id="projectList" hidden> <div class="pc-project-dropdown" id="projectList" hidden>
{% for item in process_cost_projects %} {% for item in process_cost_projects %}
<a <a
href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}&code={{ item.support_dept_code }}{% if process_cost_include_related %}&include_related=1{% endif %}" href="/process-cost?source={{ process_cost_source }}{% if process_cost_selected_start_year %}&start_year={{ process_cost_selected_start_year }}{% endif %}{% if process_cost_selected_end_year %}&end_year={{ process_cost_selected_end_year }}{% endif %}&code={{ item.support_dept_code }}{% if process_cost_include_related %}&include_related=1&active_related={% if process_cost_active_related_codes %}{{ process_cost_active_related_codes|join(',') }}{% else %}-{% endif %}{% endif %}"
class="pc-project-item {% if process_cost_selected_code == item.support_dept_code %}active{% endif %}" class="pc-project-item {% if process_cost_selected_code == item.support_dept_code %}active{% endif %}"
data-project-search="{{ (item.support_dept_code ~ ' ' ~ item.support_dept_name ~ ' ' ~ (item.client_name or ''))|lower }}" data-project-search="{{ (item.support_dept_code ~ ' ' ~ item.support_dept_name ~ ' ' ~ (item.client_name or ''))|lower }}"
data-project-kind="{{ item.project_kind_code|lower }}" data-project-kind="{{ item.project_kind_code|lower }}"
@@ -777,8 +819,8 @@
<div class="pc-note" id="projectListEmpty" hidden style="padding: 12px;">검색 결과가 없습니다.</div> <div class="pc-note" id="projectListEmpty" hidden style="padding: 12px;">검색 결과가 없습니다.</div>
</div> </div>
</div> </div>
{% if selected_project %} {% if selected_project and (selected_project.support_dept_code not in (process_cost_quick_link_codes or [])) %}
<div class="pc-selected-card"> <div class="pc-selected-card" id="selectedProjectCard">
<div class="pc-selected-head"> <div class="pc-selected-head">
<div class="pc-selected-title">{{ selected_project.support_dept_code }}</div> <div class="pc-selected-title">{{ selected_project.support_dept_code }}</div>
<button <button
@@ -835,7 +877,7 @@
<span class="meta">목표원가 {{ "{:,.0f}".format(overview.target_cost_amount|default(0)) }}원</span> <span class="meta">목표원가 {{ "{:,.0f}".format(overview.target_cost_amount|default(0)) }}원</span>
</article> </article>
<article class="pc-card"> <article class="pc-card">
<span class="label">예상 영업수지</span> <span class="label">영업수지</span>
<strong class="value">{{ "{:,.0f}".format(overview.profit_amount|default(0)) }}원</strong> <strong class="value">{{ "{:,.0f}".format(overview.profit_amount|default(0)) }}원</strong>
<span class="meta">마진율 {{ "{:.1f}".format(overview.profit_rate|default(0)) }}%</span> <span class="meta">마진율 {{ "{:.1f}".format(overview.profit_rate|default(0)) }}%</span>
</article> </article>
@@ -872,14 +914,14 @@
</section> </section>
<section class="pc-block"> <section class="pc-block">
<h3>설계 프로세스별 원가 현황</h3> <h3>지출 내역</h3>
<div class="pc-table-wrap"> <div class="pc-table-wrap">
<table class="pc-table"> <table class="pc-table">
<thead> <thead>
<tr> <tr>
<th>구분</th> <th>구분</th>
<th>목표 원가</th> <th>목표 원가</th>
<th>제 집행</th> <th>지출</th>
<th>차이(목표-실제)</th> <th>차이(목표-실제)</th>
<th>집행률</th> <th>집행률</th>
</tr> </tr>
@@ -905,13 +947,13 @@
</section> </section>
<section class="pc-block"> <section class="pc-block">
<h3>계정별 집행 상위 항목</h3> <h3>지출 상위 항목</h3>
<div class="pc-table-wrap"> <div class="pc-table-wrap">
<table class="pc-table"> <table class="pc-table">
<thead> <thead>
<tr> <tr>
<th>계정</th> <th>계정</th>
<th>집행금액</th> <th>실지출</th>
<th>비중</th> <th>비중</th>
<th>전표건수</th> <th>전표건수</th>
<th>최근 증빙일</th> <th>최근 증빙일</th>
@@ -1007,6 +1049,7 @@
const selectedCode = {{ process_cost_selected_code | tojson }}; const selectedCode = {{ process_cost_selected_code | tojson }};
const selectedProject = {{ (process_cost_selected_project or {}) | tojson }}; const selectedProject = {{ (process_cost_selected_project or {}) | tojson }};
const initialRelatedCodes = {{ (process_cost_related_codes or []) | tojson }}; const initialRelatedCodes = {{ (process_cost_related_codes or []) | tojson }};
const initialActiveRelatedCodes = {{ (process_cost_active_related_codes or []) | tojson }};
const initialQuickLinkCodes = {{ (process_cost_quick_link_codes or []) | tojson }}; const initialQuickLinkCodes = {{ (process_cost_quick_link_codes or []) | tojson }};
const searchInput = document.getElementById("projectSearchInput"); const searchInput = document.getElementById("projectSearchInput");
const list = document.getElementById("projectList"); const list = document.getElementById("projectList");
@@ -1020,9 +1063,11 @@
const processModalNote = document.getElementById("processFlowModalNote"); const processModalNote = document.getElementById("processFlowModalNote");
const processModalClose = document.getElementById("processFlowModalClose"); const processModalClose = document.getElementById("processFlowModalClose");
const processButtons = document.querySelectorAll("[data-process-modal]"); const processButtons = document.querySelectorAll("[data-process-modal]");
const includeRelatedEnabled = {{ 1 if process_cost_include_related else 0 }};
let bookmarkedCodes = Array.isArray(initialQuickLinkCodes) ? [...initialQuickLinkCodes] : []; let bookmarkedCodes = Array.isArray(initialQuickLinkCodes) ? [...initialQuickLinkCodes] : [];
let activeRelatedCodes = Array.isArray(initialActiveRelatedCodes) ? [...initialActiveRelatedCodes] : [];
function buildProcessCostUrl(code) { function buildProcessCostUrl(code, options = {}) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("source", {{ process_cost_source | tojson }}); params.set("source", {{ process_cost_source | tojson }});
if ({{ process_cost_selected_start_year | tojson }}) { if ({{ process_cost_selected_start_year | tojson }}) {
@@ -1034,8 +1079,15 @@
if (code) { if (code) {
params.set("code", code); params.set("code", code);
} }
if ({{ 1 if process_cost_include_related else 0 }}) { const includeRelated = Object.prototype.hasOwnProperty.call(options, "includeRelated")
? Boolean(options.includeRelated)
: Boolean(includeRelatedEnabled);
const relatedCodes = Array.isArray(options.activeRelatedCodes)
? options.activeRelatedCodes
: activeRelatedCodes;
if (includeRelated) {
params.set("include_related", "1"); params.set("include_related", "1");
params.set("active_related", relatedCodes.length ? relatedCodes.join(",") : "-");
} }
return `/process-cost?${params.toString()}`; return `/process-cost?${params.toString()}`;
} }
@@ -1072,11 +1124,20 @@
selectedProjectBookmark.setAttribute("title", active ? "바로가기 해제" : "바로가기 저장"); selectedProjectBookmark.setAttribute("title", active ? "바로가기 해제" : "바로가기 저장");
} }
function syncSelectedProjectCard() {
const selectedProjectCard = document.getElementById("selectedProjectCard");
if (!selectedProjectCard || !selectedProject || !selectedProject.support_dept_code) {
return;
}
selectedProjectCard.hidden = isBookmarked(selectedProject.support_dept_code);
}
function renderQuickLinks() { function renderQuickLinks() {
if (!quickLinksContainer) return; if (!quickLinksContainer) return;
const items = getBookmarkedProjects(); const items = getBookmarkedProjects();
if (!items.length) { if (!items.length) {
quickLinksContainer.innerHTML = ""; quickLinksContainer.innerHTML = "";
syncSelectedProjectCard();
return; return;
} }
quickLinksContainer.innerHTML = items.map((item) => ` quickLinksContainer.innerHTML = items.map((item) => `
@@ -1102,6 +1163,7 @@
.then(() => { .then(() => {
renderQuickLinks(); renderQuickLinks();
syncBookmarkButton(); syncBookmarkButton();
syncSelectedProjectCard();
closeProjectList(); closeProjectList();
filterProjectList(); filterProjectList();
}) })
@@ -1110,6 +1172,7 @@
}); });
}); });
}); });
syncSelectedProjectCard();
} }
function getVisibleProjectItems() { function getVisibleProjectItems() {
@@ -1228,6 +1291,7 @@
renderQuickLinks(); renderQuickLinks();
syncBookmarkButton(); syncBookmarkButton();
syncSelectedProjectCard();
filterProjectList(); filterProjectList();
if (selectedProjectBookmark) { if (selectedProjectBookmark) {
@@ -1242,6 +1306,7 @@
await persistBookmarkedCodes(nextCodes); await persistBookmarkedCodes(nextCodes);
renderQuickLinks(); renderQuickLinks();
syncBookmarkButton(); syncBookmarkButton();
syncSelectedProjectCard();
closeProjectList(); closeProjectList();
filterProjectList(); filterProjectList();
} catch (error) { } catch (error) {
@@ -1266,19 +1331,57 @@
function renderRelatedChips() { function renderRelatedChips() {
if (!relatedChipRow) return; if (!relatedChipRow) return;
const fixedHead = selectedCode ? `<span class="pc-chip">${selectedCode} 기준</span>` : ""; const fixedHead = selectedCode ? `<span class="pc-chip">${selectedCode} 기준</span>` : "";
const chips = [...relatedSet].sort().map((code) => ` const chips = [...relatedSet].sort().map((code) => {
const isActive = activeRelatedCodes.includes(code);
const nextActiveCodes = isActive
? activeRelatedCodes.filter((item) => item !== code)
: [...activeRelatedCodes, code];
const openActiveCodes = [...new Set(
[selectedCode, ...activeRelatedCodes]
.filter((item) => item && item !== code)
)];
return `
<span class="pc-chip" data-related-code="${code}"> <span class="pc-chip" data-related-code="${code}">
<span>${code}</span> <a
href="${buildProcessCostUrl(code, { activeRelatedCodes: openActiveCodes, includeRelated: includeRelatedEnabled })}"
class="pc-chip-link"
>${code}</a>
<button
type="button"
class="pc-related-include ${isActive ? "is-active" : ""}"
data-related-toggle="${code}"
data-related-active="${isActive ? "1" : "0"}"
aria-label="${isActive ? "합산 제외" : "합산 포함"}"
title="${isActive ? "합산 제외" : "합산 포함"}"
${includeRelatedEnabled ? "" : "disabled"}
>${isActive ? "" : "+"}</button>
<button type="button" class="pc-related-remove" data-related-remove="${code}">x</button> <button type="button" class="pc-related-remove" data-related-remove="${code}">x</button>
</span> </span>
`).join(""); `;
}).join("");
const empty = ""; const empty = "";
relatedChipRow.innerHTML = fixedHead + chips + empty; relatedChipRow.innerHTML = fixedHead + chips + empty;
relatedChipRow.querySelectorAll("[data-related-toggle]").forEach((button) => {
button.addEventListener("click", () => {
if (!includeRelatedEnabled) return;
const code = button.getAttribute("data-related-toggle") || "";
if (!code) return;
const currentlyActive = button.getAttribute("data-related-active") === "1";
const nextActiveCodes = currentlyActive
? activeRelatedCodes.filter((item) => item !== code)
: [...activeRelatedCodes, code];
window.location.href = buildProcessCostUrl(selectedCode, {
includeRelated: true,
activeRelatedCodes: [...new Set(nextActiveCodes)],
});
});
});
relatedChipRow.querySelectorAll("[data-related-remove]").forEach((button) => { relatedChipRow.querySelectorAll("[data-related-remove]").forEach((button) => {
button.addEventListener("click", () => { button.addEventListener("click", () => {
const code = button.dataset.relatedRemove || ""; const code = button.dataset.relatedRemove || "";
if (code) { if (code) {
relatedSet.delete(code); relatedSet.delete(code);
activeRelatedCodes = activeRelatedCodes.filter((item) => item !== code);
renderRelatedChips(); renderRelatedChips();
} }
}); });
@@ -1338,6 +1441,7 @@
for (const code of initialRelatedCodes || []) { for (const code of initialRelatedCodes || []) {
relatedSet.add(code); relatedSet.add(code);
} }
activeRelatedCodes = Array.isArray(initialActiveRelatedCodes) ? [...initialActiveRelatedCodes] : [];
if (relatedSearchInput) relatedSearchInput.value = ""; if (relatedSearchInput) relatedSearchInput.value = "";
if (relatedSuggestList) { if (relatedSuggestList) {
relatedSuggestList.classList.remove("active"); relatedSuggestList.classList.remove("active");
+887 -98
View File
File diff suppressed because it is too large Load Diff