Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs/sn/dev_kic

This commit is contained in:
kiminchan
2026-08-26 10:38:51 +09:00
9 changed files with 493 additions and 496 deletions
@@ -109,6 +109,7 @@ const BaseLayout = ({
onDeleteFile,
onAnalysis,
onResultView,
selectedRowData,
isIr = false,
isRdii = false,
}: BaseLayout) => {
@@ -283,11 +284,7 @@ const BaseLayout = ({
</Button>
)}
{resultViewBtn && (
<Button
type="button"
color="white"
onClick={onResultView}
>
<Button type="button" color="white" onClick={onResultView}>
{resultViewBtn}
</Button>
)}
@@ -295,6 +292,7 @@ const BaseLayout = ({
<Button
type="button"
color="red"
disabled={!selectedRowData}
onClick={isDeleteFile}
>
삭제
@@ -321,14 +319,15 @@ const BaseLayout = ({
{titleName?.secondCardTitle}
</h3>
<div className="ml-auto flex items-center gap-2">
{graphBtn && (
<Button type="button">
엑셀다운로드
</Button>
)}
{graphBtn && <Button type="button">엑셀다운로드</Button>}
{/* 그래프 있을 시 */}
{graphBtn && (
<Button type="button" color="red">
<Button
type="button"
color="red"
disabled={!selectedRowData}
onClick={isDeleteFile}
>
삭제
</Button>
)}
@@ -342,7 +342,12 @@ const Page = () => {
<Button type="button" onClick={handleRegisterClick}>
등록
</Button>
<Button type="button" color="red" onClick={isDeleteFile}>
<Button
type="button"
color="red"
disabled={isLoading || !sbmrgPrevntBizSn}
onClick={isDeleteFile}
>
삭제
</Button>
</>
@@ -317,7 +317,12 @@ const Page = () => {
<Button type="button" onClick={handleRegister}>
등록
</Button>
<Button type="button" color="red" onClick={handleDelete}>
<Button
type="button"
color="red"
disabled={loading || !businessId}
onClick={handleDelete}
>
삭제
</Button>
</>
@@ -438,7 +438,7 @@ export function ExmnBusinessRegisterPopupPage({
disabled={isLoading || formLocked}
onClick={() => void handleSave()}
>
{isEditMode ? "수정" : "저장"}
저장
</Button>
<Button
type="button"
@@ -60,9 +60,9 @@ export function ExmnPplUnitDetailTable({
const tableWidth = COLUMN_WIDTHS.reduce((sum, width) => sum + width, 0);
return (
<div className="min-h-0 flex-1 overflow-x-auto overflow-y-visible border border-border bg-background">
<div className="min-w-0 overflow-x-auto overflow-y-visible bg-background">
<table
className="border-separate border-spacing-0 text-center text-[1.05rem] text-foreground"
className="table-ui default border-separate border-spacing-0 text-center text-foreground"
style={{ width: "100%", minWidth: `${tableWidth}px` }}
>
<colgroup>
@@ -70,17 +70,17 @@ export function ExmnPplUnitDetailTable({
<col key={`unit-column-${index}`} style={{ width: `${width}px` }} />
))}
</colgroup>
<thead className="sticky top-0 z-30 bg-primary text-center text-[1.2rem] font-medium leading-[1.2] text-primary-foreground">
<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 bg-primary px-2 py-2 text-center text-primary-foreground"
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 bg-primary px-2 py-2 text-center text-primary-foreground"
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
>
단위
<br />
@@ -88,7 +88,7 @@ export function ExmnPplUnitDetailTable({
</th>
<th
rowSpan={2}
className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground"
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
>
단위
<br />
@@ -98,7 +98,7 @@ export function ExmnPplUnitDetailTable({
</th>
<th
rowSpan={2}
className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground"
className="border-b border-r border-border px-2 py-[1.2rem] text-center"
>
결함
<br />
@@ -108,58 +108,58 @@ export function ExmnPplUnitDetailTable({
</th>
<th
colSpan={6}
className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground"
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 bg-primary px-2 py-2 text-center text-primary-foreground"
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 bg-primary px-2 py-2 text-center text-primary-foreground"
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 bg-primary px-2 py-2 text-center text-primary-foreground"
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 bg-primary px-2 py-2 text-center text-primary-foreground">
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
종류
</th>
<th className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground">
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
코드
</th>
<th className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground">
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
등급
</th>
<th className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground">
<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 bg-primary px-2 py-2 text-center text-primary-foreground">
<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 bg-primary px-2 py-2 text-center text-primary-foreground">
<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 bg-primary px-2 py-2 text-center text-primary-foreground">
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
구조적
</th>
<th className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground">
<th className="border-b border-r border-border px-2 py-[1.2rem] text-center">
운영적
</th>
</tr>
@@ -181,7 +181,7 @@ export function ExmnPplUnitDetailTable({
{showPipeCell && (
<td
rowSpan={pipeSpan?.count}
className="border-b border-r border-border bg-background px-2 py-2.5 align-middle font-medium text-primary"
className="border-b border-r border-border bg-background px-2 py-[1.2rem] align-middle font-medium text-primary"
>
{row.pipeNo}
</td>
@@ -190,49 +190,49 @@ export function ExmnPplUnitDetailTable({
<>
<td
rowSpan={unitSpan?.count}
className="border-b border-r border-border bg-background px-2 py-2.5 align-middle font-medium"
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-2.5 align-middle"
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-2.5">
<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-2.5 text-left">
<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-2.5">
<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-2.5">
<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-2.5">
<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-2.5">
<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-2.5">
<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-2.5">
<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-2.5">
<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-2.5">
<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-2.5">
<td className="border-b border-r border-border px-2 py-[1.2rem]">
{formatOptionalValue(row.remark)}
</td>
</tr>
@@ -633,7 +633,7 @@ export function ExmnRegistrationPage() {
<Button
type="button"
color="primary"
disabled={isLoading}
disabled={isLoading || !selectedBusiness}
onClick={openBusinessUpdate}
>
수정
@@ -76,7 +76,7 @@ const FIXED_COLUMNS = [
const REPORT_COLUMN_LEFT = 0;
const PIPE_COLUMN_LEFT = FIXED_COLUMNS[0].width;
const STICKY_COLUMN_COUNT = 2;
const STICKY_DIVIDER_SHADOW = "2px 0 0 #b8c5d1";
const STICKY_DIVIDER_SHADOW = "2px 0 0 hsl(var(--border))";
const SEVERITY_COLUMN_WIDTH = 52;
type ExmnDetailFixedColumnId = (typeof FIXED_COLUMNS)[number]["id"];
@@ -247,21 +247,6 @@ export function ExmnSurveyDetailPopupPage({
}, [actualUnitRows, appliedDetailNumberKeyword, isUnitMode]);
const rows = filteredRows;
/** 팝업 문서 자체 스크롤을 잠그고 넓은 표의 스크롤은 표 영역에서만 처리한다. */
useEffect(() => {
const previousHtmlOverflow = document.documentElement.style.overflow;
const previousBodyOverflow = document.body.style.overflow;
const previousBodyWidth = document.body.style.width;
document.documentElement.style.overflow = "hidden";
document.body.style.overflow = "hidden";
document.body.style.width = "100%";
return () => {
document.documentElement.style.overflow = previousHtmlOverflow;
document.body.style.overflow = previousBodyOverflow;
document.body.style.width = previousBodyWidth;
};
}, []);
/** 관로·맨홀 조사유형 또는 결함 범위가 바뀌면 이전 검색 조건과 안내를 초기화한다. */
useEffect(() => {
setDetailMode("PIPE");
@@ -730,11 +715,6 @@ export function ExmnSurveyDetailPopupPage({
: defectType === "ALL"
? `${selectedTrtdstNm} ${definition.defectGroupTitle}조사 집계표`
: `${selectedTrtdstNm} ${definition.defectGroupTitle}조사 ${defectTypeLabel} 집계표`;
const visibleDefectGroupTitle =
defectType === "ALL"
? definition.defectGroupTitle
: `${definition.defectGroupTitle} ${defectTypeLabel}`;
return (
<StandardPopupLayout
title={popupTitle}
@@ -753,115 +733,122 @@ export function ExmnSurveyDetailPopupPage({
</Button>
</>
}
bodyClassName="flex flex-col overflow-hidden"
bodyClassName="overflow-x-hidden"
>
<LoadingOverlay isLoading={isLoading} />
{notice && (
<div
role="status"
className="border-primary/20 mb-3 shrink-0 rounded-sm border bg-secondary px-4 py-2 text-[1.05rem] text-primary"
className="border-primary/20 rounded-sm border bg-secondary px-4 py-2 text-[1.3rem] text-primary"
>
{notice.text}
</div>
)}
<section className="flex min-h-0 flex-1 flex-col overflow-hidden">
<section className="flex min-w-0 flex-col gap-[1.2rem]">
{!isDefectTypeView && (
<div className="card card-border !m-0 mb-3 flex shrink-0 flex-wrap items-end justify-between gap-4 !p-3">
<div className="flex flex-wrap items-end gap-3">
<div className="grid w-[18rem] gap-1.5">
<Label htmlFor="exmn-detail-district">처리분구</Label>
<Select
value={selectedTrtdstId}
onValueChange={(value) => {
setSelectedTrtdstId(value);
setActualRows([]);
setActualUnitRows([]);
setDetailNumberKeyword("");
setAppliedDetailNumberKeyword("");
setNotice(null);
}}
>
<SelectTrigger id="exmn-detail-district" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{districtOptions.length > 0 ? (
districtOptions.map((district) => (
<SelectItem
key={district.trtdstId}
value={district.trtdstId}
>
{district.trtdstNm}
</SelectItem>
))
) : (
<SelectItem value={trtdstId}>{trtdstNm}</SelectItem>
)}
</SelectContent>
</Select>
</div>
<div className="grid w-[30rem] gap-1.5">
<Label htmlFor="exmn-detail-pipe-number">관로번호</Label>
<Input
id="exmn-detail-pipe-number"
value={detailNumberKeyword}
placeholder="관로번호 입력"
onChange={(event) =>
setDetailNumberKeyword(event.target.value)
}
onKeyDown={(event) => {
if (event.key === "Enter") handleSearch();
}}
/>
</div>
<div className="card card-border !m-0 flex flex-wrap items-center gap-3 !p-[1.2rem]">
<div className="flex items-center gap-2">
<Label
htmlFor="exmn-detail-district"
className="mr-2 whitespace-nowrap text-label"
>
처리분구
</Label>
<Select
value={selectedTrtdstId}
onValueChange={(value) => {
setSelectedTrtdstId(value);
setActualRows([]);
setActualUnitRows([]);
setDetailNumberKeyword("");
setAppliedDetailNumberKeyword("");
setNotice(null);
}}
>
<SelectTrigger id="exmn-detail-district" className="w-[18rem]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{districtOptions.length > 0 ? (
districtOptions.map((district) => (
<SelectItem
key={district.trtdstId}
value={district.trtdstId}
>
{district.trtdstNm}
</SelectItem>
))
) : (
<SelectItem value={trtdstId}>{trtdstNm}</SelectItem>
)}
</SelectContent>
</Select>
</div>
<div className="flex flex-wrap items-end gap-3">
{jobType === "PPL" && (
<fieldset className="grid gap-1.5">
<legend className="mb-1 text-sm font-medium text-label">
상세구분
</legend>
<div
className="flex items-center gap-1"
role="group"
aria-label="관로 상세보기 모드"
>
{(
Object.keys(PPL_DETAIL_MODE_LABELS) as ExmnPplDetailMode[]
).map((mode) => (
<Button
key={mode}
type="button"
color={detailMode === mode ? "primary" : "white"}
aria-pressed={detailMode === mode}
onClick={() => handleDetailModeChange(mode)}
>
{PPL_DETAIL_MODE_LABELS[mode]}
</Button>
))}
</div>
</fieldset>
)}
<div className="flex items-center gap-2">
<Label
htmlFor="exmn-detail-pipe-number"
className="mr-2 whitespace-nowrap text-label"
>
관로번호
</Label>
<Input
id="exmn-detail-pipe-number"
className="w-[30rem]"
value={detailNumberKeyword}
placeholder="관로번호 입력"
onChange={(event) => setDetailNumberKeyword(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") handleSearch();
}}
/>
</div>
{jobType === "PPL" && (
<div className="flex items-center gap-2">
<Button type="button" color="primary" onClick={handleSearch}>
조회
</Button>
<Button
type="button"
color="white"
onClick={() => {
setDetailNumberKeyword("");
setAppliedDetailNumberKeyword("");
setNotice(null);
}}
<Label className="mr-2 whitespace-nowrap text-label">
상세구분
</Label>
<div
className="flex items-center gap-1"
role="group"
aria-label="관로 상세보기 모드"
>
초기화
</Button>
{(
Object.keys(PPL_DETAIL_MODE_LABELS) as ExmnPplDetailMode[]
).map((mode) => (
<Button
key={mode}
type="button"
color={detailMode === mode ? "primary" : "white"}
aria-pressed={detailMode === mode}
onClick={() => handleDetailModeChange(mode)}
>
{PPL_DETAIL_MODE_LABELS[mode]}
</Button>
))}
</div>
</div>
)}
<div className="ml-auto flex items-center gap-2">
<Button type="button" color="primary" onClick={handleSearch}>
조회
</Button>
<Button
type="button"
color="white"
onClick={() => {
setSelectedTrtdstId(trtdstId);
setDetailMode("PIPE");
setDetailNumberKeyword("");
setAppliedDetailNumberKeyword("");
setNotice(null);
}}
>
초기화
</Button>
</div>
</div>
)}
@@ -869,374 +856,375 @@ export function ExmnSurveyDetailPopupPage({
{loadError && (
<div
role="alert"
className="shrink-0 border border-red-200 bg-red-50 px-4 py-2 text-[1.05rem] text-red-700"
className="border-destructive/30 bg-destructive/10 rounded-sm border px-4 py-2 text-[1.3rem] text-destructive"
>
{loadError}
</div>
)}
<div className="flex shrink-0 items-center justify-between gap-4 py-3">
<div className="flex min-w-0 items-baseline gap-3">
<h2 className="shrink-0 text-[1.25rem] font-semibold text-title">
{tableTitle}
</h2>
<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>
<div className="flex flex-wrap items-center gap-x-7 gap-y-2 whitespace-nowrap text-xl text-muted-foreground">
{isUnitMode ? (
<>
<span>
<strong className="mr-2 font-medium text-foreground">
관로번호
</strong>
{appliedDetailNumberKeyword || "전체"}
</span>
<span>
<strong className="mr-2 font-medium text-foreground">
단위구간
</strong>
{unitSectionCount.toLocaleString("ko-KR")}개
</span>
</>
) : (
<>
<span>
<strong className="mr-2 font-medium text-foreground">
연장거리 합계
</strong>
{formatNumber(totals.extensionM)}m
</span>
<span>
<strong className="mr-2 font-medium text-foreground">
주행거리 합계
</strong>
{formatNumber(totals.drivingDistanceM)}m
</span>
</>
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-7 whitespace-nowrap text-[1.05rem] text-label">
{isUnitMode ? (
<>
<span>
<strong className="mr-2 text-foreground">관로번호</strong>
{appliedDetailNumberKeyword || "전체"}
</span>
<span>
<strong className="mr-2 text-foreground">단위구간</strong>
{unitSectionCount.toLocaleString("ko-KR")}개
</span>
</>
) : (
<>
<span>
<strong className="mr-2 text-foreground">
연장거리 합계
</strong>
{formatNumber(totals.extensionM)}m
</span>
<span>
<strong className="mr-2 text-foreground">
주행거리 합계
</strong>
{formatNumber(totals.drivingDistanceM)}m
</span>
</>
)}
</div>
</div>
{isUnitMode ? (
<ExmnPplUnitDetailTable
rows={unitRows}
emptyMessage={
appliedDetailNumberKeyword
? "입력한 관로번호에 해당하는 본단위구간이 없습니다."
: "조회된 본단위구간이 없습니다."
}
/>
) : (
<div
className="min-h-0 flex-1 overflow-x-auto overflow-y-visible border border-border bg-background"
onScroll={handleTableScroll}
>
<table
className="border-separate border-spacing-0 text-center text-[1.05rem] 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:
(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 bg-primary text-center text-[1.3rem] font-medium leading-[1.2] text-primary-foreground">
<tr>
{fixedColumns.map((column, columnIndex) => {
const sticky = columnIndex < STICKY_COLUMN_COUNT;
const columnLabel = column.label;
return (
<th
<div className="card !m-[1.2rem] min-w-0 overflow-hidden !p-0">
{isUnitMode ? (
<ExmnPplUnitDetailTable
rows={unitRows}
emptyMessage={
appliedDetailNumberKeyword
? "입력한 관로번호에 해당하는 본단위구간이 없습니다."
: "조회된 본단위구간이 없습니다."
}
/>
) : (
<div
className="min-w-0 overflow-x-auto overflow-y-visible bg-background"
onScroll={handleTableScroll}
>
<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}
rowSpan={4}
className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground"
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 bg-primary px-2 py-2 text-center text-primary-foreground"
>
이상항목
</th>
</tr>
<tr>
<th
colSpan={EXMN_DETAIL_SEVERITIES.length}
className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground"
>
합계
</th>
<th
colSpan={
visibleDefectColumns.length *
EXMN_DETAIL_SEVERITIES.length
}
className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground"
>
{visibleDefectGroupTitle}
</th>
</tr>
<tr>
{EXMN_DETAIL_SEVERITIES.map((severity) => (
<th
key={`total-${severity}`}
rowSpan={2}
className="border-b border-r border-border bg-primary px-2 py-2 text-center text-primary-foreground"
>
{SEVERITY_LABELS[severity]}
</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 bg-primary px-2 py-2 text-center text-primary-foreground"
>
{column.label} ({column.code})
</th>
))}
</tr>
<tr>
{visibleDefectColumns.flatMap((column) =>
EXMN_DETAIL_SEVERITIES.map((severity) => (
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
key={`${column.code}-${severity}`}
className="border-b border-r border-border bg-primary px-2 py-1.5 text-center font-medium text-primary-foreground"
colSpan={
(visibleDefectColumns.length + 1) *
EXMN_DETAIL_SEVERITIES.length
}
className="border-b border-r border-border !py-[0.8rem] px-2 text-center"
>
{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"
</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"
>
{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-2.5"
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>
);
})}
{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.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"
>
{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) => (
<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={{
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-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={`${row.reportNo}-total-${severity}`}
className="border-b border-r border-border bg-background px-2 py-2.5"
key={`subtotal-anomaly-${severity}`}
className="border-b border-r border-border px-2 py-[1.2rem]"
>
{formatCount(visibleAnomalyTotals[severity])}
{formatCount(
defectSummary.anomalySubtotals[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-2.5"
key={`subtotal-${column.code}-${severity}`}
className="border-b border-r border-border px-2 py-[1.2rem]"
>
{formatCount(
row.defectCounts[column.code]?.[severity],
defectSummary.defectSubtotals[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-2.5 text-center align-middle"
style={{
boxShadow: showStickyDivider
? STICKY_DIVIDER_SHADOW
: undefined,
}}
>
<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-background px-2 py-2.5"
/>
)}
{fixedSummaryValues.map((summary) => (
<td
key={`subtotal-fixed-${summary.key}`}
className="border-b border-r border-border bg-background px-2 py-2.5 text-center"
>
{summary.value}
</td>
))}
{EXMN_DETAIL_SEVERITIES.map((severity) => (
<td
key={`subtotal-anomaly-${severity}`}
className="border-b border-r border-border px-2 py-2.5"
>
{formatCount(
defectSummary.anomalySubtotals[severity],
)}
</td>
))}
{visibleDefectColumns.flatMap((column) =>
EXMN_DETAIL_SEVERITIES.map((severity) => (
<tr className="bg-secondary font-medium">
<td
key={`subtotal-${column.code}-${severity}`}
className="border-b border-r border-border px-2 py-2.5"
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,
}}
>
{formatCount(
defectSummary.defectSubtotals[column.code]?.[
severity
],
)}
<span
className="inline-block whitespace-nowrap"
style={{
transform: `translateX(${summaryLabelOffset}px)`,
}}
>
합계
</span>
</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-2.5 text-center align-middle"
style={{
boxShadow: showStickyDivider
? STICKY_DIVIDER_SHADOW
: undefined,
}}
>
<span
className="inline-block whitespace-nowrap"
style={{
transform: `translateX(${summaryLabelOffset}px)`,
}}
>
합계
</span>
</td>
{summaryLabelColumnCount > STICKY_COLUMN_COUNT && (
{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.defectTotals[column.code],
)}
</td>
))}
</tr>
</>
) : (
<tr>
<td
aria-hidden="true"
colSpan={
summaryLabelColumnCount - STICKY_COLUMN_COUNT
fixedColumns.length +
(visibleDefectColumns.length + 1) *
EXMN_DETAIL_SEVERITIES.length
}
className="border-b border-r border-border bg-secondary px-2 py-2.5"
/>
)}
{fixedSummaryValues.map((summary) => (
<td
key={`total-fixed-${summary.key}`}
className="border-b border-r border-border bg-secondary px-2 py-2.5 text-center"
className="h-28 border-b border-r border-border text-center text-muted-foreground"
>
{summary.value}
조회된 상세 결과가 없습니다.
</td>
))}
<td
colSpan={EXMN_DETAIL_SEVERITIES.length}
className="border-b border-r border-border px-2 py-2.5 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-2.5 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>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
)}
</div>
</section>
</StandardPopupLayout>
);
@@ -626,7 +626,7 @@ export function TrtdstManagementPage() {
<Button
type="button"
color="primary"
disabled={isLoading}
disabled={isLoading || !selectedRow}
onClick={handleEdit}
>
수정
@@ -644,7 +644,7 @@ export function TrtdstManagementPage() {
color={
selectedRow?.rowStatus === "deletePending" ? "white" : "red"
}
disabled={isLoading}
disabled={isLoading || !selectedRow}
onClick={() => void handleDelete()}
>
{selectedRow?.rowStatus === "deletePending"
@@ -25,7 +25,7 @@ export const windowPopupDefinitions = {
key: "exmnSurveyDetail",
title: "조사 상세현황",
width: 1500,
height: 720,
height: 650,
requiredParams: ["exmnBizId", "trtdstId", "trtdstNm", "jobType"],
},
dredgingRegister: {