Files
HM_project_Viewer_Board/templates/process_cost.html
T

1589 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}프로세스 원가{% endblock %}
{% block head_extra %}
<style>
.pc-grid {
display: grid;
grid-template-columns: minmax(260px, 300px) minmax(0, 1fr) minmax(280px, 340px);
gap: 14px;
align-items: start;
}
.pc-shell {
display: grid;
gap: 14px;
}
.pc-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
padding-bottom: 14px;
border-bottom: 1px solid var(--line);
}
.pc-source-tabs {
display: inline-flex;
gap: 6px;
background: rgba(245, 247, 249, 0.95);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px;
}
.pc-source-tabs a {
text-decoration: none;
color: var(--ink);
font-size: 13px;
font-weight: 800;
padding: 8px 12px;
border-radius: 999px;
white-space: nowrap;
}
.pc-source-tabs a.active {
background: var(--accent);
color: #fff;
}
.pc-tab-row {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.pc-filter {
display: inline-flex;
align-items: center;
gap: 8px;
}
.pc-filter select {
min-width: 110px;
}
.pc-side-panel {
display: grid;
gap: 14px;
overflow: visible;
min-width: 0;
}
.pc-collapse {
border-top: 1px solid var(--line);
padding-top: 14px;
display: grid;
gap: 12px;
}
.pc-collapse.is-first {
border-top: 0;
padding-top: 0;
}
.pc-collapse-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
cursor: pointer;
}
.pc-collapse-head h3,
.pc-collapse-head h4 {
margin: 0;
font-size: 15px;
}
.pc-collapse-toggle {
width: 28px;
height: 28px;
min-width: 28px;
border: 0;
background: transparent;
box-shadow: none;
padding: 0;
color: var(--muted);
}
.pc-collapse-toggle:hover {
transform: none;
background: transparent;
color: var(--ink);
}
.pc-collapse-toggle svg {
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 2;
transition: transform 0.18s ease;
}
.pc-collapse.is-collapsed .pc-collapse-toggle svg {
transform: rotate(180deg);
}
.pc-collapse-body {
display: grid;
gap: 12px;
}
.pc-collapse.is-collapsed .pc-collapse-body {
display: none;
}
.pc-quick-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
}
.pc-quick-link,
.pc-related-link {
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 100%;
min-height: 32px;
padding: 5px 12px;
border: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
color: var(--ink);
text-decoration: none;
font-size: 12px;
font-weight: 700;
min-width: 0;
}
.pc-quick-link strong,
.pc-quick-link span,
.pc-related-link span {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.pc-quick-link strong {
flex: 0 0 auto;
overflow: visible;
text-overflow: clip;
white-space: nowrap;
}
.pc-quick-link {
width: 100%;
justify-content: space-between;
}
.pc-quick-link-main {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1 1 auto;
color: inherit;
text-decoration: none;
}
.pc-bookmark-button {
width: 28px;
height: 28px;
min-width: 28px;
border: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 999px;
background: rgba(255, 255, 255, 0.92);
color: var(--muted);
box-shadow: none;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.pc-bookmark-button:hover {
transform: none;
background: #f3f6f9;
color: var(--ink);
}
.pc-bookmark-button.is-active {
color: #d97706;
border-color: rgba(217, 221, 227, 0.9);
background: #fff7ed;
}
.pc-bookmark-button svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.9;
}
.pc-selector-grid {
display: grid;
gap: 10px;
min-width: 0;
}
.pc-selector-field {
position: relative;
display: grid;
gap: 8px;
overflow: visible;
}
.pc-project-search {
width: 100%;
}
.pc-project-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
z-index: 40;
display: grid;
gap: 0;
max-height: 360px;
overflow: auto;
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 16px 28px rgba(15, 23, 42, 0.08);
}
.pc-project-dropdown[hidden] {
display: none;
}
.pc-project-item {
border: 0;
border-bottom: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 0;
background: transparent;
padding: 10px 12px;
text-decoration: none;
color: var(--ink);
display: grid;
gap: 5px;
}
.pc-project-item[hidden] {
display: none;
}
.pc-project-item.active {
border-color: rgba(217, 221, 227, 0.9);
background: rgba(245, 247, 250, 0.72);
box-shadow: none;
}
.pc-project-head {
display: flex;
justify-content: space-between;
gap: 8px;
align-items: baseline;
font-size: 13px;
}
.pc-project-code {
font-weight: 800;
}
.pc-project-name {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pc-type-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
padding: 2px 8px;
border-radius: 999px;
background: #eef2f6;
color: #3a4654;
font-size: 11px;
font-weight: 800;
}
.pc-selected-card {
display: grid;
gap: 6px;
padding: 10px 12px;
border: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 14px;
background: rgba(248, 250, 252, 0.75);
min-width: 0;
}
.pc-selected-card[hidden] {
display: none;
}
.pc-selected-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-width: 0;
}
.pc-selected-title {
font-size: 14px;
font-weight: 800;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pc-selected-sub {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pc-process-list {
display: grid;
gap: 8px;
min-width: 0;
}
.pc-process-button {
width: 100%;
justify-content: flex-start;
padding: 10px 12px;
min-height: 40px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.78);
color: var(--ink);
border: 1px solid rgba(217, 221, 227, 0.9);
box-shadow: none;
font-size: 13px;
font-weight: 700;
}
.pc-process-button:hover {
transform: none;
background: #f3f6f9;
}
.pc-metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
align-items: start;
}
.pc-card {
border: 0;
border-bottom: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 0;
background: transparent;
padding: 0 0 12px;
display: grid;
gap: 6px;
align-content: start;
align-items: start;
min-height: 100px;
}
.pc-card .label {
font-size: 12px;
color: var(--muted);
font-weight: 700;
}
.pc-card .value {
font-size: 24px;
font-weight: 900;
letter-spacing: -0.03em;
line-height: 1.15;
}
.pc-card .meta {
font-size: 12px;
color: var(--muted);
}
.pc-note {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.pc-related-box {
display: grid;
gap: 5px;
}
.pc-chip-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.pc-chip {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: #f8fafc;
padding: 6px 10px;
font-size: 12px;
min-height: 32px;
}
.pc-chip-link {
color: inherit;
text-decoration: none;
font-weight: 700;
}
.pc-chip-link:hover {
text-decoration: underline;
}
.pc-chip button {
border: none;
background: transparent;
color: var(--muted);
padding: 0;
min-width: 16px;
min-height: 16px;
line-height: 1;
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 {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 8px;
align-items: center;
}
.pc-related-head {
align-items: center;
justify-content: space-between;
gap: 10px;
}
.pc-related-head-main {
display: flex;
align-items: center;
gap: 10px;
flex: 1 1 auto;
min-width: 0;
flex-wrap: wrap;
}
.pc-related-form.pc-related-form--inline {
display: flex;
align-items: center;
gap: 8px;
flex: 1 1 460px;
min-width: 0;
}
.pc-related-form.pc-related-form--inline input[type="text"] {
flex: 1 1 260px;
min-width: 180px;
}
.pc-related-list {
border: 1px solid var(--line);
border-radius: 10px;
background: #fff;
max-height: 200px;
overflow: auto;
display: none;
}
.pc-related-list.active {
display: block;
}
.pc-related-item {
width: 100%;
border: none;
background: transparent;
text-align: left;
padding: 9px 10px;
cursor: pointer;
font-size: 12px;
color: var(--ink);
}
.pc-related-item:hover {
background: #f5f7fa;
}
.pc-block {
border: 0;
border-top: 1px solid var(--line);
border-radius: 0;
overflow: hidden;
background: transparent;
}
.pc-grid > .pc-block:first-child,
.pc-grid > .pc-shell > :first-child,
.pc-grid > .pc-side-stack > :first-child {
border-top: 0;
}
.pc-grid > section,
.pc-grid > aside,
.pc-grid > .pc-shell,
.pc-grid > .pc-side-stack {
min-width: 0;
}
.pc-block h3 {
font-size: 15px;
padding: 14px 0 10px;
border-bottom: 0;
background: transparent;
}
.pc-grid > .pc-block:first-child h3,
.pc-grid > .pc-shell > :first-child h3,
.pc-grid > .pc-side-stack > :first-child h3 {
padding-top: 0;
}
.pc-table-wrap {
overflow: auto;
max-height: 390px;
}
.pc-table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
min-width: 620px;
}
.pc-table th,
.pc-table td {
border-bottom: 1px solid #eceff4;
padding: 9px 10px;
text-align: right;
white-space: nowrap;
}
.pc-table th:first-child,
.pc-table td:first-child {
text-align: left;
}
.pc-table thead th {
position: sticky;
top: 0;
z-index: 1;
background: #f5f7fa;
font-weight: 800;
color: #353b45;
}
.pc-table tr:nth-child(even) td {
background: #fcfcfd;
}
.pc-table td.negative {
color: #b0272d;
font-weight: 800;
}
.pc-table td.positive {
color: #1f6f3d;
font-weight: 800;
}
.pc-side-stack {
display: grid;
gap: 12px;
}
.pc-kpi {
border: 0;
border-top: 1px solid var(--line);
border-radius: 0;
background: transparent;
padding: 14px 0 0;
display: grid;
gap: 10px;
}
.pc-kpi h3 {
font-size: 15px;
}
.pc-kpi-row {
display: grid;
gap: 6px;
}
.pc-kpi-head {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
}
.pc-bar {
height: 8px;
border-radius: 999px;
background: #edf1f5;
overflow: hidden;
}
.pc-bar > span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #445a7e, #67a2d4);
width: 0;
}
.pc-mini-chart {
border: 0;
border-top: 1px solid var(--line);
border-radius: 0;
background: transparent;
padding: 14px 0 0;
}
.pc-mini-chart h3 {
font-size: 15px;
margin-bottom: 8px;
}
.pc-mini-chart svg {
width: 100%;
height: auto;
display: block;
min-height: 170px;
}
.pc-badge {
display: inline-flex;
align-items: center;
border: 1px solid rgba(217, 221, 227, 0.9);
border-radius: 999px;
padding: 4px 8px;
font-size: 11px;
color: var(--muted);
background: rgba(255, 255, 255, 0.72);
}
body[data-view-mode="dual"] .pc-grid {
grid-template-columns: minmax(250px, 280px) minmax(0, 1fr) minmax(300px, 360px);
}
body[data-view-mode="dual"] .pc-project-dropdown {
max-height: calc(100vh - 250px);
}
@media (max-width: 1260px) {
.pc-grid {
grid-template-columns: 1fr;
}
.pc-metric-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
{% endblock %}
{% block content %}
{% set detail = process_cost_detail or {} %}
{% set overview = detail.overview or {} %}
{% set diagnostics = detail.diagnostics or {} %}
{% set selected_project = process_cost_selected_project or {} %}
<section class="panel pc-shell">
<div class="pc-toolbar">
<div class="pc-tab-row">
<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&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&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 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 %}&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>
<form method="get" action="/process-cost" class="pc-filter">
<input type="hidden" name="source" value="{{ process_cost_source }}">
<input type="hidden" name="code" value="{{ process_cost_selected_code }}">
{% if process_cost_include_related %}
<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 %}
<select name="start_year" aria-label="시작 연도 선택" id="processCostStartYear">
{% if not process_cost_years %}
<option value="">연도 없음</option>
{% else %}
{% for y in process_cost_years_desc %}
<option value="{{ y }}" {% if process_cost_selected_start_year == y %}selected{% endif %} {% if process_cost_selected_end_year and y > process_cost_selected_end_year %}disabled{% endif %}>{{ y }}</option>
{% endfor %}
{% endif %}
</select>
<span>~</span>
<select name="end_year" aria-label="종료 연도 선택" id="processCostEndYear">
{% if not process_cost_years %}
<option value="">연도 없음</option>
{% else %}
{% for y in process_cost_years_desc %}
<option value="{{ y }}" {% if process_cost_selected_end_year == y %}selected{% endif %} {% if process_cost_selected_start_year and y < process_cost_selected_start_year %}disabled{% endif %}>{{ y }}</option>
{% endfor %}
{% endif %}
</select>
<button type="submit" class="button-secondary">적용</button>
</form>
</div>
<div class="pc-grid">
<aside class="pc-side-panel">
<section class="pc-collapse is-first" id="pcQuickLinksSection">
<div class="pc-collapse-head">
<h3>바로가기</h3>
<button type="button" class="pc-collapse-toggle" data-collapse-target="pcQuickLinksSection" aria-label="프로젝트 바로가기 접기">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 15l6-6 6 6"></path>
</svg>
</button>
</div>
<div class="pc-collapse-body">
<div class="pc-quick-links" id="pcQuickLinks"></div>
</div>
</section>
<section class="pc-collapse" id="pcProjectSelectorSection">
<div class="pc-collapse-head">
<h3>프로젝트 선택</h3>
</div>
<div class="pc-collapse-body">
<div class="pc-selector-grid">
<div class="field">
<select id="projectTypeFilter" aria-label="프로젝트 종류 선택">
<option value="">전체</option>
<option value="x">사전사업</option>
<option value="y">설계</option>
<option value="z">감리</option>
</select>
</div>
<div class="pc-selector-field">
<input type="text" id="projectSearchInput" class="pc-project-search" placeholder="프로젝트 코드/명 검색" autocomplete="off">
<div class="pc-project-dropdown" id="projectList" hidden>
{% for item in process_cost_projects %}
<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&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 %}"
data-project-search="{{ (item.support_dept_code ~ ' ' ~ item.support_dept_name ~ ' ' ~ (item.client_name or ''))|lower }}"
data-project-kind="{{ item.project_kind_code|lower }}"
title="{{ item.support_dept_code }} {{ item.support_dept_name }}"
>
<div class="pc-project-head">
<span class="pc-project-code">{{ item.support_dept_code }}</span>
<span class="pc-type-pill">{{ item.project_kind_label }}</span>
</div>
<div class="pc-project-name">{{ item.support_dept_name }}</div>
{% if item.client_name %}
<div class="pc-project-name">{{ item.client_name }}</div>
{% endif %}
</a>
{% endfor %}
<div class="pc-note" id="projectListEmpty" hidden style="padding: 12px;">검색 결과가 없습니다.</div>
</div>
</div>
{% if selected_project and (selected_project.support_dept_code not in (process_cost_quick_link_codes or [])) %}
<div class="pc-selected-card" id="selectedProjectCard">
<div class="pc-selected-head">
<div class="pc-selected-title">{{ selected_project.support_dept_code }}</div>
<button
type="button"
class="pc-bookmark-button"
id="selectedProjectBookmark"
data-bookmark-code="{{ selected_project.support_dept_code }}"
data-bookmark-name="{{ selected_project.support_dept_name }}"
data-bookmark-kind="{{ selected_project.project_kind_label }}"
aria-label="바로가기 저장"
title="바로가기 저장"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 4h10a1 1 0 0 1 1 1v15l-6-3-6 3V5a1 1 0 0 1 1-1z"></path>
</svg>
</button>
</div>
<div class="pc-selected-sub">{{ selected_project.support_dept_name }}</div>
{% if selected_project.client_name %}
<div class="pc-selected-sub">{{ selected_project.client_name }}</div>
{% endif %}
</div>
{% endif %}
</div>
</div>
</section>
<section class="pc-collapse" id="pcProcessSection">
<div class="pc-collapse-body">
<div class="pc-process-list">
<button type="button" class="pc-process-button" data-process-modal="contract">계약정보</button>
<button type="button" class="pc-process-button" data-process-modal="planning">계획수립</button>
<button type="button" class="pc-process-button" data-process-modal="collection">수금관리</button>
<button type="button" class="pc-process-button" data-process-modal="expense">경비/외주비 지출</button>
<button type="button" class="pc-process-button" data-process-modal="progress">진행률관리</button>
</div>
</div>
</section>
</aside>
<section class="pc-shell" style="gap: 12px;">
<div class="pc-metric-grid">
<article class="pc-card">
<span class="label">계약금액</span>
<strong class="value">{{ "{:,.0f}".format(overview.contract_amount|default(0)) }}원</strong>
</article>
<article class="pc-card">
<span class="label">누적 수익</span>
<strong class="value">{{ "{:,.0f}".format(overview.revenue_amount|default(0)) }}원</strong>
</article>
<article class="pc-card">
<span class="label">누적 비용</span>
<strong class="value">{{ "{:,.0f}".format(overview.expense_amount|default(0)) }}원</strong>
<span class="meta">목표원가 {{ "{:,.0f}".format(overview.target_cost_amount|default(0)) }}원</span>
</article>
<article class="pc-card">
<span class="label">영업수지</span>
<strong class="value">{{ "{:,.0f}".format(overview.profit_amount|default(0)) }}원</strong>
<span class="meta">마진율 {{ "{:.1f}".format(overview.profit_rate|default(0)) }}%</span>
</article>
</div>
<section class="pc-collapse is-first" id="pcRelatedSection">
<div class="pc-collapse-head pc-related-head">
<div class="pc-related-head-main">
<h3>연계 프로젝트</h3>
<div class="pc-related-form pc-related-form--inline">
<input type="text" id="relatedSearchInput" placeholder="추가할 프로젝트 코드/명 검색" {% if not process_cost_selected_code %}disabled{% endif %}>
<button type="button" class="button-secondary" id="relatedSaveButton" {% if not process_cost_selected_code %}disabled{% endif %}>저장</button>
<button type="button" class="button-secondary" id="relatedResetButton" {% if not process_cost_selected_code %}disabled{% endif %}>초기화</button>
</div>
</div>
<button type="button" class="pc-collapse-toggle" data-collapse-target="pcRelatedSection" aria-label="연계 프로젝트 접기">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 15l6-6 6 6"></path>
</svg>
</button>
</div>
<div class="pc-collapse-body pc-related-box">
<div class="pc-chip-row" id="relatedChipRow">
{% if process_cost_selected_code %}
<span class="pc-chip">{{ process_cost_selected_code }} 기준</span>
{% endif %}
{% for related_code in process_cost_related_codes or [] %}
<span class="pc-chip" data-related-code="{{ related_code }}">
<span>{{ related_code }}</span>
<button type="button" class="pc-related-remove" data-related-remove="{{ related_code }}">x</button>
</span>
{% endfor %}
</div>
<div class="pc-related-list" id="relatedSuggestList"></div>
</div>
</section>
<section class="pc-block">
<h3>지출 내역</h3>
<div class="pc-table-wrap">
<table class="pc-table">
<thead>
<tr>
<th>구분</th>
<th>목표 원가</th>
<th>실지출</th>
<th>차이(목표-실제)</th>
<th>집행률</th>
</tr>
</thead>
<tbody>
{% for row in detail.phase_rows or [] %}
<tr>
<td>{{ row.phase }}</td>
<td>{{ "{:,.0f}".format(row.target_amount|default(0)) }}</td>
<td>{{ "{:,.0f}".format(row.actual_amount|default(0)) }}</td>
<td class="{% if (row.gap_amount|default(0)) < 0 %}negative{% elif (row.gap_amount|default(0)) > 0 %}positive{% endif %}">
{{ "{:,.0f}".format(row.gap_amount|default(0)) }}
</td>
<td>{{ "{:.1f}".format(row.progress_rate|default(0)) }}%</td>
</tr>
{% endfor %}
{% if not (detail.phase_rows or []) %}
<tr><td colspan="5" style="text-align:center; color: transparent;">&nbsp;</td></tr>
{% endif %}
</tbody>
</table>
</div>
</section>
<section class="pc-block">
<h3>지출 상위 항목</h3>
<div class="pc-table-wrap">
<table class="pc-table">
<thead>
<tr>
<th>계정</th>
<th>실지출</th>
<th>비중</th>
<th>전표건수</th>
<th>최근 증빙일</th>
</tr>
</thead>
<tbody>
{% for row in detail.account_rows or [] %}
<tr>
<td title="{{ row.account_code }} {{ row.account_name }}">{{ row.account_code }} {{ row.account_name }}</td>
<td>{{ "{:,.0f}".format(row.amount|default(0)) }}</td>
<td>{{ "{:.1f}".format(row.share_rate|default(0)) }}%</td>
<td>{{ "{:,}".format(row.row_count|default(0)) }}</td>
<td>{{ row.last_posting_date or '-' }}</td>
</tr>
{% endfor %}
{% if not (detail.account_rows or []) %}
<tr><td colspan="5" style="text-align:center; color: transparent;">&nbsp;</td></tr>
{% endif %}
</tbody>
</table>
</div>
</section>
</section>
<aside class="pc-side-stack">
<section class="pc-kpi">
<h3>원가 건전성 지표</h3>
<div class="pc-kpi-row">
<div class="pc-kpi-head"><span>비용/수익 비율</span><strong>{{ "{:.1f}".format(diagnostics.cost_to_revenue_ratio|default(0)) }}%</strong></div>
<div class="pc-bar"><span data-rate="{{ diagnostics.cost_to_revenue_ratio|default(0) }}"></span></div>
</div>
<div class="pc-kpi-row">
<div class="pc-kpi-head"><span>직접인건비 비중</span><strong>{{ "{:.1f}".format(diagnostics.labor_ratio|default(0)) }}%</strong></div>
<div class="pc-bar"><span data-rate="{{ diagnostics.labor_ratio|default(0) }}"></span></div>
</div>
<div class="pc-kpi-row">
<div class="pc-kpi-head"><span>외주비 비중</span><strong>{{ "{:.1f}".format(diagnostics.outsourcing_ratio|default(0)) }}%</strong></div>
<div class="pc-bar"><span data-rate="{{ diagnostics.outsourcing_ratio|default(0) }}"></span></div>
</div>
<div class="pc-kpi-row">
<div class="pc-kpi-head"><span>제경비 비중</span><strong>{{ "{:.1f}".format(diagnostics.design_cost_ratio|default(0)) }}%</strong></div>
<div class="pc-bar"><span data-rate="{{ diagnostics.design_cost_ratio|default(0) }}"></span></div>
</div>
<div class="pc-kpi-row">
<div class="pc-kpi-head"><span>판관비 비중</span><strong>{{ "{:.1f}".format(diagnostics.sga_ratio|default(0)) }}%</strong></div>
<div class="pc-bar"><span data-rate="{{ diagnostics.sga_ratio|default(0) }}"></span></div>
</div>
</section>
<section class="pc-kpi">
<h3>데이터 현황</h3>
<div class="pc-kpi-head"><span>선택 소스</span><strong>{% if process_cost_source == 'wehago' %}WEHAGO{% else %}Hanmac ERP{% endif %}</strong></div>
<div class="pc-kpi-head"><span>기간</span><strong>{% if process_cost_selected_start_year and process_cost_selected_end_year %}{{ process_cost_selected_start_year }} ~ {{ process_cost_selected_end_year }}{% else %}전체 기간{% endif %}</strong></div>
<div class="pc-kpi-head"><span>전표 건수</span><strong>{{ "{:,}".format(overview.voucher_count|default(0)) }}</strong></div>
<div class="pc-kpi-head"><span>행 건수</span><strong>{{ "{:,}".format(overview.row_count|default(0)) }}</strong></div>
<div class="pc-kpi-head"><span>실행률</span><strong>{{ "{:.1f}".format(overview.execution_rate|default(0)) }}%</strong></div>
<div class="pc-kpi-head"><span>연계 반영</span><strong>{% if process_cost_include_related %}반영{% else %}미반영{% endif %}</strong></div>
</section>
<section class="pc-mini-chart">
<h3>최근 월별 수익/비용 추이</h3>
<svg id="processMonthlyChart" viewBox="0 0 540 220" preserveAspectRatio="xMidYMid meet"></svg>
</section>
</aside>
</div>
</section>
<div class="modal-backdrop" id="processFlowModal">
<div class="related-modal-card" style="max-width:520px;">
<div class="related-modal-head related-modal-head--singleline">
<h3 class="related-modal-title" id="processFlowModalTitle"></h3>
<button type="button" class="button-icon button-secondary" id="processFlowModalClose" aria-label="닫기">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 7l10 10"></path>
<path d="M17 7L7 17"></path>
</svg>
</button>
</div>
<div class="pc-note" id="processFlowModalNote"></div>
</div>
</div>
{% endblock %}
{% block script %}
<script>
(() => {
const processCostProjects = {{ process_cost_projects | tojson }};
const processCostProjectMap = new Map(
processCostProjects
.filter((item) => item && item.support_dept_code)
.map((item) => [item.support_dept_code, item])
);
const selectedCode = {{ process_cost_selected_code | tojson }};
const selectedProject = {{ (process_cost_selected_project 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 searchInput = document.getElementById("projectSearchInput");
const list = document.getElementById("projectList");
const projectListEmpty = document.getElementById("projectListEmpty");
const typeFilter = document.getElementById("projectTypeFilter");
const quickLinksContainer = document.getElementById("pcQuickLinks");
const selectedProjectBookmark = document.getElementById("selectedProjectBookmark");
const collapseButtons = document.querySelectorAll("[data-collapse-target]");
const processModal = document.getElementById("processFlowModal");
const processModalTitle = document.getElementById("processFlowModalTitle");
const processModalNote = document.getElementById("processFlowModalNote");
const processModalClose = document.getElementById("processFlowModalClose");
const processButtons = document.querySelectorAll("[data-process-modal]");
const includeRelatedEnabled = {{ 1 if process_cost_include_related else 0 }};
let bookmarkedCodes = Array.isArray(initialQuickLinkCodes) ? [...initialQuickLinkCodes] : [];
let activeRelatedCodes = Array.isArray(initialActiveRelatedCodes) ? [...initialActiveRelatedCodes] : [];
function buildProcessCostUrl(code, options = {}) {
const params = new URLSearchParams();
params.set("source", {{ process_cost_source | tojson }});
if ({{ process_cost_selected_start_year | tojson }}) {
params.set("start_year", {{ process_cost_selected_start_year | tojson }});
}
if ({{ process_cost_selected_end_year | tojson }}) {
params.set("end_year", {{ process_cost_selected_end_year | tojson }});
}
if (code) {
params.set("code", code);
}
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("active_related", relatedCodes.length ? relatedCodes.join(",") : "-");
}
return `/process-cost?${params.toString()}`;
}
function getBookmarkedProjects() {
return bookmarkedCodes
.map((code) => processCostProjectMap.get(code) || { support_dept_code: code, support_dept_name: "", project_kind_label: "" })
.filter((item) => item && item.support_dept_code);
}
async function persistBookmarkedCodes(nextCodes) {
const response = await fetch("/process-cost/quick-links", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ codes: nextCodes }),
});
const payload = await response.json();
if (!response.ok || payload.error) {
throw new Error(payload.error || "바로가기를 저장하지 못했습니다.");
}
bookmarkedCodes = Array.isArray(payload.codes) ? payload.codes : nextCodes;
}
function isBookmarked(code) {
return bookmarkedCodes.includes(code);
}
function syncBookmarkButton() {
if (!selectedProjectBookmark) return;
const code = selectedProjectBookmark.dataset.bookmarkCode || "";
const active = Boolean(code) && isBookmarked(code);
selectedProjectBookmark.classList.toggle("is-active", active);
selectedProjectBookmark.setAttribute("aria-label", 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() {
if (!quickLinksContainer) return;
const items = getBookmarkedProjects();
if (!items.length) {
quickLinksContainer.innerHTML = "";
syncSelectedProjectCard();
return;
}
quickLinksContainer.innerHTML = items.map((item) => `
<div class="pc-quick-link" title="${item.support_dept_code} ${item.support_dept_name || ""}">
<a href="${buildProcessCostUrl(item.support_dept_code)}" class="pc-quick-link-main">
<strong>${item.support_dept_code}</strong>
<span>${item.support_dept_name || ""}</span>
</a>
<button type="button" class="pc-bookmark-button is-active" data-bookmark-toggle="${item.support_dept_code}" aria-label="바로가기 해제" title="바로가기 해제">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 4h10a1 1 0 0 1 1 1v15l-6-3-6 3V5a1 1 0 0 1 1-1z"></path>
</svg>
</button>
</div>
`).join("");
quickLinksContainer.querySelectorAll("[data-bookmark-toggle]").forEach((button) => {
button.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const code = button.getAttribute("data-bookmark-toggle") || "";
const nextCodes = bookmarkedCodes.filter((item) => item !== code);
persistBookmarkedCodes(nextCodes)
.then(() => {
renderQuickLinks();
syncBookmarkButton();
syncSelectedProjectCard();
closeProjectList();
filterProjectList();
})
.catch((error) => {
window.alert(error.message || "바로가기를 저장하지 못했습니다.");
});
});
});
syncSelectedProjectCard();
}
function getVisibleProjectItems() {
return [...list.querySelectorAll("[data-project-search]")].filter((item) => item.style.display !== "none");
}
function filterProjectList() {
if (!searchInput || !list) return;
const keyword = (searchInput.value || "").trim().toLowerCase();
const typeValue = (typeFilter?.value || "").trim().toLowerCase();
const bookmarkedSet = new Set(bookmarkedCodes);
let visibleCount = 0;
list.querySelectorAll("[data-project-search]").forEach((item) => {
const text = (item.dataset.projectSearch || "").toLowerCase();
const kind = (item.dataset.projectKind || "").toLowerCase();
const matchesKeyword = !keyword || text.includes(keyword);
const matchesType = !typeValue || kind === typeValue;
const codeNode = item.querySelector(".pc-project-code");
const code = (codeNode?.textContent || "").trim();
const isVisible = matchesKeyword && matchesType && !bookmarkedSet.has(code);
item.hidden = !isVisible;
if (isVisible) {
visibleCount += 1;
}
});
if (projectListEmpty) {
projectListEmpty.hidden = visibleCount !== 0;
}
return visibleCount;
}
function openProjectList() {
if (!list) return;
const visibleCount = filterProjectList();
const visibleItems = getVisibleProjectItems();
list.hidden = false;
if (projectListEmpty) {
projectListEmpty.hidden = (visibleCount || visibleItems.length) !== 0;
}
}
function closeProjectList() {
if (list) {
list.hidden = true;
}
}
if (searchInput && list) {
searchInput.addEventListener("focus", () => {
openProjectList();
});
searchInput.addEventListener("input", openProjectList);
searchInput.addEventListener("click", openProjectList);
document.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
if (target.closest(".pc-selector-field")) return;
closeProjectList();
});
}
if (typeFilter) {
typeFilter.addEventListener("change", () => {
openProjectList();
});
}
collapseButtons.forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.getAttribute("data-collapse-target");
if (!targetId) return;
const section = document.getElementById(targetId);
if (!section) return;
section.classList.toggle("is-collapsed");
});
});
processButtons.forEach((button) => {
button.addEventListener("click", () => {
const kind = button.getAttribute("data-process-modal") || "";
const titleMap = {
contract: "계약정보",
planning: "계획수립",
collection: "수금관리",
expense: "경비/외주비 지출",
progress: "진행률관리",
};
if (processModalTitle) {
processModalTitle.textContent = titleMap[kind] || "프로세스";
}
if (processModalNote) {
processModalNote.textContent = "";
}
if (processModal) {
processModal.classList.add("is-open");
}
});
});
function closeProcessModal() {
if (processModal) {
processModal.classList.remove("is-open");
}
}
if (processModalClose) {
processModalClose.addEventListener("click", closeProcessModal);
}
if (processModal) {
processModal.addEventListener("click", (event) => {
if (event.target === processModal) {
closeProcessModal();
}
});
}
renderQuickLinks();
syncBookmarkButton();
syncSelectedProjectCard();
filterProjectList();
if (selectedProjectBookmark) {
selectedProjectBookmark.addEventListener("click", async () => {
const code = selectedProjectBookmark.dataset.bookmarkCode || "";
if (!code) return;
const nextCodes = bookmarkedCodes.filter((item) => item !== code);
if (!isBookmarked(code)) {
nextCodes.unshift(code);
}
try {
await persistBookmarkedCodes(nextCodes);
renderQuickLinks();
syncBookmarkButton();
syncSelectedProjectCard();
closeProjectList();
filterProjectList();
} catch (error) {
window.alert(error.message || "바로가기를 저장하지 못했습니다.");
}
});
}
document.querySelectorAll(".pc-bar > span[data-rate]").forEach((bar) => {
const raw = Number(bar.dataset.rate || 0);
const width = Math.max(0, Math.min(raw, 100));
bar.style.width = `${width}%`;
});
const relatedSearchInput = document.getElementById("relatedSearchInput");
const relatedSuggestList = document.getElementById("relatedSuggestList");
const relatedChipRow = document.getElementById("relatedChipRow");
const relatedSaveButton = document.getElementById("relatedSaveButton");
const relatedResetButton = document.getElementById("relatedResetButton");
const relatedSet = new Set(initialRelatedCodes || []);
function renderRelatedChips() {
if (!relatedChipRow) return;
const fixedHead = selectedCode ? `<span class="pc-chip">${selectedCode} 기준</span>` : "";
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}">
<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>
</span>
`;
}).join("");
const 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) => {
button.addEventListener("click", () => {
const code = button.dataset.relatedRemove || "";
if (code) {
relatedSet.delete(code);
activeRelatedCodes = activeRelatedCodes.filter((item) => item !== code);
renderRelatedChips();
}
});
});
}
function renderRelatedSuggestions(keyword) {
if (!relatedSuggestList) return;
const normalized = (keyword || "").trim().toLowerCase();
if (!normalized || !selectedCode) {
relatedSuggestList.classList.remove("active");
relatedSuggestList.innerHTML = "";
return;
}
const rows = processCostProjects
.filter((item) => item.support_dept_code !== selectedCode)
.filter((item) => !relatedSet.has(item.support_dept_code))
.filter((item) => `${item.support_dept_code} ${item.support_dept_name || ""} ${item.client_name || ""}`.toLowerCase().includes(normalized))
.slice(0, 30);
if (!rows.length) {
relatedSuggestList.classList.remove("active");
relatedSuggestList.innerHTML = "";
return;
}
relatedSuggestList.innerHTML = rows.map((item) => `
<button type="button" class="pc-related-item" data-related-pick="${item.support_dept_code}">
${item.support_dept_code} ${item.support_dept_name || ""}
</button>
`).join("");
relatedSuggestList.classList.add("active");
relatedSuggestList.querySelectorAll("[data-related-pick]").forEach((button) => {
button.addEventListener("click", () => {
const code = button.dataset.relatedPick || "";
if (code) {
relatedSet.add(code);
if (relatedSearchInput) relatedSearchInput.value = "";
relatedSuggestList.classList.remove("active");
relatedSuggestList.innerHTML = "";
renderRelatedChips();
}
});
});
}
if (relatedSearchInput) {
relatedSearchInput.addEventListener("input", () => {
renderRelatedSuggestions(relatedSearchInput.value || "");
});
relatedSearchInput.addEventListener("focus", () => {
renderRelatedSuggestions(relatedSearchInput.value || "");
});
}
if (relatedResetButton) {
relatedResetButton.addEventListener("click", () => {
relatedSet.clear();
for (const code of initialRelatedCodes || []) {
relatedSet.add(code);
}
activeRelatedCodes = Array.isArray(initialActiveRelatedCodes) ? [...initialActiveRelatedCodes] : [];
if (relatedSearchInput) relatedSearchInput.value = "";
if (relatedSuggestList) {
relatedSuggestList.classList.remove("active");
relatedSuggestList.innerHTML = "";
}
renderRelatedChips();
});
}
if (relatedSaveButton) {
relatedSaveButton.addEventListener("click", async () => {
if (!selectedCode) return;
relatedSaveButton.disabled = true;
try {
const response = await fetch("/projects/related-links", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
base_code: selectedCode,
related_codes: [...relatedSet],
}),
});
const payload = await response.json();
if (!response.ok || payload.error) {
throw new Error(payload.error || "연계 프로젝트 저장에 실패했습니다.");
}
window.location.reload();
} catch (error) {
alert(error.message || "연계 프로젝트 저장에 실패했습니다.");
} finally {
relatedSaveButton.disabled = false;
}
});
}
renderRelatedChips();
const startYearSelect = document.getElementById("processCostStartYear");
const endYearSelect = document.getElementById("processCostEndYear");
const syncYearRangeOptions = () => {
if (!startYearSelect || !endYearSelect) return;
const startValue = Number(startYearSelect.value || 0);
const endValue = Number(endYearSelect.value || 0);
Array.from(startYearSelect.options).forEach((option) => {
const value = Number(option.value || 0);
option.disabled = Boolean(endValue && value > endValue);
});
Array.from(endYearSelect.options).forEach((option) => {
const value = Number(option.value || 0);
option.disabled = Boolean(startValue && value < startValue);
});
};
if (startYearSelect && endYearSelect) {
startYearSelect.addEventListener("change", () => {
const startValue = Number(startYearSelect.value || 0);
const endValue = Number(endYearSelect.value || 0);
if (startValue && endValue && startValue > endValue) {
endYearSelect.value = String(startValue);
}
syncYearRangeOptions();
});
endYearSelect.addEventListener("change", () => {
const startValue = Number(startYearSelect.value || 0);
const endValue = Number(endYearSelect.value || 0);
if (startValue && endValue && endValue < startValue) {
startYearSelect.value = String(endValue);
}
syncYearRangeOptions();
});
syncYearRangeOptions();
}
const monthlyRows = {{ (process_cost_detail.monthly_rows or []) | tojson }};
const svg = document.getElementById("processMonthlyChart");
if (!svg || !Array.isArray(monthlyRows) || !monthlyRows.length) {
return;
}
const width = 540;
const height = 220;
const pad = { top: 16, right: 14, bottom: 36, left: 42 };
const plotW = width - pad.left - pad.right;
const plotH = height - pad.top - pad.bottom;
const maxVal = Math.max(
...monthlyRows.map((row) => Number(row.revenue_amount || 0)),
...monthlyRows.map((row) => Number(row.expense_amount || 0)),
1
);
const stepX = monthlyRows.length > 1 ? plotW / (monthlyRows.length - 1) : 0;
const y = (v) => pad.top + (1 - Math.min(v / maxVal, 1)) * plotH;
const x = (i) => pad.left + stepX * i;
const revenuePoints = monthlyRows.map((row, i) => `${x(i)},${y(Number(row.revenue_amount || 0))}`).join(" ");
const expensePoints = monthlyRows.map((row, i) => `${x(i)},${y(Number(row.expense_amount || 0))}`).join(" ");
let labels = "";
monthlyRows.forEach((row, i) => {
labels += `<text x="${x(i)}" y="${height - 12}" text-anchor="middle" fill="#677182" font-size="10">${(row.month_label || '').slice(2)}</text>`;
});
svg.innerHTML = `
<line x1="${pad.left}" y1="${pad.top}" x2="${pad.left}" y2="${height - pad.bottom}" stroke="#d9dde3" />
<line x1="${pad.left}" y1="${height - pad.bottom}" x2="${width - pad.right}" y2="${height - pad.bottom}" stroke="#d9dde3" />
<polyline points="${revenuePoints}" fill="none" stroke="#2c5f96" stroke-width="2.3" />
<polyline points="${expensePoints}" fill="none" stroke="#d9822b" stroke-width="2.3" />
${labels}
<text x="${pad.left}" y="${pad.top - 3}" fill="#2c5f96" font-size="11">수익</text>
<text x="${pad.left + 36}" y="${pad.top - 3}" fill="#d9822b" font-size="11">비용</text>
`;
})();
</script>
{% endblock %}