Files

628 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;
}
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;
}
.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"],
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,
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(11, 107, 99, 0.12);
}
.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;
}
.table-wrap {
overflow: auto;
border: 1px solid var(--line);
border-radius: 18px;
background: var(--white);
}
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>