"디지털하수관로" UI 수정
This commit is contained in:
+2
-2
@@ -12,8 +12,8 @@ NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
NEXT_PUBLIC_NGII_PARAM = 04trYP9_xwLAfALjwZ-B8g&service=WMTS&request=GetTile&version=1.0.0&format=image/png&layer=korean_map&style=korean&tilematrixset=korean&tilematrix={z}&tilecol={x}&tilerow={y}
|
||||
SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
SEWERAGE_API_URL = http://localhost:8883
|
||||
# SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
|
||||
# SEWERAGE_API_URL = http://localhost:8883
|
||||
SEWERAGE_API_URL = http://con.mind-one.co.kr:11083
|
||||
DC_URL = https://apigateway.apps.rsonesoft.com
|
||||
# NEXT_PUBLIC_CM_LOGIN_PAGE_URL = http://dev.onasys.co.kr:3000/auth/login
|
||||
NEXT_PUBLIC_CM_LOGIN_PAGE_URL = /auth/login
|
||||
|
||||
@@ -6,6 +6,39 @@
|
||||
현재 목표는 리팩토링 전용 작업이 아니라, 하수관로 운영관리 시스템 2차사업 고도화를 수행하는 것이다.
|
||||
즉, 기존 `sn` 화면과 흐름을 안정적으로 유지하면서 2차 요구사항에 해당하는 화면 개발, 기능 확장, 백엔드 연동 보완을 수행해야 한다.
|
||||
|
||||
## 사용자 작업 범위: UI/UX 및 퍼블리싱
|
||||
|
||||
현재 사용자와 진행하는 작업의 기본 범위는 UI/UX 및 프론트엔드 퍼블리싱으로 제한한다.
|
||||
사용자가 별도로 명시하지 않는 한 아래 기준을 모든 후속 작업에 적용한다.
|
||||
|
||||
### 허용 범위
|
||||
|
||||
- 화면 레이아웃과 시각적 위계 조정
|
||||
- JSX 마크업, Tailwind CSS, CSS 스타일 수정
|
||||
- 색상, 여백, 간격, 크기, 타이포그래피, 아이콘, 테두리, 배경 조정
|
||||
- 반응형 화면, 접근성, 포커스와 시각적 상태 개선
|
||||
- 표시 전용 컴포넌트 구성과 퍼블리싱 품질 개선
|
||||
- 기존 화면 내용과 동작을 유지하는 범위의 UI 구조 정리
|
||||
|
||||
### 기본 금지 범위
|
||||
|
||||
- 백엔드 코드, API 계약, 요청·응답 데이터 구조 변경
|
||||
- `.env*`, middleware, 프록시, 인증과 권한 설정 변경
|
||||
- 업무 로직, 상태 모델, 데이터 가공 및 저장 흐름 변경
|
||||
- 데이터베이스, 서버 프로세스와 배포 설정 변경
|
||||
- 패키지 설치·업데이트와 의존성 구조 변경
|
||||
- 사용자 요청 없이 화면의 문구, 데이터 항목, 기능 동작 변경
|
||||
|
||||
금지 범위의 변경이 필요해 보이면 직접 수정하지 않고 원인과 필요한 작업만 사용자에게 설명한다.
|
||||
|
||||
### UI 검증 안전 기준
|
||||
|
||||
- 사용자가 실행한 개발 서버가 있는지 실제 앱 포트까지 먼저 확인한다.
|
||||
- 개발 서버 실행 중에는 같은 앱의 `next build`를 실행하지 않는다.
|
||||
- UI 작업의 기본 검증은 실행 중인 개발 서버의 HMR, 브라우저 화면 확인, `git diff --check`로 제한한다.
|
||||
- 개발 서버 종료, `.next` 정리, 전체 빌드와 프로세스 재시작은 사용자가 명시적으로 승인한 경우에만 수행한다.
|
||||
- UI 변경 전후에 기존 문구, 데이터, 이벤트와 업무 동작이 유지되는지 확인한다.
|
||||
|
||||
---
|
||||
|
||||
## 공통 시작 기준
|
||||
|
||||
@@ -54,23 +54,18 @@ export default function AgedPipeLayerSettingPanel({
|
||||
};
|
||||
|
||||
const inputClass =
|
||||
"h-[20px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
|
||||
const numberInputClass = inputClass + " w-[40px]";
|
||||
"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 =
|
||||
"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" />
|
||||
);
|
||||
|
||||
"h-[26px] shrink-0 rounded border border-[#A9C5E5] bg-[#EDF4FC] px-2 py-0 text-[12px] font-medium leading-none text-[#2463A6]";
|
||||
const settingRow = (
|
||||
<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}
|
||||
<div className="flex min-h-[26px] flex-col items-stretch gap-1.5 whitespace-nowrap text-[1.15rem] font-medium text-label">
|
||||
<div className="flex min-w-0 items-center">
|
||||
<label htmlFor="aged-pipe-elapsed-years" className="shrink-0">
|
||||
노후관로 경과년도(년)
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<input
|
||||
id="aged-pipe-elapsed-years"
|
||||
className={numberInputClass}
|
||||
@@ -95,7 +90,7 @@ export default function AgedPipeLayerSettingPanel({
|
||||
title="적용"
|
||||
aria-label="경과년도 적용"
|
||||
>
|
||||
<span className="relative left-[1px]">✓</span>
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
||||
import {
|
||||
ChartColumnBig,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
PanelsTopLeft,
|
||||
PencilRuler,
|
||||
@@ -32,10 +31,10 @@ type MenuIconProps = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
const InftwtMenuIcon = ({ isActive }: MenuIconProps) => (
|
||||
const InftwtMenuIcon = () => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={`h-[15px] w-[15px] shrink-0 ${isActive ? "text-primary" : "text-label"}`}
|
||||
className="h-[15px] w-[15px] shrink-0 text-current"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
>
|
||||
@@ -167,31 +166,44 @@ export default function AnalysisPopupMenu({
|
||||
}, [initialActiveKey]);
|
||||
|
||||
return (
|
||||
<div className="absolute right-[15px] top-[8px] z-[45]">
|
||||
<div className="flex w-[154px] flex-col overflow-hidden rounded-[6px] border border-border bg-background shadow-[0_8px_18px_rgba(15,23,42,0.14)]">
|
||||
<div className="flex h-[36px] shrink-0 items-center justify-between gap-2 border-b border-border bg-secondary pl-[10px] pr-[4px] text-[13px] font-bold text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<PanelsTopLeft className="h-4 w-4 text-primary" />
|
||||
<span>분석도구</span>
|
||||
<div className="pointer-events-none absolute right-[68px] top-[16px] z-[45] flex flex-col items-end gap-[8px]">
|
||||
<button
|
||||
type="button"
|
||||
className={`pointer-events-auto flex h-[40px] w-[40px] shrink-0 items-center justify-center overflow-hidden rounded-[3px] p-0 shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)] ${
|
||||
isOpen
|
||||
? "bg-[#0279FA] text-white"
|
||||
: "bg-white text-[#666666] hover:bg-[#0279FA] hover:text-white"
|
||||
}`}
|
||||
onClick={() => setIsOpen((value) => !value)}
|
||||
title={isOpen ? "분석 메뉴 숨기기" : "분석도구"}
|
||||
aria-label={isOpen ? "분석 메뉴 숨기기" : "분석 메뉴 보이기"}
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="analysis-popup-menu"
|
||||
>
|
||||
<PanelsTopLeft className="h-[20px] w-[20px]" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{isOpen ? (
|
||||
<div
|
||||
id="analysis-popup-menu"
|
||||
className="pointer-events-auto flex w-max min-w-[154px] flex-col overflow-hidden rounded-[6px] bg-background shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]"
|
||||
>
|
||||
<div className="flex h-[36px] shrink-0 items-center justify-between gap-2 border-b border-border bg-white pl-[10px] pr-[4px] text-[13px] font-bold text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<PanelsTopLeft className="h-4 w-4 text-primary" />
|
||||
<span>분석도구</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-[4px] text-label hover:bg-accent hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#0279FA]"
|
||||
onClick={() => setIsOpen(false)}
|
||||
title="분석 메뉴 숨기기"
|
||||
aria-label="분석 메뉴 숨기기"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-[4px] text-label transition hover:bg-accent hover:text-primary"
|
||||
onClick={() => setIsOpen((value) => !value)}
|
||||
title={isOpen ? "분석 메뉴 숨기기" : "분석 메뉴 보이기"}
|
||||
aria-label={isOpen ? "분석 메뉴 숨기기" : "분석 메뉴 보이기"}
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{isOpen ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isOpen ? (
|
||||
<div className="flex flex-col gap-[3px] p-[6px]">
|
||||
{ANALYSIS_POPUP_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
@@ -208,7 +220,7 @@ export default function AnalysisPopupMenu({
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={`relative flex h-[33px] items-center gap-[7px] rounded-[5px] border px-[8px] pl-[10px] text-left text-[12px] font-semibold transition ${
|
||||
className={`relative flex h-[33px] items-center gap-[7px] rounded-[5px] border px-[8px] pl-[10px] text-left text-[12px] font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#0279FA] ${
|
||||
isActive
|
||||
? "border-primary bg-secondary font-bold text-primary before:absolute before:bottom-[7px] before:left-0 before:top-[7px] before:w-[3px] before:rounded-r-[2px] before:bg-primary"
|
||||
: "border-transparent bg-background text-foreground hover:border-border hover:bg-accent hover:text-primary"
|
||||
@@ -244,19 +256,17 @@ export default function AnalysisPopupMenu({
|
||||
>
|
||||
{item.renderIcon ? item.renderIcon({ isActive }) : null}
|
||||
{Icon ? (
|
||||
<Icon
|
||||
className={`h-[15px] w-[15px] shrink-0 ${
|
||||
isActive ? "text-primary" : "text-label"
|
||||
}`}
|
||||
/>
|
||||
<Icon className="h-[15px] w-[15px] shrink-0 text-current" />
|
||||
) : null}
|
||||
<span className="whitespace-nowrap">{label}</span>
|
||||
<span className="whitespace-nowrap" title={label}>
|
||||
{label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ExmnLayerSettingPanel({
|
||||
});
|
||||
};
|
||||
const selectClass =
|
||||
"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";
|
||||
"h-[26px] w-full rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground outline-none focus:border-primary";
|
||||
const labelClass = "shrink-0 text-[1.15rem] font-medium text-label";
|
||||
const title = jobType === "PPL" ? "관로조사" : "맨홀조사";
|
||||
|
||||
@@ -41,8 +41,8 @@ export default function ExmnLayerSettingPanel({
|
||||
onEnabledChange={onEnabledChange}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="space-y-[1px]">
|
||||
<label className="flex min-h-[24px] items-center justify-between ">
|
||||
<div className="space-y-[6px]">
|
||||
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
|
||||
<span className={`flex items-center gap-1 ${labelClass}`}>
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full ${jobType === "PPL" ? "bg-[#facc15]" : "bg-[#8b5cf6]"}`}
|
||||
@@ -59,7 +59,7 @@ export default function ExmnLayerSettingPanel({
|
||||
onChange={(event) => updateFilter("bizCycl", event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex min-h-[24px] items-center justify-between ">
|
||||
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
|
||||
<span className={labelClass}>구조적 상태등급</span>
|
||||
<select
|
||||
className={selectClass}
|
||||
@@ -75,7 +75,7 @@ export default function ExmnLayerSettingPanel({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="flex min-h-[24px] items-center justify-between ">
|
||||
<label className="flex min-h-[24px] flex-col items-stretch gap-1">
|
||||
<span className={labelClass}>운영적 상태등급</span>
|
||||
<select
|
||||
className={selectClass}
|
||||
|
||||
+13
-37
@@ -6,6 +6,7 @@ import {
|
||||
normalizeFloodHistoryYearRange,
|
||||
type FloodHistoryYearRange,
|
||||
} from "./fldngHstryTypes";
|
||||
import LayerSettingCard from "../layerSetting/LayerSettingCard";
|
||||
|
||||
type Props = {
|
||||
yearRange: FloodHistoryYearRange;
|
||||
@@ -36,7 +37,7 @@ export default function FldngHstryLayerSettingPanel({
|
||||
.filter((year) => Number(year) >= Number(normalizedRange.startYear))
|
||||
.map((year) => ({ val: year, text: year }));
|
||||
const yearSelectClass =
|
||||
"h-[20px] w-[62px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
|
||||
"!h-[26px] w-full !rounded border border-input bg-background px-2 py-0 text-center text-[12px] leading-none text-foreground";
|
||||
|
||||
const changeYear = (key: keyof FloodHistoryYearRange, value: string) => {
|
||||
onYearRangeChange(
|
||||
@@ -48,41 +49,16 @@ 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-[28px] items-center justify-between border-b border-border px-2">
|
||||
<span className="text-[1.15rem] font-semibold text-label">
|
||||
침수흔적도
|
||||
</span>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
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-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="침수년도 설정 닫기"
|
||||
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>
|
||||
<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" />
|
||||
<LayerSettingCard
|
||||
title="침수흔적도"
|
||||
enabled={enabled}
|
||||
onEnabledChange={onEnabledChange}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className="flex flex-col gap-1.5 font-medium text-label">
|
||||
<span className="shrink-0">침수년도</span>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1.5">
|
||||
<div className="w-[62px]">
|
||||
<div className="flex w-full items-center gap-1.5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass={yearSelectClass}
|
||||
@@ -97,7 +73,7 @@ export default function FldngHstryLayerSettingPanel({
|
||||
<span className="px-[1px] text-[0.95rem] font-normal text-muted-foreground">
|
||||
~
|
||||
</span>
|
||||
<div className="w-[62px]">
|
||||
<div className="min-w-0 flex-1">
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass={yearSelectClass}
|
||||
@@ -111,6 +87,6 @@ export default function FldngHstryLayerSettingPanel({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayerSettingCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function GisEditToolbar({
|
||||
data-testid="gis-edit-toolbar"
|
||||
className="absolute bottom-4 left-1/2 z-[60] flex w-[min(980px,calc(100%-2rem))] -translate-x-1/2 flex-col"
|
||||
>
|
||||
<div className="card card-border !m-0 flex !bg-background !p-[0.8rem] shadow-[0_8px_24px_rgba(23,42,70,0.18)]">
|
||||
<div className="card !m-0 flex !border-0 !bg-background !p-[0.8rem] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]">
|
||||
<div className="flex w-full items-center gap-3 overflow-hidden">
|
||||
<div className="flex w-[168px] shrink-0 flex-col justify-center border-r border-border pr-3">
|
||||
<span className="whitespace-nowrap text-[1.3rem] font-semibold leading-5 text-label">
|
||||
|
||||
@@ -22,8 +22,16 @@ export default function LayerSettingCard({
|
||||
}: 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 min-h-[28px] items-center justify-between border-b border-border pl-[0.8rem] pr-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-[7px] w-[7px] shrink-0 rotate-45 bg-[#7CCE5C]"
|
||||
/>
|
||||
<span className="truncate text-[12px] font-semibold text-label">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{showToggle ? (
|
||||
<button
|
||||
@@ -52,7 +60,9 @@ export default function LayerSettingCard({
|
||||
</div>
|
||||
</div>
|
||||
{children ? (
|
||||
<div className="px-2 py-1 text-[1.15rem] text-label">{children}</div>
|
||||
<div className="flex flex-col gap-[6px] p-[0.8rem] text-[1.15rem] text-label">
|
||||
{children}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function SinkholeLayerSettingPanel({
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const yearSelectClass =
|
||||
"h-[20px] !w-[58px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
|
||||
"!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),
|
||||
@@ -124,24 +124,19 @@ export default function SinkholeLayerSettingPanel({
|
||||
.map((year) => ({ val: year, text: year }));
|
||||
|
||||
const inputClass =
|
||||
"h-[20px] !rounded border border-input bg-background px-[1px] text-center text-[1.1rem] leading-none text-foreground";
|
||||
const numberInputClass = inputClass + " w-[40px]";
|
||||
"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 =
|
||||
"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" />
|
||||
);
|
||||
|
||||
"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] items-center gap-1.5 whitespace-nowrap">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
{indicator}
|
||||
<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="ml-auto flex shrink-0 items-center gap-1">
|
||||
<div className="flex w-full items-center gap-1">
|
||||
<input
|
||||
id="sinkhole-radius"
|
||||
className={numberInputClass}
|
||||
@@ -165,19 +160,18 @@ export default function SinkholeLayerSettingPanel({
|
||||
title="적용"
|
||||
aria-label="반경 적용"
|
||||
>
|
||||
<span className="relative left-[1px]">✓</span>
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex min-h-[26px] items-center gap-1.5 whitespace-nowrap">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
{indicator}
|
||||
<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="ml-auto flex shrink-0 items-center gap-1.5">
|
||||
<div id="sinkhole-start-year" className="w-[62px]">
|
||||
<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}
|
||||
@@ -194,7 +188,7 @@ export default function SinkholeLayerSettingPanel({
|
||||
<span className="px-[1px] text-[0.95rem] font-normal text-muted-foreground">
|
||||
~
|
||||
</span>
|
||||
<div id="sinkhole-end-year" className="w-[62px]">
|
||||
<div id="sinkhole-end-year" className="min-w-0 flex-1">
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass={yearSelectClass}
|
||||
@@ -215,7 +209,7 @@ export default function SinkholeLayerSettingPanel({
|
||||
title="적용"
|
||||
aria-label="연도범위 적용"
|
||||
>
|
||||
<span className="relative left-[1px]">✓</span>
|
||||
확인
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { ListTree } from "lucide-react";
|
||||
import {
|
||||
Fragment,
|
||||
useCallback,
|
||||
@@ -6764,17 +6765,17 @@ const Map = () => {
|
||||
showToggle={false}
|
||||
>
|
||||
{isWaterDepthLegendEnabled ? (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-[6px]">
|
||||
{waterDepthLegendItems.map((item) => (
|
||||
<div key={item.key} className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block h-4 w-10 shrink-0"
|
||||
className="inline-block h-6 w-10 shrink-0"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
border: `1px solid ${item.borderColor}`,
|
||||
}}
|
||||
/>
|
||||
<span className="text-[1rem] font-medium text-label">
|
||||
<span className="text-[11px] font-medium text-label">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
@@ -6852,16 +6853,19 @@ const Map = () => {
|
||||
if (activeLayerSettingItems.length === 0) return null;
|
||||
|
||||
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">
|
||||
<span className="text-[1.2rem] font-semibold text-label">
|
||||
<div className="flex h-[34vh] max-h-[32rem] min-h-[24rem] flex-col overflow-hidden rounded-[6px] border border-border bg-background shadow-[0_4px_14px_rgba(15,23,42,0.08)]">
|
||||
<div className="flex h-[38px] shrink-0 items-center gap-2 border-b border-border px-3">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-[#EDF8EA] text-[#65B84A]">
|
||||
<ListTree size={14} strokeWidth={2} />
|
||||
</span>
|
||||
<span className="text-[1.4rem] font-bold text-foreground">
|
||||
선택 레이어 설정
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
ref={restoreActiveLayerSettingScroll}
|
||||
onScroll={handleActiveLayerSettingScroll}
|
||||
className="min-h-0 flex-1 overflow-y-scroll px-1 py-2 [scrollbar-gutter:stable] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]"
|
||||
className="min-h-0 flex-1 overflow-y-scroll px-2 py-2 [scrollbar-gutter:stable] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{activeLayerSettingItems.map((item) => (
|
||||
@@ -9379,7 +9383,7 @@ const Map = () => {
|
||||
>
|
||||
<GisEditSessionPanel
|
||||
{...gisEditSessionSnapshot}
|
||||
className="max-h-[calc(50dvh-82px)]"
|
||||
className="max-h-[calc(50dvh-82px)] !border-0 !shadow-[0_1px_2px_rgba(60,64,67,0.3),0_1px_3px_1px_rgba(60,64,67,0.15)]"
|
||||
variant="inline"
|
||||
onOpenPopup={handleOpenGisEditSessionPopup}
|
||||
onRemoveFocusedChange={gisEdit.removeFocusedChange}
|
||||
@@ -9423,10 +9427,13 @@ const Map = () => {
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={handleCheckedLayersChange}
|
||||
onOpenPopup={handleOpenLayerPopup}
|
||||
searchBarContainerClassName="!top-[16px] ml-[12px]"
|
||||
alwaysVisibleMapTabs={true}
|
||||
searchBar={
|
||||
<ProcSearchBar
|
||||
gisMaps={gisMaps}
|
||||
facilityTypes={facilityTypes}
|
||||
integrated
|
||||
sidoSearchZoom={
|
||||
isAgedPipeChecked ? AGED_PIPE_GUIDE_MIN_ZOOM : undefined
|
||||
}
|
||||
@@ -9435,15 +9442,17 @@ const Map = () => {
|
||||
}
|
||||
waterDepth={undefined}
|
||||
layerSettingView={ActiveLayerSettingView}
|
||||
treeSectionClassName="rounded-sm border border-border bg-background px-1 py-2 text-foreground"
|
||||
treeSectionClassName="px-1 py-2 text-foreground"
|
||||
detachedTreeToggleButton={true}
|
||||
showLayerPanelTitle={false}
|
||||
sectionedTreePanel={true}
|
||||
isPlaying={isPlaying}
|
||||
setIsPlaying={setIsPlaying as any}
|
||||
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
|
||||
checkInterval={2500}
|
||||
checkedLayerChangeDelay={0}
|
||||
showZoomLevel={true}
|
||||
toolBarIconSize={20}
|
||||
showZoomScaleInfo={true}
|
||||
showFloatingSearchButton={false}
|
||||
treeTogglePosition="center"
|
||||
@@ -9497,9 +9506,19 @@ const Map = () => {
|
||||
)}
|
||||
<style jsx global>{`
|
||||
.sn-map-control-layout .mapbtn {
|
||||
top: auto;
|
||||
right: 15px;
|
||||
bottom: 439px;
|
||||
top: 16px;
|
||||
right: 120px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .map-type-tabs,
|
||||
.sn-map-control-layout .tool-bar ul,
|
||||
.sn-map-control-layout .zoom-level-panel,
|
||||
.sn-map-control-layout .zoom-level-summary {
|
||||
border: 0;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(60, 64, 67, 0.3),
|
||||
0 1px 3px 1px rgba(60, 64, 67, 0.15);
|
||||
}
|
||||
|
||||
.sn-map-control-layout .custom-toolbar {
|
||||
@@ -9507,19 +9526,32 @@ const Map = () => {
|
||||
}
|
||||
|
||||
.sn-map-control-layout .tool-bar {
|
||||
top: auto;
|
||||
right: 15px;
|
||||
bottom: 230px;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
bottom: auto;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .tool-bar-vector-icons ul li div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: none !important;
|
||||
color: #666;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .tool-bar-vector-icons ul li div:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .zoom-level-control {
|
||||
right: 15px;
|
||||
bottom: 36px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .zoom-level-summary {
|
||||
right: 56px;
|
||||
bottom: -15px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.sn-map-control-layout .ol-scale-line {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# FE-MAP 레이어 패널 헤더 구현 기록
|
||||
|
||||
## 작업 목적
|
||||
|
||||
- 사용자 제공 기준 이미지에 맞춰 지도 좌측 패널을 `레이어 관리`와 `선택 레이어 설정` 두 영역으로 구분한다.
|
||||
- 레이어 트리, 체크 상태, 선택 레이어별 설정 내용과 동작은 변경하지 않는다.
|
||||
|
||||
## 적용 기준
|
||||
|
||||
- 지도형 화면은 기존 `Gis` 조합을 유지한다.
|
||||
- 사용자 지정 스타일은 패널 외곽 여백 `4px`, 영역 사이 간격 `6px`, 패널 배경 `#E5E5E5`이다.
|
||||
- 두 영역은 흰색 카드와 제목 헤더로 구분한다.
|
||||
|
||||
## 수정 대상
|
||||
|
||||
- `apps/sn/app/(map-layout)/map/page.tsx`
|
||||
- `packages/common/business_components/src/ui/map/Gis.js`
|
||||
- `packages/common/business_components/src/ui/map/GisTree.js`
|
||||
|
||||
## 변경 내용
|
||||
|
||||
- 공통 GIS 패널에 기본값이 비활성인 섹션형 패널 옵션을 추가했다.
|
||||
- `sn` 관망도에서만 옵션을 활성화해 다른 앱의 기존 GIS 패널에는 영향을 주지 않는다.
|
||||
- `레이어 관리`와 `선택 레이어 설정`에 구분 아이콘과 고정 높이 헤더를 적용했다.
|
||||
- 두 카드 모두 헤더와 스크롤 본문을 형제 영역으로 분리해 헤더 아래에서만 콘텐츠가 스크롤되도록 통일했다.
|
||||
- 두 스크롤바는 카드 본문 오른쪽 끝에 배치하고 동일한 `4px` 두께와 안정적인 스크롤 여백을 적용했다.
|
||||
- 레이어 트리의 목록 액션은 아이콘 전용 표시를 해제하고 기존 `목록보기` 텍스트 버튼으로 복원했다.
|
||||
- 선택 레이어 설정 카드 제목 왼쪽에는 `#7CCE5C` 다이아몬드 표식을 공통 적용했다.
|
||||
- 설정 카드의 필드 제목과 입력 컨트롤은 위아래 배치로 정리했다.
|
||||
- 카드 제목의 다이아몬드는 `7px × 7px`, 카드 제목은 `13px`, 입력 텍스트는 `12px`로 조정했다.
|
||||
- 적용용 원형 체크 버튼은 입력과 동일한 `20px` 높이의 `확인` 텍스트 버튼으로 변경하고, 입력과 함께 카드 본문 너비를 채우도록 배치했다.
|
||||
- `sn` 레이어 관리 행의 마우스 호버 배경은 `#E8F0FD`로 적용했다.
|
||||
- `확인` 버튼은 배경 `#EDF4FC`, 글자 `#2463A6`, `1px` `#A9C5E5` 테두리로 통일했다.
|
||||
- 설정 카드 제목은 `12px`, 본문은 `8px` 패딩과 `6px` 간격으로 조정했다.
|
||||
- 침수심 범례 항목 텍스트는 `11px`로 조정했다.
|
||||
- 관망도 전용 패널 접기·열기 버튼은 너비를 `20px`로 유지하면서 아이콘을 `32px`로 확대하고 아이콘 색상을 `#666666`으로 적용했다.
|
||||
- 패널에 맞닿는 버튼의 좌측 위·아래 모서리는 각지게 하고 우측 모서리만 `4px` 둥글게 처리했다.
|
||||
|
||||
## 지도 검색바 퍼블리싱
|
||||
|
||||
- 실제 관망도에서만 활성화되는 `integrated` 옵션으로 기존 검색 상태와 조회 동작을 유지했다.
|
||||
- 검색바는 `656px × 44px`, 셀렉트 3개는 각각 `192px`, 조회 버튼은 `80px`로 구성했다.
|
||||
- 셀렉트 사이 간격과 래퍼 패딩을 제거하고 첫 셀렉트 왼쪽과 조회 버튼 오른쪽에만 `8px` 모서리를 적용했다.
|
||||
- 첫 번째와 두 번째 셀렉트 오른쪽 내부에 `24px` 높이의 `#D5DCE5` 구분선을 배치했다.
|
||||
- 셀렉트 글자·화살표·말줄임과 조회 버튼 타이포그래피를 요청 사양에 맞추고 Radix Portal 드롭다운 구조는 유지했다.
|
||||
- 선택값은 기본 `line-clamp` 표시 영향을 제거하고 `20px` 행간의 한 줄 말줄임으로 고정해 글자 세로 잘림을 방지했다.
|
||||
- 셀렉트 포커스·열림 라인은 `#2094FA`로 적용하고, 포커스된 영역 옆 구분선은 숨겨 라인이 겹치지 않도록 했다.
|
||||
- 전체 래퍼의 `8px` 모서리로 셀렉트와 조회 버튼을 함께 클리핑해 우측 라운드를 정확히 맞추고 조회 버튼 배경을 `#2094FA`로 적용했다.
|
||||
- 셀렉트 포커스·열림 라인은 `2px`로 조정하고, 첫 번째 시도 셀렉트는 왼쪽 위·아래에 검색바와 동일한 `8px` 모서리를 적용했다.
|
||||
- 세 셀렉트의 아래 방향 화살표는 모두 `32px × 32px`로 확대했다.
|
||||
- GIS 편집 세션, 검색바, 분석도구의 상단 기준을 `8px`로 통일했다.
|
||||
- 검색바는 편집 세션 노출 여부와 관계없이 오른쪽 `12px` 위치에 고정해 GIS 편집을 켜고 끌 때 움직이지 않도록 했다.
|
||||
- 분리형 레이어 패널 토글은 상태별 버튼을 교체하지 않고 하나의 버튼을 유지하며, 패널과 같은 `300ms` 동안 `left` 위치를 이동해 패널 가장자리에 붙은 채 함께 열리고 닫히도록 했다.
|
||||
- 관망도 섹션형 패널은 열고 닫을 때 투명도 변화를 제거하고 불투명 상태에서 좌우 이동만 적용했다.
|
||||
- 패널과 토글 버튼 모두 동일한 `transform`, `300ms`, `ease-in-out` 방식으로 정확히 `250px` 이동시켜 애니메이션 중 버튼이 패널에서 떨어지지 않도록 맞췄다.
|
||||
- 관망도 베이스맵 선택은 숨겨진 `sprite_gis viewer` 메뉴 대신 `일반지도`와 `위성지도`를 처음부터 보여주는 탭으로 변경하고 기존 레이어 전환 함수는 유지했다.
|
||||
- 분석도구는 기존 지도 버튼과 같은 외형의 아이콘 버튼으로 축소하고, 클릭 시 버튼 아래 `8px` 간격으로 우측 정렬된 기존 메뉴가 열리도록 변경했다.
|
||||
- 지도 위젯은 지도 가장자리에서 `16px` 떨어뜨리고, 우측 상단에 도구 모음 → 분석도구 → 지도 선택 탭 순서로 `12px` 간격을 두어 상단 정렬했다.
|
||||
- 우측 상단 지도 선택·분석도구·도구 모음 버튼의 높이는 모두 `40px`로 통일했다.
|
||||
- 조회 버튼과 지도 유형 활성 탭의 배경은 `#0279FA`로 통일하고, 지도 유형 탭 버튼은 외곽 안쪽에서 `4px` 떨어지도록 배치했다.
|
||||
- 지도 위 검색바, 지도 선택, 분석도구, 도구 모음, 줌·축척 및 편집 위젯의 외곽 보더를 제거하고 Google Maps 계열의 2중 그림자로 통일했다.
|
||||
- 분석도구 실행 버튼과 우측 확대·축소·거리측정·면적측정·리셋 아이콘은 버튼 크기를 유지하면서 `20px × 20px` 벡터 아이콘으로 통일했다.
|
||||
- 분석도구 메뉴가 열린 동안 아이콘 버튼은 `#0279FA` 배경과 흰색 아이콘으로 표시한다.
|
||||
- 분석도구 메뉴는 화면 진입 시 기본으로 펼치고, 상위 배치 영역은 클릭을 통과시켜 메뉴가 열린 상태에서도 일반지도·위성지도 탭을 조작할 수 있게 했다.
|
||||
- 선택 레이어 설정의 드롭다운·입력·확인 버튼 높이는 `26px`로 통일하고 수직 패딩을 제거해 가운데 정렬했다.
|
||||
- 지반침하정보·노후관로 등 공통 설정 카드 제목의 왼쪽 패딩과 본문 전체 패딩은 `0.8rem`으로 조정했다.
|
||||
- 분석도구 하위 메뉴는 첨부 기준처럼 `15px` 아이콘과 `12px` 텍스트가 한 줄에 놓이는 세로 목록으로 복원하고, 긴 항목에 맞춰 너비가 자동 확장되도록 했다.
|
||||
- 분석도구 제목은 흰색 콘텐츠 영역 안에 배치하고 하단 구분선과 접기 버튼을 유지했다.
|
||||
- 분석도구 실행 버튼과 하위 메뉴에는 키보드 탐색 시에만 나타나는 `2px` `#0279FA` `focus-visible` 링을 적용했다.
|
||||
- 검색바는 복잡한 지도 배경에서도 경계가 분명하도록 `0 2px 4px rgba(60, 64, 67, 0.3), 0 4px 10px rgba(60, 64, 67, 0.18)` 그림자로 강화했다.
|
||||
- 줌 게이지와 줌레벨·축척 요약 박스는 지도 하단 `16px` 기준으로 아래맞춤 정렬했다.
|
||||
- 기존 트리 행과 선택 레이어 설정 카드 본문은 유지했다.
|
||||
|
||||
## 검증 기준
|
||||
|
||||
- 두 카드의 외곽 여백이 상하좌우 `4px`인지 확인한다.
|
||||
- 두 카드 사이 간격이 `6px`인지 확인한다.
|
||||
- 패널 바깥 배경이 `#E5E5E5`인지 확인한다.
|
||||
- 레이어 체크, 펼침/접힘, 설정 카드 입력과 스크롤이 기존과 동일하게 동작하는지 확인한다.
|
||||
@@ -57,6 +57,10 @@ function Gis({
|
||||
// 외부 업무 흐름에서 특정 시설물 레이어를 기존 체크 상태에 추가하도록 요청한다.
|
||||
checkedLayerRequest = null,
|
||||
searchBar: SearchBar,
|
||||
// 화면별 상단 정렬과 인접 패널 회피를 위한 검색바 위치 전용 클래스다.
|
||||
searchBarContainerClassName = "",
|
||||
// sn 관망도처럼 베이스맵 선택지를 항상 노출하는 화면에서만 탭 형태를 사용한다.
|
||||
alwaysVisibleMapTabs = false,
|
||||
waterDepth: WaterDepth = null,
|
||||
layerSettingView: LayerSettingView = null,
|
||||
inlineLayerSettingView,
|
||||
@@ -65,6 +69,8 @@ function Gis({
|
||||
layerLegendIconVisible,
|
||||
// 목록·액션 아이콘이 지도 레이어 체크를 선행 조건으로 요구하는지 여부다.
|
||||
layerIconRequiresChecked = true,
|
||||
// 목록 액션의 기능은 유지하면서 텍스트를 목록 아이콘으로 표시할지 여부다.
|
||||
layerActionIconOnly = false,
|
||||
isPlaying = false,
|
||||
setIsPlaying = () => { },
|
||||
targetLayerTypes = [],
|
||||
@@ -79,11 +85,14 @@ function Gis({
|
||||
viewExtent,
|
||||
viewMinZoom,
|
||||
showZoomLevel = false,
|
||||
toolBarIconSize,
|
||||
zoomGuide,
|
||||
showFloatingSearchButton = true,
|
||||
treeTogglePosition = "center",
|
||||
detachedTreeToggleButton = false,
|
||||
showLayerPanelTitle = true,
|
||||
// sn 관망도처럼 트리와 선택 설정을 카드 두 개로 구분해야 하는 화면에서만 활성화한다.
|
||||
sectionedTreePanel = false,
|
||||
showZoomScaleInfo = false,
|
||||
}) {
|
||||
const mapRef = useRef();
|
||||
@@ -322,6 +331,7 @@ function Gis({
|
||||
layerIconVisible={layerIconVisible}
|
||||
layerLegendIconVisible={layerLegendIconVisible}
|
||||
layerIconRequiresChecked={layerIconRequiresChecked}
|
||||
layerActionIconOnly={layerActionIconOnly}
|
||||
sinkholeLegendIconVisible={sinkholeLegendIconVisible}
|
||||
isPlaying={isPlaying}
|
||||
setIsPlaying={setIsPlaying}
|
||||
@@ -334,6 +344,7 @@ function Gis({
|
||||
toggleButtonPosition={treeTogglePosition}
|
||||
detachedToggleButton={detachedTreeToggleButton}
|
||||
showLayerPanelTitle={showLayerPanelTitle}
|
||||
sectionedPanel={sectionedTreePanel}
|
||||
/>
|
||||
|
||||
{typeof WaterDepth === "function" && (
|
||||
@@ -363,15 +374,15 @@ function Gis({
|
||||
)}
|
||||
{/* 시도/시군구 조회 영역 */}
|
||||
{SearchBar && (
|
||||
<div className="absolute left-1/2 top-4 z-30 w-max -translate-x-1/2 rounded">
|
||||
<div className={`absolute left-1/2 top-4 z-30 w-max -translate-x-1/2 rounded ${searchBarContainerClassName}`}>
|
||||
{SearchBar}
|
||||
</div>
|
||||
)}
|
||||
<GisToolBar />
|
||||
<GisToolBar iconSize={toolBarIconSize} />
|
||||
{showZoomLevel && (
|
||||
<GisZoomLevel zoomGuide={zoomGuide} showZoomScaleInfo={showZoomScaleInfo} />
|
||||
)}
|
||||
<GisMapToggle />
|
||||
<GisMapToggle alwaysVisibleTabs={alwaysVisibleMapTabs} />
|
||||
<GisSelect
|
||||
onFeatureSelect={handleFeatureSelect}
|
||||
onMapBlankClick={onMapBlankClick}
|
||||
|
||||
@@ -1,30 +1,67 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { GisContext } from './Gis';
|
||||
import React, { useContext, useState } from "react";
|
||||
import { GisContext } from "./Gis";
|
||||
|
||||
export default React.memo(function MapToggleBtn() {
|
||||
const { gisCore } = useContext(GisContext);
|
||||
const [activeMap, setActiveMap] = useState({ label: '일반', type: 'base' }); // 기본 선택값
|
||||
export default React.memo(function MapToggleBtn({ alwaysVisibleTabs = false }) {
|
||||
const { gisCore } = useContext(GisContext);
|
||||
const [activeMap, setActiveMap] = useState({ label: "일반", type: "base" }); // 기본 선택값
|
||||
|
||||
const handleSelect = mapObj => {
|
||||
setActiveMap(mapObj);
|
||||
gisCore.changeBaseLayer(mapObj.type);
|
||||
};
|
||||
const handleSelect = (mapObj) => {
|
||||
setActiveMap(mapObj);
|
||||
gisCore.changeBaseLayer(mapObj.type);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mapbtn">
|
||||
<button className="sprite_gis viewer" title="지도선택">지도 선택</button>
|
||||
<ul className="choice_map">
|
||||
<li className={activeMap.type === 'base' ? 'on' : ''}>
|
||||
<button className="map1" id="base" title="일반지도" onClick={() => handleSelect({ label: '일반', type: 'base' })}>
|
||||
<div>일반지도</div>
|
||||
</button>
|
||||
</li>
|
||||
<li className={activeMap.type === 'satellite' ? 'on' : ''}>
|
||||
<button className="map2" id="satelite" title="위성지도" onClick={() => handleSelect({ label: '위성', type: 'satellite' })}>
|
||||
<div>위성지도</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
if (alwaysVisibleTabs) {
|
||||
return (
|
||||
<div className="mapbtn mapbtn-tabs">
|
||||
<div className="map-type-tabs" role="group" aria-label="지도 유형 선택">
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={activeMap.type === "base"}
|
||||
className={activeMap.type === "base" ? "on" : ""}
|
||||
onClick={() => handleSelect({ label: "일반", type: "base" })}
|
||||
>
|
||||
일반지도
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={activeMap.type === "satellite"}
|
||||
className={activeMap.type === "satellite" ? "on" : ""}
|
||||
onClick={() => handleSelect({ label: "위성", type: "satellite" })}
|
||||
>
|
||||
위성지도
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mapbtn">
|
||||
<button className="sprite_gis viewer" title="지도선택">
|
||||
지도 선택
|
||||
</button>
|
||||
<ul className="choice_map">
|
||||
<li className={activeMap.type === "base" ? "on" : ""}>
|
||||
<button
|
||||
className="map1"
|
||||
id="base"
|
||||
title="일반지도"
|
||||
onClick={() => handleSelect({ label: "일반", type: "base" })}
|
||||
>
|
||||
<div>일반지도</div>
|
||||
</button>
|
||||
</li>
|
||||
<li className={activeMap.type === "satellite" ? "on" : ""}>
|
||||
<button
|
||||
className="map2"
|
||||
id="satelite"
|
||||
title="위성지도"
|
||||
onClick={() => handleSelect({ label: "위성", type: "satellite" })}
|
||||
>
|
||||
<div>위성지도</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,71 +1,102 @@
|
||||
import React from 'react';
|
||||
import GisApp from './lib/gis.core';
|
||||
import GisOlMeasure from './lib/gis.measure';
|
||||
import { useContext } from 'react';
|
||||
import { useGisContext } from './Gis';
|
||||
export default React.memo(function GisToolBar() {
|
||||
const { gisMap } = useGisContext();
|
||||
if (!gisMap) return <></>;
|
||||
const gisMeasuer = new GisOlMeasure(gisMap);
|
||||
GisApp.Module.measure = gisMeasuer;
|
||||
const zoomIn = () => {
|
||||
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
|
||||
gisMap.getView().animate({
|
||||
zoom: gisMap.getView().getZoom() + 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
};
|
||||
const zoomOut = () => {
|
||||
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
|
||||
gisMap.getView().animate({
|
||||
zoom: gisMap.getView().getZoom() - 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
};
|
||||
import React from "react";
|
||||
import { RotateCcw, Ruler, Scan, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import GisApp from "./lib/gis.core";
|
||||
import GisOlMeasure from "./lib/gis.measure";
|
||||
import { useContext } from "react";
|
||||
import { useGisContext } from "./Gis";
|
||||
export default React.memo(function GisToolBar({ iconSize }) {
|
||||
const { gisMap } = useGisContext();
|
||||
const useVectorIcons = Number.isFinite(iconSize) && iconSize > 0;
|
||||
if (!gisMap) return <></>;
|
||||
const gisMeasuer = new GisOlMeasure(gisMap);
|
||||
GisApp.Module.measure = gisMeasuer;
|
||||
const zoomIn = () => {
|
||||
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
|
||||
gisMap.getView().animate({
|
||||
zoom: gisMap.getView().getZoom() + 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
};
|
||||
const zoomOut = () => {
|
||||
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
|
||||
gisMap.getView().animate({
|
||||
zoom: gisMap.getView().getZoom() - 1,
|
||||
duration: 250,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const measure = type => {
|
||||
gisMeasuer.addInteraction(type);
|
||||
};
|
||||
const measure = (type) => {
|
||||
gisMeasuer.addInteraction(type);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
gisMeasuer.removeFeature();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="custom-toolbar">
|
||||
<div className="tool-bar">
|
||||
<h3>toolbar 영역</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div className="btn_control_zoomin" title="확대" onClick={zoomIn}>
|
||||
<span>확대</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_control_zoomout" title="축소" onClick={zoomOut}>
|
||||
<span>축소</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_control_ruler btn_control_measure" title="거리 측정" onClick={() => measure(GisApp.measure.LineString)}>
|
||||
<span>거리측정</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="btn_control_area btn_control_measure" title="면적 측정" onClick={() => measure(GisApp.measure.Polygon)}>
|
||||
<span>면적측정</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="sprite_gis icon4 btn_select_cancel" title="리셋" onClick={() => reset()}>
|
||||
<span>리셋</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
const reset = () => {
|
||||
gisMeasuer.removeFeature();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="custom-toolbar">
|
||||
<div
|
||||
className={`tool-bar ${useVectorIcons ? "tool-bar-vector-icons" : ""}`}
|
||||
>
|
||||
<h3>toolbar 영역</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<div className="btn_control_zoomin" title="확대" onClick={zoomIn}>
|
||||
{useVectorIcons && (
|
||||
<ZoomIn size={iconSize} aria-hidden="true" />
|
||||
)}
|
||||
<span>확대</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
className="btn_control_zoomout"
|
||||
title="축소"
|
||||
onClick={zoomOut}
|
||||
>
|
||||
{useVectorIcons && (
|
||||
<ZoomOut size={iconSize} aria-hidden="true" />
|
||||
)}
|
||||
<span>축소</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
className="btn_control_ruler btn_control_measure"
|
||||
title="거리 측정"
|
||||
onClick={() => measure(GisApp.measure.LineString)}
|
||||
>
|
||||
{useVectorIcons && <Ruler size={iconSize} aria-hidden="true" />}
|
||||
<span>거리측정</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
className="btn_control_area btn_control_measure"
|
||||
title="면적 측정"
|
||||
onClick={() => measure(GisApp.measure.Polygon)}
|
||||
>
|
||||
{useVectorIcons && <Scan size={iconSize} aria-hidden="true" />}
|
||||
<span>면적측정</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div
|
||||
className="sprite_gis icon4 btn_select_cancel"
|
||||
title="리셋"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
{useVectorIcons && (
|
||||
<RotateCcw size={iconSize} aria-hidden="true" />
|
||||
)}
|
||||
<span>리셋</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getBackendOptions,
|
||||
DndProvider,
|
||||
} from "@minoru/react-dnd-treeview";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, Layers } from "lucide-react";
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef, useCallback } from "react";
|
||||
|
||||
import { showBusinessUserMessage } from "../../lib/user-message";
|
||||
@@ -33,7 +33,9 @@ import GisApp from "./lib/gis.core";
|
||||
* - layerIconVisible: (선택) 레이어 행 우측 액션 아이콘 노출 여부를 결정하는 함수
|
||||
* - layerLegendIconVisible: (선택) 레이어 행 범례 아이콘 노출 여부를 결정하는 함수
|
||||
* - layerIconRequiresChecked: (선택) 액션 실행 전에 레이어 체크를 요구할지 여부
|
||||
* - layerActionIconOnly: (선택) 목록 액션을 텍스트 대신 목록 아이콘으로 표시할지 여부
|
||||
* - detachedToggleButton: (선택) 접기/펼치기 버튼을 패널 밖 고정 핸들로 노출할지 여부
|
||||
* - sectionedPanel: (선택) 트리와 선택 레이어 설정을 회색 배경 위의 독립 카드로 표시할지 여부
|
||||
*/
|
||||
export default React.memo(function GisTree({
|
||||
checkedNodes = [],
|
||||
@@ -53,12 +55,14 @@ export default React.memo(function GisTree({
|
||||
layerIconVisible,
|
||||
layerLegendIconVisible = () => true,
|
||||
layerIconRequiresChecked = true,
|
||||
layerActionIconOnly = false,
|
||||
sinkholeLegendIconVisible = false,
|
||||
inlineLayerSettingView,
|
||||
treeSectionClassName = "",
|
||||
toggleButtonPosition = "center",
|
||||
showLayerPanelTitle = true,
|
||||
detachedToggleButton = false,
|
||||
sectionedPanel = false,
|
||||
}) {
|
||||
const { gisCore, treeData } = useGisContext();
|
||||
|
||||
@@ -584,38 +588,57 @@ export default React.memo(function GisTree({
|
||||
// 렌더링
|
||||
// =====================
|
||||
|
||||
if (!treeData || treeData.length === 0) return null;
|
||||
|
||||
const toggleButtonPositionClass =
|
||||
toggleButtonPosition === "top" ? "top-4" : "top-1/2 -translate-y-1/2";
|
||||
const thinScrollbarClass =
|
||||
"[scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]";
|
||||
const treeSectionClassNames = [treeSectionClassName, thinScrollbarClass].filter(Boolean).join(" ");
|
||||
"[scrollbar-gutter:stable] [scrollbar-width:thin] [&::-webkit-scrollbar]:w-[4px]";
|
||||
const treeSectionClassNames = [treeSectionClassName].filter(Boolean).join(" ");
|
||||
// 섹션형 패널은 지도 트리와 선택 레이어 설정을 각각 독립된 카드로 구분한다.
|
||||
const panelShellClassName = sectionedPanel
|
||||
? "gap-[6px] border-r-0 bg-[#E5E5E5] p-1"
|
||||
: "border-r border-border bg-background";
|
||||
const treePanelClassName = sectionedPanel
|
||||
? "min-h-0 grow flex-col overflow-hidden rounded-[6px] border border-border bg-background shadow-none"
|
||||
: "min-h-0 grow overflow-hidden border-none bg-background shadow-none";
|
||||
const treeContentScrollClassName = sectionedPanel
|
||||
? `min-h-0 flex-1 overflow-y-scroll ${thinScrollbarClass}`
|
||||
: `h-full overflow-y-auto p-2 ${showLayerPanelTitle ? "pt-0" : "pt-2"} ${thinScrollbarClass}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 사이드 패널 */}
|
||||
<div
|
||||
className={`gis-layer-panel absolute left-0 top-0 z-40 flex h-full w-[250px] flex-col overflow-hidden border-r border-border bg-background text-foreground transition-all duration-300 ${visible
|
||||
? "translate-x-0 opacity-100"
|
||||
: "pointer-events-none -translate-x-full opacity-0"
|
||||
className={`gis-layer-panel absolute left-0 top-0 z-40 flex h-full w-[250px] flex-col overflow-hidden text-foreground duration-300 ${sectionedPanel ? "transition-transform ease-in-out" : "transition-all"} ${panelShellClassName} ${visible
|
||||
? sectionedPanel ? "translate-x-0" : "translate-x-0 opacity-100"
|
||||
: sectionedPanel ? "pointer-events-none -translate-x-full" : "pointer-events-none -translate-x-full opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
ref={layerPanelScrollRef}
|
||||
onScroll={handleLayerPanelScroll}
|
||||
className={`grow overflow-y-auto border-none p-2 ${showLayerPanelTitle ? "pt-0" : "pt-2"} shadow-none ${activeTab === "layers" ? "block" : "hidden"
|
||||
className={`${treePanelClassName} ${activeTab === "layers" ? (sectionedPanel ? "flex" : "block") : "hidden"
|
||||
}`}
|
||||
>
|
||||
{showLayerPanelTitle && (
|
||||
<>
|
||||
<h3 className="gis-layer-panel-title mx-2 my-4 text-center text-3xl font-bold text-label">시설물</h3>
|
||||
<hr className="mb-4 border-border" />
|
||||
</>
|
||||
{sectionedPanel && (
|
||||
<div className="gis-layer-section-header flex h-[38px] shrink-0 items-center gap-2 border-b border-border bg-background px-3">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-sm bg-[#EAF3FF] text-[#168CFF]">
|
||||
<Layers size={14} strokeWidth={2} />
|
||||
</span>
|
||||
<h3 className="text-[1.4rem] font-bold text-foreground">레이어 관리</h3>
|
||||
</div>
|
||||
)}
|
||||
{/* DnD Tree */}
|
||||
<div className={treeSectionClassNames || undefined}>
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<div
|
||||
ref={layerPanelScrollRef}
|
||||
onScroll={handleLayerPanelScroll}
|
||||
className={treeContentScrollClassName}
|
||||
>
|
||||
{showLayerPanelTitle && (
|
||||
<>
|
||||
<h3 className="gis-layer-panel-title mx-2 my-4 text-center text-3xl font-bold text-label">시설물</h3>
|
||||
<hr className="mb-4 border-border" />
|
||||
</>
|
||||
)}
|
||||
{/* DnD Tree */}
|
||||
<div className={treeSectionClassNames || undefined}>
|
||||
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
|
||||
<Tree
|
||||
tree={nodes}
|
||||
rootId={null}
|
||||
@@ -642,7 +665,7 @@ export default React.memo(function GisTree({
|
||||
return (
|
||||
<div style={{ display: isVisible ? "block" : "none" }}>
|
||||
<div
|
||||
className="gis-layer-row flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-accent"
|
||||
className={`gis-layer-row flex w-full items-center gap-2 rounded px-2 py-1 ${sectionedPanel ? "hover:bg-[#f5f5f5]" : "hover:bg-accent"}`}
|
||||
style={{
|
||||
marginInlineStart: depth * 16,
|
||||
width: `calc(100% - ${depth * 16}px)`,
|
||||
@@ -671,21 +694,39 @@ export default React.memo(function GisTree({
|
||||
>
|
||||
{node.text}
|
||||
</span>
|
||||
{showLayerActionButton && (
|
||||
<button
|
||||
type="button"
|
||||
className="gis-layer-action-button ml-auto shrink-0 rounded-sm border border-primary bg-background px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-primary transition-colors hover:bg-accent"
|
||||
onClick={() =>
|
||||
handleLayerImgClick(
|
||||
{showLayerActionButton && (
|
||||
<button
|
||||
type="button"
|
||||
className={layerActionIconOnly
|
||||
? "gis-layer-action-button ml-auto grid h-[24px] w-[24px] shrink-0 place-items-center rounded-sm bg-transparent p-0 text-[#168CFF] transition-colors hover:bg-accent"
|
||||
: "gis-layer-action-button ml-auto shrink-0 rounded-sm border border-primary bg-background px-2 py-[1px] text-[1.1rem] font-medium leading-5 text-primary transition-colors hover:bg-accent"}
|
||||
onClick={() =>
|
||||
handleLayerImgClick(
|
||||
node.data.lyrTyNm,
|
||||
node.id,
|
||||
node
|
||||
)
|
||||
}
|
||||
title={`${node.text} 목록보기`}
|
||||
>
|
||||
목록보기
|
||||
</button>
|
||||
)
|
||||
}
|
||||
title={`${node.text} 목록보기`}
|
||||
aria-label={`${node.text} 목록보기`}
|
||||
>
|
||||
{layerActionIconOnly ? (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-[18px] w-[18px]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
>
|
||||
<path d="M9 6h11M9 12h11M9 18h11" />
|
||||
<path d="M4 6h.01M4 12h.01M4 18h.01" strokeWidth="3" />
|
||||
</svg>
|
||||
) : (
|
||||
"목록보기"
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!showLayerActionButton && showLayerActionIcon && (
|
||||
<span className="shrink-0">
|
||||
@@ -720,11 +761,16 @@ export default React.memo(function GisTree({
|
||||
return aOrder - bOrder;
|
||||
}}
|
||||
/>
|
||||
</DndProvider>
|
||||
</DndProvider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{LayerSettingView && (
|
||||
<div className="gis-layer-setting shrink-0 bg-background px-2 pb-3">
|
||||
<div
|
||||
className={sectionedPanel
|
||||
? "gis-layer-setting shrink-0 bg-transparent p-0"
|
||||
: "gis-layer-setting shrink-0 bg-background px-2 pb-3"}
|
||||
>
|
||||
<LayerSettingView />
|
||||
</div>
|
||||
)}
|
||||
@@ -737,24 +783,31 @@ export default React.memo(function GisTree({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{detachedToggleButton && visible && (
|
||||
{detachedToggleButton && (
|
||||
<button
|
||||
onClick={() => setVisible(false)}
|
||||
className={`gis-layer-toggle absolute left-[250px] z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-border bg-background text-primary shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-accent ${toggleButtonPositionClass}`}
|
||||
title="접기"
|
||||
onClick={() => setVisible(!visible)}
|
||||
className={`gis-layer-toggle absolute left-[250px] z-50 flex h-[36px] transform items-center justify-center border border-border bg-background shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition-transform duration-300 ease-in-out hover:bg-accent ${visible ? "translate-x-0" : "-translate-x-[250px]"} ${sectionedPanel
|
||||
? "w-[20px] rounded-l-none rounded-r-[4px] text-[#666666]"
|
||||
: "w-[20px] rounded-[4px] text-primary"
|
||||
} ${toggleButtonPositionClass}`}
|
||||
title={visible ? "접기" : "열기"}
|
||||
aria-label={visible ? "레이어 패널 접기" : "레이어 패널 열기"}
|
||||
aria-expanded={visible}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
{visible ? (
|
||||
<ChevronLeft className={sectionedPanel ? "h-8 w-8" : "h-4 w-4"} />
|
||||
) : (
|
||||
<ChevronRight className={sectionedPanel ? "h-8 w-8" : "h-4 w-4"} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{!visible && (
|
||||
{!visible && !detachedToggleButton && (
|
||||
<button
|
||||
onClick={() => setVisible(true)}
|
||||
className={detachedToggleButton
|
||||
? `gis-layer-toggle absolute left-0 z-50 flex h-[36px] w-[20px] transform items-center justify-center rounded-[4px] border border-border bg-background text-primary shadow-[0_8px_18px_rgba(15,23,42,0.14)] transition hover:bg-accent ${toggleButtonPositionClass}`
|
||||
: `gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-border bg-background px-1 py-2 text-foreground shadow-sm ${toggleButtonPositionClass}`}
|
||||
className={`gis-layer-toggle absolute left-[-3px] z-50 transform rounded-r border border-r border-border bg-background px-1 py-2 text-foreground shadow-sm ${toggleButtonPositionClass}`}
|
||||
title="열기"
|
||||
>
|
||||
{detachedToggleButton ? <ChevronRight className="h-4 w-4" /> : <ChevronRight size={16} />}
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -101,6 +101,56 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mapbtn.mapbtn-tabs {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.mapbtn .map-type-tabs {
|
||||
display: flex;
|
||||
width: 144px;
|
||||
height: 40px;
|
||||
padding: 2px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border: 1px solid #9aa7b8;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 5px #999;
|
||||
}
|
||||
|
||||
.mapbtn .map-type-tabs button {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
border: 0;
|
||||
color: #666;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.mapbtn .map-type-tabs button.on {
|
||||
background: #0279fa;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mapbtn .map-type-tabs button:focus-visible {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
outline: 2px solid #2094fa;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.tool-bar {
|
||||
position: absolute;
|
||||
top: 70px;
|
||||
@@ -186,9 +236,9 @@
|
||||
bottom: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 126px;
|
||||
padding: 7px 10px;
|
||||
gap: 8px;
|
||||
min-width: 130px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #9aa7b8;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
|
||||
@@ -32,6 +32,8 @@ interface SearchBarProps {
|
||||
facilityTypes: { text: string; val: string }[];
|
||||
sidoSearchZoom?: number;
|
||||
facilityLayerFilter?: FacilityLayerFilter;
|
||||
/** 관망도 전용 3개 셀렉트와 조회 버튼을 하나로 연결한 검색바 레이아웃이다. */
|
||||
integrated?: boolean;
|
||||
}
|
||||
|
||||
interface CenterSelectDataType extends SelectDataType {
|
||||
@@ -43,6 +45,7 @@ export const ProcSearchBar = ({
|
||||
facilityTypes,
|
||||
sidoSearchZoom = 11,
|
||||
facilityLayerFilter,
|
||||
integrated = false,
|
||||
}: SearchBarProps) => {
|
||||
const [sidoValue, setSidoValue] = useState("0");
|
||||
const [sigunValue, setSigunValue] = useState("0");
|
||||
@@ -473,6 +476,75 @@ export const ProcSearchBar = ({
|
||||
await searchLayer.showSearchResultsFromGeoJSON(layerNm, featureId);
|
||||
};
|
||||
|
||||
if (integrated) {
|
||||
const selectRootClass = "box-border !h-[44px] !w-full !p-0";
|
||||
const selectTriggerClass =
|
||||
"relative box-border !h-[44px] !w-full !border-0 !bg-white !px-4 !py-0 !text-[14px] !font-normal !leading-[20px] !text-[#374151] focus:!z-[2] focus:!ring-2 focus:!ring-inset focus:!ring-[#2094FA] data-[state=open]:!z-[2] data-[state=open]:!ring-2 data-[state=open]:!ring-inset data-[state=open]:!ring-[#2094FA] [&>span]:!block [&>span]:min-w-0 [&>span]:overflow-hidden [&>span]:text-ellipsis [&>span]:whitespace-nowrap [&>span]:!leading-[20px] [&>span]:pr-2 [&>svg]:!h-8 [&>svg]:!w-8 [&>svg]:shrink-0 [&>svg]:!text-[#6B7280] [&>svg]:!opacity-100";
|
||||
|
||||
return (
|
||||
<div className="flex h-[44px] w-[656px] box-border items-center overflow-hidden rounded-[8px] bg-white p-0 shadow-[0_2px_4px_rgba(60,64,67,0.3),0_4px_10px_rgba(60,64,67,0.18)]">
|
||||
<div className="group relative h-[44px] w-[192px] shrink-0 box-border">
|
||||
<SelectBox
|
||||
labelClass="hidden"
|
||||
className={selectRootClass}
|
||||
selectClass={`${selectTriggerClass} !rounded-none focus:!rounded-l-[8px] data-[state=open]:!rounded-l-[8px]`}
|
||||
label=""
|
||||
selectArray={sidoData}
|
||||
onSelectValue={(val: any) =>
|
||||
onSelectValue(val.toString(), "ctpvCd")
|
||||
}
|
||||
value={sidoValue}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute right-0 top-1/2 z-[1] h-[24px] w-px -translate-y-1/2 bg-[#D5DCE5] group-focus-within:hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="group relative h-[44px] w-[192px] shrink-0 box-border">
|
||||
<SelectBox
|
||||
labelClass="hidden"
|
||||
className={selectRootClass}
|
||||
selectClass={`${selectTriggerClass} !rounded-none`}
|
||||
label=""
|
||||
selectArray={filteredSigunData}
|
||||
onSelectValue={(val: any) =>
|
||||
onSelectValue(val.toString(), "sggCd")
|
||||
}
|
||||
value={sigunValue}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute right-0 top-1/2 z-[1] h-[24px] w-px -translate-y-1/2 bg-[#D5DCE5] group-focus-within:hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-[44px] w-[192px] shrink-0 box-border">
|
||||
<SelectBox
|
||||
labelClass="hidden"
|
||||
className={selectRootClass}
|
||||
selectClass={`${selectTriggerClass} !rounded-none`}
|
||||
label=""
|
||||
selectArray={facilityData}
|
||||
onSelectValue={(val: any) =>
|
||||
onSelectValue(val.toString(), "fcltCd")
|
||||
}
|
||||
value={facilityValue}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
className="box-border !h-[44px] !w-[80px] shrink-0 !rounded-none !bg-[#0279FA] !p-0 text-[14px] font-semibold leading-5 text-white"
|
||||
size="default"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
조회
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="col-span-4">
|
||||
|
||||
Reference in New Issue
Block a user