274 lines
8.9 KiB
TypeScript
274 lines
8.9 KiB
TypeScript
/**
|
|
* 화면명: 지반침하정보 레이어 설정
|
|
* 발생연도와 위험도 등 지도 표시 조건을 설정한다.
|
|
*/
|
|
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { SelectBox } from "@common/business_components/ui";
|
|
import { useAlertStore } from "@/shared/lib";
|
|
import {
|
|
normalizeSinkholeYearRange,
|
|
SINKHOLE_MIN_SEARCH_YEAR,
|
|
validateSinkholeYearRange,
|
|
type SinkholeSyncSummary,
|
|
type SinkholeYearRange,
|
|
} from "./sinkhole-types";
|
|
import LayerSettingCard from "../layer-setting/layer-setting-card";
|
|
|
|
type MaybePromise<T = void> = T | Promise<T>;
|
|
|
|
type Props = {
|
|
radiusMeters: number;
|
|
onApplyRadius: (radiusMeters: number) => MaybePromise;
|
|
yearRange: SinkholeYearRange;
|
|
onApplyYearRange: (yearRange: SinkholeYearRange) => MaybePromise;
|
|
lastSyncSummary?: SinkholeSyncSummary | null;
|
|
isAdmin?: boolean;
|
|
compact?: boolean;
|
|
enabled?: boolean;
|
|
onEnabledChange?: (enabled: boolean) => void;
|
|
onClose?: () => void;
|
|
};
|
|
|
|
export default function SinkholeLayerSettingPanel({
|
|
radiusMeters,
|
|
onApplyRadius,
|
|
yearRange,
|
|
onApplyYearRange,
|
|
lastSyncSummary,
|
|
isAdmin = false,
|
|
compact = false,
|
|
enabled = true,
|
|
onEnabledChange = () => {},
|
|
onClose = () => {},
|
|
}: Props) {
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
const [radiusText, setRadiusText] = useState(String(radiusMeters));
|
|
const [yearRangeDraft, setYearRangeDraft] = useState<SinkholeYearRange>(() =>
|
|
normalizeSinkholeYearRange(yearRange),
|
|
);
|
|
|
|
useEffect(() => {
|
|
setRadiusText(String(radiusMeters));
|
|
}, [radiusMeters]);
|
|
|
|
useEffect(() => {
|
|
setYearRangeDraft(normalizeSinkholeYearRange(yearRange));
|
|
}, [yearRange]);
|
|
|
|
const commitRadius = async () => {
|
|
const nextValue = Number(radiusText.replace(/[^0-9]/g, ""));
|
|
if (!Number.isFinite(nextValue) || nextValue <= 0) {
|
|
alert("반경은 1m 이상 입력해야 합니다.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await onApplyRadius(nextValue);
|
|
alert(`지반침하반경은 ${nextValue}m로 반영되었습니다.`);
|
|
} catch {
|
|
// 적용 함수 내부에서 실패 사유를 안내한다.
|
|
}
|
|
};
|
|
|
|
const commitYearRange = async () => {
|
|
const normalized = normalizeSinkholeYearRange(yearRangeDraft, yearRange);
|
|
const validationMessage = validateSinkholeYearRange(normalized);
|
|
if (validationMessage) {
|
|
alert(validationMessage);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await onApplyYearRange(normalized);
|
|
alert(
|
|
`시작년도 ${normalized.startYear} ~ 종료년도 ${normalized.endYear} 반영되었습니다.`,
|
|
);
|
|
} catch {
|
|
alert("반영 중 오류가 발생했습니다.");
|
|
}
|
|
};
|
|
|
|
const updateYearRangeDraft = (
|
|
key: keyof SinkholeYearRange,
|
|
value: string,
|
|
) => {
|
|
setYearRangeDraft((current) => ({
|
|
...current,
|
|
[key]: String(value)
|
|
.replace(/[^0-9]/g, "")
|
|
.slice(0, 4),
|
|
}));
|
|
};
|
|
|
|
const currentYear = new Date().getFullYear();
|
|
const yearSelectClass =
|
|
"!h-[26px] !w-full !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
|
|
const startYearOptions = Array.from(
|
|
{ length: currentYear - SINKHOLE_MIN_SEARCH_YEAR + 1 },
|
|
(_, index) => String(currentYear - index),
|
|
)
|
|
.filter(
|
|
(year) =>
|
|
!yearRangeDraft.endYear ||
|
|
Number(year) <= Number(yearRangeDraft.endYear),
|
|
)
|
|
.map((year) => ({ val: year, text: year }));
|
|
const endYearOptions = Array.from(
|
|
{ length: currentYear - SINKHOLE_MIN_SEARCH_YEAR + 1 },
|
|
(_, index) => String(currentYear - index),
|
|
)
|
|
.filter(
|
|
(year) =>
|
|
!yearRangeDraft.startYear ||
|
|
Number(year) >= Number(yearRangeDraft.startYear),
|
|
)
|
|
.map((year) => ({ val: year, text: year }));
|
|
|
|
const inputClass =
|
|
"h-[26px] !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
|
|
const numberInputClass = inputClass + " min-w-0 flex-1";
|
|
const applyButtonClass =
|
|
"h-[26px] shrink-0 rounded border border-[#A9C5E5] bg-[#EDF4FC] px-2 py-0 text-[12px] font-medium leading-none text-[#2463A6]";
|
|
const settingRows = (
|
|
<div className="space-y-1.5 text-[1.15rem] font-medium text-label">
|
|
<div className="flex min-h-[26px] flex-col items-stretch gap-1.5 whitespace-nowrap">
|
|
<div className="flex min-w-0 items-center">
|
|
<label htmlFor="sinkhole-radius" className="shrink-0">
|
|
지반침하반경(m)
|
|
</label>
|
|
</div>
|
|
<div className="flex w-full items-center gap-1">
|
|
<input
|
|
id="sinkhole-radius"
|
|
className={numberInputClass}
|
|
type="text"
|
|
inputMode="numeric"
|
|
pattern="[0-9]*"
|
|
autoComplete="off"
|
|
value={radiusText}
|
|
onFocus={(event) => event.currentTarget.select()}
|
|
onChange={(event) =>
|
|
setRadiusText(event.target.value.replace(/[^0-9]/g, ""))
|
|
}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Enter") void commitRadius();
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className={applyButtonClass}
|
|
onClick={() => void commitRadius()}
|
|
title="적용"
|
|
aria-label="반경 적용"
|
|
>
|
|
확인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div className="flex min-h-[26px] flex-col items-stretch gap-1.5 whitespace-nowrap">
|
|
<div className="flex min-w-0 items-center">
|
|
<label htmlFor="sinkhole-start-year" className="shrink-0">
|
|
발생년도
|
|
</label>
|
|
</div>
|
|
<div className="flex w-full items-center gap-1.5">
|
|
<div id="sinkhole-start-year" className="min-w-0 flex-1">
|
|
<SelectBox
|
|
className="w-full"
|
|
selectClass={yearSelectClass}
|
|
label=""
|
|
selectArray={startYearOptions}
|
|
value={yearRangeDraft.startYear}
|
|
onSelectValue={(value) =>
|
|
updateYearRangeDraft("startYear", String(value))
|
|
}
|
|
selectContentClass="z-[101]"
|
|
labelClass="hidden"
|
|
/>
|
|
</div>
|
|
<span className="px-[1px] text-[0.95rem] font-normal text-muted-foreground">
|
|
~
|
|
</span>
|
|
<div id="sinkhole-end-year" className="min-w-0 flex-1">
|
|
<SelectBox
|
|
className="w-full"
|
|
selectClass={yearSelectClass}
|
|
label=""
|
|
selectArray={endYearOptions}
|
|
value={yearRangeDraft.endYear}
|
|
onSelectValue={(value) =>
|
|
updateYearRangeDraft("endYear", String(value))
|
|
}
|
|
selectContentClass="z-[101]"
|
|
labelClass="hidden"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className={applyButtonClass}
|
|
onClick={() => void commitYearRange()}
|
|
title="적용"
|
|
aria-label="연도범위 적용"
|
|
>
|
|
확인
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (compact) {
|
|
return (
|
|
<LayerSettingCard
|
|
title="지반침하정보"
|
|
enabled={enabled}
|
|
onEnabledChange={onEnabledChange}
|
|
onClose={onClose}
|
|
>
|
|
<div className="overflow-visible">{settingRows}</div>
|
|
</LayerSettingCard>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full overflow-hidden rounded-sm border border-[#f0d7b0] bg-[#fff8ef]">
|
|
{/* 지반침하 상세는 지도 위험정보 경고영역이므로 주황 의미색을 유지한다. */}
|
|
<div className="relative flex min-h-[30px] items-center justify-center border-b border-[#f2dfc3] bg-background px-8">
|
|
<button
|
|
type="button"
|
|
className="absolute left-2 top-1/2 h-[18px] w-[18px] -translate-y-1/2 bg-transparent text-[1.2rem] leading-none text-muted-foreground"
|
|
onClick={() => setIsCollapsed((value) => !value)}
|
|
title={isCollapsed ? "펼치기" : "접기"}
|
|
>
|
|
{isCollapsed ? "+" : "-"}
|
|
</button>
|
|
<span className="text-[1.2rem] font-semibold text-[#d97706]">
|
|
지반침하정보
|
|
</span>
|
|
</div>
|
|
|
|
{!isCollapsed && (
|
|
<div className="px-2 py-2">
|
|
<div className="overflow-visible">{settingRows}</div>
|
|
|
|
{isAdmin && lastSyncSummary ? (
|
|
<div className="mt-2 rounded-sm border border-[#f2dfc3] bg-background px-2 py-2 text-[1rem] text-[#7c5b2f]">
|
|
<div className="font-semibold">최근 동기화</div>
|
|
<div className="mt-1">
|
|
완료시각: {lastSyncSummary.finishedAt || "-"}
|
|
</div>
|
|
<div className="mt-1">
|
|
성공 {lastSyncSummary.successCount}건 / 실패{" "}
|
|
{lastSyncSummary.failedCount}건
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|