팝업 병합 그리드 기능 추가
This commit is contained in:
@@ -13,7 +13,10 @@ import { useAlertStore } from "@/shared/lib";
|
||||
import useStore from "@/store/useStore";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import MapPopupDataTable from "@/features/windowPopup/ui/MapPopupDataTable";
|
||||
import PopupSearchPanel from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import PopupSearchPanel, {
|
||||
PopupSearchActions,
|
||||
PopupSearchField,
|
||||
} from "@/features/windowPopup/ui/PopupSearchPanel";
|
||||
import type { SelectDataType } from "@common/business_components";
|
||||
import {
|
||||
buildSinkholeDefaultFilters,
|
||||
@@ -318,13 +321,10 @@ export default function SinkholePopupPage() {
|
||||
<StandardPopupLayout title="지반침하 목록">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
|
||||
<PopupSearchPanel contentClassName="flex flex-wrap items-end gap-x-5 gap-y-3">
|
||||
<label className="block md:max-w-[170px]">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-label">
|
||||
발생기간(시작)
|
||||
</div>
|
||||
<PopupSearchPanel>
|
||||
<PopupSearchField label="발생기간(시작)">
|
||||
<Input
|
||||
className="h-[3.2rem] !text-[1.3rem]"
|
||||
className="h-[3.2rem] w-[12rem] !text-[1.3rem]"
|
||||
type="date"
|
||||
min={formatYmdToDateInput(SINKHOLE_MIN_SEARCH_YMD)}
|
||||
value={formatYmdToDateInput(filters.startYmd)}
|
||||
@@ -335,14 +335,11 @@ export default function SinkholePopupPage() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</PopupSearchField>
|
||||
|
||||
<label className="block md:max-w-[170px]">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-label">
|
||||
발생기간(종료)
|
||||
</div>
|
||||
<PopupSearchField label="발생기간(종료)">
|
||||
<Input
|
||||
className="h-[3.2rem] !text-[1.3rem]"
|
||||
className="h-[3.2rem] w-[12rem] !text-[1.3rem]"
|
||||
type="date"
|
||||
min={formatYmdToDateInput(SINKHOLE_MIN_SEARCH_YMD)}
|
||||
value={formatYmdToDateInput(filters.endYmd)}
|
||||
@@ -353,12 +350,11 @@ export default function SinkholePopupPage() {
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</PopupSearchField>
|
||||
|
||||
<label className="block md:max-w-[220px]">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-label">시도</div>
|
||||
<PopupSearchField label="시도">
|
||||
<select
|
||||
className="h-[3.2rem] w-full rounded-md border border-border px-3 text-[1.3rem]"
|
||||
className="h-[3.2rem] w-[12rem] rounded-md border border-input bg-field px-3 text-[1.3rem] text-foreground"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
setFilters({
|
||||
@@ -375,14 +371,11 @@ export default function SinkholePopupPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</PopupSearchField>
|
||||
|
||||
<label className="block md:max-w-[220px]">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-label">
|
||||
시군구
|
||||
</div>
|
||||
<PopupSearchField label="시군구">
|
||||
<select
|
||||
className="h-[3.2rem] w-full rounded-md border border-border px-3 text-[1.3rem]"
|
||||
className="h-[3.2rem] w-[12rem] rounded-md border border-input bg-field px-3 text-[1.3rem] text-foreground"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) =>
|
||||
setFilters({ ...filters, sggCd: event.target.value })
|
||||
@@ -395,14 +388,11 @@ export default function SinkholePopupPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</PopupSearchField>
|
||||
|
||||
<label className="block md:max-w-[180px]">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-label">
|
||||
복구상태
|
||||
</div>
|
||||
<PopupSearchField label="복구상태">
|
||||
<select
|
||||
className="h-[3.2rem] w-full rounded-md border border-border px-3 text-[1.3rem]"
|
||||
className="h-[3.2rem] w-[10rem] rounded-md border border-input bg-field px-3 text-[1.3rem] text-foreground"
|
||||
value={filters.restoreStatus}
|
||||
onChange={(event) =>
|
||||
setFilters({ ...filters, restoreStatus: event.target.value })
|
||||
@@ -414,14 +404,11 @@ export default function SinkholePopupPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</PopupSearchField>
|
||||
|
||||
<label className="block md:max-w-[180px]">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-label">
|
||||
좌표보정상태
|
||||
</div>
|
||||
<PopupSearchField label="좌표보정상태">
|
||||
<select
|
||||
className="h-[3.2rem] w-full rounded-md border border-border px-3 text-[1.3rem]"
|
||||
className="h-[3.2rem] w-[10rem] rounded-md border border-input bg-field px-3 text-[1.3rem] text-foreground"
|
||||
value={filters.coordStatus}
|
||||
onChange={(event) =>
|
||||
setFilters({ ...filters, coordStatus: event.target.value })
|
||||
@@ -433,9 +420,9 @@ export default function SinkholePopupPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</PopupSearchField>
|
||||
|
||||
<div className="ml-auto flex flex-wrap justify-end gap-2">
|
||||
<PopupSearchActions>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
@@ -457,7 +444,7 @@ export default function SinkholePopupPage() {
|
||||
>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</PopupSearchActions>
|
||||
</PopupSearchPanel>
|
||||
|
||||
<MapPopupDataTable
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Table } from "@common/components";
|
||||
|
||||
import {
|
||||
type ExmnDetailSeverity,
|
||||
type ExmnPplUnitDetailRow,
|
||||
@@ -60,196 +62,195 @@ export function ExmnPplUnitDetailTable({
|
||||
const tableWidth = COLUMN_WIDTHS.reduce((sum, width) => sum + width, 0);
|
||||
|
||||
return (
|
||||
<div className="min-w-0 overflow-x-auto overflow-y-visible bg-background">
|
||||
<table
|
||||
className="table-ui default border-separate border-spacing-0 text-center text-foreground"
|
||||
style={{ width: "100%", minWidth: `${tableWidth}px` }}
|
||||
>
|
||||
<colgroup>
|
||||
{COLUMN_WIDTHS.map((width, index) => (
|
||||
<col key={`unit-column-${index}`} style={{ width: `${width}px` }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className="sticky top-0 z-30 text-center font-medium leading-[1.2]">
|
||||
<tr>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
관로번호
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
단위
|
||||
<br />
|
||||
구간
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
단위
|
||||
<br />
|
||||
길이
|
||||
<br />
|
||||
(m)
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
결함
|
||||
<br />
|
||||
위치
|
||||
<br />
|
||||
(m)
|
||||
</th>
|
||||
<th
|
||||
colSpan={6}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
결함
|
||||
</th>
|
||||
<th
|
||||
colSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
등급점수(점)
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
조사중단사유
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
비고
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
종류
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
코드
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
등급
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
발생
|
||||
<br />
|
||||
위치
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
침입수
|
||||
<br />
|
||||
가점
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
연속
|
||||
<br />
|
||||
결함
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
구조적
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
운영적
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length > 0 ? (
|
||||
rows.map((row, index) => {
|
||||
const pipeSpan = pipeSpans.get(row.pipeNo);
|
||||
const unitKey = `${row.pipeNo}::${row.unitSeq}`;
|
||||
const unitSpan = unitSpans.get(unitKey);
|
||||
const showPipeCell = pipeSpan?.firstIndex === index;
|
||||
const showUnitCells = unitSpan?.firstIndex === index;
|
||||
<Table
|
||||
variant="default"
|
||||
className="border-separate border-spacing-0 text-center text-foreground"
|
||||
style={{ width: "100%", minWidth: `${tableWidth}px` }}
|
||||
>
|
||||
<colgroup>
|
||||
{COLUMN_WIDTHS.map((width, index) => (
|
||||
<col key={`unit-column-${index}`} style={{ width: `${width}px` }} />
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className="sticky top-0 z-30 text-center font-medium leading-[1.2]">
|
||||
<tr>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
관로번호
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
단위
|
||||
<br />
|
||||
구간
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
단위
|
||||
<br />
|
||||
길이
|
||||
<br />
|
||||
(m)
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
결함
|
||||
<br />
|
||||
위치
|
||||
<br />
|
||||
(m)
|
||||
</th>
|
||||
<th
|
||||
colSpan={6}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
결함
|
||||
</th>
|
||||
<th
|
||||
colSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
등급점수(점)
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
조사중단사유
|
||||
</th>
|
||||
<th
|
||||
rowSpan={2}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
비고
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
종류
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
코드
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
등급
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
발생
|
||||
<br />
|
||||
위치
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
침입수
|
||||
<br />
|
||||
가점
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
연속
|
||||
<br />
|
||||
결함
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
구조적
|
||||
</th>
|
||||
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
|
||||
운영적
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length > 0 ? (
|
||||
rows.map((row, index) => {
|
||||
const pipeSpan = pipeSpans.get(row.pipeNo);
|
||||
const unitKey = `${row.pipeNo}::${row.unitSeq}`;
|
||||
const unitSpan = unitSpans.get(unitKey);
|
||||
const showPipeCell = pipeSpan?.firstIndex === index;
|
||||
const showUnitCells = unitSpan?.firstIndex === index;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={`${row.pipeNo}-${row.unitSeq}-${row.defectPositionM}-${row.code}-${index}`}
|
||||
className="transition-colors hover:bg-container"
|
||||
>
|
||||
{showPipeCell && (
|
||||
<td
|
||||
rowSpan={pipeSpan?.count}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] align-middle font-medium text-primary"
|
||||
>
|
||||
{row.pipeNo}
|
||||
</td>
|
||||
)}
|
||||
{showUnitCells && (
|
||||
<>
|
||||
<td
|
||||
rowSpan={unitSpan?.count}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] align-middle font-medium"
|
||||
>
|
||||
{row.unitSeq}
|
||||
</td>
|
||||
<td
|
||||
rowSpan={unitSpan?.count}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] align-middle"
|
||||
>
|
||||
{row.unitLengthM}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.defectPositionM)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem] text-left">
|
||||
{row.defectName}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.code}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.severity ? SEVERITY_LABELS[row.severity] : "-"}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.occurrencePosition)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.infiltrationBonus)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.continuous == null ? "-" : row.continuous ? "Y" : "N"}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.structuralScore)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.operationalScore)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.surveyStopReason)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.remark)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={COLUMN_WIDTHS.length}
|
||||
className="h-28 border-b border-r border-border text-center text-muted-foreground"
|
||||
return (
|
||||
<tr
|
||||
key={`${row.pipeNo}-${row.unitSeq}-${row.defectPositionM}-${row.code}-${index}`}
|
||||
className="transition-colors hover:bg-container"
|
||||
>
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{showPipeCell && (
|
||||
<td
|
||||
rowSpan={pipeSpan?.count}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] align-middle font-medium text-primary"
|
||||
>
|
||||
{row.pipeNo}
|
||||
</td>
|
||||
)}
|
||||
{showUnitCells && (
|
||||
<>
|
||||
<td
|
||||
rowSpan={unitSpan?.count}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] align-middle font-medium"
|
||||
>
|
||||
{row.unitSeq}
|
||||
</td>
|
||||
<td
|
||||
rowSpan={unitSpan?.count}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] align-middle"
|
||||
>
|
||||
{row.unitLengthM}
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.defectPositionM)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem] text-left">
|
||||
{row.defectName}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.code}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.severity ? SEVERITY_LABELS[row.severity] : "-"}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.occurrencePosition)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.infiltrationBonus)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{row.continuous == null ? "-" : row.continuous ? "Y" : "N"}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.structuralScore)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.operationalScore)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.surveyStopReason)}
|
||||
</td>
|
||||
<td className="border-b border-r border-border px-2 py-[1.2rem]">
|
||||
{formatOptionalValue(row.remark)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={COLUMN_WIDTHS.length}
|
||||
className="h-28 border-b border-r border-border text-center text-muted-foreground"
|
||||
>
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Table,
|
||||
} from "@common/components";
|
||||
import { downloadExcelWithKoreanHeaders } from "@/shared/lib/download-excel";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
|
||||
import {
|
||||
getExmnFacilityDetails,
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
} from "../model/exmn-detail-table";
|
||||
|
||||
import { ExmnPplUnitDetailTable } from "./ExmnPplUnitDetailTable";
|
||||
import { ExmnSurveyStandardDataTable } from "./ExmnSurveyStandardDataTable";
|
||||
|
||||
const SEVERITY_LABELS: Record<ExmnDetailSeverity, string> = {
|
||||
L: "대",
|
||||
@@ -705,6 +706,9 @@ export function ExmnSurveyDetailPopupPage({
|
||||
() => new Set(unitRows.map((row) => `${row.pipeNo}::${row.unitSeq}`)).size,
|
||||
[unitRows],
|
||||
);
|
||||
/** 관로 집계와 본단위구간 행을 같은 StandardDataTable 데이터 유무 계약으로 전달한다. */
|
||||
const standardTableRows: Array<ExmnSurveyDetailRow | ExmnPplUnitDetailRow> =
|
||||
isUnitMode ? unitRows : rows;
|
||||
const popupTitle = isUnitMode
|
||||
? "관로조사 본단위구간 상세현황"
|
||||
: defectType === "ALL"
|
||||
@@ -735,7 +739,6 @@ export function ExmnSurveyDetailPopupPage({
|
||||
}
|
||||
bodyClassName="overflow-x-hidden"
|
||||
>
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
{notice && (
|
||||
<div
|
||||
role="status"
|
||||
@@ -862,9 +865,20 @@ export function ExmnSurveyDetailPopupPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card card-border !m-0 min-w-0 overflow-hidden !p-0">
|
||||
<div className="card-header flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 className="text-2xl font-medium text-label">{tableTitle}</h2>
|
||||
<ExmnSurveyStandardDataTable
|
||||
rows={standardTableRows}
|
||||
title={tableTitle}
|
||||
loading={isLoading}
|
||||
emptyMessage={
|
||||
isUnitMode
|
||||
? appliedDetailNumberKeyword
|
||||
? "입력한 관로번호에 해당하는 본단위구간이 없습니다."
|
||||
: "조회된 본단위구간이 없습니다."
|
||||
: "조회된 상세 결과가 없습니다."
|
||||
}
|
||||
minTableWidth={isUnitMode ? "142.2rem" : `${tableWidth}px`}
|
||||
onScroll={isUnitMode ? undefined : handleTableScroll}
|
||||
actions={
|
||||
<div className="flex flex-wrap items-center gap-x-7 gap-y-2 whitespace-nowrap text-xl text-muted-foreground">
|
||||
{isUnitMode ? (
|
||||
<>
|
||||
@@ -898,10 +912,9 @@ export function ExmnSurveyDetailPopupPage({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card !m-[1.2rem] min-w-0 overflow-hidden !p-0">
|
||||
{isUnitMode ? (
|
||||
}
|
||||
renderTable={() =>
|
||||
isUnitMode ? (
|
||||
<ExmnPplUnitDetailTable
|
||||
rows={unitRows}
|
||||
emptyMessage={
|
||||
@@ -911,320 +924,315 @@ export function ExmnSurveyDetailPopupPage({
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="min-w-0 overflow-x-auto overflow-y-visible bg-background"
|
||||
onScroll={handleTableScroll}
|
||||
<Table
|
||||
variant="default"
|
||||
className="border-separate border-spacing-0 text-center text-foreground"
|
||||
style={{
|
||||
width: `${tableWidth}px`,
|
||||
minWidth: `${tableWidth}px`,
|
||||
}}
|
||||
>
|
||||
<table
|
||||
className="table-ui default border-separate border-spacing-0 text-center text-foreground"
|
||||
style={{
|
||||
width: `${tableWidth}px`,
|
||||
minWidth: `${tableWidth}px`,
|
||||
}}
|
||||
>
|
||||
<colgroup>
|
||||
{fixedColumns.map((column) => (
|
||||
<col
|
||||
key={column.id}
|
||||
style={{ width: `${column.width}px` }}
|
||||
/>
|
||||
))}
|
||||
{Array.from({
|
||||
length:
|
||||
<colgroup>
|
||||
{fixedColumns.map((column) => (
|
||||
<col
|
||||
key={column.id}
|
||||
style={{ width: `${column.width}px` }}
|
||||
/>
|
||||
))}
|
||||
{Array.from({
|
||||
length:
|
||||
(visibleDefectColumns.length + 1) *
|
||||
EXMN_DETAIL_SEVERITIES.length,
|
||||
}).map((_, index) => (
|
||||
<col
|
||||
key={`severity-col-${index}`}
|
||||
style={{ width: `${SEVERITY_COLUMN_WIDTH}px` }}
|
||||
/>
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className="sticky top-0 z-30 text-center font-medium leading-[1.2]">
|
||||
<tr>
|
||||
{fixedColumns.map((column, columnIndex) => {
|
||||
const sticky = columnIndex < STICKY_COLUMN_COUNT;
|
||||
const columnLabel = column.label;
|
||||
return (
|
||||
<th
|
||||
key={column.id}
|
||||
rowSpan={3}
|
||||
className="border-b border-r border-border !py-[0.8rem] px-2 text-center"
|
||||
style={
|
||||
sticky
|
||||
? {
|
||||
position: "sticky",
|
||||
left:
|
||||
columnIndex === 0
|
||||
? REPORT_COLUMN_LEFT
|
||||
: PIPE_COLUMN_LEFT,
|
||||
zIndex: 60,
|
||||
boxShadow:
|
||||
columnIndex === STICKY_COLUMN_COUNT - 1 &&
|
||||
showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<span className="whitespace-pre-line">
|
||||
{columnLabel}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th
|
||||
colSpan={
|
||||
(visibleDefectColumns.length + 1) *
|
||||
EXMN_DETAIL_SEVERITIES.length,
|
||||
}).map((_, index) => (
|
||||
<col
|
||||
key={`severity-col-${index}`}
|
||||
style={{ width: `${SEVERITY_COLUMN_WIDTH}px` }}
|
||||
/>
|
||||
))}
|
||||
</colgroup>
|
||||
<thead className="sticky top-0 z-30 text-center font-medium leading-[1.2]">
|
||||
<tr>
|
||||
{fixedColumns.map((column, columnIndex) => {
|
||||
const sticky = columnIndex < STICKY_COLUMN_COUNT;
|
||||
const columnLabel = column.label;
|
||||
return (
|
||||
<th
|
||||
key={column.id}
|
||||
rowSpan={3}
|
||||
className="border-b border-r border-border !py-[0.8rem] px-2 text-center"
|
||||
style={
|
||||
sticky
|
||||
? {
|
||||
position: "sticky",
|
||||
left:
|
||||
columnIndex === 0
|
||||
? REPORT_COLUMN_LEFT
|
||||
: PIPE_COLUMN_LEFT,
|
||||
zIndex: 60,
|
||||
boxShadow:
|
||||
columnIndex === STICKY_COLUMN_COUNT - 1 &&
|
||||
showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<span className="whitespace-pre-line">
|
||||
{columnLabel}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th
|
||||
colSpan={
|
||||
(visibleDefectColumns.length + 1) *
|
||||
EXMN_DETAIL_SEVERITIES.length
|
||||
}
|
||||
className="border-b border-r border-border !py-[0.8rem] px-2 text-center"
|
||||
>
|
||||
이상항목
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
EXMN_DETAIL_SEVERITIES.length
|
||||
}
|
||||
className="border-b border-r border-border !py-[0.8rem] px-2 text-center"
|
||||
>
|
||||
이상항목
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
className="border-b border-r border-border !py-[0.6rem] px-2 text-center"
|
||||
>
|
||||
합계
|
||||
</th>
|
||||
{visibleDefectColumns.map((column) => (
|
||||
<th
|
||||
key={column.code}
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
title={`${column.label} (${column.code})`}
|
||||
className="whitespace-nowrap border-b border-r border-border !py-[0.6rem] px-2 text-center"
|
||||
>
|
||||
{column.label} ({column.code})
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<th
|
||||
key={`total-${severity}`}
|
||||
className="border-b border-r border-border !py-[0.6rem] px-2 text-center"
|
||||
>
|
||||
합계
|
||||
{SEVERITY_LABELS[severity]}
|
||||
</th>
|
||||
{visibleDefectColumns.map((column) => (
|
||||
))}
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<th
|
||||
key={column.code}
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
title={`${column.label} (${column.code})`}
|
||||
className="whitespace-nowrap border-b border-r border-border !py-[0.6rem] px-2 text-center"
|
||||
>
|
||||
{column.label} ({column.code})
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
<tr>
|
||||
{EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<th
|
||||
key={`total-${severity}`}
|
||||
className="border-b border-r border-border !py-[0.6rem] px-2 text-center"
|
||||
key={`${column.code}-${severity}`}
|
||||
className="border-b border-r border-border !py-[0.6rem] px-2 text-center font-medium"
|
||||
>
|
||||
{SEVERITY_LABELS[severity]}
|
||||
</th>
|
||||
))}
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<th
|
||||
key={`${column.code}-${severity}`}
|
||||
className="border-b border-r border-border !py-[0.6rem] px-2 text-center font-medium"
|
||||
)),
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length > 0 ? (
|
||||
<>
|
||||
{rows.map((row) => {
|
||||
const visibleAnomalyTotals = getVisibleAnomalyTotals(
|
||||
row,
|
||||
visibleDefectColumns,
|
||||
);
|
||||
return (
|
||||
<tr
|
||||
key={row.reportNo}
|
||||
className="transition-colors hover:bg-container"
|
||||
>
|
||||
{SEVERITY_LABELS[severity]}
|
||||
</th>
|
||||
)),
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length > 0 ? (
|
||||
<>
|
||||
{rows.map((row) => {
|
||||
const visibleAnomalyTotals = getVisibleAnomalyTotals(
|
||||
row,
|
||||
visibleDefectColumns,
|
||||
);
|
||||
return (
|
||||
<tr
|
||||
key={row.reportNo}
|
||||
className="transition-colors hover:bg-container"
|
||||
>
|
||||
{fixedColumns.map((column, columnIndex) => {
|
||||
const sticky =
|
||||
columnIndex < STICKY_COLUMN_COUNT;
|
||||
return (
|
||||
<td
|
||||
key={`${row.reportNo}-${column.id}`}
|
||||
className="whitespace-nowrap border-b border-r border-border bg-background px-2 py-[1.2rem]"
|
||||
style={
|
||||
sticky
|
||||
? {
|
||||
position: "sticky",
|
||||
left:
|
||||
columnIndex === 0
|
||||
? REPORT_COLUMN_LEFT
|
||||
: PIPE_COLUMN_LEFT,
|
||||
zIndex: 10,
|
||||
boxShadow:
|
||||
columnIndex ===
|
||||
STICKY_COLUMN_COUNT - 1 &&
|
||||
showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{getFixedCellValue(row, column.id)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
{EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
{fixedColumns.map((column, columnIndex) => {
|
||||
const sticky = columnIndex < STICKY_COLUMN_COUNT;
|
||||
return (
|
||||
<td
|
||||
key={`${row.reportNo}-total-${severity}`}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem]"
|
||||
key={`${row.reportNo}-${column.id}`}
|
||||
className="whitespace-nowrap border-b border-r border-border bg-background px-2 py-[1.2rem]"
|
||||
style={
|
||||
sticky
|
||||
? {
|
||||
position: "sticky",
|
||||
left:
|
||||
columnIndex === 0
|
||||
? REPORT_COLUMN_LEFT
|
||||
: PIPE_COLUMN_LEFT,
|
||||
zIndex: 10,
|
||||
boxShadow:
|
||||
columnIndex ===
|
||||
STICKY_COLUMN_COUNT - 1 &&
|
||||
showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatCount(visibleAnomalyTotals[severity])}
|
||||
{getFixedCellValue(row, column.id)}
|
||||
</td>
|
||||
))}
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`${row.reportNo}-${column.code}-${severity}`}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem]"
|
||||
>
|
||||
{formatCount(
|
||||
row.defectCounts[column.code]?.[severity],
|
||||
)}
|
||||
</td>
|
||||
)),
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr className="bg-background font-medium">
|
||||
<td
|
||||
colSpan={STICKY_COLUMN_COUNT}
|
||||
className="sticky left-0 z-20 border-b border-border bg-background px-2 py-[1.2rem] text-center align-middle"
|
||||
);
|
||||
})}
|
||||
{EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`${row.reportNo}-total-${severity}`}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem]"
|
||||
>
|
||||
{formatCount(visibleAnomalyTotals[severity])}
|
||||
</td>
|
||||
))}
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`${row.reportNo}-${column.code}-${severity}`}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem]"
|
||||
>
|
||||
{formatCount(
|
||||
row.defectCounts[column.code]?.[severity],
|
||||
)}
|
||||
</td>
|
||||
)),
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr className="bg-background font-medium">
|
||||
<td
|
||||
colSpan={STICKY_COLUMN_COUNT}
|
||||
className="sticky left-0 z-20 border-b border-border bg-background px-2 py-[1.2rem] text-center align-middle"
|
||||
style={{
|
||||
boxShadow: showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block whitespace-nowrap"
|
||||
style={{
|
||||
boxShadow: showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
transform: `translateX(${summaryLabelOffset}px)`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block whitespace-nowrap"
|
||||
style={{
|
||||
transform: `translateX(${summaryLabelOffset}px)`,
|
||||
}}
|
||||
>
|
||||
소계
|
||||
</span>
|
||||
소계
|
||||
</span>
|
||||
</td>
|
||||
{summaryLabelColumnCount > STICKY_COLUMN_COUNT && (
|
||||
<td
|
||||
aria-hidden="true"
|
||||
colSpan={
|
||||
summaryLabelColumnCount - STICKY_COLUMN_COUNT
|
||||
}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem]"
|
||||
/>
|
||||
)}
|
||||
{fixedSummaryValues.map((summary) => (
|
||||
<td
|
||||
key={`subtotal-fixed-${summary.key}`}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
{summary.value}
|
||||
</td>
|
||||
{summaryLabelColumnCount > STICKY_COLUMN_COUNT && (
|
||||
))}
|
||||
{EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`subtotal-anomaly-${severity}`}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem]"
|
||||
>
|
||||
{formatCount(
|
||||
defectSummary.anomalySubtotals[severity],
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
aria-hidden="true"
|
||||
colSpan={
|
||||
summaryLabelColumnCount - STICKY_COLUMN_COUNT
|
||||
}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem]"
|
||||
/>
|
||||
)}
|
||||
{fixedSummaryValues.map((summary) => (
|
||||
<td
|
||||
key={`subtotal-fixed-${summary.key}`}
|
||||
className="border-b border-r border-border bg-background px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
{summary.value}
|
||||
</td>
|
||||
))}
|
||||
{EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`subtotal-anomaly-${severity}`}
|
||||
key={`subtotal-${column.code}-${severity}`}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem]"
|
||||
>
|
||||
{formatCount(
|
||||
defectSummary.anomalySubtotals[severity],
|
||||
defectSummary.defectSubtotals[column.code]?.[
|
||||
severity
|
||||
],
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
{visibleDefectColumns.flatMap((column) =>
|
||||
EXMN_DETAIL_SEVERITIES.map((severity) => (
|
||||
<td
|
||||
key={`subtotal-${column.code}-${severity}`}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem]"
|
||||
>
|
||||
{formatCount(
|
||||
defectSummary.defectSubtotals[column.code]?.[
|
||||
severity
|
||||
],
|
||||
)}
|
||||
</td>
|
||||
)),
|
||||
)}
|
||||
</tr>
|
||||
<tr className="bg-secondary font-medium">
|
||||
<td
|
||||
colSpan={STICKY_COLUMN_COUNT}
|
||||
className="sticky left-0 z-20 border-b border-border bg-secondary px-2 py-[1.2rem] text-center align-middle"
|
||||
)),
|
||||
)}
|
||||
</tr>
|
||||
<tr className="bg-secondary font-medium">
|
||||
<td
|
||||
colSpan={STICKY_COLUMN_COUNT}
|
||||
className="sticky left-0 z-20 border-b border-border bg-secondary px-2 py-[1.2rem] text-center align-middle"
|
||||
style={{
|
||||
boxShadow: showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block whitespace-nowrap"
|
||||
style={{
|
||||
boxShadow: showStickyDivider
|
||||
? STICKY_DIVIDER_SHADOW
|
||||
: undefined,
|
||||
transform: `translateX(${summaryLabelOffset}px)`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block whitespace-nowrap"
|
||||
style={{
|
||||
transform: `translateX(${summaryLabelOffset}px)`,
|
||||
}}
|
||||
>
|
||||
합계
|
||||
</span>
|
||||
</td>
|
||||
{summaryLabelColumnCount > STICKY_COLUMN_COUNT && (
|
||||
<td
|
||||
aria-hidden="true"
|
||||
colSpan={
|
||||
summaryLabelColumnCount - STICKY_COLUMN_COUNT
|
||||
}
|
||||
className="border-b border-r border-border bg-secondary px-2 py-[1.2rem]"
|
||||
/>
|
||||
)}
|
||||
{fixedSummaryValues.map((summary) => (
|
||||
<td
|
||||
key={`total-fixed-${summary.key}`}
|
||||
className="border-b border-r border-border bg-secondary px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
{summary.value}
|
||||
</td>
|
||||
))}
|
||||
합계
|
||||
</span>
|
||||
</td>
|
||||
{summaryLabelColumnCount > STICKY_COLUMN_COUNT && (
|
||||
<td
|
||||
aria-hidden="true"
|
||||
colSpan={
|
||||
summaryLabelColumnCount - STICKY_COLUMN_COUNT
|
||||
}
|
||||
className="border-b border-r border-border bg-secondary px-2 py-[1.2rem]"
|
||||
/>
|
||||
)}
|
||||
{fixedSummaryValues.map((summary) => (
|
||||
<td
|
||||
key={`total-fixed-${summary.key}`}
|
||||
className="border-b border-r border-border bg-secondary px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
{summary.value}
|
||||
</td>
|
||||
))}
|
||||
<td
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
{formatCount(defectSummary.anomalyTotal)}
|
||||
</td>
|
||||
{visibleDefectColumns.map((column) => (
|
||||
<td
|
||||
key={`total-${column.code}`}
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
{formatCount(defectSummary.anomalyTotal)}
|
||||
{formatCount(
|
||||
defectSummary.defectTotals[column.code],
|
||||
)}
|
||||
</td>
|
||||
{visibleDefectColumns.map((column) => (
|
||||
<td
|
||||
key={`total-${column.code}`}
|
||||
colSpan={EXMN_DETAIL_SEVERITIES.length}
|
||||
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
|
||||
>
|
||||
{formatCount(
|
||||
defectSummary.defectTotals[column.code],
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={
|
||||
fixedColumns.length +
|
||||
(visibleDefectColumns.length + 1) *
|
||||
EXMN_DETAIL_SEVERITIES.length
|
||||
}
|
||||
className="h-28 border-b border-r border-border text-center text-muted-foreground"
|
||||
>
|
||||
조회된 상세 결과가 없습니다.
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={
|
||||
fixedColumns.length +
|
||||
(visibleDefectColumns.length + 1) *
|
||||
EXMN_DETAIL_SEVERITIES.length
|
||||
}
|
||||
className="h-28 border-b border-r border-border text-center text-muted-foreground"
|
||||
>
|
||||
조회된 상세 결과가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
</StandardPopupLayout>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, type ReactNode, type UIEventHandler } from "react";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { StandardDataTable } from "@common/business_components";
|
||||
|
||||
interface ExmnSurveyStandardDataTableProps<T extends object> {
|
||||
rows: T[];
|
||||
title: ReactNode;
|
||||
actions?: ReactNode;
|
||||
loading?: boolean;
|
||||
emptyMessage: string;
|
||||
minTableWidth?: string;
|
||||
onScroll?: UIEventHandler<HTMLDivElement>;
|
||||
renderTable: () => ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조사 복합표의 병합 셀은 업무 화면에서 유지하고 카드·헤더·로딩·빈 상태·스크롤은
|
||||
* StandardDataTable이 소유하도록 연결한다.
|
||||
*/
|
||||
export function ExmnSurveyStandardDataTable<T extends object>({
|
||||
rows,
|
||||
title,
|
||||
actions,
|
||||
loading = false,
|
||||
emptyMessage,
|
||||
minTableWidth = "100%",
|
||||
onScroll,
|
||||
renderTable,
|
||||
}: ExmnSurveyStandardDataTableProps<T>) {
|
||||
/** 공통 표가 데이터 유무와 로딩 상태를 판단할 수 있도록 복합 행을 최소 열로 연결한다. */
|
||||
const columns = useMemo<ColumnDef<T>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "complexRow",
|
||||
accessorFn: (_row, index) => index,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<StandardDataTable
|
||||
table={table}
|
||||
title={title}
|
||||
actions={actions}
|
||||
loading={loading}
|
||||
emptyMessage={emptyMessage}
|
||||
className="!w-full !max-w-full"
|
||||
scrollMode="horizontal"
|
||||
minTableWidth={minTableWidth}
|
||||
viewPagination={false}
|
||||
flushOuterMargin
|
||||
onScroll={onScroll}
|
||||
renderTable={renderTable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type { ExmnSurveyStandardDataTableProps };
|
||||
@@ -10,6 +10,18 @@ type PopupSearchPanelProps = {
|
||||
contentClassName?: string;
|
||||
};
|
||||
|
||||
type PopupSearchFieldProps = {
|
||||
label: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
};
|
||||
|
||||
type PopupSearchActionsProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** WindowPopup 조회조건의 외곽 카드, 패딩과 가로 스크롤 규칙을 한 곳에서 관리한다. */
|
||||
export default function PopupSearchPanel({
|
||||
children,
|
||||
@@ -24,9 +36,59 @@ export default function PopupSearchPanel({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={cn("min-w-0", contentClassName)}>{children}</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 flex-wrap items-center gap-x-5 gap-y-3",
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export type { PopupSearchPanelProps };
|
||||
/** 조회조건 라벨과 입력 컨트롤을 같은 행에 배치한다. */
|
||||
export function PopupSearchField({
|
||||
label,
|
||||
children,
|
||||
className,
|
||||
labelClassName,
|
||||
}: PopupSearchFieldProps) {
|
||||
return (
|
||||
<label className={cn("flex shrink-0 items-center gap-2", className)}>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 whitespace-nowrap text-[1.3rem] font-medium text-label",
|
||||
labelClassName,
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/** 조회·초기화 버튼을 검색조건의 우측 끝에 같은 간격으로 배치한다. */
|
||||
export function PopupSearchActions({
|
||||
children,
|
||||
className,
|
||||
}: PopupSearchActionsProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"ml-auto flex min-w-fit shrink-0 items-center justify-end gap-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type {
|
||||
PopupSearchActionsProps,
|
||||
PopupSearchFieldProps,
|
||||
PopupSearchPanelProps,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
useState,
|
||||
type CSSProperties,
|
||||
type ReactNode,
|
||||
type UIEventHandler,
|
||||
} from "react";
|
||||
import type { Table as TableInstance } from "@tanstack/react-table";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
@@ -21,6 +22,10 @@ import {
|
||||
|
||||
type ScrollMode = "none" | "horizontal" | "vertical" | "both";
|
||||
|
||||
type StandardDataTableRenderContext<T> = {
|
||||
table: TableInstance<T>;
|
||||
};
|
||||
|
||||
type StandardDataTableProps<T> = {
|
||||
table: TableInstance<T>;
|
||||
title?: ReactNode;
|
||||
@@ -38,6 +43,10 @@ type StandardDataTableProps<T> = {
|
||||
flushOuterMargin?: boolean;
|
||||
/** 빈 목록일 때 부모의 남은 높이를 채워 팝업·페이지의 과도한 바깥 여백을 줄인다. */
|
||||
fillAvailableHeight?: boolean;
|
||||
/** 병합 셀·고정 열이 필요한 복합표가 공통 카드·로딩·스크롤 안에서 표 본문만 교체한다. */
|
||||
renderTable?: (context: StandardDataTableRenderContext<T>) => ReactNode;
|
||||
/** 고정 열 경계처럼 가로 스크롤 위치가 필요한 소비 화면에 공통 스크롤 이벤트를 전달한다. */
|
||||
onScroll?: UIEventHandler<HTMLDivElement>;
|
||||
onRowClick?: (row: T) => void;
|
||||
};
|
||||
|
||||
@@ -185,6 +194,8 @@ function StandardDataTable<T>({
|
||||
viewPagination = true,
|
||||
flushOuterMargin = false,
|
||||
fillAvailableHeight = false,
|
||||
renderTable,
|
||||
onScroll,
|
||||
onRowClick,
|
||||
}: StandardDataTableProps<T>) {
|
||||
const rowCount = table.getRowModel().rows.length;
|
||||
@@ -272,6 +283,7 @@ function StandardDataTable<T>({
|
||||
>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={onScroll}
|
||||
className={cn(
|
||||
scrollClassName,
|
||||
shouldFillAvailableHeight && "min-h-96 flex-1",
|
||||
@@ -286,19 +298,27 @@ function StandardDataTable<T>({
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingWithAnimation color="primary" />
|
||||
<div
|
||||
className="relative size-16 shrink-0"
|
||||
role="status"
|
||||
aria-label="조회 중"
|
||||
>
|
||||
<LoadingWithAnimation color="primary" />
|
||||
</div>
|
||||
) : (
|
||||
emptyMessage
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant={variant}
|
||||
className={cn("w-full", className)}
|
||||
viewPagination={false}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
(renderTable?.({ table }) ?? (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant={variant}
|
||||
className={cn("w-full", className)}
|
||||
viewPagination={false}
|
||||
onRowClick={onRowClick}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{viewPagination && !loading && rowCount > 0 ? (
|
||||
@@ -313,4 +333,9 @@ function StandardDataTable<T>({
|
||||
}
|
||||
|
||||
export { STANDARD_ROW_NUMBER_HEADER, StandardDataTable, getStandardRowNumber };
|
||||
export type { ScrollMode, StandardDataTableProps, StandardRowNumberParams };
|
||||
export type {
|
||||
ScrollMode,
|
||||
StandardDataTableProps,
|
||||
StandardDataTableRenderContext,
|
||||
StandardRowNumberParams,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user