Organize root directory: Moved legacy HTML files to legacy/
This commit is contained in:
437
legacy/main_page.html
Normal file
437
legacy/main_page.html
Normal 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>
|
||||
Reference in New Issue
Block a user