Files
frontend/apps/sn/app/(map-layout)/map/components/sinkhole/SinkholeDetailPanel.tsx
T

284 lines
10 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { Button } from "@common/components";
import { VerticalTable } from "@/components/ui/verticalTable";
import { getCmsData } from "@/components/lib/api";
import type { SinkholeDetail, SinkholeNearbyPipe } from "./sinkholeTypes";
type Props = {
detail: SinkholeDetail;
nearbyPipes: SinkholeNearbyPipe[];
radiusMeters: number;
onClose: () => void;
onSelectPipe: (pipe: SinkholeNearbyPipe) => void;
};
type PipeUseCodeItem = {
lwpoCd: string;
lwpoCdNm: string;
};
let pipeUseCodeCache: PipeUseCodeItem[] | null = null;
let pipeUseCodePromise: Promise<PipeUseCodeItem[]> | null = null;
const display = (value: unknown) => {
const normalized = String(value ?? "").trim();
return normalized ? normalized : "-";
};
const displayNumber = (value: unknown, suffix = "") => {
if (value === null || value === undefined || value === "") return "-";
const normalized = Number(value);
if (Number.isNaN(normalized)) return display(value);
return `${normalized.toLocaleString()}${suffix}`;
};
const parseElapsedYears = (value: unknown) => {
if (value === null || value === undefined || value === "") return null;
const normalized = Number(value);
return Number.isFinite(normalized) ? normalized : null;
};
const displayElapsedYears = (value: unknown) => displayNumber(value, "년");
const normalizePipeUseCodes = (data: any): PipeUseCodeItem[] => {
if (!Array.isArray(data)) return [];
return data.map((item: any) => ({
lwpoCd: String(item?.lwpoCd ?? "").trim(),
lwpoCdNm: String(item?.lwpoCdNm ?? "").trim(),
}));
};
const loadPipeUseCodes = async () => {
if (pipeUseCodeCache) return pipeUseCodeCache;
if (!pipeUseCodePromise) {
pipeUseCodePromise = getCmsData(`/api/v1/code/cache/OGC011/Y`)
.catch(() => getCmsData(`/api/v1/code/cache/OGC011`))
.then((data) => {
const codes = normalizePipeUseCodes(data);
pipeUseCodeCache = codes;
return codes;
})
.finally(() => {
pipeUseCodePromise = null;
});
}
return pipeUseCodePromise;
};
export default function SinkholeDetailPanel({
detail,
nearbyPipes,
radiusMeters,
onClose,
onSelectPipe,
}: Props) {
const [pipeUseCodeMap, setPipeUseCodeMap] = useState<Map<string, string>>(
new Map(),
);
const canShowNearby = String(detail.coordStatus ?? "").trim() === "SUCCESS";
useEffect(() => {
let cancelled = false;
loadPipeUseCodes()
.then((codes) => {
if (cancelled) return;
setPipeUseCodeMap(
new Map(codes.map((item) => [item.lwpoCd, item.lwpoCdNm])),
);
})
.catch((error) => {
console.error("Failed to load OGC011 pipe use codes:", error);
});
return () => {
cancelled = true;
};
}, []);
const getPipeUseName = (pipe: SinkholeNearbyPipe) => {
const rawCode = String(pipe.pipeTypeCd ?? pipe.pipeTypeName ?? "").trim();
if (!rawCode) return "-";
return pipeUseCodeMap.get(rawCode) ?? rawCode;
};
const nearbyPipeElapsedSummary = useMemo(() => {
const elapsedYears = nearbyPipes
.map((pipe) => parseElapsedYears(pipe.elapsedYears))
.filter((value): value is number => value !== null);
if (elapsedYears.length === 0) {
return { average: null, oldest: null };
}
const total = elapsedYears.reduce((sum, value) => sum + value, 0);
const average = Math.round((total / elapsedYears.length) * 10) / 10;
const oldest = Math.max(...elapsedYears);
return { average, oldest };
}, [nearbyPipes]);
const summaryTableData = useMemo(
() => [
{
acdntNo: display(detail.acdntNo),
ocrnYmd: display(detail.ocrnYmd),
fullAddress: display(detail.fullAddress || detail.daddr),
sizeWidth: display(detail.sizeWidth),
sizeLength: display(detail.sizeLength),
sizeDepth: display(detail.sizeDepth),
detailCause: display(detail.detailCause),
restoreStatus: display(detail.restoreStatus),
restoreCompletedYmd: display(detail.restoreCompletedYmd),
},
],
[detail],
);
const summaryHeadList = [
{ id: "acdntNo", title: "사고번호" },
{ id: "ocrnYmd", title: "발생일자" },
{ id: "fullAddress", title: "주소" },
{ id: "sizeWidth", title: "발생규모폭" },
{ id: "sizeLength", title: "발생규모연장" },
{ id: "sizeDepth", title: "발생규모깊이" },
{ id: "detailCause", title: "상세발생원인" },
{ id: "restoreStatus", title: "복구상태" },
{ id: "restoreCompletedYmd", title: "복구완료일자" },
];
return (
<div
id="popup"
className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[350px] flex-col overflow-hidden border-r border-border bg-background transition-all duration-300"
>
<div className="card-header relative flex items-center justify-between bg-primary p-4">
<div className="flex-1">
<h3 className="text-2xl font-medium text-primary-foreground">
지반침하정보
<br />
사고번호 : {display(detail.acdntNo)}
</h3>
</div>
<Button
onClick={onClose}
className="text-primary-foreground/80 flex items-center space-x-4 hover:text-primary-foreground focus:outline-none"
size="xs"
>
✕
</Button>
</div>
<div className="flex-1 overflow-y-auto overflow-x-hidden">
<VerticalTable
headName="발생정보"
tableData={summaryTableData}
headlist={summaryHeadList}
pageSize={20}
total={0}
cellClick={undefined}
onPageChange={function (): void {
throw new Error("Function not implemented.");
}}
/>
<section className="pb-[1.2rem]">
<div className="card-header !block px-[1.2rem]">
<h3 className="mx-3 whitespace-nowrap text-2xl font-medium text-label">
침하발생 반경내 관로정보
</h3>
{canShowNearby ? (
<div className="mx-3 mt-2 whitespace-nowrap text-left text-[0.95rem] leading-5 text-muted-foreground">
<span>반경 {radiusMeters}m · </span>
<span className="font-bold text-primary">
{nearbyPipes.length.toLocaleString()}건
</span>
<span>, 평균경과 </span>
<span className="font-bold text-primary">
{displayElapsedYears(nearbyPipeElapsedSummary.average)}
</span>
<span>, 최장경과 </span>
<span className="font-bold text-primary">
{displayElapsedYears(nearbyPipeElapsedSummary.oldest)}
</span>
</div>
) : null}
</div>
{!canShowNearby ? (
<div className="card !m-[1.2rem] !mt-0 rounded-sm border border-amber-200 bg-amber-50 px-3 py-3 text-[1.05rem] text-amber-700">
좌표가 없어 반경 내 하수관로를 조회할 수 없습니다.
{detail.geocodeErrorMessage ? (
<div className="mt-2 break-words text-[0.98rem] text-amber-800">
사유: {display(detail.geocodeErrorMessage)}
</div>
) : null}
</div>
) : nearbyPipes.length === 0 ? (
<div className="card !m-[1.2rem] !mt-0 rounded-sm border border-border bg-secondary px-3 py-3 text-[1.05rem] text-label">
반경 내 하수관로 정보가 없습니다.
</div>
) : (
<div className="card !mx-[1.2rem] !mb-[1.2rem] !mt-0 overflow-hidden">
<table className="w-full table-fixed border-collapse">
<colgroup>
<col className="w-[34px]" />
<col className="w-[90px]" />
<col className="w-[60px]" />
<col className="w-[56px]" />
<col className="w-[56px]" />
</colgroup>
<thead>
<tr>
<th className="whitespace-nowrap border border-border bg-secondary px-1 py-2 text-center text-[0.95rem] font-bold text-label">
NO
</th>
<th className="whitespace-nowrap border border-border bg-secondary px-1 py-2 text-center text-[0.95rem] font-bold text-label">
관로 ID
</th>
<th className="whitespace-nowrap border border-border bg-secondary px-1 py-2 text-center text-[0.95rem] font-bold text-label">
용도
</th>
<th className="whitespace-nowrap border border-border bg-secondary px-1 py-2 text-center text-[0.95rem] font-bold text-label">
경과년도
</th>
<th className="whitespace-nowrap border border-border bg-secondary px-1 py-2 text-center text-[0.95rem] font-bold text-label">
거리(m)
</th>
</tr>
</thead>
<tbody>
{nearbyPipes.map((pipe, index) => (
<tr
key={`${pipe.fid}-${index}`}
className="cursor-pointer hover:bg-accent"
onClick={() => onSelectPipe(pipe)}
>
<td className="border border-border px-1 py-2 text-center text-[0.95rem] text-foreground">
{index + 1}
</td>
<td className="border border-border px-1 py-2 text-left text-[0.95rem] font-semibold text-primary underline underline-offset-2">
{display(pipe.pipeId)}
</td>
<td className="whitespace-nowrap border border-border px-1 py-2 text-center text-[0.95rem] text-foreground">
{getPipeUseName(pipe)}
</td>
<td className="border border-border px-1 py-2 text-right text-[0.95rem] text-foreground">
{displayElapsedYears(pipe.elapsedYears)}
</td>
<td className="border border-border px-1 py-2 text-right text-[0.95rem] text-foreground">
{displayNumber(pipe.distanceMeters)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
</div>
</div>
);
}