Organize root directory: Moved legacy HTML files to legacy/

This commit is contained in:
2026-03-23 14:45:20 +09:00
parent 35ababe236
commit 358585da53
3 changed files with 0 additions and 0 deletions

437
legacy/main_page.html Normal file
View File

@@ -0,0 +1,437 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Profit Main</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+KR:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--ink: #0f1c2e;
--muted: #66788f;
--line: #d8e2ec;
--blue: #113f67;
--cyan: #1f7a8c;
--mist: #eff5fb;
--soft: rgba(255,255,255,0.88);
--warn: #b85c38;
--good: #1b7f5a;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'IBM Plex Sans KR', sans-serif;
color: var(--ink);
background:
linear-gradient(125deg, rgba(17,63,103,0.10), transparent 30%),
radial-gradient(circle at 85% 10%, rgba(31,122,140,0.12), transparent 22%),
linear-gradient(180deg, #f8fbff 0%, #edf2f7 100%);
}
.page { width: min(1460px, calc(100vw - 28px)); margin: 0 auto; padding: 26px 0 60px; }
.panel {
background: var(--soft);
border: 1px solid rgba(216,226,236,0.95);
border-radius: 26px;
box-shadow: 0 18px 42px rgba(15, 28, 46, 0.07);
backdrop-filter: blur(10px);
}
.hero {
display: grid;
grid-template-columns: 1.35fr 1fr;
gap: 18px;
}
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
.two-col {
display: grid;
grid-template-columns: 1.08fr 0.92fr;
gap: 18px;
}
.three-col {
display: grid;
grid-template-columns: 1.1fr 1fr 1fr;
gap: 18px;
}
.metric {
border-radius: 20px;
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(255,255,255,0.97), rgba(244,248,252,0.98));
padding: 18px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 12px;
border-radius: 999px;
background: #e6f1fb;
color: var(--blue);
font-size: 11px;
font-weight: 700;
}
.subtle { color: var(--muted); font-size: 12px; line-height: 1.65; }
.field, .select {
width: 100%;
height: 42px;
border: 1px solid var(--line);
border-radius: 12px;
background: white;
padding: 0 12px;
font-size: 13px;
color: var(--ink);
outline: none;
}
.field:focus, .select:focus { border-color: var(--cyan); box-shadow: 0 0 0 3px rgba(31,122,140,0.12); }
.toolbar {
display: grid;
grid-template-columns: 1.2fr 220px 180px;
gap: 12px;
}
.card-title { font-size: 18px; font-weight: 700; margin: 0 0 6px; }
.table-wrap { overflow: auto; border: 1px solid var(--line); border-radius: 18px; background: white; }
table { width: 100%; border-collapse: collapse; min-width: 720px; }
th {
position: sticky;
top: 0;
z-index: 1;
background: var(--mist);
color: #38506b;
text-align: left;
padding: 12px 14px;
font-size: 12px;
font-weight: 700;
border-bottom: 1px solid var(--line);
}
td {
padding: 11px 14px;
font-size: 13px;
border-bottom: 1px solid #edf2f7;
vertical-align: top;
}
tr:hover td { background: #f9fbfd; }
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
padding: 6px 10px;
font-size: 11px;
font-weight: 700;
}
.badge-good { background: #e8f8f0; color: var(--good); }
.badge-warn { background: #fff0e8; color: var(--warn); }
.badge-blue { background: #eaf3fb; color: var(--blue); }
.list {
display: flex;
flex-direction: column;
gap: 10px;
}
.list-item {
border: 1px solid var(--line);
border-radius: 16px;
padding: 12px 14px;
background: white;
}
@media (max-width: 1100px) {
.hero, .two-col, .three-col, .kpis, .toolbar { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useEffect, useMemo, useState } = React;
const API_BASE = "http://localhost:4000";
const fmt = (value) => new Intl.NumberFormat("ko-KR").format(Math.round(value || 0));
function App() {
const [keyword, setKeyword] = useState("");
const [projectType, setProjectType] = useState("전체");
const [inOut, setInOut] = useState("전체");
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [summary, setSummary] = useState(null);
const [projectTypes, setProjectTypes] = useState([]);
const [topAccounts, setTopAccounts] = useState([]);
const [topProjects, setTopProjects] = useState([]);
const [mismatches, setMismatches] = useState([]);
const [transactions, setTransactions] = useState([]);
const query = useMemo(() => {
const params = new URLSearchParams();
if (keyword.trim()) params.set("keyword", keyword.trim());
if (projectType) params.set("project_type", projectType);
if (inOut) params.set("in_out", inOut);
return params.toString();
}, [keyword, projectType, inOut]);
useEffect(() => {
let ignore = false;
async function load() {
setLoading(true);
setError("");
try {
const [healthRes, summaryRes, typeRes, accountRes, projectRes, mismatchRes, txRes] = await Promise.all([
fetch(`${API_BASE}/api/health`),
fetch(`${API_BASE}/api/summary?${query}`),
fetch(`${API_BASE}/api/project-types`),
fetch(`${API_BASE}/api/top-accounts?${query}`),
fetch(`${API_BASE}/api/top-projects?${query}`),
fetch(`${API_BASE}/api/project-mismatches`),
fetch(`${API_BASE}/api/transactions?${query}&limit=30`)
]);
if (!healthRes.ok) throw new Error("API health check failed");
const health = await healthRes.json();
if (!health.ok) throw new Error("API server not ready");
const [summaryData, typeData, accountData, projectData, mismatchData, txData] = await Promise.all([
summaryRes.json(), typeRes.json(), accountRes.json(), projectRes.json(), mismatchRes.json(), txRes.json()
]);
if (ignore) return;
setSummary(summaryData);
setProjectTypes(typeData.items || []);
setTopAccounts(accountData.items || []);
setTopProjects(projectData.items || []);
setMismatches(mismatchData.items || []);
setTransactions(txData.items || []);
} catch (err) {
if (!ignore) setError("PTC 데이터 서버에 연결하지 못했습니다. API 서버를 먼저 실행해 주세요.");
} finally {
if (!ignore) setLoading(false);
}
}
load();
return () => { ignore = true; };
}, [query]);
return (
<div className="page">
<section className="panel" style={{ padding: 28 }}>
<div className="hero">
<div>
<div className="eyebrow">Project Profit Main</div>
<h1 style={{ fontSize: 38, lineHeight: 1.2, margin: "16px 0 12px", fontWeight: 700 }}>
PTC 실행 데이터 기반
<br />
메인 대시보드
</h1>
<p className="subtle" style={{ maxWidth: 760 }}>
`jhpps.hmac.kr`처럼 로그인 게이트 구조를 가정하되, 지금은 인증 없이 메인 페이지만 먼저 보이도록 구성했습니다.
별도 데이터 서버에서 PTC 원장을 읽고, 프론트는 메인 대시보드 역할만 하도록 분리했습니다.
</p>
<div style={{ display: "flex", gap: 10, flexWrap: "wrap", marginTop: 18 }}>
<span className="badge badge-blue">Main Only</span>
<span className="badge badge-blue">PTC Source</span>
<span className="badge badge-blue">Separate API Server</span>
</div>
</div>
<div className="metric">
<div className="card-title">연결 상태</div>
<div className="subtle" style={{ marginBottom: 14 }}>
메인 페이지는 `localhost:8000`, 데이터 서버는 `localhost:4000` 사용합니다.
</div>
{loading && <div className="badge badge-blue">불러오는 </div>}
{!loading && !error && <div className="badge badge-good">API 연결 정상</div>}
{error && <div className="badge badge-warn">{error}</div>}
<div className="subtle" style={{ marginTop: 14 }}>
원본 파일: `PTC(2023-2026.02).xlsx`
</div>
</div>
</div>
</section>
<section className="panel" style={{ padding: 22, marginTop: 18 }}>
<div className="toolbar">
<div>
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 8 }}>검색</div>
<input className="field" value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="계정코드, 계정명, 부서, 거래처, 프로젝트명, 적요 검색" />
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 8 }}>프로젝트 구분</div>
<select className="select" value={projectType} onChange={(e) => setProjectType(e.target.value)}>
<option value="전체">전체</option>
{projectTypes.map((item) => <option key={item} value={item}>{item}</option>)}
</select>
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 700, marginBottom: 8 }}>입출금</div>
<select className="select" value={inOut} onChange={(e) => setInOut(e.target.value)}>
<option value="전체">전체</option>
<option value="입금">입금</option>
<option value="출금">출금</option>
</select>
</div>
</div>
</section>
<section className="kpis" style={{ marginTop: 18 }}>
<div className="metric">
<div className="subtle">조회 건수</div>
<div style={{ fontSize: 34, fontWeight: 700, marginTop: 8 }}>{fmt(summary?.count || 0)}</div>
</div>
<div className="metric">
<div className="subtle">기간</div>
<div style={{ fontSize: 20, fontWeight: 700, marginTop: 8 }}>{summary?.min_date || "-"} {summary?.max_date ? `~ ${summary?.max_date}` : ""}</div>
</div>
<div className="metric">
<div className="subtle">공급가액 합계</div>
<div style={{ fontSize: 28, fontWeight: 700, marginTop: 8 }}>{fmt(summary?.supply_sum || 0)}</div>
</div>
<div className="metric">
<div className="subtle">핵심 누락값</div>
<div style={{ fontSize: 28, fontWeight: 700, marginTop: 8 }}>{fmt(summary?.missing_critical || 0)}</div>
</div>
</section>
<section className="two-col" style={{ marginTop: 18 }}>
<div className="panel" style={{ padding: 22 }}>
<div className="card-title">계정코드 상위 집계</div>
<div className="subtle" style={{ marginBottom: 14 }}>공급가액 기준 상위 계정입니다.</div>
<div className="table-wrap">
<table>
<thead>
<tr>
<th>계정코드</th>
<th>계정명</th>
<th>건수</th>
<th>공급가액</th>
</tr>
</thead>
<tbody>
{topAccounts.map((item) => (
<tr key={`${item.code}-${item.name}`}>
<td>{item.code}</td>
<td>{item.name}</td>
<td>{fmt(item.count)}</td>
<td>{fmt(item.total)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="panel" style={{ padding: 22 }}>
<div className="card-title">프로젝트 불일치 체크</div>
<div className="subtle" style={{ marginBottom: 14 }}>같은 프로젝트코드에 이름/구분이 여러 개인 경우입니다.</div>
<div className="list">
{mismatches.slice(0, 8).map((item) => (
<div className="list-item" key={item.project_code}>
<div style={{ fontWeight: 700 }}>{item.project_code}</div>
<div className="subtle" style={{ marginTop: 6 }}>
프로젝트명 {item.name_count}, 프로젝트구분 {item.type_count}
</div>
</div>
))}
</div>
</div>
</section>
<section className="three-col" style={{ marginTop: 18 }}>
<div className="panel" style={{ padding: 22 }}>
<div className="card-title">입금 / 출금</div>
<div className="subtle" style={{ marginBottom: 12 }}>거래 방향별 건수</div>
<div className="metric">
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 10 }}>
<span>입금</span>
<strong>{fmt(summary?.income_count || 0)}</strong>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span>출금</span>
<strong>{fmt(summary?.expense_count || 0)}</strong>
</div>
</div>
</div>
<div className="panel" style={{ padding: 22 }}>
<div className="card-title">부가세 합계</div>
<div className="subtle" style={{ marginBottom: 12 }}>현재 필터 기준</div>
<div className="metric">
<div style={{ fontSize: 30, fontWeight: 700 }}>{fmt(summary?.vat_sum || 0)}</div>
</div>
</div>
<div className="panel" style={{ padding: 22 }}>
<div className="card-title">합계금액</div>
<div className="subtle" style={{ marginBottom: 12 }}>현재 필터 기준</div>
<div className="metric">
<div style={{ fontSize: 30, fontWeight: 700 }}>{fmt(summary?.total_sum || 0)}</div>
</div>
</div>
</section>
<section className="panel" style={{ padding: 22, marginTop: 18 }}>
<div className="card-title">프로젝트 상위 집계</div>
<div className="subtle" style={{ marginBottom: 14 }}>공급가액 기준 상위 프로젝트입니다.</div>
<div className="table-wrap">
<table>
<thead>
<tr>
<th>프로젝트코드</th>
<th>프로젝트명</th>
<th>구분</th>
<th>건수</th>
<th>공급가액</th>
</tr>
</thead>
<tbody>
{topProjects.map((item) => (
<tr key={`${item.project_code}-${item.project_name}`}>
<td>{item.project_code}</td>
<td>{item.project_name}</td>
<td>{item.project_type}</td>
<td>{fmt(item.count)}</td>
<td>{fmt(item.total)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="panel" style={{ padding: 22, marginTop: 18 }}>
<div className="card-title">원본 거래 미리보기</div>
<div className="subtle" style={{ marginBottom: 14 }}>메인 페이지에서는 최근 구조 확인을 위해 30건만 노출합니다.</div>
<div className="table-wrap">
<table>
<thead>
<tr>
<th>거래일</th>
<th>/출금</th>
<th>계정코드</th>
<th>계정명</th>
<th>부서</th>
<th>프로젝트코드</th>
<th>프로젝트명</th>
<th>적요</th>
<th>공급가액</th>
</tr>
</thead>
<tbody>
{transactions.map((item) => (
<tr key={item.source_row_no}>
<td>{item.transaction_date}</td>
<td>{item.in_out}</td>
<td>{item.account_code}</td>
<td>{item.account_name}</td>
<td>{item.department_name}</td>
<td>{item.project_code || "-"}</td>
<td>{item.project_name || "-"}</td>
<td>{item.description || "-"}</td>
<td>{fmt(item.supply_amount)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>