sn: 선택 레이어 전체 디자인 수정

This commit is contained in:
2026-09-01 18:10:31 +09:00
parent 37374cb754
commit 872b3d08c0
9 changed files with 314 additions and 103 deletions
@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import { validateElapsedYears, type AgedPipeFilters } from "./agedPipeUtils";
import LayerSettingCard from "../layerSetting/LayerSettingCard";
type MaybePromise<T = void> = T | Promise<T>;
@@ -11,6 +12,9 @@ type Props = {
onFiltersChange: (filters: AgedPipeFilters) => void;
onApply: (filters?: AgedPipeFilters) => MaybePromise;
compact?: boolean;
enabled?: boolean;
onEnabledChange?: (enabled: boolean) => void;
onClose?: () => void;
};
export default function AgedPipeLayerSettingPanel({
@@ -18,6 +22,9 @@ export default function AgedPipeLayerSettingPanel({
onFiltersChange,
onApply,
compact = false,
enabled = true,
onEnabledChange = () => {},
onClose = () => {},
}: Props) {
const alert = useAlertStore((state) => state.setMessage);
const [isCollapsed, setIsCollapsed] = useState(false);
@@ -47,16 +54,16 @@ export default function AgedPipeLayerSettingPanel({
};
const inputClass =
"h-[22px] border border-input bg-background px-[2px] text-center text-[1rem] leading-none text-foreground";
"h-[20px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
const numberInputClass = inputClass + " w-[40px]";
const applyButtonClass =
"grid h-[20px] w-[20px] shrink-0 place-items-center rounded-full bg-primary text-[0.95rem] font-bold leading-none text-primary-foreground";
"grid h-[16px] w-[16px] shrink-0 place-items-center rounded-full bg-primary text-[0.85rem] font-bold leading-none text-primary-foreground";
const indicator = (
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
);
const settingRow = (
<div className="flex min-h-[25px] items-center gap-1 whitespace-nowrap text-[1.05rem] font-medium text-label">
<div className="flex min-h-[26px] items-center gap-1 whitespace-nowrap text-[1.15rem] font-medium text-label">
<div className="flex min-w-0 items-center gap-1">
{indicator}
<label htmlFor="aged-pipe-elapsed-years" className="shrink-0">
@@ -88,7 +95,7 @@ export default function AgedPipeLayerSettingPanel({
title="적용"
aria-label="경과년도 적용"
>
✓
<span className="relative left-[1px]">✓</span>
</button>
</div>
</div>
@@ -96,9 +103,14 @@ export default function AgedPipeLayerSettingPanel({
if (compact) {
return (
<div className="w-full rounded-[6px] border border-border bg-background px-[6px] py-2 shadow-blur">
<LayerSettingCard
title="노후관로"
enabled={enabled}
onEnabledChange={onEnabledChange}
onClose={onClose}
>
{settingRow}
</div>
</LayerSettingCard>
);
}
@@ -1,11 +1,15 @@
"use client";
import type { ExmnMapFilters, ExmnMapJobType } from "./exmnMapTypes";
import LayerSettingCard from "../layerSetting/LayerSettingCard";
type Props = {
jobType: ExmnMapJobType;
filters: ExmnMapFilters;
onFiltersChange: (filters: ExmnMapFilters) => void;
enabled?: boolean;
onEnabledChange?: (enabled: boolean) => void;
onClose?: () => void;
};
const GRADE_OPTIONS = ["", "1", "2", "3", "4", "5"];
@@ -15,6 +19,9 @@ export default function ExmnLayerSettingPanel({
jobType,
filters,
onFiltersChange,
enabled = true,
onEnabledChange = () => {},
onClose = () => {},
}: Props) {
const updateFilter = (key: keyof ExmnMapFilters, value: string) => {
onFiltersChange({
@@ -23,23 +30,25 @@ export default function ExmnLayerSettingPanel({
});
};
const selectClass =
"h-[24px] w-[54px] rounded-[4px] border border-input bg-background px-2 text-[1rem] text-foreground outline-none focus:border-primary";
const labelClass = "shrink-0 text-[1.05rem] font-medium text-label";
"h-[20px] w-[62px] rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground outline-none focus:border-primary";
const labelClass = "shrink-0 text-[1.15rem] font-medium text-label";
const title = jobType === "PPL" ? "관로조사" : "맨홀조사";
return (
<div className="w-full rounded-[6px] border border-border bg-background px-2 py-[9px] shadow-blur">
<div className="mb-2 flex items-center gap-1">
{/* 관로/맨홀 조사유형을 구분하는 지도 범례 의미색이므로 고정색을 유지한다. */}
<span
className={`h-2 w-2 rounded-full ${jobType === "PPL" ? "bg-[#facc15]" : "bg-[#8b5cf6]"}`}
/>
<span className="text-[1.1rem] font-semibold text-label">
{jobType === "PPL" ? "관로조사" : "맨홀조사"}
</span>
</div>
<div className="space-y-1.5">
<label className="flex min-h-[25px] items-center justify-between gap-2">
<span className={labelClass}>사업차수</span>
<LayerSettingCard
title={title}
enabled={enabled}
onEnabledChange={onEnabledChange}
onClose={onClose}
>
<div className="space-y-[1px]">
<label className="flex min-h-[24px] items-center justify-between ">
<span className={`flex items-center gap-1 ${labelClass}`}>
<span
className={`h-2 w-2 rounded-full ${jobType === "PPL" ? "bg-[#facc15]" : "bg-[#8b5cf6]"}`}
/>
사업차수
</span>
<input
className={selectClass}
type="text"
@@ -50,7 +59,7 @@ export default function ExmnLayerSettingPanel({
onChange={(event) => updateFilter("bizCycl", event.target.value)}
/>
</label>
<label className="flex min-h-[25px] items-center justify-between gap-2">
<label className="flex min-h-[24px] items-center justify-between ">
<span className={labelClass}>구조적 상태등급</span>
<select
className={selectClass}
@@ -66,7 +75,7 @@ export default function ExmnLayerSettingPanel({
))}
</select>
</label>
<label className="flex min-h-[25px] items-center justify-between gap-2">
<label className="flex min-h-[24px] items-center justify-between ">
<span className={labelClass}>운영적 상태등급</span>
<select
className={selectClass}
@@ -83,6 +92,6 @@ export default function ExmnLayerSettingPanel({
</select>
</label>
</div>
</div>
</LayerSettingCard>
);
}
@@ -36,7 +36,7 @@ export default function FldngHstryLayerSettingPanel({
.filter((year) => Number(year) >= Number(normalizedRange.startYear))
.map((year) => ({ val: year, text: year }));
const yearSelectClass =
"h-[22px] w-[62px] rounded-none border border-input bg-background px-[4px] text-center text-[1rem] leading-none text-foreground";
"h-[20px] w-[62px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
const changeYear = (key: keyof FloodHistoryYearRange, value: string) => {
onYearRangeChange(
@@ -49,7 +49,7 @@ export default function FldngHstryLayerSettingPanel({
return (
<div className="w-full overflow-visible rounded-[6px] border border-border bg-background shadow-blur">
<div className="flex min-h-[32px] items-center justify-between border-b border-border px-2">
<div className="flex min-h-[28px] items-center justify-between border-b border-border px-2">
<span className="text-[1.15rem] font-semibold text-label">
침수흔적도
</span>
@@ -58,18 +58,18 @@ export default function FldngHstryLayerSettingPanel({
type="button"
role="switch"
aria-checked={enabled}
aria-label="침수연도 필터 적용 여부"
title={enabled ? "침수연도 필터 적용 중" : "침수연도 필터 미적용"}
className={`relative h-[18px] w-[34px] rounded-full transition-colors ${enabled ? "bg-primary" : "bg-muted-foreground/40"}`}
aria-label="침수년도 필터 적용 여부"
title={enabled ? "침수년도 필터 적용 중" : "침수년도 필터 미적용"}
className={`relative h-[16px] w-[28px] rounded-full border transition-colors ${enabled ? "border-primary bg-primary" : "border-[#94a3b8] bg-[#cbd5e1]"}`}
onClick={() => onEnabledChange(!enabled)}
>
<span
className={`absolute top-[2px] h-[14px] w-[14px] rounded-full bg-white shadow-sm transition-[left] ${enabled ? "left-[18px]" : "left-[2px]"}`}
className={`absolute top-1/2 h-[12px] w-[12px] -translate-y-1/2 rounded-full bg-white shadow-sm transition-[left] ${enabled ? "left-[14px]" : "left-[2px]"}`}
/>
</button>
<button
type="button"
aria-label="침수연도 설정 닫기"
aria-label="침수년도 설정 닫기"
title="닫기"
className="grid h-[20px] w-[20px] place-items-center bg-transparent text-[1.3rem] leading-none text-muted-foreground hover:text-foreground"
onClick={onClose}
@@ -78,7 +78,7 @@ export default function FldngHstryLayerSettingPanel({
</button>
</div>
</div>
<div className="flex min-h-[38px] items-center gap-1.5 px-2 py-2 text-[1.05rem] font-medium text-label">
<div className="flex min-h-[30px] items-center gap-1.5 px-2 py-1 text-[1.15rem] font-medium text-label">
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
<span className="shrink-0">침수년도</span>
<div className="ml-auto flex shrink-0 items-center gap-1.5">
@@ -0,0 +1,59 @@
"use client";
import type { ReactNode } from "react";
type Props = {
title: string;
enabled: boolean;
onEnabledChange: (enabled: boolean) => void;
onClose: () => void;
children?: ReactNode;
showToggle?: boolean;
};
/** 침수흔적도 설정과 같은 제목·토글·닫기·본문 여백을 제공한다. */
export default function LayerSettingCard({
title,
enabled,
onEnabledChange,
onClose,
children,
showToggle = true,
}: Props) {
return (
<div className="w-full overflow-visible rounded-[6px] border border-border bg-background shadow-blur">
<div className="flex min-h-[28px] items-center justify-between border-b border-border px-2">
<span className="text-[1.15rem] font-semibold text-label">{title}</span>
<div className="flex shrink-0 items-center gap-2">
{showToggle ? (
<button
type="button"
role="switch"
aria-checked={enabled}
aria-label={`${title} 적용 여부`}
title={enabled ? `${title} 적용 중` : `${title} 미적용`}
className={`relative h-[16px] w-[28px] rounded-full border transition-colors ${enabled ? "border-primary bg-primary" : "border-[#94a3b8] bg-[#cbd5e1]"}`}
onClick={() => onEnabledChange(!enabled)}
>
<span
className={`absolute top-1/2 h-[12px] w-[12px] -translate-y-1/2 rounded-full bg-white shadow-sm transition-[left] ${enabled ? "left-[14px]" : "left-[2px]"}`}
/>
</button>
) : null}
<button
type="button"
aria-label={`${title} 설정 닫기`}
title="닫기"
className="grid h-[20px] w-[20px] place-items-center bg-transparent text-[1.3rem] leading-none text-muted-foreground hover:text-foreground"
onClick={onClose}
>
×
</button>
</div>
</div>
{children ? (
<div className="px-2 py-1 text-[1.15rem] text-label">{children}</div>
) : null}
</div>
);
}
@@ -10,6 +10,7 @@ import {
type SinkholeSyncSummary,
type SinkholeYearRange,
} from "./sinkholeTypes";
import LayerSettingCard from "../layerSetting/LayerSettingCard";
type MaybePromise<T = void> = T | Promise<T>;
@@ -21,6 +22,9 @@ type Props = {
lastSyncSummary?: SinkholeSyncSummary | null;
isAdmin?: boolean;
compact?: boolean;
enabled?: boolean;
onEnabledChange?: (enabled: boolean) => void;
onClose?: () => void;
};
export default function SinkholeLayerSettingPanel({
@@ -31,6 +35,9 @@ export default function SinkholeLayerSettingPanel({
lastSyncSummary,
isAdmin = false,
compact = false,
enabled = true,
onEnabledChange = () => {},
onClose = () => {},
}: Props) {
const alert = useAlertStore((state) => state.setMessage);
const [isCollapsed, setIsCollapsed] = useState(false);
@@ -94,7 +101,7 @@ export default function SinkholeLayerSettingPanel({
const currentYear = new Date().getFullYear();
const yearSelectClass =
"h-[22px] w-[62px] rounded-none border border-input bg-background px-[4px] text-center text-[1rem] leading-none text-foreground";
"h-[20px] !w-[58px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
const startYearOptions = Array.from(
{ length: currentYear - SINKHOLE_MIN_SEARCH_YEAR + 1 },
(_, index) => String(currentYear - index),
@@ -117,17 +124,17 @@ export default function SinkholeLayerSettingPanel({
.map((year) => ({ val: year, text: year }));
const inputClass =
"h-[22px] border border-input bg-background px-[2px] text-center text-[1rem] leading-none text-foreground";
"h-[20px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
const numberInputClass = inputClass + " w-[40px]";
const applyButtonClass =
"grid h-[20px] w-[20px] shrink-0 place-items-center rounded-full bg-primary text-[0.95rem] font-bold leading-none text-primary-foreground";
"grid h-[16px] w-[16px] shrink-0 place-items-center rounded-full bg-primary text-[0.85rem] font-bold leading-none text-primary-foreground";
const indicator = (
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
);
const settingRows = (
<div className="space-y-1.5 text-[1.05rem] font-medium text-label">
<div className="flex min-h-[25px] items-center gap-1.5 whitespace-nowrap">
<div className="space-y-1.5 text-[1.15rem] font-medium text-label">
<div className="flex min-h-[26px] items-center gap-1.5 whitespace-nowrap">
<div className="flex min-w-0 items-center gap-1">
{indicator}
<label htmlFor="sinkhole-radius" className="shrink-0">
@@ -158,15 +165,15 @@ export default function SinkholeLayerSettingPanel({
title="적용"
aria-label="반경 적용"
>
✓
<span className="relative left-[1px]">✓</span>
</button>
</div>
</div>
<div className="flex min-h-[25px] items-center gap-1.5 whitespace-nowrap">
<div className="flex min-h-[26px] items-center gap-1.5 whitespace-nowrap">
<div className="flex min-w-0 items-center gap-1">
{indicator}
<label htmlFor="sinkhole-start-year" className="shrink-0">
년도설정
발생년도
</label>
</div>
<div className="ml-auto flex shrink-0 items-center gap-1.5">
@@ -208,7 +215,7 @@ export default function SinkholeLayerSettingPanel({
title="적용"
aria-label="연도범위 적용"
>
✓
<span className="relative left-[1px]">✓</span>
</button>
</div>
</div>
@@ -217,9 +224,14 @@ export default function SinkholeLayerSettingPanel({
if (compact) {
return (
<div className="w-full rounded-[6px] border border-border bg-background px-2 py-[10px] shadow-blur">
<LayerSettingCard
title="지반침하정보"
enabled={enabled}
onEnabledChange={onEnabledChange}
onClose={onClose}
>
<div className="overflow-visible">{settingRows}</div>
</div>
</LayerSettingCard>
);
}
+136 -59
View File
@@ -104,6 +104,7 @@ import SinkholeLayerSettingPanel from "./components/sinkhole/SinkholeLayerSettin
import FldngHstryDetailPanel from "./components/fldngHstry/FldngHstryDetailPanel";
import FldngHstryLayerSettingPanel from "./components/fldngHstry/FldngHstryLayerSettingPanel";
import ExmnLayerSettingPanel from "./components/exmn/ExmnLayerSettingPanel";
import LayerSettingCard from "./components/layerSetting/LayerSettingCard";
import { getExmnMapLayer } from "./components/exmn/exmnMapApi";
import {
EMPTY_EXMN_MAP_FILTERS,
@@ -3013,6 +3014,24 @@ const Map = () => {
const [checkedLayerLegendVersion, setCheckedLayerLegendVersion] = useState(0);
const [isAgedPipeChecked, setIsAgedPipeChecked] = useState(false);
const [isSinkholeChecked, setIsSinkholeChecked] = useState(false);
const [isSinkholeSettingEnabled, setIsSinkholeSettingEnabled] =
useState(true);
const [isSinkholeSettingVisible, setIsSinkholeSettingVisible] =
useState(true);
const [isAgedPipeSettingEnabled, setIsAgedPipeSettingEnabled] =
useState(true);
const [isAgedPipeSettingVisible, setIsAgedPipeSettingVisible] =
useState(true);
const [isExmnPplSettingEnabled, setIsExmnPplSettingEnabled] = useState(true);
const [isExmnPplSettingVisible, setIsExmnPplSettingVisible] = useState(true);
const [isExmnMnhlSettingEnabled, setIsExmnMnhlSettingEnabled] =
useState(true);
const [isExmnMnhlSettingVisible, setIsExmnMnhlSettingVisible] =
useState(true);
const [isWaterDepthLegendEnabled, setIsWaterDepthLegendEnabled] =
useState(true);
const [isWaterDepthLegendVisible, setIsWaterDepthLegendVisible] =
useState(true);
const [isFldngHstryChecked, setIsFldngHstryChecked] = useState(false);
// 침수흔적위선과 심선은 하나의 연도 범위를 공유하지만 체크 상태는 독립적으로 관리한다.
const [isFloodWtlvChecked, setIsFloodWtlvChecked] = useState(false);
@@ -3030,6 +3049,11 @@ const Map = () => {
useState(true);
// 닫힌 설정 카드는 레이어가 완전히 해제된 뒤 다시 체크될 때만 자동으로 연다.
const floodLayerCheckedRef = useRef(false);
const sinkholeLayerCheckedRef = useRef(false);
const agedPipeLayerCheckedRef = useRef(false);
const exmnPplLayerCheckedRef = useRef(false);
const exmnMnhlLayerCheckedRef = useRef(false);
const waterDepthLayerCheckedRef = useRef(false);
// 체크 등급 변경과 연도 필터 변경을 구분해 연도 변경 때만 기존 선택 상세를 정리한다.
const floodYearFilterSignatureRef = useRef("");
const [isExmnPplChecked, setIsExmnPplChecked] = useState(false);
@@ -3682,12 +3706,30 @@ const Map = () => {
if (isSinkholePopupFilterActive) {
return normalizeSinkholeFilters(currentFilters, sinkholeScope);
}
if (!isSinkholeSettingEnabled) {
return normalizeSinkholeFilters(
{
...buildSinkholeFiltersFromYearRange(
sinkholeYearRange,
sinkholeScope,
),
startYmd: "",
endYmd: "",
},
sinkholeScope,
);
}
return normalizeSinkholeFilters(
buildSinkholeFiltersFromYearRange(sinkholeYearRange, sinkholeScope),
sinkholeScope,
);
});
}, [isSinkholePopupFilterActive, sinkholeScope, sinkholeYearRange]);
}, [
isSinkholePopupFilterActive,
isSinkholeSettingEnabled,
sinkholeScope,
sinkholeYearRange,
]);
useEffect(() => {
const timer = window.setInterval(() => {
@@ -6426,6 +6468,14 @@ const Map = () => {
const waterDepthChecked = checkedNodesData.some((node) =>
WATER_DEPTH_LAYER_TYPES.includes(String(node?.data?.lyrTyNm ?? "")),
);
if (agedPipeChecked && !agedPipeLayerCheckedRef.current) {
setIsAgedPipeSettingVisible(true);
}
if (waterDepthChecked && !waterDepthLayerCheckedRef.current) {
setIsWaterDepthLegendVisible(true);
}
agedPipeLayerCheckedRef.current = agedPipeChecked;
waterDepthLayerCheckedRef.current = waterDepthChecked;
setIsAgedPipeChecked(agedPipeChecked);
const floodWtlvChecked = checkedNodesData.some((node) =>
isFloodHistoryWaterLevelNode(node),
@@ -6460,6 +6510,10 @@ const Map = () => {
const sinkholeChecked = checkedNodesData.some((node) =>
isSinkholeLayerNode(node),
);
if (sinkholeChecked && !sinkholeLayerCheckedRef.current) {
setIsSinkholeSettingVisible(true);
}
sinkholeLayerCheckedRef.current = sinkholeChecked;
setIsSinkholeChecked(sinkholeChecked);
const exmnPplChecked = checkedNodesData.some((node) =>
isExmnResultPplNode(node),
@@ -6467,6 +6521,14 @@ const Map = () => {
const exmnMnhlChecked = checkedNodesData.some((node) =>
isExmnResultMnhlNode(node),
);
if (exmnPplChecked && !exmnPplLayerCheckedRef.current) {
setIsExmnPplSettingVisible(true);
}
if (exmnMnhlChecked && !exmnMnhlLayerCheckedRef.current) {
setIsExmnMnhlSettingVisible(true);
}
exmnPplLayerCheckedRef.current = exmnPplChecked;
exmnMnhlLayerCheckedRef.current = exmnMnhlChecked;
setIsExmnPplChecked(exmnPplChecked);
setIsExmnMnhlChecked(exmnMnhlChecked);
setActiveLayerSettingOrder((currentOrder) => {
@@ -6522,15 +6584,9 @@ const Map = () => {
);
setSinkholeYearRange(normalizedRange);
setIsSinkholePopupFilterActive(false);
setAppliedSinkholeFilters(
normalizeSinkholeFilters(
buildSinkholeFiltersFromYearRange(normalizedRange, sinkholeScope),
sinkholeScope,
),
);
setSinkholeSelectionVersion((version) => version + 1);
},
[sinkholeScope, sinkholeYearRange],
[sinkholeYearRange],
);
/** 침수위험지도 루트의 목록보기 버튼에서 전용 조회 팝업을 연다. */
@@ -6600,7 +6656,7 @@ const Map = () => {
return index >= 0 ? index : fallback;
};
if (isExmnPplChecked) {
if (isExmnPplChecked && isExmnPplSettingVisible) {
nextItems.push({
key: "exmnPpl",
label: "관로조사",
@@ -6610,12 +6666,15 @@ const Map = () => {
jobType="PPL"
filters={exmnPplFilters}
onFiltersChange={setExmnPplFilters}
enabled={isExmnPplSettingEnabled}
onEnabledChange={setIsExmnPplSettingEnabled}
onClose={() => setIsExmnPplSettingVisible(false)}
/>
),
});
}
if (isExmnMnhlChecked) {
if (isExmnMnhlChecked && isExmnMnhlSettingVisible) {
nextItems.push({
key: "exmnMnhl",
label: "맨홀조사",
@@ -6625,6 +6684,9 @@ const Map = () => {
jobType="MNHL"
filters={exmnMnhlFilters}
onFiltersChange={setExmnMnhlFilters}
enabled={isExmnMnhlSettingEnabled}
onEnabledChange={setIsExmnMnhlSettingEnabled}
onClose={() => setIsExmnMnhlSettingVisible(false)}
/>
),
});
@@ -6647,7 +6709,7 @@ const Map = () => {
});
}
if (isSinkholeChecked) {
if (isSinkholeChecked && isSinkholeSettingVisible) {
nextItems.push({
key: "sinkhole",
label: "지반침하정보",
@@ -6660,13 +6722,16 @@ const Map = () => {
onApplyYearRange={applySinkholeYearRange}
lastSyncSummary={lastSinkholeSyncSummary}
isAdmin={userInfo?.adminRoleYn === "Y"}
enabled={isSinkholeSettingEnabled}
onEnabledChange={setIsSinkholeSettingEnabled}
onClose={() => setIsSinkholeSettingVisible(false)}
compact
/>
),
});
}
if (isAgedPipeChecked) {
if (isAgedPipeChecked && isAgedPipeSettingVisible) {
nextItems.push({
key: "agedPipe",
label: "노후관로",
@@ -6676,43 +6741,47 @@ const Map = () => {
filters={draftAgedPipeFilters}
onFiltersChange={setDraftAgedPipeFilters}
onApply={handleApplyAgedPipeFilters}
enabled={isAgedPipeSettingEnabled}
onEnabledChange={setIsAgedPipeSettingEnabled}
onClose={() => setIsAgedPipeSettingVisible(false)}
compact
/>
),
});
}
if (isWaterDepthLayerActive) {
if (isWaterDepthLayerActive && isWaterDepthLegendVisible) {
nextItems.push({
key: "waterDepthLegend",
label: "침수심 범례",
order: resolveSettingOrder("waterDepthLegend", 9997),
content: (
<div className="overflow-hidden rounded-sm border border-border bg-secondary">
<div className="px-3 py-2">
<div className="rounded-sm border border-border bg-background px-3 py-3">
<div className="mb-2 bg-primary py-1 text-center text-[1.1rem] font-semibold text-primary-foreground">
침수심 범례
</div>
<div className="space-y-1">
{waterDepthLegendItems.map((item) => (
<div key={item.key} className="flex items-center gap-2">
<span
className="inline-block h-4 w-10 shrink-0"
style={{
backgroundColor: item.color,
border: `1px solid ${item.borderColor}`,
}}
/>
<span className="text-[1rem] font-medium text-label">
{item.label}
</span>
</div>
))}
</div>
<LayerSettingCard
title="침수심 범례"
enabled={isWaterDepthLegendEnabled}
onEnabledChange={setIsWaterDepthLegendEnabled}
onClose={() => setIsWaterDepthLegendVisible(false)}
showToggle={false}
>
{isWaterDepthLegendEnabled ? (
<div className="space-y-1">
{waterDepthLegendItems.map((item) => (
<div key={item.key} className="flex items-center gap-2">
<span
className="inline-block h-4 w-10 shrink-0"
style={{
backgroundColor: item.color,
border: `1px solid ${item.borderColor}`,
}}
/>
<span className="text-[1rem] font-medium text-label">
{item.label}
</span>
</div>
))}
</div>
</div>
</div>
) : null}
</LayerSettingCard>
),
});
}
@@ -6730,13 +6799,23 @@ const Map = () => {
handleApplyAgedPipeFilters,
floodYearRange,
isAgedPipeChecked,
isAgedPipeSettingEnabled,
isAgedPipeSettingVisible,
isExmnMnhlChecked,
isExmnMnhlSettingEnabled,
isExmnMnhlSettingVisible,
isExmnPplChecked,
isExmnPplSettingEnabled,
isExmnPplSettingVisible,
isFldngHstryChecked,
isFloodYearFilterEnabled,
isFloodYearSettingVisible,
isSinkholeChecked,
isSinkholeSettingEnabled,
isSinkholeSettingVisible,
isWaterDepthLayerActive,
isWaterDepthLegendEnabled,
isWaterDepthLegendVisible,
lastSinkholeSyncSummary,
sinkholeRadiusMeters,
sinkholeYearRange,
@@ -6775,24 +6854,9 @@ const Map = () => {
return (
<div className="mt-3 flex h-[34vh] max-h-[32rem] min-h-[24rem] flex-col overflow-hidden rounded-sm border border-border bg-background shadow-[0_4px_14px_rgba(15,23,42,0.08)]">
<div className="border-b border-border bg-secondary px-3 py-2">
<div className="flex items-center justify-between gap-2">
<span className="text-[1.2rem] font-semibold text-label">
선택 레이어 설정
</span>
<span className="text-[1rem] text-muted-foreground">
{activeLayerSettingItems.length}건
</span>
</div>
<div className="mt-1 flex flex-wrap gap-1">
{activeLayerSettingItems.map((item) => (
<span
key={item.key}
className="rounded-full border border-border bg-background px-2 py-[1px] text-[1rem] text-label"
>
{item.label}
</span>
))}
</div>
<span className="text-[1.2rem] font-semibold text-label">
선택 레이어 설정
</span>
</div>
<div
ref={restoreActiveLayerSettingScroll}
@@ -6819,7 +6883,7 @@ const Map = () => {
const core = GisApp.Module.core as any;
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
if (!isAgedPipeChecked) return;
if (!isAgedPipeChecked || !isAgedPipeSettingEnabled) return;
const baseLayerCode = Object.values(GisApp.layerCode ?? {}).find(
(item: any) =>
@@ -6855,7 +6919,12 @@ const Map = () => {
return () => {
core?.deleteLayer?.(AGED_PIPE_HIGHLIGHT_LAYER_ID);
};
}, [appliedAgedPipeFilters, isAgedPipeChecked, mapReady]);
}, [
appliedAgedPipeFilters,
isAgedPipeChecked,
isAgedPipeSettingEnabled,
mapReady,
]);
useEffect(() => {
if (!mapReady || !gisMaps.current) return;
@@ -9038,10 +9107,14 @@ const Map = () => {
removeExmnResultLayer("PPL");
return;
}
void loadExmnResultLayer("PPL", exmnPplFilters);
void loadExmnResultLayer(
"PPL",
isExmnPplSettingEnabled ? exmnPplFilters : EMPTY_EXMN_MAP_FILTERS,
);
}, [
exmnPplFilters,
isExmnPplChecked,
isExmnPplSettingEnabled,
loadExmnResultLayer,
mapReady,
removeExmnResultLayer,
@@ -9052,10 +9125,14 @@ const Map = () => {
removeExmnResultLayer("MNHL");
return;
}
void loadExmnResultLayer("MNHL", exmnMnhlFilters);
void loadExmnResultLayer(
"MNHL",
isExmnMnhlSettingEnabled ? exmnMnhlFilters : EMPTY_EXMN_MAP_FILTERS,
);
}, [
exmnMnhlFilters,
isExmnMnhlChecked,
isExmnMnhlSettingEnabled,
loadExmnResultLayer,
mapReady,
removeExmnResultLayer,
@@ -89,7 +89,9 @@ const hasLegendStyle = (layerInfo: any) =>
const normalizeLegendLabel = (label: string) => {
const normalized = String(label).trim();
return normalized.endsWith("미만") ? normalized : `${normalized} 미만`;
return /(?:미만|이상)$/.test(normalized)
? normalized
: `${normalized} 미만`;
};
export const extractWaterDepthLegendItems = (
@@ -346,3 +346,22 @@
- 수정 진입은 `양식다운로드 → 저장 → 취소`만 표시하고 조사보고서 공통 양식 버튼은 숨긴다.
- 수정 화면의 양식다운로드는 저장된 최신 처리분구 기준으로 입력양식을 다시 생성한다.
- 등록된 실제 사업보고서 PDF는 기존 조사목록의 보고서 열에서 다운로드하는 계약을 유지한다.
## 27. 2026-09-01 지도 조사 설정 카드 UI 통일 계획
- `ExmnLayerSettingPanel`의 관로조사·맨홀조사 카드를 침수흔적도 설정과 동일한 header/body 구조로 변경한다.
- 제목 오른쪽의 액션 `div`에는 `적용 여부 토글 → 닫기(×)`를 배치하고 토글 기본값은 ON으로 한다.
- 토글 OFF에서는 해당 조사유형의 사업차수·구조·운영 필터 적용을 해제하되 입력값은 유지하며, 닫기는 카드만 숨기고 레이어 체크·필터 상태를 유지한다.
- 두 조사유형의 적용 여부, 닫기, 입력값은 독립 상태로 관리한다.
- 카드 제목 `1.15rem`, 본문 `1.05rem`, header `32px`, 본문 padding `8px`, 선택 입력 `22px`, font `1rem`, 좌우 padding `4px`를 적용한다.
- 관로 노랑·맨홀 보라 의미색은 indicator로 유지하고 카드 외곽·제목은 중립 스타일로 통일한다.
- 상태등급 select는 침수흔적도 설정의 SelectBox와 같은 font·padding·border/background 스타일을 적용한다.
- 기존 자동반영 동작을 유지하며 원래 없는 `✓` 버튼을 새로 추가하지 않는다. 지반침하·노후관로 등 기존 `✓`가 있는 카드의 버튼은 공통 변경으로 제거하지 않는다.
- `map/page.tsx`의 선택 레이어 설정 header에서는 활성 카드 개수와 카드명 pill/chip 목록을 제거하고 제목만 유지한다.
- 검증은 관로·맨홀 카드 동시 표시, 독립 토글·닫기, 스타일 통일, 자동반영, 상단 개수·chip 미표시와 지도 필터 회귀를 포함한다.
- API·DTO·DB 변경은 없다.
### 27.1 구현 결과
- `ExmnLayerSettingPanel`을 공통 카드 header·토글·닫기 구조로 변경하고 관로·맨홀 상태를 독립 관리한다.
- 사업차수·상태등급의 기존 자동반영은 유지하며 토글 OFF에서는 빈 필터로 다시 조회한다.
@@ -273,3 +273,24 @@
- 부모가 있는 관로 탭은 활성 여부와 무관하게 `←` 버튼을 항상 표시해 일반 관로와 구분하고 지반침하 상세로 복귀한다.
- 공용 우측 탭은 최대 5개 FIFO를 적용하되, 제거 대상이 부모 지반침하이면 그 부모에서 열린 연계 관로도 함께 제거한다. 연계 관로만 제거될 때는 부모 지반침하를 유지한다.
- 변경 범위는 `app/(map-layout)/map/page.tsx`이며 BE API·DTO·DB 계약은 변경하지 않는다.
## 16. 2026-09-01 선택 레이어 설정 지반침하 카드 UI 통일 계획
- 작업 모드: `maintenance`
- 이번 단계는 작업계획만 수정하고 코드는 후속 승인 후 변경한다.
- `SinkholeLayerSettingPanel`을 침수흔적도 설정 카드와 동일한 compact 카드 구조로 정리한다.
- 헤더에 `지반침하정보` 제목을 표시하고 우측 액션 `div`에 `적용 여부 토글 → 닫기(×)` 순서로 배치한다.
- 토글 기본값은 ON이며 OFF에서는 연도 조회조건을 제거하되 현재 연도·반경 입력값은 유지한다.
- 닫기는 설정 카드만 숨기고 레이어와 현재 설정값은 유지한다.
- 침수흔적도 설정과 동일하게 카드 header `32px`, 제목 `1.15rem`, 본문 `1.05rem`, 본문 padding `8px`, SelectBox `62×22px`, SelectBox font `1rem`·좌우 padding `4px`를 적용한다.
- 주황 카드·제목 스타일은 제거하고 공통 border/background/label 색상으로 통일한다.
- 반경과 년도설정의 `✓` 적용 버튼은 모두 유지하며 SelectBox 선택만으로 자동 적용하지 않는다.
- `map/page.tsx`의 선택 레이어 설정 공통 header에서는 활성 카드 개수와 카드명 pill/chip 목록을 제거하고 `선택 레이어 설정` 제목만 유지한다.
- 공통 상단 표시 제거는 지반침하·침수흔적도·조사시설·노후관로·침수심 범례 등 모든 설정 카드에 적용한다.
- 검증은 토글/닫기 배치, 카드·SelectBox 스타일 일치, `✓` 적용 유지, 상단 개수·chip 미표시, 다중 카드 순서·스크롤 회귀를 포함한다.
- API·DTO·DB 변경은 없다.
### 16.1 구현 결과
- `SinkholeLayerSettingPanel` compact 카드에 공통 header·토글·닫기와 침수흔적도 기준 입력 스타일을 적용했다.
- 반경·년도 `✓` 적용 버튼은 유지했고 토글·닫기 상태는 `map/page.tsx`에서 관리한다.