663 lines
22 KiB
HTML
663 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>회계 데이터 인트라넷 대시보드</title>
|
|
<style>
|
|
:root {
|
|
--bg-a: #f3efe7;
|
|
--bg-b: #d7e5eb;
|
|
--panel: rgba(255, 252, 247, 0.92);
|
|
--ink: #14212f;
|
|
--muted: #5a6672;
|
|
--line: #d8dee5;
|
|
--accent: #0b6b63;
|
|
--accent-strong: #074b49;
|
|
--accent-soft: #e6f5f2;
|
|
--warn: #fff0c9;
|
|
--table-alt: #f9fbfc;
|
|
--white: #ffffff;
|
|
--page-gutter: clamp(14px, 1.8vw, 24px);
|
|
--panel-pad: clamp(16px, 1.4vw, 20px);
|
|
--page-frame-width: min(1520px, calc(100vw - (var(--page-gutter) * 2)));
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
::selection {
|
|
background: rgba(11, 107, 99, 0.16);
|
|
color: var(--ink);
|
|
}
|
|
|
|
::-moz-selection {
|
|
background: rgba(11, 107, 99, 0.16);
|
|
color: var(--ink);
|
|
}
|
|
|
|
body {
|
|
font-family: "Noto Sans KR", "Malgun Gothic", sans-serif;
|
|
color: var(--ink);
|
|
background:
|
|
radial-gradient(circle at top left, rgba(255, 255, 255, 0.9), transparent 28%),
|
|
linear-gradient(155deg, var(--bg-a), var(--bg-b));
|
|
min-height: 100vh;
|
|
padding: var(--page-gutter);
|
|
}
|
|
|
|
.page {
|
|
width: min(100%, var(--page-frame-width));
|
|
margin: 0 auto;
|
|
display: grid;
|
|
gap: var(--page-gutter);
|
|
}
|
|
|
|
.page > * {
|
|
width: 100%;
|
|
}
|
|
|
|
.panel {
|
|
background: var(--panel);
|
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
|
border-radius: 24px;
|
|
padding: var(--panel-pad);
|
|
box-shadow: 0 20px 45px rgba(51, 76, 92, 0.12);
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.section-title h2 {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.section-title p {
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.message {
|
|
background: var(--warn);
|
|
border: 1px solid #efd486;
|
|
color: #624c0b;
|
|
border-radius: 16px;
|
|
padding: 14px 16px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
.summary-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 18px;
|
|
margin-top: 18px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--white);
|
|
border: 1px solid var(--line);
|
|
border-radius: 18px;
|
|
padding: 18px;
|
|
}
|
|
|
|
.stat-card .label {
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.stat-card .value {
|
|
font-size: 28px;
|
|
font-weight: 800;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.two-col {
|
|
display: grid;
|
|
grid-template-columns: 0.92fr 1.08fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
.stack {
|
|
display: grid;
|
|
gap: 20px;
|
|
}
|
|
|
|
form {
|
|
display: grid;
|
|
gap: 14px;
|
|
}
|
|
|
|
.upload-box {
|
|
background: linear-gradient(180deg, #f8fffd, #eef8f6);
|
|
border: 1px dashed #a7d1c8;
|
|
border-radius: 18px;
|
|
padding: 18px;
|
|
content-visibility: auto;
|
|
contain-intrinsic-size: 240px;
|
|
}
|
|
|
|
.upload-box p {
|
|
color: var(--muted);
|
|
line-height: 1.65;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
.field {
|
|
display: grid;
|
|
gap: 7px;
|
|
}
|
|
|
|
.field-wide {
|
|
grid-column: span 2;
|
|
}
|
|
|
|
.field-full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
label {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
}
|
|
|
|
input,
|
|
select,
|
|
textarea,
|
|
button {
|
|
font: inherit;
|
|
}
|
|
|
|
input[type="text"],
|
|
input[type="number"],
|
|
input[type="date"],
|
|
input[type="file"],
|
|
select,
|
|
textarea {
|
|
width: 100%;
|
|
border: 1px solid var(--line);
|
|
background: var(--white);
|
|
border-radius: 14px;
|
|
padding: 12px 14px;
|
|
color: var(--ink);
|
|
}
|
|
|
|
textarea {
|
|
min-height: 96px;
|
|
resize: vertical;
|
|
}
|
|
|
|
input:focus,
|
|
select:focus,
|
|
textarea:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 4px rgba(11, 107, 99, 0.12);
|
|
}
|
|
|
|
select option {
|
|
color: var(--ink);
|
|
background: var(--white);
|
|
}
|
|
|
|
select option:checked,
|
|
select option:hover,
|
|
select option:focus {
|
|
color: var(--ink);
|
|
background: #e7f0f5;
|
|
box-shadow: inset 0 0 0 999px #e7f0f5;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
button,
|
|
.button-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
border-radius: 999px;
|
|
border: none;
|
|
padding: 12px 18px;
|
|
background: var(--accent);
|
|
color: var(--white);
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
|
|
button:hover,
|
|
.button-link:hover {
|
|
background: var(--accent-strong);
|
|
}
|
|
|
|
.button-secondary {
|
|
background: #e7f0f5;
|
|
color: #204257;
|
|
}
|
|
|
|
.button-secondary:hover,
|
|
.button-secondary:focus-visible {
|
|
background: #dbeaf2;
|
|
color: #163345;
|
|
}
|
|
|
|
.table-wrap {
|
|
overflow: auto;
|
|
border: 1px solid var(--line);
|
|
border-radius: 18px;
|
|
background: var(--white);
|
|
content-visibility: auto;
|
|
contain-intrinsic-size: 360px;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
min-width: 780px;
|
|
}
|
|
|
|
th,
|
|
td {
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--line);
|
|
text-align: left;
|
|
vertical-align: top;
|
|
font-size: 14px;
|
|
}
|
|
|
|
th {
|
|
position: sticky;
|
|
top: 0;
|
|
background: #eff5f7;
|
|
color: #345061;
|
|
z-index: 1;
|
|
}
|
|
|
|
tbody tr:nth-child(even) td {
|
|
background: var(--table-alt);
|
|
}
|
|
|
|
.empty {
|
|
padding: 24px;
|
|
color: var(--muted);
|
|
text-align: center;
|
|
}
|
|
|
|
.note-list {
|
|
display: grid;
|
|
gap: 8px;
|
|
color: var(--muted);
|
|
line-height: 1.7;
|
|
font-size: 14px;
|
|
}
|
|
|
|
details.panel summary {
|
|
list-style: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
details.panel summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.search-box {
|
|
margin: 16px 0;
|
|
}
|
|
|
|
.mono {
|
|
font-family: "Consolas", "Courier New", monospace;
|
|
font-size: 13px;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.stats {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
|
|
.summary-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.two-col {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.form-grid {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
@media (max-width: 720px) {
|
|
body {
|
|
padding: 14px;
|
|
}
|
|
|
|
.stats,
|
|
.form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.field-wide,
|
|
.field-full {
|
|
grid-column: auto;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="page">
|
|
{% if message %}
|
|
<div class="message">{{ message }}</div>
|
|
{% endif %}
|
|
|
|
<section class="panel">
|
|
<div class="section-title">
|
|
<h2>현황 요약</h2>
|
|
<p>DB에 저장된 전체 자료 기준</p>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="label">전체 데이터 건수</div>
|
|
<div class="value">{{ overview.total_rows or 0 }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">업로드 파일 수</div>
|
|
<div class="value">{{ overview.source_files or 0 }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">집계 대상 사업 수</div>
|
|
<div class="value">{{ overview.business_count or 0 }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">원가 총액</div>
|
|
<div class="value">{{ "{:,.0f}".format(overview.total_cost or 0) }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">판관비 총액</div>
|
|
<div class="value">{{ "{:,.0f}".format(overview.total_sga or 0) }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="summary-grid">
|
|
<div class="table-wrap">
|
|
{% if yearly_summary %}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>연도</th>
|
|
<th>원가 합계</th>
|
|
<th>판관비 합계</th>
|
|
<th>원가인건비</th>
|
|
<th>원가외주비</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in yearly_summary %}
|
|
<tr>
|
|
<td>{{ item.year }}</td>
|
|
<td>{{ "{:,.0f}".format(item.cost_sum or 0) }}</td>
|
|
<td>{{ "{:,.0f}".format(item.sga_sum or 0) }}</td>
|
|
<td>{{ "{:,.0f}".format(item.labor_sum or 0) }}</td>
|
|
<td>{{ "{:,.0f}".format(item.outsourcing_sum or 0) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty">연간 집계 데이터가 없습니다.</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="table-wrap">
|
|
{% if monthly_summary %}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>연도</th>
|
|
<th>월</th>
|
|
<th>원가 합계</th>
|
|
<th>판관비 합계</th>
|
|
<th>원가인건비</th>
|
|
<th>원가외주비</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in monthly_summary %}
|
|
<tr>
|
|
<td>{{ item.year }}</td>
|
|
<td>{{ item.month }}</td>
|
|
<td>{{ "{:,.0f}".format(item.cost_sum or 0) }}</td>
|
|
<td>{{ "{:,.0f}".format(item.sga_sum or 0) }}</td>
|
|
<td>{{ "{:,.0f}".format(item.labor_sum or 0) }}</td>
|
|
<td>{{ "{:,.0f}".format(item.outsourcing_sum or 0) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty">월별 집계 데이터가 없습니다.</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="section-title">
|
|
<h2>계약·청구 동기화 현황</h2>
|
|
<p>계약현황 / 기성청구현황 파일 기준</p>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="label">계약 프로젝트</div>
|
|
<div class="value">{{ import_sync_summary.contract_project_count or 0 }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">청구 프로젝트</div>
|
|
<div class="value">{{ import_sync_summary.billing_project_count or 0 }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">청구 행 수</div>
|
|
<div class="value">{{ import_sync_summary.billing_entry_count or 0 }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">변경계약 검토</div>
|
|
<div class="value">{{ import_sync_summary.review_needed_count or 0 }}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="label">청구 수금합계</div>
|
|
<div class="value">{{ "{:,.0f}".format(import_sync_summary.total_collected_amount or 0) }}</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="two-col">
|
|
<div class="stack">
|
|
<section class="panel">
|
|
<div class="section-title">
|
|
<h2>엑셀 업로드</h2>
|
|
<p>업로드 즉시 DB 저장</p>
|
|
</div>
|
|
<div class="upload-box">
|
|
<p>
|
|
업로드 파일은 이미지에 보인 열 형식 기준으로 읽습니다.
|
|
예: 결재상태, 가전표번호, 계정코드, 계정명칭, 차변공급가, 대변공급가, 지원부서코드,
|
|
지원부서명, 원가부서코드, 원가부서명, 적요1, 관리항목 등
|
|
</p>
|
|
<p>
|
|
현재 프로젝트 폴더에 있는 엑셀 파일은 서버 시작 시 DB가 비어 있으면 자동으로 적재됩니다.
|
|
</p>
|
|
<form action="/upload" method="post" enctype="multipart/form-data">
|
|
<div class="field">
|
|
<label for="excel_file">엑셀 파일 선택</label>
|
|
<input id="excel_file" type="file" name="excel_file" accept=".xlsx,.xlsm,.xltx,.xltm" required>
|
|
</div>
|
|
<div class="actions">
|
|
<button type="submit">엑셀을 DB에 저장</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<div class="section-title">
|
|
<h2>{{ "데이터 수정" if edit_record.id else "DB 직접 입력" }}</h2>
|
|
<p>엑셀 없이도 직접 등록/수정 가능</p>
|
|
</div>
|
|
<form action="/records/save" method="post">
|
|
<input type="hidden" name="id" value="{{ edit_record.id }}">
|
|
<div class="form-grid">
|
|
{% for field_name, field_label in field_labels.items() %}
|
|
<div class="field {% if field_name in ['memo1', 'memo2', 'management_item'] %}field-wide{% endif %}">
|
|
<label for="{{ field_name }}">{{ field_label }}</label>
|
|
{% if field_name in ['memo1', 'memo2', 'management_item'] %}
|
|
<textarea id="{{ field_name }}" name="{{ field_name }}">{{ edit_record[field_name] }}</textarea>
|
|
{% elif field_name == 'posting_date' %}
|
|
<input id="{{ field_name }}" type="date" name="{{ field_name }}" value="{{ edit_record[field_name] }}">
|
|
{% elif field_name in ['debit_supply', 'debit_vat', 'credit_supply', 'credit_vat'] %}
|
|
<input id="{{ field_name }}" type="number" step="0.01" name="{{ field_name }}" value="{{ edit_record[field_name] }}">
|
|
{% else %}
|
|
<input id="{{ field_name }}" type="text" name="{{ field_name }}" value="{{ edit_record[field_name] }}">
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="actions">
|
|
<button type="submit">{{ "수정 내용을 저장" if edit_record.id else "새 데이터 저장" }}</button>
|
|
{% if edit_record.id %}
|
|
<a class="button-link button-secondary" href="/">수정 취소</a>
|
|
{% endif %}
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="stack">
|
|
<details class="panel">
|
|
<summary>
|
|
<span>집계 대상 사업</span>
|
|
<span style="font-size:14px;color:var(--muted);">검색해서 펼쳐보기</span>
|
|
</summary>
|
|
<div class="search-box">
|
|
<input type="text" id="support-business-search" placeholder="지원부서코드 또는 사업명을 입력하세요.">
|
|
</div>
|
|
<div class="table-wrap">
|
|
{% if support_businesses %}
|
|
<table id="support-business-table">
|
|
<thead>
|
|
<tr>
|
|
<th>지원부서코드</th>
|
|
<th>사업명</th>
|
|
<th>행 수</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in support_businesses %}
|
|
<tr data-search="{{ item.support_dept_code }} {{ item.support_dept_name }}">
|
|
<td class="mono">{{ item.support_dept_code }}</td>
|
|
<td>{{ item.support_dept_name }}</td>
|
|
<td>{{ item.row_count }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty">아직 표시할 사업 데이터가 없습니다. 엑셀 업로드 또는 수동 입력을 먼저 진행해주세요.</div>
|
|
{% endif %}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</section>
|
|
|
|
<details class="panel">
|
|
<summary>
|
|
<span>사업별 연도/월 사용 비용</span>
|
|
<span style="font-size:14px;color:var(--muted);">검색해서 펼쳐보기</span>
|
|
</summary>
|
|
<div class="search-box">
|
|
<input type="text" id="business-cost-search" placeholder="연도, 월, 지원부서코드, 사업명으로 검색하세요.">
|
|
</div>
|
|
<div class="table-wrap">
|
|
{% if business_monthly_summary %}
|
|
<table id="business-cost-table">
|
|
<thead>
|
|
<tr>
|
|
<th>연도</th>
|
|
<th>월</th>
|
|
<th>지원부서코드</th>
|
|
<th>사업명</th>
|
|
<th>원가</th>
|
|
<th>판관비</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for item in business_monthly_summary %}
|
|
<tr data-search="{{ item.year }} {{ item.month }} {{ item.support_dept_code }} {{ item.support_dept_name }}">
|
|
<td>{{ item.year }}</td>
|
|
<td>{{ item.month }}</td>
|
|
<td class="mono">{{ item.support_dept_code }}</td>
|
|
<td>{{ item.support_dept_name }}</td>
|
|
<td>{{ "{:,.0f}".format(item.cost_sum or 0) }}</td>
|
|
<td>{{ "{:,.0f}".format(item.sga_sum or 0) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty">사업별 월 집계 데이터가 없습니다.</div>
|
|
{% endif %}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<script>
|
|
function bindTableSearch(inputId, tableId) {
|
|
const input = document.getElementById(inputId);
|
|
const table = document.getElementById(tableId);
|
|
if (!input || !table) return;
|
|
const rows = Array.from(table.querySelectorAll("tbody tr"));
|
|
input.addEventListener("input", () => {
|
|
const keyword = input.value.trim().toLowerCase();
|
|
rows.forEach((row) => {
|
|
const haystack = (row.dataset.search || row.textContent || "").toLowerCase();
|
|
row.style.display = !keyword || haystack.includes(keyword) ? "" : "none";
|
|
});
|
|
});
|
|
}
|
|
|
|
bindTableSearch("support-business-search", "support-business-table");
|
|
bindTableSearch("business-cost-search", "business-cost-table");
|
|
</script>
|
|
</body>
|
|
</html>
|