[st frontend] as 사용자 뷰 정보 표시 정리

This commit is contained in:
2026-05-29 13:25:16 +09:00
parent 68ac1168e9
commit 49ec6423bd
2 changed files with 172 additions and 69 deletions
@@ -9,6 +9,7 @@ import { Button } from "@common/components";
import { InstlSncnApi } from "@/entities/instl-sncn";
import { dayjs } from "@/shared/lib";
import { PROXY_HOST } from "@/shared/config";
import { formatFileSize } from "@/shared/lib/file-utils";
import "./cnfm-page.css";
@@ -47,6 +48,8 @@ function CnfmViewPage({ id }: { id: string }) {
return (spaces / 4) * 16;
};
/* formatFileSize moved to shared lib: @/shared/lib/file-utils */
return (
<div className="mt-6 space-y-4">
<div className="cnfm-card p-4">
@@ -61,25 +64,26 @@ function CnfmViewPage({ id }: { id: string }) {
<tr>
<th>사업명</th>
<td>{det.bizNm || "-"}</td>
<th>업무구분</th>
<td>{det.taskSeCdNm || det.taskSeComCd || "-"}</td>
</tr>
<tr>
<th>사업기간</th>
<td>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</td>
<th>진행상태</th>
<td>{det.prgrsSttsCdNm || det.prgrsSttsComCd || "-"}</td>
</tr>
<tr>
<th>행정동ID</th>
<td>{det.dongId || "-"}</td>
<th>예산사업코드</th>
<td>{det.bgtBizCd || "-"}</td>
<th>사업기간</th>
<td>{(det.bizBgngYr || "-") + " ~ " + (det.bizEndYr || "-")}</td>
</tr>
<tr>
<th>사업내용</th>
<td colSpan={3}>{det.bizCn || "-"}</td>
</tr>
<tr>
<th>업무구분</th>
<td>{det.taskSeCdNm || det.taskSeComCd || "-"}</td>
<th>행정동ID</th>
<td>{det.dongId || "-"}</td>
</tr>
<tr>
<th>진행상태</th>
<td>{det.prgrsSttsCdNm || det.prgrsSttsComCd || "-"}</td>
<th>예산사업코드</th>
<td>{det.bgtBizCd || "-"}</td>
</tr>
</tbody>
</table>
</div>
@@ -158,16 +162,16 @@ function CnfmViewPage({ id }: { id: string }) {
) : (
bizSclList.map((node: any) => {
const pl = indentPx(node.indCnstpSeCdNm);
const a = Number(node.bscPlanQty) || 0;
const b = Number(node.bgtRfltQty) || 0;
const c = Number(node.tstmDesignQty) || 0;
const diff = c - b;
const bscPlanQty = Number(node.bscPlanQty) || 0;
const budgetReflct = Number(node.bgtRfltQty) || 0;
const tstmDesignQty = Number(node.tstmDesignQty) || 0;
const diff = tstmDesignQty - budgetReflct;
return (
<tr key={node.cnstpSeCd} className={node.treeLevel === 1 ? "bg-gray-50 font-semibold" : ""}>
<td className="bg-gray-50 pl-2 text-left" style={{ paddingLeft: `${pl + 8}px` }}>{node.cnstpSeCdNm}</td>
<td><div className="text-right">{fmt(a)}</div></td>
<td><div className="text-right">{fmt(b)}</div></td>
<td><div className="text-right">{fmt(c)}</div></td>
<td><div className="text-right">{fmt(bscPlanQty)}</div></td>
<td><div className="text-right">{fmt(budgetReflct)}</div></td>
<td><div className="text-right">{fmt(tstmDesignQty)}</div></td>
<td><div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div></td>
</tr>
);
@@ -185,38 +189,70 @@ function CnfmViewPage({ id }: { id: string }) {
<table className="cnfm-table w-full table-fixed">
<thead>
<tr>
<th colSpan={2} className="w-[30rem]">구분</th>
<th>예산반영(A)</th>
<th>표준사업비(B)</th>
<th>금회설계시(C)</th>
<th>증감</th>
<th>%</th>
<th colSpan={2} className="w-[30rem]" rowSpan={2}>
구분
</th>
<th rowSpan={2}>예산반영(A)</th>
<th rowSpan={2}>표준사업비(B)</th>
<th rowSpan={2}>금회설계시(C)</th>
<th colSpan={2}>증감</th>
</tr>
<tr>
<th className="w-[12rem]">(C-B)</th>
<th className="w-[6rem]">%</th>
</tr>
</thead>
<tbody>
{bizCstList.map((node: any) => {
const pl = indentPx(node.indAcntgSeCdNm);
const a = Number(node.bscPlanAmt) || 0;
const b = Number(node.bgtRfltAmt) || 0;
const c = Number(node.tstmDesignAmt) || 0;
const diff = c - b;
const bscPlanAmt = Number(node.bscPlanAmt) || 0;
const budgetReflct = Number(node.bgtRfltAmt) || 0;
const tstmDesignAmt = Number(node.tstmDesignAmt) || 0;
const diff = tstmDesignAmt - budgetReflct;
const showSubIcon = node.treeLevel >= 4;
const isLeaf = !bizCstList.some((n: any) => n.upAcntgSeCd === node.acntgSeCd);
return (
<tr key={node.acntgSeCd} className={node.treeLevel === 1 ? "bg-blue-50 font-bold" : node.treeLevel === 2 ? "bg-gray-100 font-semibold" : ""}>
<tr
key={node.acntgSeCd}
className={
node.treeLevel === 1
? "bg-blue-50 font-bold"
: node.treeLevel === 2
? "bg-gray-100 font-semibold"
: ""
}
>
{node.treeLevel <= 2 ? (
<td colSpan={2} className="pl-2 text-left">{node.acntgSeCdNm}</td>
) : (
<>
<td className="w-[4rem]"></td>
<td className="bg-gray-50 text-left" style={{ paddingLeft: `${pl + 8}px` }}>{node.acntgSeCdNm}</td>
<td
className={`bg-gray-50 text-left ${isLeaf ? "text-gray-700" : ""}`}
style={{ paddingLeft: `${pl + 8}px` }}
>
{showSubIcon && <span className="mr-1 text-gray-400">└</span>}
{node.acntgSeCdNm}
</td>
</>
)}
<td><div className="text-right">{fmt(a)}</div></td>
<td><div className="text-right">{fmt(b)}</div></td>
<td><div className="text-right">{fmt(c)}</div></td>
<td><div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div></td>
<td><div className="text-right">{b !== 0 ? `${Math.round((diff / b) * 100)}%` : "-"}</div></td>
<td>
<div className="text-right">{fmt(bscPlanAmt)}</div>
</td>
<td>
<div className="text-right">{fmt(budgetReflct)}</div>
</td>
<td>
<div className="text-right">{fmt(tstmDesignAmt)}</div>
</td>
<td>
<div className="text-right">{diff > 0 ? `+${fmt(diff)}` : fmt(diff)}</div>
</td>
<td>
<div className="text-right">{budgetReflct !== 0 ? `${Math.round((diff / budgetReflct) * 100)}%` : "-"}</div>
</td>
</tr>
);
})}
@@ -234,7 +270,25 @@ function CnfmViewPage({ id }: { id: string }) {
<tr>
<th className="w-[20rem]">검토항목</th>
<th>주요 검토내용</th>
<th>검토 결과</th>
<th colSpan={2}>검토 결과(-)</th>
</tr>
<tr>
<th />
<th />
<th className="w-[9rem]">
<button
type="button"
>
예
</button>
</th>
<th className="w-[9rem]">
<button
type="button"
>
아니오
</button>
</th>
</tr>
</thead>
<tbody>
@@ -243,29 +297,49 @@ function CnfmViewPage({ id }: { id: string }) {
<td colSpan={3} className="cnfm-td-empty">조회된 체크리스트 항목이 없습니다.</td>
</tr>
) : (
cklstResultList.map((row: any) => (
<tr key={`${row.cklstSn}_${row.cklstRvwSn}`}>
<td className="text-center text-[1.2rem]">{row.cklstArtclNm}</td>
<td className="!text-left pl-2 text-[1.2rem] text-gray-600" style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}>{row.treeLevel > 1 ? "└ " : ""}{row.rvwCn}</td>
<td className="text-center text-[1.2rem]">{row.bchckUseYn === "Y" ? (row.rvwRsltAnmlyYn === "Y" ? "예" : row.rvwRsltAnmlyYn === "N" ? "아니오" : "-") : "-"}</td>
</tr>
))
cklstResultList.map((row: any) => {
const key = `${row.cklstSn}_${row.cklstRvwSn}`;
const hasResult = row.rvwRsltAnmlyYn === "Y" || row.rvwRsltAnmlyYn === "N";
const showRadios = row.bchckUseYn === "Y" || hasResult;
return (
<tr key={key}>
<td className="text-center text-[1.2rem]">{row.cklstArtclNm}</td>
<td className="!text-left pl-2 text-[1.2rem] text-gray-600" style={{ paddingLeft: `${Math.max(0, row.treeLevel - 1) * 16 + 8}px` }}>{row.treeLevel > 1 ? "└ " : ""}{row.rvwCn}</td>
{!showRadios ? (
<>
<td className="text-center">-</td>
<td className="text-center">-</td>
</>
) : (
<>
<td className="text-center">
<input type="radio" name={`chk_${key}`} value="Y" checked={row.rvwRsltAnmlyYn === "Y"} disabled />
</td>
<td className="text-center">
<input type="radio" name={`chk_${key}`} value="N" checked={row.rvwRsltAnmlyYn === "N"} disabled />
</td>
</>
)}
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* 첨부파일 */}
{/* 필수 첨부파일 */}
<div className="cnfm-card p-4">
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">필수 첨부파일</h3>
<div className="cnfm-table-wrap">
<table className="cnfm-table">
<thead>
<tr>
<th>필수 자료명</th>
<th>파일형식</th>
<th>파일</th>
<th>필수 자료명</th>
<th>파일명</th>
<th className="w-[8rem] text-right">크기</th>
</tr>
</thead>
<tbody>
@@ -282,7 +356,6 @@ function CnfmViewPage({ id }: { id: string }) {
{f.esntlAtchYn === "Y" ? <span className="ml-1 text-red-500">*</span> : null}
</div>
</td>
<td>{(f.esntlAtchFileRmrk || "").replaceAll(",", "/") || "-"}</td>
<td>
{f.atflCd ? (
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`} target="_blank" rel="noreferrer">{f.orgnlFileNm || "다운로드"}</a>
@@ -290,30 +363,48 @@ function CnfmViewPage({ id }: { id: string }) {
<span className="text-gray-500">첨부없음</span>
)}
</td>
<td className="w-[8rem] text-right"><div className="text-right">{formatFileSize(f.atflFlieSz)}</div></td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<h4 className="mt-4 mb-2 text-[1.25rem] font-semibold">기타 첨부파일</h4>
{etcFileList.length === 0 ? (
<div className="text-gray-500">등록된 기타첨부파일이 없습니다.</div>
) : (
<ul className="space-y-2">
{etcFileList.map((f: any) => (
<li key={f.atflId}>
{f.orgnlFileNm ? (
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`} target="_blank" rel="noreferrer">{f.orgnlFileNm}</a>
) : (
<span className="text-gray-700">{f.physFileNm || f.atflId}</span>
)}
<span className="ml-2 text-gray-500">{f.atflFlieSz ? `(${Math.round(f.atflFlieSz / 1024)}KB)` : ""}</span>
</li>
))}
</ul>
)}
{/* 기타 첨부파일 */}
<div className="cnfm-card p-4">
<h3 className="mb-3 border-b pb-2 text-[1.5rem] font-bold text-gray-700">기타 첨부파일</h3>
<div className="cnfm-table-wrap">
<table className="cnfm-table">
<thead>
<tr>
<th>파일명</th>
<th className="w-[8rem] text-right">크기</th>
</tr>
</thead>
<tbody>
{etcFileList.length === 0 ? (
<tr>
<td colSpan={2} className="cnfm-td-empty">등록된 기타첨부파일이 없습니다.</td>
</tr>
) : (
etcFileList.map((f: any) => (
<tr key={f.atflId}>
<td>
{f.orgnlFileNm ? (
<a className="text-blue-600" href={`${PROXY_HOST}/attach-service/api/v1/atflcd/download/${f.atflCd}`} target="_blank" rel="noreferrer">{f.orgnlFileNm}</a>
) : (
<span className="text-gray-700">{f.physFileNm || f.atflId}</span>
)}
</td>
<td className="w-[8rem] text-right text-gray-500">{formatFileSize(f.atflFlieSz)}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<div className="cnfm-action-row">
+12
View File
@@ -0,0 +1,12 @@
/* Shared file utilities for apps/as */
/** 파일 크기는 byte 단위로 제공된다고 가정하고 크기에 따라서 KB, MB 단위로 표시 */
export const formatFileSize = (bytes?: number | null): string => {
if (bytes === null || bytes === undefined) return "-";
const b = Number(bytes) || 0;
if (b >= 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)}MB`;
if (b >= 1024) return `${Math.round(b / 1024)}KB`;
return `${b}B`;
};
export default { formatFileSize };