Merge branch 'iipbs/sn/dev' into iipbs/dm/dev
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
## Project
|
||||
This repository contains a Next.js web application.
|
||||
|
||||
Main technologies:
|
||||
- Next.js
|
||||
- TypeScript
|
||||
- pnpm workspace
|
||||
- turborepo
|
||||
|
||||
이 저장소는 pnpm workspace + Turborepo 기반 monorepo이다.
|
||||
|
||||
- 워크스페이스 범위는 `apps/*`, `apps/common/*`, `packages/*`, `packages/common/*`
|
||||
- 루트 스크립트는 `turbo run build`, `turbo run dev` 를 사용한다
|
||||
- `build` 작업은 의존 패키지의 `build` 를 먼저 수행한다 (`dependsOn: ["^build"]`)
|
||||
- `dev` 는 캐시하지 않는 지속 실행 작업이다
|
||||
- 변경 범위만 선택 실행할 수 있으므로, 전체 빌드보다 `--filter` 사용을 우선한다
|
||||
|
||||
## Encoding
|
||||
- UTF-8
|
||||
- 한글이 깨지지 않도록 주의할 것
|
||||
|
||||
## 주석 comment
|
||||
- 한글 주석을 삭제하지 말 것
|
||||
|
||||
## Monorepo structure
|
||||
apps/
|
||||
_template
|
||||
as
|
||||
cm
|
||||
cs
|
||||
dc
|
||||
dm
|
||||
dm_new
|
||||
fo
|
||||
sn
|
||||
st
|
||||
wr
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
## Project
|
||||
This repository contains a Next.js web application.
|
||||
|
||||
Main technologies:
|
||||
- Next.js
|
||||
- TypeScript
|
||||
- pnpm workspace
|
||||
- turborepo
|
||||
|
||||
이 저장소는 pnpm workspace + Turborepo 기반 monorepo이다.
|
||||
|
||||
- 워크스페이스 범위는 `apps/*`, `apps/common/*`, `packages/*`, `packages/common/*`
|
||||
- 루트 스크립트는 `turbo run build`, `turbo run dev` 를 사용한다
|
||||
- `build` 작업은 의존 패키지의 `build` 를 먼저 수행한다 (`dependsOn: ["^build"]`)
|
||||
- `dev` 는 캐시하지 않는 지속 실행 작업이다
|
||||
- 변경 범위만 선택 실행할 수 있으므로, 전체 빌드보다 `--filter` 사용을 우선한다
|
||||
|
||||
## Encoding
|
||||
- UTF-8
|
||||
- 한글이 깨지지 않도록 주의할 것
|
||||
|
||||
## 주석 comment
|
||||
- 한글 주석을 삭제하지 말 것
|
||||
|
||||
## Monorepo structure
|
||||
apps/
|
||||
_template
|
||||
as
|
||||
cm
|
||||
cs
|
||||
dc
|
||||
dm
|
||||
dm_new
|
||||
fo
|
||||
sn
|
||||
st
|
||||
wr
|
||||
|
||||
@@ -48,6 +48,16 @@ type SuggestionSeed = {
|
||||
bldgNoMno: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 주소 검색 API를 호출해 우편번호/주소 목록을 조회한다.
|
||||
* @param searchQuery 조회 조건 객체
|
||||
* @param searchQuery.roadNm 도로명
|
||||
* @param searchQuery.bldgNoMno 건물 본번
|
||||
* @param searchQuery.bldgNoSno 건물 부번
|
||||
* @param searchQuery.page 0부터 시작하는 페이지 번호
|
||||
* @param searchQuery.size 페이지당 조회 건수
|
||||
* @returns 주소 목록과 전체 건수를 포함한 응답 객체
|
||||
*/
|
||||
async function findAddress(searchQuery: ZipSearchParams): Promise<ZipListResponse> {
|
||||
if (!searchQuery) return {};
|
||||
|
||||
@@ -68,6 +78,12 @@ async function findAddress(searchQuery: ZipSearchParams): Promise<ZipListRespons
|
||||
return (await response.json()) as ZipListResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동완성 후보를 만들기 위해 주소 항목에서 도로명과 건물 본번을 추출한다.
|
||||
* 우선 API 응답 필드의 값을 사용하고, 값이 없으면 표시 주소 문자열을 파싱해 보정한다.
|
||||
* @param item 주소 검색 결과 1건
|
||||
* @returns 자동완성 생성에 사용할 도로명/건물본번 또는 추출 실패 시 null
|
||||
*/
|
||||
function extractSuggestionSeed(item: Zip): SuggestionSeed | null {
|
||||
const ext = item as Zip & {
|
||||
roadNm?: string;
|
||||
@@ -97,6 +113,11 @@ function extractSuggestionSeed(item: Zip): SuggestionSeed | null {
|
||||
return { roadNm, bldgNoMno };
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색어에 허용되지 않는 특수문자나 SQL 예약어가 포함되어 있는지 검사한다.
|
||||
* @param search 사용자가 입력한 검색 문자열
|
||||
* @returns 검색 가능하면 true, 차단 대상이면 false
|
||||
*/
|
||||
function searchFilter(search: string): boolean {
|
||||
if (search.length === 0) return true;
|
||||
|
||||
@@ -132,6 +153,13 @@ function searchFilter(search: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 입력 주소 문자열을 "도로명 + 건물번호" 구조로 파싱해 API 조회 파라미터로 변환한다.
|
||||
* 예) "테헤란로 152-1" -> { roadNm: "테헤란로", bldgNoMno: 152, bldgNoSno: 1, page }
|
||||
* @param input 사용자가 입력한 주소 검색어
|
||||
* @param page 1부터 시작하는 요청 페이지 번호
|
||||
* @returns 파싱 성공 시 API 조회 파라미터와 정규화된 검색어, 실패 시 오류 메시지
|
||||
*/
|
||||
export function parsePostcodeInput(
|
||||
input: string,
|
||||
page = 1,
|
||||
@@ -205,6 +233,12 @@ export function parsePostcodeInput(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 도로명 주소 문자열에서 재검색에 사용할 "도로명 건물번호" 입력값만 추출한다.
|
||||
* 예) "테헤란로 152, OO빌딩" -> "테헤란로 152"
|
||||
* @param roadAddr 화면에 표시된 도로명 주소 문자열
|
||||
* @returns 재검색 입력란에 넣을 수 있는 문자열, 추출 실패 시 빈 문자열
|
||||
*/
|
||||
export function extractSearchInputFromRoadAddr(roadAddr: string): string {
|
||||
const firstSegment = roadAddr.split(",")[0]?.trim() ?? "";
|
||||
if (!firstSegment) return "";
|
||||
@@ -216,6 +250,12 @@ export function extractSearchInputFromRoadAddr(roadAddr: string): string {
|
||||
return `${parts.slice(0, -1).join(" ")} ${bldgToken}`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 입력 중인 도로명을 기준으로 자동완성 후보를 조회한다.
|
||||
* 건물번호가 함께 입력된 경우에는 마지막 숫자 토큰을 제외한 도로명만으로 후보를 찾는다.
|
||||
* @param input 사용자가 입력창에 입력한 원본 문자열
|
||||
* @returns 최대 5개의 "도로명 건물본번" 자동완성 문자열 목록
|
||||
*/
|
||||
export async function fetchPostcodeSuggestions(input: string): Promise<string[]> {
|
||||
const normalized = input.replace(/\s+/g, " ").trim();
|
||||
const parts = normalized.split(" ");
|
||||
@@ -252,6 +292,15 @@ export async function fetchPostcodeSuggestions(input: string): Promise<string[]>
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 우편번호 검색 API를 호출해 화면 표시용 우편번호 목록 형태로 변환한다.
|
||||
* @param params 조회 조건 객체
|
||||
* @param params.roadNm 검색할 도로명
|
||||
* @param params.bldgNoMno 건물 본번
|
||||
* @param params.bldgNoSno 건물 부번
|
||||
* @param params.page 1부터 시작하는 화면 페이지 번호
|
||||
* @returns 전체 건수와 화면 표시용 주소 목록
|
||||
*/
|
||||
export async function fetchPostcode(
|
||||
params: PostcodeSearchParams,
|
||||
): Promise<PostcodeSearchResult> {
|
||||
|
||||
@@ -22,6 +22,11 @@ Main technologies:
|
||||
## 주석 comment
|
||||
- 한글 주석을 삭제하지 말 것
|
||||
|
||||
## html 디렉토리
|
||||
- apps/sn/app/html
|
||||
- 이 디렉토리의 목적은 코드를 화면 publishing과 로직 설정을 위한 폴더이다.
|
||||
- html 하위 *.md 요청 작업시 html 디렉토리 이외의 수정이 되지 않도록 해야 한다.
|
||||
|
||||
## Monorepo structure
|
||||
apps/
|
||||
_template
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@common/components";
|
||||
import {
|
||||
AGED_PIPE_DETAIL_FIELDS,
|
||||
getElapsedYears,
|
||||
getPipeDisplayValue,
|
||||
} from "./agedPipeUtils";
|
||||
|
||||
type Props = {
|
||||
featureData: any;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function AgedPipeDetailPanel({ featureData, onClose }: Props) {
|
||||
const source = featureData?.values_ ?? featureData ?? {};
|
||||
const facilityNo = String(getPipeDisplayValue(source, "fclt_mng_no") ?? "-");
|
||||
const elapsedYears = getElapsedYears(source.instl_ymd ?? source.instlYmd);
|
||||
|
||||
return (
|
||||
<div className="absolute right-0 top-0 z-[50] flex h-[calc(100vh-5rem)] w-[320px] flex-col overflow-hidden border-l border-slate-200 bg-white shadow-[-12px_0_24px_rgba(15,23,42,0.1)]">
|
||||
<div className="flex items-start justify-between border-b border-slate-200 bg-slate-900 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-[1.45rem] font-semibold text-white">노후관로 상세</div>
|
||||
<div className="mt-1 text-[1.05rem] text-slate-300">시설관리번호 {facilityNo}</div>
|
||||
<div className="mt-1 text-[1.05rem] text-amber-300">
|
||||
경과년도 {elapsedYears ?? "-"}년
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
|
||||
onClick={onClose}
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto px-4 py-3">
|
||||
<div className="space-y-2">
|
||||
{AGED_PIPE_DETAIL_FIELDS.map((field) => (
|
||||
<div key={field.id} className="rounded-lg border border-slate-200 px-3 py-2.5">
|
||||
<div className="text-[1rem] font-medium text-slate-500">{field.title}</div>
|
||||
<div className="mt-1 break-words text-[1.15rem] leading-6 text-slate-900">
|
||||
{String(getPipeDisplayValue(source, field.id) ?? "-")}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Input } from "@common/components";
|
||||
import useStore from "@/store/useStore";
|
||||
import type { AgedPipeFilters } from "./agedPipeUtils";
|
||||
|
||||
type OverlayProps = {
|
||||
onClose?: () => void;
|
||||
isPlaying?: boolean;
|
||||
setIsPlaying?: (value: boolean) => void;
|
||||
totalSteps?: number;
|
||||
currentStep?: number;
|
||||
};
|
||||
|
||||
type Props = OverlayProps & {
|
||||
filters: AgedPipeFilters;
|
||||
onFiltersChange: (filters: AgedPipeFilters) => void;
|
||||
onApply: () => void;
|
||||
};
|
||||
|
||||
export default function AgedPipeFilterPanel({
|
||||
filters,
|
||||
onFiltersChange,
|
||||
onApply,
|
||||
}: Props) {
|
||||
const { ctpv, sgg } = useStore();
|
||||
const sggOptions = filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)));
|
||||
|
||||
return (
|
||||
<div className="w-[280px] rounded-xl border border-slate-200 bg-white/95 p-3 shadow-[0_14px_30px_rgba(15,23,42,0.16)] backdrop-blur">
|
||||
<div className="mb-2">
|
||||
<div className="text-[1.35rem] font-semibold text-slate-900">노후관로 기준</div>
|
||||
<div className="mt-1 text-[1.05rem] leading-5 text-slate-500">
|
||||
경과년수 이상 관로만 지도와 목록에 반영합니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
|
||||
<Input
|
||||
className="h-10 !text-[1.15rem]"
|
||||
inputMode="numeric"
|
||||
value={String(filters.elapsedYears)}
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
|
||||
onFiltersChange({ ...filters, elapsedYears: next });
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") onApply();
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) => onFiltersChange({ ...filters, sggCd: event.target.value })}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{sggOptions.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Button className="mt-3 h-10 w-full !text-[1.15rem]" onClick={onApply}>
|
||||
GIS 반영
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { Button, Input } from "@common/components";
|
||||
import { UiTable } from "@common/business_components/ui";
|
||||
import LoadingOverlay from "@/components/ui/LoadingBar";
|
||||
import useStore from "@/store/useStore";
|
||||
import {
|
||||
AGED_PIPE_LIST_HEADLIST,
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
} from "./agedPipeUtils";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
filters: AgedPipeFilters;
|
||||
onFiltersChange: (filters: AgedPipeFilters) => void;
|
||||
rows: AgedPipeRow[];
|
||||
total: number;
|
||||
currentPage: number;
|
||||
isLoading: boolean;
|
||||
onSearch: (page?: number) => void;
|
||||
onDownload: () => void;
|
||||
onRowDoubleClick: (row: AgedPipeRow) => void;
|
||||
};
|
||||
|
||||
export default function AgedPipeListModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
filters,
|
||||
onFiltersChange,
|
||||
rows,
|
||||
total,
|
||||
currentPage,
|
||||
isLoading,
|
||||
onSearch,
|
||||
onDownload,
|
||||
onRowDoubleClick,
|
||||
}: Props) {
|
||||
const { ctpv, sgg } = useStore();
|
||||
const lastClickRef = useRef<{ index: number; clickedAt: number } | null>(null);
|
||||
const sggOptions = filters.ctpvCd === "0"
|
||||
? sgg
|
||||
: sgg.filter((item) => String(item.val).startsWith(filters.ctpvCd.slice(0, 2)));
|
||||
|
||||
const handleRowClick = (index?: number) => {
|
||||
if (typeof index !== "number") return;
|
||||
const row = rows[index];
|
||||
if (!row) return;
|
||||
|
||||
const now = Date.now();
|
||||
const lastClick = lastClickRef.current;
|
||||
if (lastClick && lastClick.index === index && now - lastClick.clickedAt < 300) {
|
||||
lastClickRef.current = null;
|
||||
onRowDoubleClick(row);
|
||||
return;
|
||||
}
|
||||
|
||||
lastClickRef.current = { index, clickedAt: now };
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-[70] flex items-center justify-center px-6 py-8">
|
||||
<div className="pointer-events-auto relative flex max-h-[82vh] w-[min(1120px,94vw)] flex-col overflow-hidden rounded-2xl border border-slate-300 bg-white shadow-[0_28px_80px_rgba(15,23,42,0.28)]">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-900 px-5 py-3">
|
||||
<div>
|
||||
<div className="text-[1.45rem] font-semibold text-white">노후관로 목록</div>
|
||||
<div className="mt-1 text-[1rem] text-slate-300">
|
||||
지도 위에 띄워지는 윈도우형 팝업 미리보기
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
color="white"
|
||||
size="xs"
|
||||
className="border border-white/30 px-3 py-1 text-[1.05rem] text-white"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
닫기
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="relative overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-1 gap-3 pb-4 md:grid-cols-[110px_minmax(0,1fr)_110px_minmax(0,1fr)_auto_auto]">
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">경과년수</div>
|
||||
<Input
|
||||
className="h-10 !text-[1.1rem]"
|
||||
inputMode="numeric"
|
||||
value={String(filters.elapsedYears)}
|
||||
onChange={(event) => {
|
||||
const next = Number(event.target.value.replace(/[^0-9]/g, "") || "0");
|
||||
onFiltersChange({ ...filters, elapsedYears: next });
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") onSearch(1);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시도</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.ctpvCd}
|
||||
onChange={(event) =>
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
ctpvCd: event.target.value,
|
||||
sggCd: "0",
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{ctpv.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="block">
|
||||
<div className="mb-1 text-[1.05rem] font-medium text-slate-700">시군구</div>
|
||||
<select
|
||||
className="h-10 w-full rounded-md border border-slate-300 px-3 text-[1.1rem]"
|
||||
value={filters.sggCd}
|
||||
onChange={(event) => onFiltersChange({ ...filters, sggCd: event.target.value })}
|
||||
>
|
||||
<option value="0">전체</option>
|
||||
{sggOptions.map((item) => (
|
||||
<option key={item.val} value={String(item.val)}>
|
||||
{item.text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-wrap gap-2 md:col-span-3 md:justify-end">
|
||||
<Button type="button" size="sm" className="h-10 px-5 !text-[1.1rem]" onClick={() => onSearch(1)}>
|
||||
조회
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
color="green"
|
||||
className="h-10 px-5 !text-[1.1rem]"
|
||||
onClick={onDownload}
|
||||
>
|
||||
다운로드
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-200 pt-3">
|
||||
<UiTable<AgedPipeRow>
|
||||
headName={<>노후관로 조회 결과</>}
|
||||
tableData={rows}
|
||||
headlist={AGED_PIPE_LIST_HEADLIST}
|
||||
pageSize={10}
|
||||
total={total}
|
||||
cellClick={(index) => handleRowClick(index)}
|
||||
onPageChange={(page) => onSearch(page)}
|
||||
curPage={currentPage}
|
||||
widthFixed={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="px-[1.2rem] pb-1 text-[1rem] text-slate-500">
|
||||
목록 행을 더블클릭하면 지도 이동 후 상세 패널이 열립니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
|
||||
import type { SelectDataType } from "@common/business_components";
|
||||
import { formatDisplayData } from "@/components/lib/utils/formatDisplayValue";
|
||||
import {
|
||||
MS_SWAGE_PPL_CODES,
|
||||
MS_SWAGE_PPL_USE_CODES,
|
||||
MsSwagePplCol,
|
||||
} from "../map/components/common/table";
|
||||
|
||||
export type AgedPipeFilters = {
|
||||
elapsedYears: number;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
};
|
||||
|
||||
export type AgedPipeRow = {
|
||||
fid: string;
|
||||
lyrTypeNm: string;
|
||||
rowNumber: number;
|
||||
fcltMngNo: string;
|
||||
instlYmd: string;
|
||||
pdmtVl: string | number;
|
||||
prlgVl: string | number;
|
||||
usgCd: string;
|
||||
mqltCd: string;
|
||||
elapsedYears: number | string;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
type PipeSource = Record<string, unknown>;
|
||||
|
||||
const useMap = new Map(MS_SWAGE_PPL_USE_CODES.map((item) => [item.val.trim(), item.text]));
|
||||
const materialMap = new Map(MS_SWAGE_PPL_CODES.map((item) => [item.val.trim(), item.text]));
|
||||
|
||||
export const AGED_PIPE_DEFAULT_FILTERS: AgedPipeFilters = {
|
||||
elapsedYears: 20,
|
||||
ctpvCd: "0",
|
||||
sggCd: "0",
|
||||
};
|
||||
|
||||
export const AGED_PIPE_LIST_HEADLIST = [
|
||||
{ id: "rowNumber", title: "NO", widthPercent: 6 },
|
||||
{ id: "fcltMngNo", title: "시설관리번호", widthPercent: 18 },
|
||||
{ id: "instlYmd", title: "설치일자", widthPercent: 12 },
|
||||
{ id: "pdmtVl", title: "관경(mm)", widthPercent: 10 },
|
||||
{ id: "prlgVl", title: "연장(m)", widthPercent: 10 },
|
||||
{ id: "usgCd", title: "용도", widthPercent: 14 },
|
||||
{ id: "mqltCd", title: "재질", widthPercent: 18 },
|
||||
{ id: "elapsedYears", title: "경과년도", widthPercent: 12, align: "right" },
|
||||
];
|
||||
|
||||
export const AGED_PIPE_EXCEL_HEADER = {
|
||||
rowNumber: "NO",
|
||||
fcltMngNo: "시설관리번호",
|
||||
instlYmd: "설치일자",
|
||||
pdmtVl: "관경(mm)",
|
||||
prlgVl: "연장(m)",
|
||||
usgCd: "용도",
|
||||
mqltCd: "재질",
|
||||
elapsedYears: "경과년도",
|
||||
};
|
||||
|
||||
export const AGED_PIPE_DETAIL_FIELDS = MsSwagePplCol;
|
||||
|
||||
const getToday = () => new Date();
|
||||
|
||||
const pad2 = (value: number) => String(value).padStart(2, "0");
|
||||
|
||||
export const getCutoffYmd = (elapsedYears: number) => {
|
||||
const today = getToday();
|
||||
const cutoff = new Date(today.getFullYear() - elapsedYears, today.getMonth(), today.getDate());
|
||||
return `${cutoff.getFullYear()}${pad2(cutoff.getMonth() + 1)}${pad2(cutoff.getDate())}`;
|
||||
};
|
||||
|
||||
const normalizeYmd = (value: unknown) => String(value ?? "").replace(/[^0-9]/g, "").slice(0, 8);
|
||||
|
||||
const getCodePrefix = (source: PipeSource) =>
|
||||
String(source.pbadmsEmdCd ?? source.pbadms_emd_cd ?? "").trim();
|
||||
|
||||
const resolveRegionName = (
|
||||
code: string,
|
||||
directName: unknown,
|
||||
options: SelectDataType[],
|
||||
prefixLength: number,
|
||||
) => {
|
||||
if (String(directName ?? "").trim()) return String(directName);
|
||||
const prefix = code.slice(0, prefixLength);
|
||||
return options.find((item) => String(item.val).trim() === prefix)?.text ?? "";
|
||||
};
|
||||
|
||||
export const getElapsedYears = (value: unknown) => {
|
||||
const normalized = normalizeYmd(value);
|
||||
if (normalized.length < 4) return null;
|
||||
|
||||
const year = Number(normalized.slice(0, 4));
|
||||
const month = Number(normalized.slice(4, 6) || "1");
|
||||
const day = Number(normalized.slice(6, 8) || "1");
|
||||
if (!year) return null;
|
||||
|
||||
const today = getToday();
|
||||
let elapsed = today.getFullYear() - year;
|
||||
if (
|
||||
today.getMonth() + 1 < month ||
|
||||
(today.getMonth() + 1 === month && today.getDate() < day)
|
||||
) {
|
||||
elapsed -= 1;
|
||||
}
|
||||
|
||||
return Math.max(elapsed, 0);
|
||||
};
|
||||
|
||||
export const buildAgedPipeCqlFilters = (filters: AgedPipeFilters) => {
|
||||
const cqlFilters: string[] = [];
|
||||
cqlFilters.push(`instl_ymd <= '${getCutoffYmd(filters.elapsedYears)}'`);
|
||||
|
||||
if (filters.sggCd !== "0") {
|
||||
cqlFilters.push(`pbadms_emd_cd LIKE '${filters.sggCd}%'`);
|
||||
} else if (filters.ctpvCd !== "0") {
|
||||
cqlFilters.push(`pbadms_emd_cd LIKE '${filters.ctpvCd}%'`);
|
||||
}
|
||||
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
export const matchesAgedPipeFilters = (source: PipeSource, filters: AgedPipeFilters) => {
|
||||
const instlYmd = normalizeYmd(source.instl_ymd ?? source.instlYmd);
|
||||
if (!instlYmd || instlYmd > getCutoffYmd(filters.elapsedYears)) return false;
|
||||
|
||||
const regionCode = getCodePrefix(source);
|
||||
if (filters.sggCd !== "0") return regionCode.startsWith(filters.sggCd);
|
||||
if (filters.ctpvCd !== "0") return regionCode.startsWith(filters.ctpvCd);
|
||||
return true;
|
||||
};
|
||||
|
||||
export const toDisplayPipeRow = (
|
||||
source: PipeSource,
|
||||
index: number,
|
||||
page: number,
|
||||
ctpvOptions: SelectDataType[],
|
||||
sggOptions: SelectDataType[],
|
||||
): AgedPipeRow => {
|
||||
const formatted = formatDisplayData(source) as PipeSource;
|
||||
const codePrefix = getCodePrefix(source);
|
||||
const instlYmd = formatted.instlYmd ?? source.instlYmd ?? formatted.instl_ymd ?? source.instl_ymd;
|
||||
const trimmedUse = String(source.usgCd ?? source.usg_cd ?? "").trim();
|
||||
const trimmedMaterial = String(source.mqltCd ?? source.mqlt_cd ?? "").trim();
|
||||
const pdmtVl = formatted.pdmtVl ?? source.pdmtVl ?? source.pdmt_vl ?? "-";
|
||||
const prlgVl = formatted.prlgVl ?? source.prlgVl ?? source.prlg_vl ?? "-";
|
||||
|
||||
return {
|
||||
...source,
|
||||
...formatted,
|
||||
fid: String(source.fid ?? ""),
|
||||
lyrTypeNm: String(source.lyrTypeNm ?? "tb_ms_swage_ppl"),
|
||||
rowNumber: (page - 1) * 10 + index + 1,
|
||||
fcltMngNo: String(formatted.fcltMngNo ?? source.fcltMngNo ?? source.fclt_mng_no ?? "-"),
|
||||
instlYmd: String(instlYmd ?? "-"),
|
||||
pdmtVl: typeof pdmtVl === "number" ? pdmtVl : String(pdmtVl),
|
||||
prlgVl: typeof prlgVl === "number" ? prlgVl : String(prlgVl),
|
||||
usgCd: useMap.get(trimmedUse) ?? String(formatted.usgCd ?? source.usgCd ?? source.usg_cd ?? "-"),
|
||||
mqltCd:
|
||||
materialMap.get(trimmedMaterial) ??
|
||||
String(formatted.mqltCd ?? source.mqltCd ?? source.mqlt_cd ?? "-"),
|
||||
elapsedYears: getElapsedYears(source.instlYmd ?? source.instl_ymd) ?? "-",
|
||||
ctpvNm: resolveRegionName(
|
||||
codePrefix,
|
||||
formatted.ctpvNm ?? source.ctpvNm ?? source.ctpv_nm,
|
||||
ctpvOptions,
|
||||
2,
|
||||
),
|
||||
sggNm: resolveRegionName(
|
||||
codePrefix,
|
||||
formatted.sggNm ?? source.sggNm ?? source.sgg_nm,
|
||||
sggOptions,
|
||||
5,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const getPipeDisplayValue = (source: PipeSource, key: string) => {
|
||||
const formatted = formatDisplayData(source) as PipeSource;
|
||||
const keyMap: Record<string, string[]> = {
|
||||
fclt_mng_no: ["fclt_mng_no", "fcltMngNo"],
|
||||
inst_mng_no: ["inst_mng_no", "instMngNo"],
|
||||
instl_ymd: ["instl_ymd", "instlYmd"],
|
||||
mapsht_no: ["mapsht_no", "mapshtNo"],
|
||||
pdmt_vl: ["pdmt_vl", "pdmtVl"],
|
||||
prlg_vl: ["prlg_vl", "prlgVl"],
|
||||
usg_cd: ["usg_cd", "usgCd"],
|
||||
mqlt_cd: ["mqlt_cd", "mqltCd"],
|
||||
avg_grdn: ["avg_grdn", "avgGrdn"],
|
||||
drtw_mltv_area: ["drtw_mltv_area", "drtwMltvArea"],
|
||||
excln_mltv_area: ["excln_mltv_area", "exclnMltvArea"],
|
||||
tmpt_dpth: ["tmpt_dpth", "tmptDpth"],
|
||||
tmpt_mhl_mng_no: ["tmpt_mhl_mng_no", "tmptMhlMngNo"],
|
||||
scend_dpth: ["scend_dpth", "scendDpth"],
|
||||
scend_mhl_mng_no: ["scend_mhl_mng_no", "scendMhlMngNo"],
|
||||
trfln_cnt: ["trfln_cnt", "trflnCnt"],
|
||||
vrtc_len: ["vrtc_len", "vrtcLen"],
|
||||
wdth_len: ["wdth_len", "wdthLen"],
|
||||
};
|
||||
|
||||
const candidates = keyMap[key] ?? [key];
|
||||
for (const candidate of candidates) {
|
||||
const value = formatted[candidate] ?? source[candidate];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
if (candidate === "usg_cd" || candidate === "usgCd") {
|
||||
return useMap.get(String(source.usg_cd ?? source.usgCd ?? "").trim()) ?? value;
|
||||
}
|
||||
if (candidate === "mqlt_cd" || candidate === "mqltCd") {
|
||||
return materialMap.get(String(source.mqlt_cd ?? source.mqltCd ?? "").trim()) ?? value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return "-";
|
||||
};
|
||||
@@ -0,0 +1,403 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { downloadExcelWithKoreanHeaders } from "@common/business_components/ui";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import Style from "ol/style/Style.js";
|
||||
import useStore from "@/store/useStore";
|
||||
import { getData } from "@/components/lib/api";
|
||||
import AgedPipeFilterPanel from "./AgedPipeFilterPanel";
|
||||
import AgedPipeDetailPanel from "./AgedPipeDetailPanel";
|
||||
import AgedPipeListModal from "./AgedPipeListModal";
|
||||
import {
|
||||
AGED_PIPE_DEFAULT_FILTERS,
|
||||
AGED_PIPE_EXCEL_HEADER,
|
||||
buildAgedPipeCqlFilters,
|
||||
matchesAgedPipeFilters,
|
||||
toDisplayPipeRow,
|
||||
type AgedPipeFilters,
|
||||
type AgedPipeRow,
|
||||
} from "./agedPipeUtils";
|
||||
|
||||
const agedPipeHighlightStyle = new Style({
|
||||
stroke: new Stroke({
|
||||
color: "rgba(239, 68, 68, 0.95)",
|
||||
width: 5,
|
||||
}),
|
||||
zIndex: 999,
|
||||
});
|
||||
|
||||
const PIPE_LAYER_TYPE = "tb_ms_swage_ppl";
|
||||
const EmptySearchView = () => null;
|
||||
const DESIGN_PREVIEW_FEATURE = {
|
||||
fid: "preview-aged-pipe",
|
||||
lyrTypeNm: PIPE_LAYER_TYPE,
|
||||
fcltMngNo: "SN-PPL-000123",
|
||||
instMngNo: "INST-4458",
|
||||
instlYmd: "19981215",
|
||||
mapshtNo: "M-12-34",
|
||||
pdmtVl: 450,
|
||||
prlgVl: 132.5,
|
||||
usgCd: "합류관",
|
||||
mqltCd: "철근콘크리트관(RC)",
|
||||
avgGrdn: 0.0021,
|
||||
drtwMltvArea: 2140,
|
||||
exclnMltvArea: 980,
|
||||
tmptDpth: 2.1,
|
||||
tmptMhlMngNo: "TMH-2201",
|
||||
scendDpth: 2.4,
|
||||
scendMhlMngNo: "SMH-2202",
|
||||
trflnCnt: 2,
|
||||
vrtcLen: 1.2,
|
||||
wdthLen: 1.2,
|
||||
};
|
||||
|
||||
const Map = () => {
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
const [treeData, setTreeData] = useState<any>([]);
|
||||
const [isDetailOpen, setIsDetailOpen] = useState(true);
|
||||
const [featureData, setFeatureData] = useState<any>(DESIGN_PREVIEW_FEATURE);
|
||||
const [draftFilters, setDraftFilters] = useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
|
||||
const [appliedFilters, setAppliedFilters] = useState<AgedPipeFilters>(AGED_PIPE_DEFAULT_FILTERS);
|
||||
const [isListOpen, setIsListOpen] = useState(true);
|
||||
const [listRows, setListRows] = useState<AgedPipeRow[]>([
|
||||
{
|
||||
fid: "preview-aged-pipe",
|
||||
lyrTypeNm: PIPE_LAYER_TYPE,
|
||||
rowNumber: 1,
|
||||
fcltMngNo: "SN-PPL-000123",
|
||||
instlYmd: "1998-12-15",
|
||||
pdmtVl: 450,
|
||||
prlgVl: 132.5,
|
||||
usgCd: "합류관",
|
||||
mqltCd: "철근콘크리트관(RC)",
|
||||
elapsedYears: 27,
|
||||
ctpvNm: "서울특별시",
|
||||
sggNm: "강남구",
|
||||
},
|
||||
{
|
||||
fid: "preview-aged-pipe-2",
|
||||
lyrTypeNm: PIPE_LAYER_TYPE,
|
||||
rowNumber: 2,
|
||||
fcltMngNo: "SN-PPL-000124",
|
||||
instlYmd: "2001-03-10",
|
||||
pdmtVl: 300,
|
||||
prlgVl: 88.4,
|
||||
usgCd: "오수관",
|
||||
mqltCd: "염화비닐관(PVC)",
|
||||
elapsedYears: 25,
|
||||
ctpvNm: "서울특별시",
|
||||
sggNm: "강남구",
|
||||
},
|
||||
]);
|
||||
const [totalCount, setTotalCount] = useState(2);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [isListLoading, setIsListLoading] = useState(false);
|
||||
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
|
||||
const { ctpv, sgg } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const response = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (response.ok) {
|
||||
const contentLength = response.headers.get("content-length");
|
||||
if (contentLength === "0") data = null;
|
||||
|
||||
const text = await response.text();
|
||||
if (!text || text.trim() === "") data = null;
|
||||
else data = JSON.parse(text);
|
||||
}
|
||||
|
||||
setTreeData(data);
|
||||
} catch (error) {
|
||||
console.error("노후관로 레이어 설정 조회 중 오류:", error);
|
||||
throw new Error("Invalid JSON response from server");
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
if (gisMaps.current) {
|
||||
setMapReady(true);
|
||||
window.clearInterval(timer);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const fetchAgedPipeRows = useCallback(
|
||||
async (page: number, filters: AgedPipeFilters, size: number) => {
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: PIPE_LAYER_TYPE,
|
||||
page: String(page - 1),
|
||||
size: String(size),
|
||||
sort: "string",
|
||||
});
|
||||
|
||||
const cqlFilters = buildAgedPipeCqlFilters(filters);
|
||||
if (cqlFilters.length > 0) {
|
||||
queryParams.append("cqlFiter", cqlFilters.join(" and "));
|
||||
}
|
||||
|
||||
const response = await getData(`v1/fclts?${queryParams.toString()}`);
|
||||
const rows = Array.isArray(response?.data) ? response.data : [];
|
||||
|
||||
return {
|
||||
rows: rows.map((item: Record<string, unknown>, index: number) =>
|
||||
toDisplayPipeRow(item, index, page, ctpv, sgg),
|
||||
),
|
||||
total: Number(response?.count ?? 0),
|
||||
};
|
||||
},
|
||||
[ctpv, sgg],
|
||||
);
|
||||
|
||||
const loadAgedPipeList = useCallback(
|
||||
async (page = 1, filters = appliedFilters) => {
|
||||
setIsListLoading(true);
|
||||
try {
|
||||
const response = await fetchAgedPipeRows(page, filters, 10);
|
||||
setListRows(response.rows);
|
||||
setTotalCount(response.total);
|
||||
setCurrentPage(page);
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 조회 중 오류:", error);
|
||||
setListRows([]);
|
||||
setTotalCount(0);
|
||||
} finally {
|
||||
setIsListLoading(false);
|
||||
}
|
||||
},
|
||||
[appliedFilters, fetchAgedPipeRows],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadAgedPipeList(1, appliedFilters);
|
||||
}, [appliedFilters, loadAgedPipeList]);
|
||||
|
||||
const handleApplyFilters = useCallback(() => {
|
||||
const normalized = {
|
||||
...draftFilters,
|
||||
elapsedYears: draftFilters.elapsedYears > 0 ? draftFilters.elapsedYears : 20,
|
||||
};
|
||||
setDraftFilters(normalized);
|
||||
setAppliedFilters(normalized);
|
||||
|
||||
if (isListOpen) {
|
||||
loadAgedPipeList(1, normalized);
|
||||
}
|
||||
}, [draftFilters, isListOpen, loadAgedPipeList]);
|
||||
|
||||
const handleFeatureSelect = useCallback((data: any) => {
|
||||
const map = gisMaps.current;
|
||||
if (!map) return;
|
||||
|
||||
const select = GisApp.Module.select as any;
|
||||
if (!select) return;
|
||||
select.resetSelectedFeature();
|
||||
|
||||
let foundFeature = null;
|
||||
if (data && typeof data.getGeometry === "function" && typeof data.setStyle === "function") {
|
||||
foundFeature = data;
|
||||
} else {
|
||||
const featureId = data?.fid ?? data?.ftrIdn ?? data?.id;
|
||||
map
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.forEach((layer: any) => {
|
||||
const source = layer.getSource?.();
|
||||
if (!source?.getFeatureById) return;
|
||||
const feature = source.getFeatureById(String(featureId).trim());
|
||||
if (feature) foundFeature = feature;
|
||||
});
|
||||
}
|
||||
|
||||
if (foundFeature?.getGeometry) {
|
||||
map.getView().fit(foundFeature.getGeometry().getExtent(), {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
});
|
||||
select.selectFeature(foundFeature, true);
|
||||
}
|
||||
|
||||
setFeatureData(data);
|
||||
setIsDetailOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleOpenPipeList = useCallback(() => {
|
||||
setIsListOpen(true);
|
||||
loadAgedPipeList(1, appliedFilters);
|
||||
}, [appliedFilters, loadAgedPipeList]);
|
||||
|
||||
const handleTreePopup = useCallback(
|
||||
(layerNm: string) => {
|
||||
if (String(layerNm).includes("하수관로")) {
|
||||
handleOpenPipeList();
|
||||
}
|
||||
},
|
||||
[handleOpenPipeList],
|
||||
);
|
||||
|
||||
const handleListRowDoubleClick = useCallback(
|
||||
async (row: AgedPipeRow) => {
|
||||
try {
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(PIPE_LAYER_TYPE, row.fid);
|
||||
} catch (error) {
|
||||
console.error("노후관로 위치 이동 중 오류:", error);
|
||||
}
|
||||
|
||||
setFeatureData(row);
|
||||
setIsDetailOpen(true);
|
||||
setIsListOpen(false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
try {
|
||||
setIsListLoading(true);
|
||||
const response = await fetchAgedPipeRows(1, appliedFilters, 10000);
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: response.rows as JSON[],
|
||||
fileName: `노후관로_목록_${new Date().toISOString().slice(0, 10)}`,
|
||||
sheetNameArray: ["노후관로 목록"],
|
||||
headerRange: "A1:H1",
|
||||
headerMapping: AGED_PIPE_EXCEL_HEADER,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("노후관로 목록 다운로드 중 오류:", error);
|
||||
alert("노후관로 목록 다운로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setIsListLoading(false);
|
||||
}
|
||||
}, [appliedFilters, fetchAgedPipeRows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapReady || !gisMaps.current) return;
|
||||
|
||||
const map = gisMaps.current;
|
||||
const cleanups: Array<() => void> = [];
|
||||
const filterFn = (feature: any) =>
|
||||
matchesAgedPipeFilters(feature?.getProperties?.() ?? feature?.values_ ?? {}, appliedFilters);
|
||||
|
||||
const decorateLayer = (layer: any) => {
|
||||
if (layer?.get?.("lyrTyNm") !== PIPE_LAYER_TYPE) return;
|
||||
|
||||
const originalStyle =
|
||||
layer.get("_agedPipeOriginalStyle") ?? layer.getStyleFunction?.() ?? layer.getStyle?.();
|
||||
if (!layer.get("_agedPipeOriginalStyle")) {
|
||||
layer.set("_agedPipeOriginalStyle", originalStyle);
|
||||
}
|
||||
|
||||
layer.setStyle((feature: any, resolution: number) => {
|
||||
const baseStyle =
|
||||
typeof originalStyle === "function" ? originalStyle(feature, resolution) : originalStyle;
|
||||
const styles = Array.isArray(baseStyle) ? [...baseStyle] : baseStyle ? [baseStyle] : [];
|
||||
if (filterFn(feature)) {
|
||||
styles.push(agedPipeHighlightStyle);
|
||||
}
|
||||
return styles;
|
||||
});
|
||||
|
||||
const source = layer.getSource?.();
|
||||
if (source?.on) {
|
||||
const onLoadEnd = () => layer.changed?.();
|
||||
source.on("featuresloadend", onLoadEnd);
|
||||
cleanups.push(() => source.un?.("featuresloadend", onLoadEnd));
|
||||
}
|
||||
|
||||
layer.changed?.();
|
||||
};
|
||||
|
||||
map.getLayers().getArray().forEach(decorateLayer);
|
||||
const layers = map.getLayers();
|
||||
const onAdd = (event: any) => decorateLayer(event.element);
|
||||
layers.on?.("add", onAdd);
|
||||
cleanups.push(() => layers.un?.("add", onAdd));
|
||||
|
||||
return () => {
|
||||
cleanups.forEach((cleanup) => cleanup());
|
||||
};
|
||||
}, [appliedFilters, mapReady]);
|
||||
|
||||
const FilterOverlay = useMemo(
|
||||
() =>
|
||||
function FilterOverlayComponent(props: any) {
|
||||
return (
|
||||
<AgedPipeFilterPanel
|
||||
{...props}
|
||||
filters={draftFilters}
|
||||
onFiltersChange={setDraftFilters}
|
||||
onApply={handleApplyFilters}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[draftFilters, handleApplyFilters],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-container">
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={false}
|
||||
gisMaps={gisMaps}
|
||||
isMiniMap={false}
|
||||
searchView={EmptySearchView}
|
||||
detailView={AgedPipeDetailPanel}
|
||||
onFeatureSelect={handleFeatureSelect}
|
||||
onOpenPopup={handleTreePopup}
|
||||
featureData={featureData}
|
||||
setFeatureData={setFeatureData}
|
||||
isOpen={isDetailOpen}
|
||||
setIsOpen={setIsDetailOpen}
|
||||
isSearchModalOpen={isSearchModalOpen}
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={() => undefined}
|
||||
searchBar={<></>}
|
||||
waterDepth={FilterOverlay}
|
||||
showZoomLevel={true}
|
||||
defaultZoom={7.5}
|
||||
defaultLonLat={[127.75, 36]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AgedPipeListModal
|
||||
open={isListOpen}
|
||||
onOpenChange={setIsListOpen}
|
||||
filters={draftFilters}
|
||||
onFiltersChange={setDraftFilters}
|
||||
rows={listRows}
|
||||
total={totalCount}
|
||||
currentPage={currentPage}
|
||||
isLoading={isListLoading}
|
||||
onSearch={(page = 1) => {
|
||||
const normalized = {
|
||||
...draftFilters,
|
||||
elapsedYears: draftFilters.elapsedYears > 0 ? draftFilters.elapsedYears : 20,
|
||||
};
|
||||
setDraftFilters(normalized);
|
||||
setAppliedFilters(normalized);
|
||||
loadAgedPipeList(page, normalized);
|
||||
}}
|
||||
onDownload={handleDownload}
|
||||
onRowDoubleClick={handleListRowDoubleClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 모든 페이지
|
||||
*
|
||||
* 📄 Map Layout
|
||||
* Next.js App Router (app/) 기반 맵 레이아웃 컴포넌트
|
||||
*
|
||||
* - 메뉴 클릭 이벤트 처리 및 라우팅
|
||||
* - 현재 메뉴 정보 Zustand를 통한 상태 관리
|
||||
* - 시도/시군구/사업 구분/사업 유형 데이터 초기 로드
|
||||
* - NoFooterLayout을 통한 공통 레이아웃 적용
|
||||
* - QueryClient를 통한 데이터 캐싱 및 관리
|
||||
* - 경로 변경 시 메뉴 정보 동기화
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { NoFooterLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import useStore from "@/store/useStore";
|
||||
|
||||
export default function MapLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const { fetchCtpv, fetchSgg, fetchBIZ_SE_CD, fetchBIZ_TY_CD } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchCtpv();
|
||||
fetchSgg();
|
||||
fetchBIZ_SE_CD();
|
||||
fetchBIZ_TY_CD();
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
router.push(item.urlPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<NoFooterLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
{children}
|
||||
</NoFooterLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@common/components/ui";
|
||||
import React from "react";
|
||||
|
||||
interface InputSectionProps {
|
||||
title: string;
|
||||
data: { year: string; type: string, bizInfoNm: string, bizType: string }[];
|
||||
sectionKey: string;
|
||||
onInputChange: (section: string, index: number, field: string, value: string) => void;
|
||||
}
|
||||
|
||||
const InputSection: React.FC<InputSectionProps> = ({ title, data, sectionKey, onInputChange }) => {
|
||||
return (
|
||||
<div>
|
||||
{/* 섹션 제목 */}
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl mx-3 font-medium text-label whitespace-nowrap">{title}</h3>
|
||||
</div>
|
||||
{/* 테이블 */}
|
||||
<div className="card !m-[1.2rem] !p-0r">
|
||||
<Table variant="secondary" className="w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="border border-gray-300 px-2 py-2 text-sm w-auto whitespace-nowrap">구분</TableHead>
|
||||
<TableHead className="border border-gray-300 px-2 py-2 text-sm w-[80px] whitespace-nowrap">진단연도</TableHead>
|
||||
<TableHead className="border border-gray-300 px-2 py-2 text-sm w-auto whitespace-nowrap">사업명</TableHead>
|
||||
<TableHead className="border border-gray-300 px-2 py-2 text-sm w-auto whitespace-nowrap">유형</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((row, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal w-auto whitespace-nowrap">
|
||||
{row.type}
|
||||
</TableCell>
|
||||
<TableCell className="border border-gray-300 px-2 py-2 text-sm w-[80px] whitespace-nowrap">
|
||||
<input
|
||||
type="text"
|
||||
value={row.year}
|
||||
onChange={(e) => onInputChange(sectionKey, index, "year", e.target.value)}
|
||||
className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal w-auto">
|
||||
{row.bizInfoNm}
|
||||
</TableCell>
|
||||
<TableCell className="border border-gray-300 px-2 py-2 text-sm text-center leading-normal w-auto whitespace-nowrap">
|
||||
{row.bizType}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputSection;
|
||||
@@ -0,0 +1,162 @@
|
||||
import { MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from './table';
|
||||
|
||||
interface CodeMapperProps {
|
||||
layerName: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레이어별 코드 변환 함수
|
||||
* @param layerName - 레이어 이름
|
||||
* @param data - 변환할 데이터 객체
|
||||
* @returns 변환된 데이터 객체
|
||||
*/
|
||||
export const codeMapper = ({ layerName, data }: CodeMapperProps): any => {
|
||||
if (!data) return data;
|
||||
|
||||
const processedData = { ...data };
|
||||
|
||||
switch (layerName) {
|
||||
case '하수관로':
|
||||
// usg_cd 값 변환 (용도코드)
|
||||
if (processedData.usg_cd) {
|
||||
const usageCodeItem = MS_SWAGE_PPL_USE_CODES.find(
|
||||
code => code.val === processedData.usg_cd.trim()
|
||||
);
|
||||
if (usageCodeItem) {
|
||||
processedData.usg_cd = usageCodeItem.text;
|
||||
}
|
||||
}
|
||||
|
||||
// mqlt_cd 값 변환 (재질코드)
|
||||
if (processedData.mqlt_cd) {
|
||||
const materialCodeItem = MS_SWAGE_PPL_CODES.find(
|
||||
code => code.val === processedData.mqlt_cd.trim()
|
||||
);
|
||||
if (materialCodeItem) {
|
||||
processedData.mqlt_cd = materialCodeItem.text;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '하수맨홀':
|
||||
// usg_cd 값 변환 (용도코드)
|
||||
if (processedData.usg_cd) {
|
||||
const usageCodeItem = MS_SWAGE_MHL_USE_CODES.find(
|
||||
code => code.val.trim() === processedData.usg_cd.trim()
|
||||
);
|
||||
if (usageCodeItem) {
|
||||
processedData.usg_cd = usageCodeItem.text;
|
||||
}
|
||||
}
|
||||
|
||||
// mqlt_cd 값 변환 (재질코드)
|
||||
if (processedData.mqlt_cd) {
|
||||
const materialCodeItem = MS_SWAGE_MHL_CODES.find(
|
||||
code => code.val === processedData.mqlt_cd.trim()
|
||||
);
|
||||
if (materialCodeItem) {
|
||||
processedData.mqlt_cd = materialCodeItem.text;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '하수펌프장':
|
||||
// usg_cd 값 변환 (용도코드)
|
||||
if (processedData.usg_cd) {
|
||||
const usageCodeItem = DRTW_SEWAGE_PUMP_USE_CODES.find(
|
||||
code => code.val === processedData.usg_cd.trim()
|
||||
);
|
||||
if (usageCodeItem) {
|
||||
processedData.usg_cd = usageCodeItem.text;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '우수토실':
|
||||
// prrtymngYn 값 변환 (중점여부)
|
||||
if (processedData.prrtymngYn) {
|
||||
if (processedData.prrtymngYn === 'Y') {
|
||||
processedData.prrtymngYn = '중점우수토실';
|
||||
} else if (processedData.prrtymngYn === 'N') {
|
||||
processedData.prrtymngYn = '우수토실';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '중점우수토실':
|
||||
if (processedData.manage_no) {
|
||||
processedData.mqlt_cd = processedData.manage_no;
|
||||
}
|
||||
if (processedData.mesure_spot_se) {
|
||||
const mesureSpotSe = [{ id: '0', text: '합류식' }, { id: '1', text: '분류식' }];
|
||||
const mesureSpotSeItem = mesureSpotSe.find(
|
||||
code => code.id === processedData.mesure_spot_se.trim()
|
||||
);
|
||||
if (mesureSpotSeItem) processedData.mesure_spot_se = mesureSpotSeItem.text;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// 다른 레이어는 변환하지 않음
|
||||
break;
|
||||
}
|
||||
|
||||
return processedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* 저장 시 코드 역변환 함수 (한글 → 코드)
|
||||
* @param layerName - 레이어 이름
|
||||
* @param data - 역변환할 데이터 객체
|
||||
* @returns 역변환된 데이터 객체
|
||||
*/
|
||||
export const reverseCodeMapper = ({ layerName, data }: CodeMapperProps): any => {
|
||||
if (!data) return data;
|
||||
|
||||
const processedData = { ...data };
|
||||
|
||||
switch (layerName) {
|
||||
case '하수맨홀':
|
||||
// mqlt_cd 값 역변환 (텍스트 → 코드)
|
||||
if (processedData.mqlt_cd && typeof processedData.mqlt_cd === 'string') {
|
||||
const materialCodeItem = MS_SWAGE_MHL_CODES.find(
|
||||
code => code.text === processedData.mqlt_cd.trim()
|
||||
);
|
||||
if (materialCodeItem) {
|
||||
processedData.mqlt_cd = materialCodeItem.val;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '우수토실':
|
||||
// prrtymngYn 값 역변환 (텍스트 → Y/N)
|
||||
if (processedData.prrtymngYn) {
|
||||
if (processedData.prrtymngYn === '중점우수토실') {
|
||||
processedData.prrtymngYn = 'Y';
|
||||
} else if (processedData.prrtymngYn === '우수토실') {
|
||||
processedData.prrtymngYn = 'N';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '하수펌프장':
|
||||
// usg_cd 값 역변환 (텍스트 → 코드)
|
||||
if (processedData.usg_cd && typeof processedData.usg_cd === 'string') {
|
||||
const usageCodeItem = DRTW_SEWAGE_PUMP_USE_CODES.find(
|
||||
code => code.text === processedData.usg_cd.trim()
|
||||
);
|
||||
if (usageCodeItem) {
|
||||
processedData.usg_cd = usageCodeItem.val;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return processedData;
|
||||
};
|
||||
|
||||
// 기본 내보내기
|
||||
export default { codeMapper, reverseCodeMapper };
|
||||
@@ -0,0 +1,68 @@
|
||||
import MsSwagePpl from "../popupComponent/MsSwagePpl"; // 하수관로
|
||||
import sewagePump from "../popupComponent/sewagePump"; // 하수펌프
|
||||
import smh from "../popupComponent/smh";
|
||||
import Ssc from "../popupComponent/Ssc";
|
||||
import StormWaterPump from "../popupComponent/stormWaterPump";
|
||||
import SwtrmtFclt from "../popupComponent/SwtrmtFclt";
|
||||
|
||||
type LayerComponentProps = {
|
||||
featureData: any;
|
||||
isMainSsc?: boolean;
|
||||
isDrtwSewagePump?: boolean;
|
||||
rainMeasureData?: any;
|
||||
onSaveClick?: () => void;
|
||||
onFormDataChange?: (data: any) => void; // 추가
|
||||
chmbrXcnts: string;
|
||||
chmbrYdnts: string;
|
||||
gisObject?: Record<string, any>; // GIS 객체 추가
|
||||
};
|
||||
type LayerContentMap = {
|
||||
[key: string]: {
|
||||
component?: React.FC<LayerComponentProps>;
|
||||
props?: any;
|
||||
};
|
||||
};
|
||||
// 레이어 이름과 컴포넌트를 매핑
|
||||
// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업
|
||||
const layerContentMap: LayerContentMap = {
|
||||
"하수처리시설": { component: SwtrmtFclt, }, // 디지털 자료관 연계정보가 없으니 일단 주석 처리
|
||||
"하수처리구역": {},
|
||||
"우수토실": { component: Ssc, },
|
||||
"중점우수토실": { component: Ssc, }, // 중점우수토실 여부 설정
|
||||
"하수관로": { component: MsSwagePpl, },
|
||||
"하수맨홀": { component: smh, },
|
||||
"하수펌프장": { component: sewagePump },
|
||||
// "빗물펌프": { component: StormWaterPump },
|
||||
// "오수펌프": { component: sewagePump },
|
||||
};
|
||||
|
||||
// 동적 컴포넌트 반환 함수
|
||||
export const getLayerContentComponent = (layerNm: string): React.FC<LayerComponentProps> | null => {
|
||||
const component = layerContentMap[layerNm];
|
||||
if (!component || !component.component) {
|
||||
// component가 없으면 null 반환
|
||||
return () => null;
|
||||
}
|
||||
return component.component
|
||||
};
|
||||
// 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단)
|
||||
export const handleInputChange = (section: string,
|
||||
index: number,
|
||||
field: string,
|
||||
value: string,
|
||||
dataMap: {
|
||||
[key: string]: {
|
||||
data: { year: string; type: string, bizInfoNm: string, bizType: string }[];
|
||||
setData: React.Dispatch<React.SetStateAction<{ year: string; type: string, bizInfoNm: string, bizType: string }[]>>;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
const target = dataMap[section];
|
||||
if (!target) return;
|
||||
|
||||
const updated = [...target.data];
|
||||
if (updated[index]) {
|
||||
updated[index][field as keyof typeof updated[number]] = value;
|
||||
}
|
||||
target.setData(updated);
|
||||
};
|
||||
@@ -0,0 +1,848 @@
|
||||
// 하수관로
|
||||
export const MsSwagePplCol = [
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
{ id: 'inst_mng_no', title: '기관관리번호' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
{ id: 'pdmt_vl', title: '관경(mm)' },
|
||||
{ id: 'prlg_vl', title: '연장(m)' },
|
||||
{ id: 'usg_cd', title: '용도' },
|
||||
{ id: 'mqlt_cd', title: '재질' },
|
||||
{ id: 'avg_grdn', title: '평균구배(m/m)' },
|
||||
{ id: 'drtw_mltv_area', title: '오수배수면적(m²)' },
|
||||
{ id: 'excln_mltv_area', title: '우수배수면적(m²)' },
|
||||
{ id: 'tmpt_dpth', title: '시점깊이(m)' },
|
||||
{ id: 'tmpt_mhl_mng_no', title: '시점맨홀관리번호' },
|
||||
{ id: 'scend_dpth', title: '종점깊이(m)' },
|
||||
{ id: 'scend_mhl_mng_no', title: '종점맨홀관리번호' },
|
||||
{ id: 'trfln_cnt', title: '차선수' },
|
||||
{ id: 'vrtc_len', title: '세로길이(m)' },
|
||||
{ id: 'wdth_len', title: '가로길이(m)' },
|
||||
// { id: 'rnyssn_fvlc', title: '우기 시 유속(m/s)' },
|
||||
// { id: 'clar_fvlc', title: '건기 시 유속(m/s)' },
|
||||
// { id: 'mng_inst_cd', title: '관리기관코드' },
|
||||
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
|
||||
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
|
||||
// { id: 'lyrTypeNm', title: '레이어타입명' },
|
||||
]
|
||||
// 하수관로 searchModal 기본정보tab
|
||||
export const MsSwagePplCol2 = [
|
||||
{ id: 'rowNumber', title: '일련\n번호' },
|
||||
{ id: 'fcltMngNo', title: '시설\n관리번호' },
|
||||
{ id: 'instMngNo', title: '기관\n관리번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
{ id: "swtrmt_zone_nm", title: "처리구역" },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
{ id: 'usgCd', title: '용도' },
|
||||
{ id: 'pdmtVl', title: '관경\n(mm)' },
|
||||
{ id: 'prlgVl', title: '연장\n(m)' },
|
||||
// { id: 'avgGrdn', title: '평균구배\n(m/m)' },
|
||||
{ id: 'tmptDpth', title: '시점깊이\n(m)' },
|
||||
{ id: 'tmptMhlMngNo', title: '시점맨홀\n관리번호' },
|
||||
{ id: 'scendDpth', title: '종점깊이\n(m)' },
|
||||
{ id: 'scendMhlMngNo', title: '종점맨홀\n관리번호' },
|
||||
{ id: 'vrtcLen', title: '세로길이\n(m)' },
|
||||
{ id: 'wdthLen', title: '가로길이\n(m)' },
|
||||
{ id: 'fcltyShpCd', title: '시설물형태코드' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'mqltCd', title: '재질코드' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
// { id: 'lyrTypeNm', title: '레이어타입명' },
|
||||
]
|
||||
export const MsSwagePplExcelCol = {
|
||||
'fcltMngNo': '시설 관리번호',
|
||||
'instMngNo': '기관 관리번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'swtrmt_zone_nm': '처리구역',
|
||||
'instlYmd': '설치일자',
|
||||
'usgCd': '용도',
|
||||
'pdmtVl': '관경(mm)',
|
||||
'prlgVl': '연장(m)',
|
||||
'tmptDpth': '시점 깊이(m)',
|
||||
'tmptMhlMngNo': '시점맨홀 관리번호',
|
||||
'scendDpth': '종점 깊이(m)',
|
||||
'scendMhlMngNo': '종점맨홀 관리번호',
|
||||
'vrtcLen': '세로길이(m)',
|
||||
'wdthLen': '가로길이(m)',
|
||||
'fcltyShpCd': '시설물형태코드',
|
||||
'mqltCd': '재질코드',
|
||||
};
|
||||
// 하수관로 용도 코드표
|
||||
export const MS_SWAGE_PPL_USE_CODES = [
|
||||
{ val: "SBA000", text: "미분류" },
|
||||
{ val: "SBA001", text: "합류관" },
|
||||
{ val: "SBA002", text: "차집관" },
|
||||
{ val: "SBA003", text: "우수관" },
|
||||
{ val: "SBA004", text: "오수관" },
|
||||
{ val: "SBA999", text: "기타" },
|
||||
];
|
||||
// 하수관로 재질 코드표
|
||||
export const MS_SWAGE_PPL_CODES = [
|
||||
{ val: "MOP000", text: "미분류" },
|
||||
{ val: "MOP001", text: "닥타일주철관(DCIP)" },
|
||||
{ val: "MOP002", text: "강관(SP)" },
|
||||
{ val: "MOP003", text: "아연도강관(GS)" },
|
||||
{ val: "MOP004", text: "염화비닐관(PVC)" },
|
||||
{ val: "MOP005", text: "폴리에틸렌관(PE)" },
|
||||
{ val: "MOP006", text: "스테인레스관(SSP)" },
|
||||
{ val: "MOP007", text: "동관(COP)" },
|
||||
{ val: "MOP008", text: "P.S.콘크리트관(PC)" },
|
||||
{ val: "MOP009", text: "석면시멘트관(AS)" },
|
||||
{ val: "MOP010", text: "회주철관(CIP)" },
|
||||
{ val: "MOP011", text: "연관(LP)" },
|
||||
{ val: "MOP012", text: "흉관(HP)" },
|
||||
{ val: "MOP013", text: "철근콘크리트관(RC)" },
|
||||
{ val: "MOP014", text: "강관(ST)" },
|
||||
{ val: "MOP015", text: "주철관(CI)" },
|
||||
{ val: "MOP016", text: "화학제품류관(PV)" },
|
||||
{ val: "MOP017", text: "도관(VC)" },
|
||||
{ val: "MOP018", text: "석축(SE)" },
|
||||
{ val: "MOP019", text: "도복장 강관(SP)" },
|
||||
{ val: "MOP020", text: "흉관(EP)" },
|
||||
{ val: "MOP021", text: "아연도강관(GP)" },
|
||||
{ val: "MOP022", text: "청동관(BR)" },
|
||||
{ val: "MOP023", text: "고무몰딩(RM)" },
|
||||
{ val: "MOP024", text: "주강(CS)" },
|
||||
{ val: "MOP025", text: "VR관" },
|
||||
{ val: "MOP026", text: "HDPE관" },
|
||||
{ val: "MOP027", text: "시멘트라이닝주철관(C)" },
|
||||
{ val: "MOP028", text: "수도용 PE관(PE)" },
|
||||
{ val: "MOP029", text: "석면시멘트관(AC)" },
|
||||
{ val: "MOP030", text: "폴리에틸렌분체라이닝강관(PEP)" },
|
||||
{ val: "MOP031", text: "에폭시코팅강관(EPS)" },
|
||||
{ val: "MOP032", text: "HI-3P" },
|
||||
{ val: "MOP033", text: "철근콘크리트박스(RCB)" },
|
||||
{ val: "MOP034", text: "콘크리트중선함(CD)" },
|
||||
{ val: "MOP035", text: "후랙시불전선함(FP)" },
|
||||
{ val: "MOP036", text: "전선함(FD)" },
|
||||
{ val: "MOP038", text: "이중보온관" },
|
||||
{ val: "MOP039", text: "폴리에틸렌피복강관(PLP)" },
|
||||
{ val: "MOP041", text: "FRP" },
|
||||
{ val: "MOP042", text: "강재" },
|
||||
{ val: "MOP999", text: "기타" },
|
||||
{ val: "MOP800", text: "2중벽PE관" },
|
||||
{ val: "MOP801", text: "3중벽PE관" },
|
||||
{ val: "MOP802", text: "가스배관용 강관" },
|
||||
{ val: "MOP803", text: "강관(피복 Double, 용접접합)" },
|
||||
{ val: "MOP804", text: "강관(피복 Single, 용접접합)" },
|
||||
{ val: "MOP805", text: "강관(CP)" },
|
||||
{ val: "MOP806", text: "강철" },
|
||||
{ val: "MOP807", text: "강화플라스틱복합관" },
|
||||
{ val: "MOP808", text: "경질염화비닐관(HI-VP)" },
|
||||
{ val: "MOP809", text: "고강성PVC이중벽관" },
|
||||
{ val: "MOP810", text: "고광도PVC이중벽관" },
|
||||
{ val: "MOP811", text: "고밀도폴리에틸렌관(THP)" },
|
||||
{ val: "MOP812", text: "고밀도폴리에틸렌파상형이중벽관" },
|
||||
{ val: "MOP813", text: "공장제 박스관거" },
|
||||
{ val: "MOP814", text: "관로없음(알케이블)" },
|
||||
{ val: "MOP815", text: "나선형강관" },
|
||||
{ val: "MOP816", text: "내충격수도관" },
|
||||
{ val: "MOP817", text: "내충격용하수도용경질염화비닐관(HI-VG)" },
|
||||
{ val: "MOP818", text: "내충격HI-VG2관" },
|
||||
{ val: "MOP819", text: "내충격PVC하수관" },
|
||||
{ val: "MOP820", text: "녹석(돌산호과 일종)" },
|
||||
{ val: "MOP821", text: "동관(구리관, 용접)" },
|
||||
{ val: "MOP822", text: "둥근잔돌" },
|
||||
{ val: "MOP823", text: "무근콘크리트" },
|
||||
{ val: "MOP824", text: "백관" },
|
||||
{ val: "MOP825", text: "복합철판파형관" },
|
||||
{ val: "MOP826", text: "분체라이닝관" },
|
||||
{ val: "MOP827", text: "석축박스" },
|
||||
{ val: "MOP828", text: "석축철콘" },
|
||||
{ val: "MOP829", text: "수도용경질염화비관" },
|
||||
{ val: "MOP830", text: "수도용경질염화비닐이음관" },
|
||||
{ val: "MOP831", text: "수도용엑포시수지분체내외면코팅강관" },
|
||||
{ val: "MOP832", text: "수지파형강관(PCS)" },
|
||||
{ val: "MOP833", text: "수지CSP" },
|
||||
{ val: "MOP834", text: "스테인레스 주름관" },
|
||||
{ val: "MOP835", text: "스테인레스관(STS)" },
|
||||
{ val: "MOP836", text: "스테인레스스틸파이프관" },
|
||||
{ val: "MOP837", text: "아스콘" },
|
||||
{ val: "MOP838", text: "아스팔트" },
|
||||
{ val: "MOP839", text: "아스팔트도복장강관(ASP)" },
|
||||
{ val: "MOP840", text: "아연" },
|
||||
{ val: "MOP841", text: "아연도금(백관)" },
|
||||
{ val: "MOP842", text: "알루미늄" },
|
||||
{ val: "MOP843", text: "알루미늄합금" },
|
||||
{ val: "MOP844", text: "압력배관용 탄소강관" },
|
||||
{ val: "MOP845", text: "양철관" },
|
||||
{ val: "MOP846", text: "양철주름관(CSP)" },
|
||||
{ val: "MOP847", text: "연합인슈강판" },
|
||||
{ val: "MOP848", text: "와사관(GSP)" },
|
||||
{ val: "MOP849", text: "원심력철근콘크리트" },
|
||||
{ val: "MOP850", text: "유리섬유관(GRP)" },
|
||||
{ val: "MOP851", text: "유리섬유복합관(GRP)" },
|
||||
{ val: "MOP852", text: "이음매없는동및동합금관" },
|
||||
{ val: "MOP853", text: "일반배관용스테인리스강관" },
|
||||
{ val: "MOP854", text: "전압 콘크리트관" },
|
||||
{ val: "MOP855", text: "주름관" },
|
||||
{ val: "MOP856", text: "주름주철파형강관" },
|
||||
{ val: "MOP857", text: "지중 저압케이블 입상관" },
|
||||
{ val: "MOP858", text: "진동및전압철근콘크리트관" },
|
||||
{ val: "MOP859", text: "철근콘크리트" },
|
||||
{ val: "MOP860", text: "철근콘크리트 블록" },
|
||||
{ val: "MOP861", text: "철근콘크리트조립흙막이" },
|
||||
{ val: "MOP862", text: "철판매입관" },
|
||||
{ val: "MOP863", text: "청동" },
|
||||
{ val: "MOP864", text: "코아식프리스트레스트콘크리트관" },
|
||||
{ val: "MOP865", text: "콘크리트" },
|
||||
{ val: "MOP866", text: "콘크리트 폴륨관" },
|
||||
{ val: "MOP867", text: "콘크리트블록" },
|
||||
{ val: "MOP868", text: "탄소강" },
|
||||
{ val: "MOP869", text: "탄소강(카본스틸)" },
|
||||
{ val: "MOP870", text: "탄소주강" },
|
||||
{ val: "MOP871", text: "트러후" },
|
||||
{ val: "MOP872", text: "파선형강관(RCF)" },
|
||||
{ val: "MOP873", text: "파형강관(CSP)" },
|
||||
{ val: "MOP874", text: "파형관" },
|
||||
{ val: "MOP875", text: "파형플라스틱관" },
|
||||
{ val: "MOP876", text: "폴리머관" },
|
||||
{ val: "MOP877", text: "폴리에스테르수지콘크리트관" },
|
||||
{ val: "MOP878", text: "폴리에틸렌 전선관" },
|
||||
{ val: "MOP879", text: "폴리에틸렌 피복강관" },
|
||||
{ val: "MOP880", text: "폴리에틸렌관 1호관" },
|
||||
{ val: "MOP881", text: "폴리에틸렌관 2호관" },
|
||||
{ val: "MOP882", text: "폴리에틸렌관(2중벽)" },
|
||||
{ val: "MOP883", text: "폴리에틸렌관(3중벽)" },
|
||||
{ val: "MOP884", text: "폴리에틸렌분체라이닝" },
|
||||
{ val: "MOP885", text: "폴리에틸렌이중벽하수관" },
|
||||
{ val: "MOP886", text: "폴리탄산에스테르" },
|
||||
{ val: "MOP887", text: "프리스트레스트콘크리트실린더관" },
|
||||
{ val: "MOP888", text: "플라스틱" },
|
||||
{ val: "MOP889", text: "플라스틱주름관" },
|
||||
{ val: "MOP890", text: "하수도용고강성경질염화비닐이중벽주름관" },
|
||||
{ val: "MOP891", text: "현장타설관" },
|
||||
{ val: "MOP892", text: "현장타설철근콘크리트관" },
|
||||
{ val: "MOP893", text: "흑관" },
|
||||
{ val: "MOP894", text: "흙" },
|
||||
{ val: "MOP895", text: "AP" },
|
||||
{ val: "MOP896", text: "API관" },
|
||||
{ val: "MOP897", text: "ASP" },
|
||||
{ val: "MOP898", text: "CMCIP" },
|
||||
{ val: "MOP899", text: "CSP" },
|
||||
{ val: "MOP900", text: "DRP" },
|
||||
{ val: "MOP901", text: "DSP" },
|
||||
{ val: "MOP902", text: "DTC관" },
|
||||
{ val: "MOP903", text: "ELP" },
|
||||
{ val: "MOP904", text: "EPP" },
|
||||
{ val: "MOP905", text: "FC관" },
|
||||
{ val: "MOP906", text: "H" },
|
||||
{ val: "MOP907", text: "H6" },
|
||||
{ val: "MOP908", text: "HCP" },
|
||||
{ val: "MOP909", text: "HI_2P관" },
|
||||
{ val: "MOP910", text: "HI-KP" },
|
||||
{ val: "MOP911", text: "HL" },
|
||||
{ val: "MOP912", text: "IP" },
|
||||
{ val: "MOP913", text: "KCGP" },
|
||||
{ val: "MOP914", text: "KP관" },
|
||||
{ val: "MOP915", text: "P2S관" },
|
||||
{ val: "MOP916", text: "PC박스(PCB)" },
|
||||
{ val: "MOP917", text: "PCS" },
|
||||
{ val: "MOP918", text: "PDP" },
|
||||
{ val: "MOP919", text: "PE이중벽관" },
|
||||
{ val: "MOP920", text: "PE적측벽1종관" },
|
||||
{ val: "MOP921", text: "PFP관" },
|
||||
{ val: "MOP922", text: "PG관" },
|
||||
{ val: "MOP923", text: "PIP관(이중보온관)" },
|
||||
{ val: "MOP924", text: "PLCSP" },
|
||||
{ val: "MOP925", text: "PS" },
|
||||
{ val: "MOP926", text: "PUB" },
|
||||
{ val: "MOP927", text: "PVB" },
|
||||
{ val: "MOP928", text: "PVC이중벽관(PVC2)" },
|
||||
{ val: "MOP929", text: "PVC-DC" },
|
||||
{ val: "MOP930", text: "PVC-DV" },
|
||||
{ val: "MOP931", text: "SBK" },
|
||||
{ val: "MOP932", text: "SCP" },
|
||||
{ val: "MOP933", text: "SEB" },
|
||||
{ val: "MOP934", text: "SIS관(이중보온관)" },
|
||||
{ val: "MOP935", text: "SMC" },
|
||||
{ val: "MOP936", text: "STW" },
|
||||
{ val: "MOP937", text: "SUS" },
|
||||
{ val: "MOP938", text: "THP나선관" },
|
||||
{ val: "MOP939", text: "U관" },
|
||||
{ val: "MOP940", text: "VG2" },
|
||||
{ val: "MOP941", text: "WSP(와사피)" },
|
||||
{ val: "MOP942", text: "Steel Grating" },
|
||||
{ val: "MOP943", text: "모래" },
|
||||
{ val: "MOP944", text: "인조석" },
|
||||
{ val: "MOP945", text: "SPOL" },
|
||||
{ val: "MOP946", text: "PCF관(강판보강 PE 나선형관)" },
|
||||
{ val: "MOP947", text: "PVC3(PVC3중벽관)" },
|
||||
{ val: "MOP948", text: "PE다중벽관" },
|
||||
{ val: "MOP977", text: "전주용입상관" },
|
||||
{ val: "MOP978", text: "MD관" },
|
||||
];
|
||||
// 하수관로 'fcltyShpCd' '시설물형태코드'
|
||||
export const MS_SWAGE_PPL_SHAPE = [
|
||||
{ val: "FOR000 ", text: "미분류" },
|
||||
{ val: "FOR001 ", text: "원형" },
|
||||
{ val: "FOR002 ", text: "원추형" },
|
||||
{ val: "FOR003 ", text: "원통형" },
|
||||
{ val: "FOR004 ", text: "구형" },
|
||||
{ val: "FOR005 ", text: "타원형" },
|
||||
{ val: "FOR006 ", text: "난원" },
|
||||
{ val: "FOR007 ", text: "반원형" },
|
||||
|
||||
{ val: "FOR010 ", text: "피라미드형" },
|
||||
{ val: "FOR011 ", text: "사면체형" },
|
||||
{ val: "FOR012 ", text: "기둥,축" },
|
||||
{ val: "FOR013 ", text: "돛대,활대처럼 둥글고 긴 원재" },
|
||||
{ val: "FOR014 ", text: "아치형" },
|
||||
{ val: "FOR015 ", text: "다중아치형" },
|
||||
{ val: "FOR016 ", text: "돔형" },
|
||||
{ val: "FOR017 ", text: "고형체형" },
|
||||
|
||||
{ val: "FOR020 ", text: "사각형" },
|
||||
{ val: "FOR021 ", text: "평방형" },
|
||||
{ val: "FOR022 ", text: "다이아몬드형" },
|
||||
{ val: "FOR023 ", text: "사다리꼴" },
|
||||
{ val: "FOR024 ", text: "각형" },
|
||||
{ val: "FOR025 ", text: "마제형관" },
|
||||
{ val: "FOR026 ", text: "태형" },
|
||||
|
||||
{ val: "FOR030 ", text: "A형" },
|
||||
{ val: "FOR031 ", text: "H형" },
|
||||
{ val: "FOR032 ", text: "I형" },
|
||||
{ val: "FOR033 ", text: "Y형" },
|
||||
{ val: "FOR034 ", text: "T형" },
|
||||
|
||||
{ val: "FOR050 ", text: "불규칙한 형태" },
|
||||
|
||||
{ val: "FOR900 ", text: "계란형" },
|
||||
{ val: "FOR901 ", text: "구(사각)형" },
|
||||
{ val: "FOR902 ", text: "높이조절철개형" },
|
||||
{ val: "FOR903 ", text: "박스형(RCB)" },
|
||||
{ val: "FOR904 ", text: "반월개거" },
|
||||
{ val: "FOR905 ", text: "반월개거(측구)" },
|
||||
{ val: "FOR906 ", text: "벽돌조" },
|
||||
{ val: "FOR907 ", text: "사각철판" },
|
||||
{ val: "FOR908 ", text: "석축개거" },
|
||||
{ val: "FOR909 ", text: "석축개거(측구)" },
|
||||
{ val: "FOR910 ", text: "승수로" },
|
||||
{ val: "FOR911 ", text: "옹벽개거" },
|
||||
{ val: "FOR912 ", text: "옹벽개거(측구)" },
|
||||
{ val: "FOR913 ", text: "이단" },
|
||||
{ val: "FOR914 ", text: "이중침전형" },
|
||||
{ val: "FOR915 ", text: "자연개거" },
|
||||
{ val: "FOR916 ", text: "자연개거(측구)" },
|
||||
{ val: "FOR917 ", text: "조립식" },
|
||||
{ val: "FOR918 ", text: "지상관" },
|
||||
{ val: "FOR919 ", text: "지상단구" },
|
||||
{ val: "FOR920 ", text: "지상쌍구" },
|
||||
{ val: "FOR921 ", text: "지하단구" },
|
||||
{ val: "FOR922 ", text: "지하쌍구" },
|
||||
{ val: "FOR923 ", text: "직사각형" },
|
||||
{ val: "FOR924 ", text: "타설식" },
|
||||
{ val: "FOR925 ", text: "토류형" },
|
||||
{ val: "FOR926 ", text: "흄관원형" },
|
||||
{ val: "FOR927 ", text: "L형측구" },
|
||||
{ val: "FOR928 ", text: "LO형측구" },
|
||||
{ val: "FOR929 ", text: "LU형측구" },
|
||||
{ val: "FOR930 ", text: "PVC이중벽관" },
|
||||
{ val: "FOR931 ", text: "RCB 맨홀" },
|
||||
{ val: "FOR932 ", text: "U형" },
|
||||
{ val: "FOR933 ", text: "U형측구(미복개)" },
|
||||
{ val: "FOR934 ", text: "U형측구(복개)" },
|
||||
|
||||
{ val: "FOR999 ", text: "기타" }
|
||||
];
|
||||
// 하수처리시설
|
||||
export const SwtrmtFcltCol = [
|
||||
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
|
||||
{ id: 'fclt_cpct', title: '시설용량(㎥/d)' },
|
||||
{ id: 'rgn_expln', title: '지역설명' },
|
||||
{ id: 'cmcn_ymd', title: '준공일자' },
|
||||
{ id: 'oprtng_strt_ymd', title: '가동개시일자' },
|
||||
{ id: 'clsg_ymd', title: '폐쇄일자' },
|
||||
// { id: 'prcs_msrc_cd', title: '처리공법코드' },
|
||||
// { id: 'lae_cd', title: '지자체코드' },
|
||||
{ id: 'lae_nm', title: '지자체명' },
|
||||
{ id: 'ctpv_nm', title: '시도명' },
|
||||
{ id: 'sgg_nm', title: '시군구명' },
|
||||
// { id: 'sarea_cd', title: '권역코드' },
|
||||
{ id: 'sarea_nm', title: '권역명' },
|
||||
{ id: 'lrge_sarea_nm', title: '대권역명' },
|
||||
{ id: 'rvr_nm', title: '하천명' },
|
||||
{ id: 'addr', title: '주소' },
|
||||
{ id: 'dtl_addr', title: '상세주소' },
|
||||
{ id: 'rdnm_addr', title: '도로명주소' },
|
||||
{ id: 'dtl_rdnm_addr', title: '상세도로명주소' },
|
||||
{ id: 'dschg_wtst_nm', title: '방류수계명' },
|
||||
{ id: 'plln_area', title: '부지면적(m²)' },
|
||||
{ id: 'mng_rprtv_bzenty_nm', title: '관리대행업제명' },
|
||||
{ id: 'swtrmt_ppltn_cnt', title: '하수처리인구수(인)' },
|
||||
{ id: 'prcs_zone_area_sum', title: '처리구역면적합계(m²)' },
|
||||
{ id: 'xcrd', title: 'X좌표' },
|
||||
{ id: 'ycrd', title: 'Y좌표' },
|
||||
// { id: 'oprtngSttsCd', title: '가동상태코드' },
|
||||
{ id: 'rprs_msrc_cn', title: '대표공법내용' },
|
||||
// { id: 'stty_crtr_bod', title: '법정기준생물화학적산소요구량(mg/L)' },
|
||||
// { id: 'stty_crtr_toc', title: '법정기준TOC((mg/L))' },
|
||||
// { id: 'stty_crtr_ss', title: '법정기준SS(mg/L)' },
|
||||
// { id: 'stty_crtr_tn', title: '법정기준총질소(mg/L)' },
|
||||
// { id: 'stty_crtr_tph', title: '법정기준총인(mg/L)' },
|
||||
// { id: 'stty_crtr_clbcl_cnt', title: '법정기준대장균수(군수/100mL)' },
|
||||
{ id: 'bsn_nm', title: '유역명' },
|
||||
// { id: 'obsrvnSptCd', title: '관측지점코드' },
|
||||
];
|
||||
export const SwtrmtFcltCol2 = [
|
||||
{ id: 'rowNumber', title: '일련\n번호' },
|
||||
{ id: 'swtrmtFcltNm', title: '하수처리\n시설명' },
|
||||
{ id: 'fcltCpct', title: '시설\n용량\n(㎥/d)' },
|
||||
{ id: 'rgnExpln', title: '지역설명' },
|
||||
// { id: 'cmcnYmd', title: '준공일자' },
|
||||
{ id: 'oprtngStrtYmd', title: '가동개시일자' },
|
||||
// { id: 'clsgYmd', title: '폐쇄일자' },
|
||||
// { id: 'prcsMsrcCd', title: '처리공법코드' },
|
||||
// { id: 'laeCd', title: '지자체코드' },
|
||||
{ id: 'laeNm', title: '지자체명' },
|
||||
// { id: 'ctpvNm', title: '시도명' },
|
||||
// { id: 'sggNm', title: '시군구명' },
|
||||
// { id: 'sareaCd', title: '권역코드' },
|
||||
{ id: 'sareaNm', title: '권역명' },
|
||||
// { id: 'lrgeSareaNm', title: '대권역명' },
|
||||
{ id: 'rvrNm', title: '하천명' },
|
||||
// { id: 'addr', title: '주소' },
|
||||
// { id: 'dtlAddr', title: '상세주소' },
|
||||
// { id: 'rdnmAddr', title: '도로명주소' },
|
||||
// { id: 'dtlRdnmAddr', title: '상세도로명주소' },
|
||||
{ id: 'dschgWtstNm', title: '방류수계명' },
|
||||
// { id: 'pllnArea', title: '부지면적(m²)' },
|
||||
// { id: 'mngRprtvBzentyNm', title: '관리대행업체명' },
|
||||
{ id: 'swtrmtPpltnCnt', title: '하수\n처리\n인구수\n(인)' },
|
||||
// { id: 'prcsZoneAreaSum', title: '처리구역면적합계(m²)' },
|
||||
// { id: 'xcrd', title: 'X좌표' },
|
||||
// { id: 'ycrd', title: 'Y좌표' },
|
||||
// { id: 'oprtngSttsCd', title: '가동상태코드' },
|
||||
{ id: 'rprsMsrcCn', title: '대표\n공법\n내용' },
|
||||
// { id: 'sttyCrtrBod', title: '법정기준생물화학적산소요구량(mg/L)' },
|
||||
// { id: 'sttyCrtrToc', title: '법정기준TOC(mg/L)' },
|
||||
// { id: 'sttyCrtrSs', title: '법정기준SS(mg/L)' },
|
||||
// { id: 'sttyCrtrTn', title: '법정기준총질소(mg/L)' },
|
||||
// { id: 'sttyCrtrTph', title: '법정기준총인(mg/L)' },
|
||||
// { id: 'sttyCrtrClbclCnt', title: '법정기준대장균수(군수/100mL)' },
|
||||
// { id: 'bsnNm', title: '유역명' },
|
||||
// { id: 'obsrvnSptCd', title: '관측지점코드' },
|
||||
];
|
||||
export const SwtrmtFcltExcelCol = {
|
||||
'swtrmtFcltNm': '하수처리시설명',
|
||||
'fcltCpct': '시설 용량(㎥/d)',
|
||||
'rgnExpln': '지역설명',
|
||||
'oprtngStrtYmd': '가동개시 일자',
|
||||
'laeNm': '지자체명',
|
||||
'sareaNm': '권역명',
|
||||
'rvrNm': '하천명',
|
||||
'dschgWtstNm': '방류수계명',
|
||||
'swtrmtPpltnCnt': '하수 처리 인구수(인)',
|
||||
'rprsMsrcCn': '대표 공법 내용'
|
||||
};
|
||||
|
||||
// 하수처리구역
|
||||
export const SwtrmtPrcsZoneCol = [
|
||||
// { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
|
||||
{ id: 'swtrmt_zone_nm', title: '하수처리구역명' },
|
||||
// { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' },
|
||||
// { id: 'ctpv_cd', title: '시도코드' },
|
||||
{ id: 'ctpv_nm', title: '시도명' },
|
||||
// { id: 'sgg_cd', title: '시군구코드' },
|
||||
{ id: 'sgg_nm', title: '시군구명' },
|
||||
{ id: 'swtrmt_fclt_nm', title: '하수처리시설명' },
|
||||
];
|
||||
export const SwtrmtPrcsZoneCol2 = [
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
// { id: 'swtrmtZoneSn', title: '하수처리구역 일련번호' },
|
||||
{ id: 'swtrmtZoneNm', title: '하수처리구역명' },
|
||||
// { id: 'swtrmt_fclt_cd', title: '하수처리시설코드' },
|
||||
// { id: 'ctpv_cd', title: '시도코드' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
// { id: 'sgg_cd', title: '시군구코드' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
{ id: 'swtrmtFcltNm', title: '하수처리시설명' },
|
||||
];
|
||||
export const SwtrmtPrcsZoneExcelCol = {
|
||||
'swtrmtZoneNm': '하수처리구역명',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'swtrmtFcltNm': '하수처리시설명'
|
||||
};
|
||||
|
||||
// 하수맨홀
|
||||
export const MsSwageMhlCol = [
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mng_inst_cd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'last_drdgn_ymd', title: '최종준설일자' },
|
||||
// { id: 'usg_cd', title: '용도코드' },
|
||||
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
|
||||
// { id: 'mhl_knd_cd', title: '맨홀종류코드' },
|
||||
{ id: 'swage_mhL_diam_vl', title: '하수맨홀구경(mm)' },
|
||||
{ id: 'mqlt_cd', title: '재질' },
|
||||
{ id: 'wdth_len', title: '가로길이(m)' },
|
||||
{ id: 'vrtc_len', title: '세로길이(m)' },
|
||||
// { id: 'ang_dir', title: '표고' },
|
||||
{ id: 'altd_vl', title: '지표고(EL.m)' },
|
||||
{ id: 'dpthl_vl', title: '맨홀저고(EL.m)' },
|
||||
// { id: 'inst_mng_no', title: '기관관리번호' }
|
||||
];
|
||||
export const MsSwageMhlCol2 = [
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'lastDrdgnYmd', title: '최종준설일자' },
|
||||
{ id: 'usgCd', title: '용도' },
|
||||
// { id: 'fcltyShpCd', title: '시설물형태코드' },
|
||||
// { id: 'mhlKndCd', title: '맨홀종류코드' },
|
||||
{ id: 'swageMhlDiamVl', title: '하수맨홀구경(mm)' },
|
||||
// { id: 'mqltCd', title: '재질코드' },
|
||||
{ id: 'wdthLen', title: '가로길이(m)' },
|
||||
{ id: 'vrtcLen', title: '세로길이(m)' },
|
||||
// { id: 'angDir', title: '표고' },
|
||||
{ id: 'altdVl', title: '지표고(EL.m)' },
|
||||
{ id: 'dpthlVl', title: '맨홀저고(EL.m)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' }
|
||||
];
|
||||
export const MsSwageMhlExcelCol = {
|
||||
'fcltMngNo': '시설관리번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'instlYmd': '설치일자',
|
||||
'usgCd': '용도',
|
||||
'swageMhlDiamVl': '하수맨홀구경(mm)',
|
||||
'wdthLen': '가로길이(m)',
|
||||
'vrtcLen': '세로길이(m)',
|
||||
'altdVl': '지표고(EL.m)',
|
||||
'dpthlVl': '맨홀저고(EL.m)'
|
||||
};
|
||||
// 하수맨홀 뚜껑재질 코드표
|
||||
export const MS_SWAGE_MHL_CODES = [
|
||||
{ val: "SBC000", text: "미분류" },
|
||||
{ val: "SBC001", text: "주철" },
|
||||
{ val: "SBC002", text: "철판" },
|
||||
{ val: "SBC003", text: "콘크리트" },
|
||||
{ val: "SBC004", text: "그레이팅" },
|
||||
{ val: "SBC091", text: "칼라" },
|
||||
{ val: "SBC092", text: "S.G" },
|
||||
{ val: "SBC999", text: "기타" },
|
||||
{ val: "SBC900", text: "각주철" },
|
||||
{ val: "SBC901", text: "강재" },
|
||||
{ val: "SBC902", text: "나무" },
|
||||
{ val: "SBC903", text: "나무,널판지,합판" },
|
||||
{ val: "SBC904", text: "대리석" },
|
||||
{ val: "SBC905", text: "뚜껑없음" },
|
||||
{ val: "SBC906", text: "보도용콘크리트" },
|
||||
{ val: "SBC907", text: "스틸(ST)" },
|
||||
{ val: "SBC908", text: "신형 CON" },
|
||||
{ val: "SBC909", text: "알루미늄" },
|
||||
{ val: "SBC910", text: "염화비닐(PE)" },
|
||||
{ val: "SBC911", text: "인조석" },
|
||||
{ val: "SBC912", text: "인조석(주철)" },
|
||||
{ val: "SBC913", text: "인조CONC" },
|
||||
{ val: "SBC914", text: "철, 철근" },
|
||||
{ val: "SBC915", text: "칼라콘크리트" },
|
||||
{ val: "SBC916", text: "플라스틱" },
|
||||
{ val: "SBC917", text: "PVC" },
|
||||
{ val: "SBC918", text: "SP" },
|
||||
{ val: "SBC919", text: "SS" },
|
||||
{ val: "SBC920", text: "닥타일주철" },
|
||||
{ val: "SBC921", text: "철근G" },
|
||||
{ val: "SBC922", text: "폴리에틸렌(PE)" },
|
||||
{ val: "SBC923", text: "회주철" }
|
||||
];
|
||||
// 하수맨홀 용도 코드표
|
||||
export const MS_SWAGE_MHL_USE_CODES = [
|
||||
{ val: "SMU000 ", text: "미분류" },
|
||||
{ val: "SMU001 ", text: "우수" },
|
||||
{ val: "SMU002 ", text: "오수" },
|
||||
{ val: "SMU003 ", text: "집수" },
|
||||
{ val: "SMU004 ", text: "합류" },
|
||||
{ val: "SMU005 ", text: "차집" },
|
||||
{ val: "SMU999 ", text: "기타" },
|
||||
{ val: "SMU900 ", text: "1호" },
|
||||
{ val: "SMU901 ", text: "2호" },
|
||||
{ val: "SMU902 ", text: "3호" },
|
||||
{ val: "SMU903 ", text: "4호" },
|
||||
{ val: "SMU904 ", text: "5호" },
|
||||
{ val: "SMU905 ", text: "6호" },
|
||||
{ val: "SMU906 ", text: "7호" },
|
||||
{ val: "SMU907 ", text: "8호" },
|
||||
{ val: "SMU908 ", text: "구거용맨홀" },
|
||||
{ val: "SMU909 ", text: "부관부착맨홀" },
|
||||
{ val: "SMU910 ", text: "조립식맨홀" },
|
||||
{ val: "SMU911 ", text: "특1호" },
|
||||
{ val: "SMU912 ", text: "특2호" },
|
||||
{ val: "SMU913 ", text: "특3호" },
|
||||
{ val: "SMU914 ", text: "특4호" },
|
||||
{ val: "SMU915 ", text: "특5호" },
|
||||
{ val: "SMU916 ", text: "하수맨홀용도" },
|
||||
{ val: "SMU917 ", text: "현장타설관거용" }
|
||||
];
|
||||
|
||||
// 우수펌프장
|
||||
export const ExclnSewagePumpCol = [
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'swage_pmstn_nm', title: '하수펌프장명' },
|
||||
{ id: 'plln_area', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altd_vl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlv_vl', title: '수위(m)' },
|
||||
{ id: 'excln_wpmp_ablt_vl', title: '우수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
export const ExclnSewagePumpCol2 = [
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'swagePmstnNm', title: '하수펌프장명' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'pllnArea', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altdVl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlvVl', title: '수위(m)' },
|
||||
{ id: 'exclnWpmpAbltVl', title: '우수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
export const ExclnSewagePumpExcelCol = {
|
||||
'fcltMngNo': '시설관리번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'swagePmstnNm': '하수펌프장명',
|
||||
'instlYmd': '설치일자',
|
||||
'dailyPrcsCpct': '일일처리용량(㎥)',
|
||||
'maxImpndCpct': '최대저수용량(㎥)',
|
||||
'altdVl': '표고(EL.m)',
|
||||
'wtlvVl': '수위(m)',
|
||||
'exclnWpmpAbltVl': '우수양수능력(m²/d)'
|
||||
};
|
||||
// 오수펌프장
|
||||
export const DrtwSewagePumpCol = [
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
{ id: 'swage_pmstn_nm', title: '하수펌프장명' },
|
||||
{ id: 'plln_area', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'daily_prcs_cpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altd_vl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlv_vl', title: '수위(m)' },
|
||||
{ id: 'excln_wpmp_ablt_vl', title: '오수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
export const DrtwSewagePumpCol2 = [
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
// { id: 'fid', title: 'FID' },
|
||||
// { id: 'geom', title: 'geometry' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
// { id: 'mapshtNo', title: '도엽번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'swagePmstnNm', title: '하수펌프장명' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'pllnArea', title: '부지면적(m²)' },
|
||||
// { id: 'opngSttsCd', title: '개통상태코드' },
|
||||
// { id: 'usgCd', title: '용도코드' },
|
||||
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
|
||||
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
|
||||
{ id: 'altdVl', title: '표고(EL.m)' },
|
||||
{ id: 'wtlvVl', title: '수위(m)' },
|
||||
{ id: 'exclnWpmpAbltVl', title: '오수양수능력(m²/d)' },
|
||||
// { id: 'instMngNo', title: '기관관리번호' },
|
||||
]
|
||||
// 펌프장 용도 코드표
|
||||
export const DRTW_SEWAGE_PUMP_USE_CODES = [
|
||||
{ val: "SBE000", text: "미분류" },
|
||||
{ val: "SBE001", text: "빗물펌프장" },
|
||||
{ val: "SBE002", text: "오수펌프장" },
|
||||
{ val: "SBE003", text: "처리장내펌프장" },
|
||||
{ val: "SBE900", text: "오수배제" },
|
||||
{ val: "SBE901", text: "우수배제" },
|
||||
{ val: "SBE999", text: "기타" },
|
||||
];
|
||||
|
||||
// 우수토실
|
||||
export const stofcbrCol = [
|
||||
// { id: 'ftre_smbol_cd', title: '지형지물부호코드' },
|
||||
{ id: 'fclt_mng_no', title: '시설관리번호' },
|
||||
// { id: 'pbadms_emd_cd', title: '행정읍면동코드' },
|
||||
{ id: 'mapsht_no', title: '도엽번호' },
|
||||
// { id: 'mng_inst_cd', title: '관리기관코드' },
|
||||
{ id: 'instl_ymd', title: '설치일자' },
|
||||
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
|
||||
{ id: 'worflw_wall_hgt', title: '월류벽높이(m)' },
|
||||
// { id: 'mhl_exst_yn_cd', title: '맨홀존재여부코드' },
|
||||
// { id: 'swage_mhl_ftre_smbol_cd', title: '하수맨홀지형지물부호코드' },
|
||||
{ id: 'swage_mhl_mng_no', title: '하수맨홀관리번호' },
|
||||
{ id: 'drtn_angl', title: '방향각도(°)' },
|
||||
]
|
||||
export const stofcbrCol2 = [
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
// { id: 'ftreSmbolCd', title: '지형지물부호코드' },
|
||||
{ id: 'fcltMngNo', title: '시설관리번호' },
|
||||
// { id: 'pbadmsEmdCd', title: '행정읍면동코드' },
|
||||
{ id: 'mapshtNo', title: '도엽번호' },
|
||||
{ id: 'ctpvNm', title: '시도' },
|
||||
{ id: 'sggNm', title: '시군구' },
|
||||
// { id: 'mngInstCd', title: '관리기관코드' },
|
||||
{ id: 'instlYmd', title: '설치일자' },
|
||||
// { id: 'fcltyShpCd', title: '시설물형태코드' },
|
||||
{ id: 'worflwWallHgt', title: '월류벽높이(m)' },
|
||||
// { id: 'mhlExstYnCd', title: '맨홀존재여부코드' },
|
||||
// { id: 'swageMhlFtreSmbolCd', title: '하수맨홀지형지물부호코드' },
|
||||
{ id: 'swageMhlMngNo', title: '하수맨홀관리번호' },
|
||||
{ id: 'drtnAngl', title: '방향각도(°)' },
|
||||
]
|
||||
export const stofcbrExcelCol = {
|
||||
'fcltMngNo': '시설관리번호',
|
||||
'mapshtNo': '도엽번호',
|
||||
'ctpvNm': '시도',
|
||||
'sggNm': '시군구',
|
||||
'instlYmd': '설치일자',
|
||||
'worflwWallHgt': '월류벽높이(m)',
|
||||
'swageMhlMngNo': '하수맨홀관리번호',
|
||||
'drtnAngl': '방향각도(°)'
|
||||
};
|
||||
|
||||
// 중점우수토실 측정지점
|
||||
export const untrtSwgRainCol1 = [
|
||||
{ id: 'manage_no', title: '관리번호' },
|
||||
{ id: 'swr_ppl_eqp_nm', title: '처리구역' },
|
||||
{ id: 'swr_ppl_trt_nm', title: '처리분구' },
|
||||
{ id: 'addr', title: '인근주소' },
|
||||
{ id: 'chmbr_xcnts', title: '토실 위도' },
|
||||
{ id: 'chmbr_ydnts', title: '토실 경도' }
|
||||
];
|
||||
|
||||
// 중점우수토실 측정지점 시설현황
|
||||
export const untrtSwgRainCol2 = [
|
||||
{ id: 'mesure_spot_se', title: '구분' }, // 유량계측정방식 기타
|
||||
{ id: 'discharge_pipe_spec', title: '방류관로 규격(m)' },
|
||||
{ id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' },
|
||||
{ id: 'swr_trt_popl', title: '하수처리 인구(인)' },
|
||||
{ id: 'screen_instl_at', title: '스크린설치 여부' },
|
||||
{ id: 'weir_hg', title: '위어 높이(m)' },
|
||||
{ id: 'weir_lt', title: '위어 길이(m)' }
|
||||
];
|
||||
|
||||
// 중점우수토실 강우정보 (id값 틀릴 수 있음)
|
||||
export const untrtSwgRainCol3 = [
|
||||
{ id: 'rainBeginDt', title: '강우시작일시' },
|
||||
{ id: 'rainEndDt', title: '강우종료일시' },
|
||||
{ id: 'precdngDry', title: '선행건기(h)' },
|
||||
{ id: 'rainInfo', title: '강우정보' },
|
||||
{ id: 'rainFall', title: '누적강우량(mm)' },
|
||||
{ id: 'cntncTime', title: '지속시간(h)' },
|
||||
{ id: 'rainIn', title: '강우강도(mm/h)' }
|
||||
];
|
||||
|
||||
// 중점우수토실 측정정보 (id값 틀릴 수 있음)
|
||||
export const untrtSwgRainCol4 = [
|
||||
{ id: 'mesureBeginDt', title: '측정시작일시' },
|
||||
{ id: 'mesureEndDt', title: '측정종료일시' },
|
||||
{ id: 'mesureIntrvl', title: '측정간격(분)' },
|
||||
{ id: 'mesureMhrlsInstlLc', title: '측정기기 설치위치' },
|
||||
{ id: 'flwmtrCombined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼
|
||||
{ id: 'qltwtrBeginDt', title: '채수시작일시' },
|
||||
{ id: 'qltwtrEndDt', title: '채수종료일시' },
|
||||
{ id: 'qltwtrCo', title: '채수횟수' }
|
||||
];
|
||||
|
||||
// 중점우수토실 수량/수질측정 정보
|
||||
export const untrtSwgRainCol5 = [
|
||||
{ id: 'odr', title: '회차' },
|
||||
{ id: 'mesureDt', title: '측정일자' },
|
||||
{ id: 'mesureQy', title: '수량(m³)' },
|
||||
{ id: 'mesureBod', title: 'BOD(mg/L)' },
|
||||
{ id: 'mesureToc', title: 'TOC(mg/L)' },
|
||||
{ id: 'mesureLoad', title: '오염부하량(kg)' }
|
||||
];
|
||||
|
||||
// 🎯 날짜 필드들 (yyyymmdd -> yyyy년 mm월 dd일)
|
||||
export const dateFields = [
|
||||
'instl_ymd', 'last_drdgn_ymd', 'cmcn_ymd', 'oprtng_strt_ymd', 'clsg_ymd',
|
||||
'mesure_begin_dt', 'mesure_end_dt', 'qltwtr_begin_dt', 'qltwtr_end_dt',
|
||||
'rain_begin_dt', 'rain_end_dt', 'mesureDt',
|
||||
// 🎯 camelCase 버전 추가
|
||||
'instlYmd', 'lastDrdgnYmd', 'cmcnYmd', 'oprtngStrtYmd', 'clsgYmd',
|
||||
'mesureBeginDt', 'mesureEndDt', 'qltwtrBeginDt', 'qltwtrEndDt',
|
||||
'rainBeginDt', 'rainEndDt',
|
||||
'flfmtBgngYmd', 'flfmtEndYmd', 'flfmtYr'
|
||||
];
|
||||
|
||||
// 🎯 연월 필드들 (yyyymm -> yyyy년 m월)
|
||||
export const yearMonthFields = ['aplcnYm',];
|
||||
|
||||
// 🎯 날짜시간 필드들 (yyyy-mm-dd hh:mm:ss -> yyyy년 m월 d일 hh:mm)
|
||||
export const dateTimeFields = ['regDt',];
|
||||
|
||||
// 🎯 숫자 필드들 (천 단위 구분자)
|
||||
export const numberFields = [
|
||||
'pdmt', 'prlg', 'avg_grdn', 'drtw_mltv_area', 'excln_mltv_area', 'tmpt_dpth', 'scend_dpth', 'trfln_cnt', 'vrtc_len', 'wdth_len',
|
||||
'fclt_cpct', 'plln_area', 'prcs_zone_area_sum', 'swtrmt_ppltn_cnt', 'stty_crtr_bod', 'stty_crtr_toc', 'stty_crtr_ss', 'stty_crtr_tn', 'stty_crtr_tph', 'stty_crtr_clbcl_cnt',
|
||||
'swage_mh_diam', 'wdth_len', 'vrtc_len', 'altd_vl', 'dpthlVl',
|
||||
'plln_area', 'daily_prcs_cpct', 'max_impnd_cpct', 'altd_vl', 'wtlv_vl', 'excln_wpmp_ablt',
|
||||
'worflw_wall_hgt',
|
||||
'swr_ppl_eqp_ar', 'swr_trt_popl', 'weir_hg', 'weir_lt',
|
||||
'precdng_dry', 'rain_fall', 'cntnc_time', 'rain_in',
|
||||
'mesure_intrvl', 'qltwtr_co', 'mesureQy', 'mesureBod', 'mesureToc', 'mesureLoad',
|
||||
// 🎯 camelCase 버전 추가
|
||||
'avgGrdn', 'drtwMltvArea', 'exclnMltvArea', 'tmptDpth', 'scendDpth', 'trflnCnt', 'vrtcLen', 'wdthLen',
|
||||
'fcltCpct', 'pllnArea', 'prcsZoneAreaSum', 'swtrmtPpltnCnt', 'sttyCltrBod', 'sttyCltrToc', 'sttyCltrSs', 'sttyCltrTn', 'sttyCltrTph', 'sttyCltrClbclCnt',
|
||||
'swageMhDiam', 'altdVl', 'dpthlVl',
|
||||
'dailyPrcsCpct', 'maxImpndCpct', 'wtlvVl', 'exclnWpmpAblt',
|
||||
'worflwWallHgt',
|
||||
'swrPplEqpAr', 'swrTrtPopl', 'weirHg', 'weirLt',
|
||||
'precdngDry', 'rainFall', 'cntncTime', 'rainIn',
|
||||
'mesureIntrvl', 'qltwtrCo', 'rnysnFvlc', 'clarFvlc',
|
||||
'bizAmt', 'count',
|
||||
'chmbr_xcnts', 'chmbr_ydnts'
|
||||
];
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수관로)
|
||||
*
|
||||
* 📄 MsSwagePpl
|
||||
* 하수관로 정보 컴포넌트
|
||||
*
|
||||
* - 하수관로의 자체입력정보 표시 및 편집 기능
|
||||
* - 보수 및 갱신, 청소 및 준설, 점검 및 진단 데이터 조회 및 표시
|
||||
* - VerticalTable을 통한 입력 폼 렌더링
|
||||
* - InputSection을 통한 운영 데이터 관리
|
||||
* - 저장 버튼을 통한 데이터 저장 기능
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 시설별 운영 정보 조회: GET /api/v1/bizInfo/bizByFclt (sewerage-service)
|
||||
*/
|
||||
'use client'
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import InputSection from '../InputSection';
|
||||
import { handleInputChange } from '../common';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import useStore from '@/store/useStore';
|
||||
|
||||
// 하수관로
|
||||
const inputColumn = [
|
||||
{ id: "instlMbdyCd", title: "설치주제", type: "select", options: ["BTL", "재정사업"] },
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
{ id: "mltvZoneNm", title: "배수구역", type: "input" },
|
||||
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
|
||||
{ id: "lineCd", title: "지선/간선", type: "select", options: ["지선", "간선"] },
|
||||
{ id: "floCd", title: "흐름방식", type: "select", options: ["자연유하", "압송"] },
|
||||
{ id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] },
|
||||
];
|
||||
|
||||
type operationDataType = { year: string; type: string, bizInfoNm: string, bizType: string };
|
||||
|
||||
const MsSwagePpl = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => {
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [repairData, setRepairData] = useState<operationDataType[]>([]);
|
||||
const [cleaningData, setCleaningData] = useState<operationDataType[]>([]);
|
||||
const [inspectionData, setInspectionData] = useState<operationDataType[]>([]);
|
||||
const dataMap = {
|
||||
repair: { data: repairData, setData: setRepairData },
|
||||
cleaning: { data: cleaningData, setData: setCleaningData },
|
||||
inspection: { data: inspectionData, setData: setInspectionData },
|
||||
};
|
||||
|
||||
const { BIZ_SE_CD, BIZ_TY_CD } = useStore();
|
||||
|
||||
// 마운트 및 업데이트 시 API 호출
|
||||
|
||||
useEffect(() => {
|
||||
if (featureData) {
|
||||
setFormData(featureData);
|
||||
}
|
||||
|
||||
const fetchAll = async () => {
|
||||
const operationData = await getData(`v1/bizInfo/bizByFclt?mngInstCd=${featureData?.mng_inst_cd}&fcltMngNo=${featureData?.fclt_mng_no}&page=0&size=1`);
|
||||
|
||||
let inspectionArr: operationDataType[] = [];
|
||||
let cleaningArr: operationDataType[] = [];
|
||||
let repairArr: operationDataType[] = [];
|
||||
|
||||
if (Array.isArray(operationData)) {
|
||||
operationData.forEach(item => {
|
||||
if (!item.flfmt_yr) return;
|
||||
// biz_se_cd에 해당하는 type명 찾기
|
||||
const seCodeObj = BIZ_SE_CD.find(code => code.lwpoCd === item.biz_se_cd);
|
||||
const tyCodeObj = BIZ_TY_CD.find(code => code.lwpoCd === item.biz_ty_cd);
|
||||
const seTypeName = seCodeObj ? seCodeObj.lwpoCdNm : "";
|
||||
const tyTypeName = tyCodeObj ? tyCodeObj.lwpoCdNm : "";
|
||||
switch (item.biz_se_cd) {
|
||||
case "100":
|
||||
inspectionArr.push({
|
||||
year: String(item.flfmt_yr),
|
||||
type: seTypeName,
|
||||
bizInfoNm: String(item.biz_info_nm),
|
||||
bizType: tyTypeName,
|
||||
});
|
||||
break;
|
||||
case "203":
|
||||
cleaningArr.push({
|
||||
year: String(item.flfmt_yr),
|
||||
type: seTypeName,
|
||||
bizInfoNm: String(item.biz_info_nm),
|
||||
bizType: tyTypeName,
|
||||
});
|
||||
break;
|
||||
case "200":
|
||||
case "201":
|
||||
case "204":
|
||||
repairArr.push({
|
||||
year: String(item.flfmt_yr),
|
||||
type: seTypeName,
|
||||
bizInfoNm: String(item.biz_info_nm),
|
||||
bizType: tyTypeName,
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setInspectionData(inspectionArr);
|
||||
setCleaningData(cleaningArr);
|
||||
setRepairData(repairArr);
|
||||
};
|
||||
fetchAll();
|
||||
}, [featureData]);
|
||||
|
||||
// 폼 데이터가 변경될 때 호출될 핸들러
|
||||
const handleFormDataChange = (newFormData: any) => {
|
||||
setFormData(newFormData);
|
||||
};
|
||||
|
||||
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
|
||||
const handleSaveClick = () => {
|
||||
if (onSaveClick) {
|
||||
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
|
||||
onSaveClick();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<VerticalTable
|
||||
headName='입력정보'
|
||||
tableData={featureData ? [formData] : []}
|
||||
headlist={inputColumn}
|
||||
pageSize={20}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
isEditable={true}
|
||||
onPageChange={function (page: number): void {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
showSaveButton={true}
|
||||
onSaveClick={handleSaveClick}
|
||||
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
|
||||
/>
|
||||
<InputSection
|
||||
title="보수 및 갱신"
|
||||
data={repairData}
|
||||
sectionKey="repair"
|
||||
onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)}
|
||||
/>
|
||||
<InputSection
|
||||
title="청소 및 준설"
|
||||
data={cleaningData}
|
||||
sectionKey="cleaning"
|
||||
onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)}
|
||||
/>
|
||||
<InputSection
|
||||
title="점검 및 진단"
|
||||
data={inspectionData}
|
||||
sectionKey="inspection"
|
||||
onInputChange={(section, index, field, value) => handleInputChange(section, index, field, value, dataMap)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MsSwagePpl
|
||||
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (우수토실)
|
||||
*
|
||||
* 📄 Ssc
|
||||
* 우수토실 정보 컴포넌트
|
||||
*
|
||||
* - 우수토실의 자체입력정보 표시 및 편집 기능
|
||||
* - 중점우수토실 운영정보 모달 표시 (강우/측정 정보, 수량/수질측정 정보)
|
||||
* - VerticalTable을 통한 입력 폼 렌더링
|
||||
* - 저장 버튼을 통한 데이터 저장 기능
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 위경도로 강우시 미처리 하수 목록 조회: GET /api/v1/onsemiro/getSwgRainsByGeomety (sewerage-service)
|
||||
* - 강우시 월류 하수 관리 모니터링 조회: GET /api/v1/onsemiro/swgRain/{adstDstrCd}/{manageNo}/mntrngs (sewerage-service)
|
||||
*/
|
||||
import { UiModal, UiTable } from '@common/business_components/ui';
|
||||
import { Button, TableHead, TableRow } from '@common/components/ui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { Label } from '@radix-ui/react-label';
|
||||
|
||||
const inputColumn = [
|
||||
{ id: "prrtymngYn", title: "중점여부", type: "select", options: ["우수토실", "중점우수토실"] },
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
];
|
||||
|
||||
const Ssc = ({ featureData, isMainSsc, chmbrXcnts, chmbrYdnts, onSaveClick }: {
|
||||
featureData: any,
|
||||
isMainSsc?: boolean,
|
||||
chmbrXcnts?: string,
|
||||
chmbrYdnts?: string,
|
||||
onSaveClick?: () => void
|
||||
}) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDataLoaded, setIsDataLoaded] = useState(false);
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
const [rainMeasureData, setRainMeasureData] = useState<any>({}); // 강우, 측정 정보
|
||||
const [mntrngsData, setMntrngsData] = useState<any[]>([]); // 수량/수질측정 정보
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
const [totalDetailCount, setTotalDetailCount] = useState<number>(0);
|
||||
const [selectedRowIndex, setSelectedRowIndex] = useState<number>(-1);
|
||||
const [curPage, setCurPage] = useState<number>(1);
|
||||
const [curDetailPage, setCurDetailPage] = useState<number>(1);
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(true);
|
||||
const [isResetDetailPage, setIsResetDetailPage] = useState<boolean>(true);
|
||||
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
|
||||
|
||||
// 강우, 측정 정보 API 호출 함수
|
||||
const fetchRainMeasureData = async (chmbrXcnts: string, chmbrYdnts: string) => {
|
||||
setIsResetDetailPage(true);
|
||||
setIsResetSelection(true);
|
||||
if (!chmbrXcnts || !chmbrYdnts) {
|
||||
console.warn('추가 데이터 호출 실패: 필수 매개변수 누락', { chmbrXcnts, chmbrYdnts });
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
chmbrXcnts: chmbrXcnts,
|
||||
chmbrYdnts: chmbrYdnts,
|
||||
page: `${curPage - 1}`,
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/getSwgRainsByGeomety?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
|
||||
// helper: "YYYY-MM-DD HH:MM:SS" -> "MM-DD HH:MM"
|
||||
const formatNoYearNoSec = (val: any) => {
|
||||
if (!val && val !== 0) return "-";
|
||||
const s = String(val);
|
||||
// 정규식으로 안전하게 매칭 후 변환
|
||||
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})(?::\d{2})?$/);
|
||||
if (m) {
|
||||
const [, , mm, dd, hh, min] = m;
|
||||
return `${mm}-${dd} ${hh}:${min}`;
|
||||
}
|
||||
// 포맷이 예상과 다르면, 길이에 따라 잘라보기
|
||||
if (s.length >= 16) return s.substring(5, 16); // "YYYY-" 제거하고 초 전까지
|
||||
return s;
|
||||
};
|
||||
|
||||
const processedDataArray: any[] = result.content.map((data: any) => {
|
||||
// 🎯 formatDisplayData로 기본 포맷팅 적용
|
||||
const formattedItem = formatDisplayData(data);
|
||||
const flwmtrInstTp = data.flwmtrInstTp === 'R' ? '이동식' : data.flwmtrInstTp === 'S' ? '고정식' : '-';
|
||||
const flwmtrMesureTp = data.flwmtrMesureTp === 'R' ? '레이더' : data.flwmtrMesureTp === 'P' ? '플륨'
|
||||
: data.flwmtrMesureTp === 'U' ? '초음파' : data.flwmtrMesureTp === 'E' ? '기타' : '-';
|
||||
const flwmtrCombined = `${flwmtrInstTp}, ${flwmtrMesureTp}`;
|
||||
// 🎯 추가 데이터 조합
|
||||
return {
|
||||
...formattedItem,
|
||||
rainInfo: `${data.rainInfo === '0' ? '강우량계' : data.observatory || '-'}`, // 강우정보 0: 강우량계,1: 관측기관 이면 OBSERVATORY 이값사용
|
||||
mesureIntrvl: `${data.mesureIntrvlGbn === '1' ? '1분' : (data.mesureIntrvlGbn === '0' ? data.mesureIntrvl + '분' : '-')}`, // 측정간격
|
||||
flwmtrCombined: `${flwmtrCombined} / ${data.flwmtrKnd || '-'}`, // 유량계종류 및 모델명
|
||||
mesureBeginDt: data.mesureBeginDt ? formatNoYearNoSec(data.mesureBeginDt) : '-', // 수량 측정시작일
|
||||
mesureEndDt: data.mesureEndDt ? formatNoYearNoSec(data.mesureEndDt) : '-', // 수량 측정종료일
|
||||
qltwtrBeginDt: data.qltwtrBeginDt ? formatNoYearNoSec(data.qltwtrBeginDt) : '-', // 수질 측정시작일(채수시작일)
|
||||
qltwtrEndDt: data.qltwtrEndDt ? formatNoYearNoSec(data.qltwtrEndDt) : '-', // 수질 측정종료일(채수종료일)
|
||||
};
|
||||
});
|
||||
setRainMeasureData(processedDataArray);
|
||||
setTotalCount(result?.totalElements || 0);
|
||||
} catch (error) {
|
||||
console.error('추가 데이터 로딩 실패:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// 수량/수질측정 정보 API 호출 함수
|
||||
const fetchMntrngsData = async (adstDstrCd: string, manageNo: string) => {
|
||||
setIsResetDetailPage(true);
|
||||
if (!adstDstrCd || !manageNo) {
|
||||
console.warn('추가 데이터 호출 실패: 필수 매개변수 누락', { adstDstrCd, manageNo });
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: `${curDetailPage - 1}`,
|
||||
size: '3',
|
||||
});
|
||||
|
||||
let result;
|
||||
const res = await fetch(`/sewerage-service/api/v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs?${queryParams.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') result = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') result = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
result = JSON.parse(text);
|
||||
}
|
||||
|
||||
// 각 아이템 전처리
|
||||
const processedDataArray = result.content.map((data: any) => {
|
||||
// 기본 포맷팅 적용
|
||||
const formattedItem = formatDisplayData(data);
|
||||
return {
|
||||
...formattedItem,
|
||||
};
|
||||
});
|
||||
|
||||
setMntrngsData(processedDataArray);
|
||||
setTotalDetailCount(result?.totalElements || 0);
|
||||
} catch (error) {
|
||||
console.error('추가 데이터 로딩 실패:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 🎯 추가 데이터는 별도 useEffect로 비동기 로딩
|
||||
useEffect(() => {
|
||||
// 강우/측정 정보 데이터 호출
|
||||
if (isMainSsc && chmbrXcnts && chmbrYdnts) {
|
||||
fetchRainMeasureData(chmbrXcnts, chmbrYdnts)
|
||||
}
|
||||
}, [chmbrXcnts, chmbrYdnts]);
|
||||
|
||||
const openModal = () => {
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false);
|
||||
if (!isDataLoaded) {
|
||||
setTimeout(() => setIsDataLoaded(true), 100);
|
||||
}
|
||||
};
|
||||
|
||||
// featureData가 변경될 때 formData 초기화
|
||||
useEffect(() => {
|
||||
if (featureData) {
|
||||
setFormData(featureData);
|
||||
setMntrngsData([]);
|
||||
}
|
||||
}, [featureData]);
|
||||
|
||||
// featureData가 변경될 때 formData 초기화
|
||||
useEffect(() => {
|
||||
// showTable이 true일 때만 fetch (최초 렌더 방지)
|
||||
setIsResetDetailPage(false);
|
||||
setSelectedRowIndex(-1);
|
||||
if (chmbrXcnts && chmbrYdnts) fetchRainMeasureData(chmbrXcnts, chmbrYdnts);
|
||||
}, [curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
if (chmbrXcnts && chmbrYdnts && selectedRowIndex !== -1) fetchMntrngsData(rainMeasureData[selectedRowIndex].adstDstrCd, rainMeasureData[selectedRowIndex].manageNo);
|
||||
}, [curDetailPage]);
|
||||
|
||||
// 폼 데이터가 변경될 때 호출될 핸들러
|
||||
const handleFormDataChange = (newFormData: any) => {
|
||||
setFormData(newFormData);
|
||||
};
|
||||
|
||||
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
|
||||
const handleSaveClick = () => {
|
||||
if (onSaveClick) {
|
||||
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
|
||||
onSaveClick();
|
||||
}
|
||||
};
|
||||
|
||||
// 테이블 셀 클릭 핸들러 추가
|
||||
const handleCellClick = async (index: number | undefined, headId: string) => {
|
||||
if (index !== undefined && rainMeasureData) {
|
||||
await fetchMntrngsData(rainMeasureData[index].adstDstrCd, rainMeasureData[index].manageNo);
|
||||
setSelectedRowIndex(index);
|
||||
}
|
||||
};
|
||||
|
||||
const renderModalBody = () => (
|
||||
<div className='grid grid-cols-1 gap-4 p-4 mr-4 mt-4'>
|
||||
<div className='border rounded-lg pt-4 bg-white'>
|
||||
<Label className='text-2xl font-bold text-label ml-4'>강우/측정 정보</Label>
|
||||
<hr className="my-4 ml-4 mr-4 border-t border-gray-300" />
|
||||
{/* 강우정보 테이블 */}
|
||||
<UiTable
|
||||
headName=''
|
||||
tableData={rainMeasureData}
|
||||
headlist={untrtSwgRainCol3}
|
||||
pageSize={3}
|
||||
total={0}
|
||||
cellClick={handleCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
index={selectedRowIndex}
|
||||
/>
|
||||
|
||||
{/* 측정정보 테이블 */}
|
||||
<UiTable
|
||||
headName=''
|
||||
tableData={rainMeasureData}
|
||||
headlist={untrtSwgRainCol4}
|
||||
pageSize={3}
|
||||
total={totalCount}
|
||||
cellClick={handleCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
index={selectedRowIndex}
|
||||
>
|
||||
{/* 커스텀 헤더 */}
|
||||
<TableRow>
|
||||
<TableHead colSpan={5} className="text-center bg-blue-100 font-bold">수량</TableHead>
|
||||
<TableHead colSpan={3} className="text-center bg-blue-100 font-bold">수질</TableHead>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead className="text-center">측정시작일시</TableHead>
|
||||
<TableHead className="text-center">측정종료일시</TableHead>
|
||||
<TableHead className="text-center">측정간격(분)</TableHead>
|
||||
<TableHead className="text-center">측정기기설치위치</TableHead>
|
||||
<TableHead className="text-center">유량계 종류 및 모델명</TableHead>
|
||||
<TableHead className="text-center">채수시작일시</TableHead>
|
||||
<TableHead className="text-center">채수종료일시</TableHead>
|
||||
<TableHead className="text-center">채수횟수</TableHead>
|
||||
</TableRow>
|
||||
</UiTable>
|
||||
</div>
|
||||
|
||||
<div className='border rounded-lg pt-4 bg-white'>
|
||||
{/* 수량/수질측정 정보 테이블 */}
|
||||
<Label className='text-2xl font-bold text-label ml-4'>수량/수질측정 정보</Label>
|
||||
<hr className="my-4 ml-4 mr-4 border-t border-gray-300" />
|
||||
<UiTable
|
||||
headName=''
|
||||
tableData={mntrngsData}
|
||||
headlist={untrtSwgRainCol5}
|
||||
pageSize={3}
|
||||
total={totalDetailCount}
|
||||
cellClick={undefined}
|
||||
onPageChange={setCurDetailPage}
|
||||
resetPageNo={isResetDetailPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isMainSsc && (
|
||||
<VerticalTable
|
||||
headName='입력정보'
|
||||
tableData={featureData ? [formData] : []}
|
||||
headlist={inputColumn}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
isEditable={true}
|
||||
onPageChange={function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
showSaveButton={true}
|
||||
onSaveClick={handleSaveClick}
|
||||
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
|
||||
/>
|
||||
)}
|
||||
|
||||
{isMainSsc && (
|
||||
<div>
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3>
|
||||
</div>
|
||||
<Button
|
||||
className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600'
|
||||
onClick={openModal}
|
||||
>
|
||||
상세보기
|
||||
</Button>
|
||||
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-none
|
||||
max-h-none
|
||||
overflow-y-auto
|
||||
z-[101]
|
||||
`}
|
||||
body={renderModalBody()}
|
||||
noHeaderYn='Y'
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Ssc
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수처리시설)
|
||||
*
|
||||
* 📄 SwtrmtFclt
|
||||
* 하수처리시설 정보 컴포넌트
|
||||
*
|
||||
* - 하수처리시설의 디지털 자료관 연계 정보 표시
|
||||
* - VerticalTable을 통한 테이블 렌더링
|
||||
* - 조회 버튼을 통한 모달 표시 및 ReportTab 연동
|
||||
*/
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { UiModal } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import ReportTab from '../../searchModal/reportTab';
|
||||
|
||||
// 하수처리시설
|
||||
const headList = [
|
||||
{ id: "title", title: "하수도정비 기본계획보고서" },
|
||||
];
|
||||
|
||||
const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const ctpvNm = gisObject?.ctpv_nm;
|
||||
const sggNm = gisObject?.sgg_nm;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VerticalTable
|
||||
children={{
|
||||
title: <Button className="gray" onClick={() => setIsModalOpen(true)}>조회</Button>
|
||||
}}
|
||||
headName='디지털 자료관 연계정보'
|
||||
tableData={[]}
|
||||
headlist={headList}
|
||||
pageSize={20}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
isEditable={true}
|
||||
onPageChange={function (page: number): void {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
/>
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle="하수도정비 기본계획보고서"
|
||||
headerTitleClass="text-3xl font-semibold text-gray-700"
|
||||
bodyClass={`
|
||||
h-[100vh]
|
||||
w-[60vw]
|
||||
max-w-[60vw]
|
||||
max-h-[100vh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={
|
||||
<ReportTab
|
||||
open={isModalOpen}
|
||||
isSearch={false}
|
||||
ctpvNm={ctpvNm}
|
||||
sggNm={sggNm}
|
||||
/>
|
||||
}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default SwtrmtFclt
|
||||
@@ -0,0 +1,271 @@
|
||||
import {useState, useEffect} from 'react';
|
||||
import { getData } from '@/components/lib/api';
|
||||
import moment from "moment";
|
||||
|
||||
export type TInftwtAnls = {
|
||||
inftwtAnlsSn:number;
|
||||
aplcnYm: string;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
swtrmtFcltNm: string;
|
||||
atflCd: string;
|
||||
atflId: string;
|
||||
atflInpNm: string;
|
||||
atflHrPttnNm: string;
|
||||
atflMpngNm: string;
|
||||
atflOilqNm: string;
|
||||
atflNm: string;
|
||||
drtwSwtcRt:number;
|
||||
anlsInfo: string;
|
||||
scsYn: string;
|
||||
};
|
||||
export type TInftwtAnlsRslt = {
|
||||
inftwtAnlsSn: number;
|
||||
pplMngNo: string;
|
||||
anlsTm: string;
|
||||
predcOilq:number;
|
||||
}
|
||||
export type TRdiiAnls = {
|
||||
rainInfwtAnlsSn:number;
|
||||
aplcnYm: string;
|
||||
ctpvCd: string;
|
||||
sggCd: string;
|
||||
swtrmtFcltNm: string;
|
||||
atflCd: string;
|
||||
atflId: string;
|
||||
atflInpNm: string;
|
||||
atflHrPttnNm: string;
|
||||
atflMpngNm: string;
|
||||
atflRainNm: string;
|
||||
atflNm: string;
|
||||
drtwSwtcRt:number;
|
||||
anlsInfo: string;
|
||||
scsYn: string;
|
||||
srcNm: string; // 출처이름
|
||||
};
|
||||
export type TRdiiAnlsRslt = {
|
||||
rainInfwtAnlsSn: number;
|
||||
pplMngNo: string;
|
||||
anlsTm: string;
|
||||
predcOilq:number;
|
||||
minq:number;
|
||||
}
|
||||
export type TOilqData = {
|
||||
inftwtAnlsSn: number; // 침입수분석 일련번호
|
||||
pplMngNo: string; // 관로관리번호
|
||||
mtrngDt: string; // 계측일시
|
||||
oilq: number; // 유량
|
||||
}
|
||||
export type TRainData = {
|
||||
rainInfwtAnlsSn: number;
|
||||
mtrngDt: string;
|
||||
amrf: number; // 강우량
|
||||
}
|
||||
|
||||
function roundTo(n: number, digits: number): number {
|
||||
const factor = Math.pow(10, digits);
|
||||
return Math.round(n * factor) / factor;
|
||||
}
|
||||
|
||||
const getOptionsAplcnYm = (yyyymms: string[]): { text: string; val: string, hidden?: boolean }[] => {
|
||||
const yyyymms1 : { text: string; val: string; hidden?: boolean }[] = yyyymms.map(dateStr => {
|
||||
const year = dateStr.slice(0, 4);
|
||||
const month = dateStr.slice(4, 6);
|
||||
return {
|
||||
text: `${year}-${month}`,
|
||||
val: dateStr
|
||||
};
|
||||
});
|
||||
yyyymms1.unshift({ val: '0', text: '선택', hidden: true });
|
||||
return yyyymms1;
|
||||
};
|
||||
const selectAplcnYms = async(gubun:string) => { // 상수사용월
|
||||
try {
|
||||
const queryString = '';
|
||||
const result = await getData(`v1/${gubun}/aplcnYm${queryString ? `?${queryString}` : ''}`);
|
||||
if (result && result.length > 0) {
|
||||
return getOptionsAplcnYm(result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const useAplcnYms = (gubun:string) => {
|
||||
const [data, setData] = useState<{ text: string; val: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetch = async (gubun:string) => {
|
||||
const result = await selectAplcnYms(gubun);
|
||||
if (result) setData(result);
|
||||
};
|
||||
fetch(gubun);
|
||||
}, [gubun]);
|
||||
|
||||
return data;
|
||||
};
|
||||
export const getOptionsInftwtAnlsInfo = (anlsArr: TInftwtAnls[]): { text: string; val: number, hidden?: boolean }[] => {
|
||||
const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({
|
||||
text: anls.anlsInfo,
|
||||
val: anls.inftwtAnlsSn,
|
||||
}));
|
||||
options.unshift({ val: 0, text: '선택', hidden: true });
|
||||
return options;
|
||||
};
|
||||
|
||||
export const selectInftwtAnls = async(aplcnYm:string) => { // 침입수분석
|
||||
try {
|
||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : '';
|
||||
const result = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const getChartDataInftwtAnlsRslt = (arr: TInftwtAnlsRslt[]): { label: string; val: number }[] => {
|
||||
const options = arr.map(anlsRslt => ({
|
||||
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("HH시"),
|
||||
val: roundTo(anlsRslt.predcOilq, 2),
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
export const selectInftwtAnlsRslt = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수분석 결과
|
||||
try {
|
||||
if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("inftwtAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/inftwtAnlsRslt/ppl/${inftwtAnlsSn}/${pplMngNo}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const getChartDataOilq = (arr: TOilqData[]): { label: string; val: number }[] => {
|
||||
const options = arr.map(anlsRslt => ({
|
||||
label: anlsRslt.mtrngDt,
|
||||
val: roundTo(anlsRslt.oilq, 2),
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
export const selectOilq = async(inftwtAnlsSn:number, pplMngNo:string) => { // 유량
|
||||
try {
|
||||
if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("inftwtAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/inftwtAnlsRslt/oilqPpl/${inftwtAnlsSn}/${pplMngNo}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const selectPplInftwtQty = async(inftwtAnlsSn:number, pplMngNo:string) => { // 침입수량
|
||||
try {
|
||||
if ((inftwtAnlsSn === undefined || inftwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("inftwtAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/inftwtAnlsRslt/pplInftwtQty/${inftwtAnlsSn}/${pplMngNo}`);
|
||||
if (result.length > 0) {
|
||||
return result[0].avg_day_flow;
|
||||
} else {
|
||||
return 0; // 데이터가 없을 경우 0 반환
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
//----------
|
||||
export const selectPplRdiiQty = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // rdii값
|
||||
try {
|
||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("rainInfwtAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/rdiiAnlsRslt/pplRdiiQty/${rainInfwtAnlsSn}/${pplMngNo}`);
|
||||
if (result?.sustaintime > 0) {
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getOptionsRdiiAnlsInfo = (anlsArr: TRdiiAnls[]): { text: string; val: number, hidden?: boolean }[] => {
|
||||
const options : { text: string; val: number; hidden?: boolean }[] = anlsArr.map(anls => ({
|
||||
text: anls.anlsInfo,
|
||||
val: anls.rainInfwtAnlsSn,
|
||||
}));
|
||||
options.unshift({ val: 0, text: '선택', hidden: true });
|
||||
return options;
|
||||
};
|
||||
|
||||
export const selectRdiiAnls = async(aplcnYm:string) => { // 침입수분석
|
||||
try {
|
||||
const queryString = aplcnYm ? `aplcnYmFrom=${aplcnYm}&aplcnYmTo=${aplcnYm}` : '';
|
||||
const result = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const getChartDataRdiiAnlsRslt = (arr: TRdiiAnlsRslt[]): { label: string; lineVal0: number, barVal0:number }[] => {
|
||||
const options = arr.map(anlsRslt => ({
|
||||
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"),
|
||||
lineVal0: roundTo(anlsRslt.predcOilq, 2),
|
||||
barVal0: 0
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
export const getChartDataRdiiAnlsRsltMinq = (arr: TRdiiAnlsRslt[]): { label: string; val: number }[] => {
|
||||
const options = arr.map(anlsRslt => ({
|
||||
label: moment(anlsRslt.anlsTm, "YYYYMMDDHHmmss").format("MM-DD HH시"),
|
||||
val: roundTo(anlsRslt.minq, 2),
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
export const selectRdiiAnlsRslt = async(rainInfwtAnlsSn:number, pplMngNo:string) => { // RDII분석 결과
|
||||
try {
|
||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0) || !pplMngNo) {
|
||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/rdiiAnlsRslt/ppl/${rainInfwtAnlsSn}/${pplMngNo}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const selectRainData = async(rainInfwtAnlsSn:number) => { // 강우데이터
|
||||
try {
|
||||
if ((rainInfwtAnlsSn === undefined || rainInfwtAnlsSn === 0)) {
|
||||
console.warn("RdiiAnlsSn or pplMngNo is not provided");
|
||||
return [];
|
||||
}
|
||||
const result = await getData(`v1/rdiiAnlsRslt/rainData/${rainInfwtAnlsSn}`);
|
||||
if (result && result.numberOfElements > 0) {
|
||||
return result.content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
export const getChartDataRainData = (arr: TRainData[]): { label: string; lineVal0: number, barVal0: number }[] => {
|
||||
const options = arr.map(data => ({
|
||||
label: moment(data.mtrngDt, "YYYY-MM-DD HH:mm:ss").format("MM-DD HH시"),
|
||||
lineVal0: 0,
|
||||
barVal0: roundTo(data.amrf,2),
|
||||
}));
|
||||
return options;
|
||||
};
|
||||
@@ -0,0 +1,297 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TInftwtAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useInfilChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
};
|
||||
|
||||
type DataType = {
|
||||
label: string;
|
||||
val: number;
|
||||
};
|
||||
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const InfilChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useInfilChartModalStore();
|
||||
|
||||
const [selectedAplcnYm, setSelectedAplcnYm] = useState('');
|
||||
const [inftwtAnlss, setInftwtAnlss] = useState<TInftwtAnls[]>([]);
|
||||
const [inftwtAnlsInfos, setInftwtAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>(
|
||||
INITIAL_ANALYSIS_OPTIONS,
|
||||
);
|
||||
const [inftwtAnls, setInftwtAnls] = useState<TInftwtAnls | null>(null);
|
||||
const [selectedInftwtAnls, setSelectedInftwtAnls] = useState<number>(0);
|
||||
const [chartData, setChartData] = useState<{ chartData1: DataType[]; chartData2: DataType[] }>({
|
||||
chartData1: [],
|
||||
chartData2: [],
|
||||
});
|
||||
const [inftwtQty, setInftwtQty] = useState<string>('0');
|
||||
|
||||
const aplcnYms = fn.useAplcnYms('inftwtAnlsRslt');
|
||||
const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : '';
|
||||
|
||||
const chartForm = CHART_FORM.LINE;
|
||||
const chartConfig = {
|
||||
chartData1: { label: '순오수량', color: 'red' },
|
||||
chartData2: { label: '실측하수량', color: 'orange' },
|
||||
} as ChartConfig;
|
||||
|
||||
const hasChartData = useMemo(() => {
|
||||
return chartData.chartData1.length > 0 || chartData.chartData2.length > 0;
|
||||
}, [chartData]);
|
||||
|
||||
const resetState = () => {
|
||||
setSelectedAplcnYm('');
|
||||
setInftwtAnlss([]);
|
||||
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
setInftwtAnls(null);
|
||||
setSelectedInftwtAnls(0);
|
||||
setChartData({ chartData1: [], chartData2: [] });
|
||||
setInftwtQty('0');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState(); // 모달이 닫힐 때 리셋
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
setSelectedAplcnYm(selectedValue);
|
||||
setInftwtAnlss([]);
|
||||
setInftwtAnls(null);
|
||||
setSelectedInftwtAnls(0);
|
||||
setInftwtQty('0');
|
||||
setChartData({ chartData1: [], chartData2: [] });
|
||||
|
||||
if (!selectedValue || selectedValue === '0') {
|
||||
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fn.selectInftwtAnls(selectedValue);
|
||||
const list = data ?? [];
|
||||
setInftwtAnlss(list);
|
||||
setInftwtAnlsInfos(fn.getOptionsInftwtAnlsInfo(list));
|
||||
} catch (error) {
|
||||
console.error('selectInftwtAnls 에러:', error);
|
||||
setInftwtAnlss([]);
|
||||
setInftwtAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInftwtAnlsSnChange = (val: string | number) => {
|
||||
const selectedValue = Number(val);
|
||||
|
||||
setSelectedInftwtAnls(selectedValue);
|
||||
setChartData({ chartData1: [], chartData2: [] });
|
||||
setInftwtQty('0');
|
||||
|
||||
if (selectedValue === 0) {
|
||||
setInftwtAnls(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAnls = inftwtAnlss.find((anls) => anls.inftwtAnlsSn === selectedValue) ?? null;
|
||||
setInftwtAnls(selectedAnls);
|
||||
};
|
||||
|
||||
const infilRsltChart = async () => {
|
||||
if (!selectedAplcnYm || selectedInftwtAnls === 0 || !normalizedPplMngNo) {
|
||||
setChartData({ chartData1: [], chartData2: [] });
|
||||
setInftwtQty('0');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [inftwtAnlsRslt, oilq, qty] = await Promise.all([
|
||||
fn.selectInftwtAnlsRslt(selectedInftwtAnls, normalizedPplMngNo),
|
||||
fn.selectOilq(selectedInftwtAnls, normalizedPplMngNo).catch((err) => {
|
||||
console.error('selectOilq 에러:', err);
|
||||
return [];
|
||||
}),
|
||||
fn.selectPplInftwtQty(selectedInftwtAnls, normalizedPplMngNo).catch((err) => {
|
||||
console.error('selectPplInftwtQty 에러:', err);
|
||||
return 0;
|
||||
}),
|
||||
]);
|
||||
|
||||
setChartData({
|
||||
chartData1: fn.getChartDataInftwtAnlsRslt(inftwtAnlsRslt ?? []),
|
||||
chartData2: fn.getChartDataOilq(oilq ?? []),
|
||||
});
|
||||
|
||||
const normalizedQty = Number(qty ?? 0);
|
||||
setInftwtQty(normalizedQty.toLocaleString('ko-KR'));
|
||||
} catch (error) {
|
||||
console.error('infilRsltChart 에러:', error);
|
||||
setChartData({ chartData1: [], chartData2: [] });
|
||||
setInftwtQty('0');
|
||||
}
|
||||
};
|
||||
|
||||
const renderInfoInput = (label: string, value?: string | number | null) => (
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto text-[14px] font-semibold text-[#5c6470] whitespace-nowrap">{label}</Label>
|
||||
<Input
|
||||
className="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white px-3 text-xl md:text-xl text-[#2b3440]"
|
||||
placeholder=""
|
||||
value={value ?? ''}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleReset = () => {
|
||||
resetState();
|
||||
}
|
||||
|
||||
const renderBody = () => (
|
||||
<div className="text-black">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div className="text-[18px] font-semibold text-[#111827]" />
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-[68vh] grid-cols-[minmax(0,38%)_minmax(0,62%)] gap-5">
|
||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
|
||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
|
||||
침입수 분석 선택 조건
|
||||
</h3>
|
||||
|
||||
<div className="rounded-[6px] border border-[#d9dde3] bg-[#eff1f4] px-4 py-6">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto text-[14px] font-semibold text-[#5c6470] whitespace-nowrap">
|
||||
상수사용월:
|
||||
</Label>
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||
label=""
|
||||
selectArray={aplcnYms}
|
||||
value={selectedAplcnYm}
|
||||
onSelectValue={handleAplcnYmChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto text-[14px] font-semibold text-[#5c6470] whitespace-nowrap">
|
||||
분석정보:
|
||||
</Label>
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||
label=""
|
||||
selectArray={inftwtAnlsInfos}
|
||||
value={selectedInftwtAnls}
|
||||
onSelectValue={handleInftwtAnlsSnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{renderInfoInput('오수전환율:', inftwtAnls?.drtwSwtcRt ?? '')}
|
||||
{renderInfoInput('시간패턴 파일명:', inftwtAnls?.atflHrPttnNm ?? '')}
|
||||
{renderInfoInput('수용가-맨홀 매칭:', inftwtAnls?.atflMpngNm ?? '')}
|
||||
{renderInfoInput('관망도 파일명:', inftwtAnls?.atflInpNm ?? '')}
|
||||
{renderInfoInput('유량 파일명:', inftwtAnls?.atflOilqNm ?? '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 mb-[40px] flex justify-center">
|
||||
<Button size="lg" onClick={infilRsltChart} >
|
||||
조회
|
||||
</Button>
|
||||
<Button size="lg" color="gray" onClick={handleReset} >
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
|
||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">침입수 분석 결과</h3>
|
||||
|
||||
<div className="mb-6 grid grid-cols-2 gap-5">
|
||||
<div className="flex min-h-[98px] items-center justify-between rounded-[14px] border border-[#4f88c6] bg-white px-6 py-4">
|
||||
<div className="flex items-center gap-2 text-[16px] font-extrabold text-[#ef4444]">
|
||||
{/* <span className="text-[14px] leading-none">●</span> */}
|
||||
<span>상수사용량 적용연월</span>
|
||||
</div>
|
||||
<div className="text-[20px] font-extrabold text-[#222]">
|
||||
{selectedAplcnYm ? moment(selectedAplcnYm, 'YYYYMM').format('YYYY-MM') : '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-[98px] items-center justify-between rounded-[14px] border border-[#4f88c6] bg-white px-6 py-4">
|
||||
<div className="flex items-center gap-2 text-[16px] font-extrabold text-[#ef4444]">
|
||||
{/* <span className="text-[14px] leading-none">●</span> */}
|
||||
<span>침입수량 산정결과</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_auto] items-center gap-7 text-[#222]">
|
||||
<span className="text-[16px] text-[#666]">침입수량</span>
|
||||
<div className='w-[66px] flex items-end justify-start gap-2'>
|
||||
<span className="text-[20px] font-extrabold">{inftwtQty}</span>
|
||||
<span className="pb-[2px] text-[16px]">m³/d</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[10px] bg-white px-2 pb-2 pt-3">
|
||||
{hasChartData ? (
|
||||
<MainChart
|
||||
chartConfig={chartConfig}
|
||||
chartForm={chartForm}
|
||||
chartData={chartData}
|
||||
chartTitle={'침입수량 그래프'}
|
||||
yFieldText="유량(m³/h)"
|
||||
yFieldText2=""
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-[464px] items-center justify-center text-[18px] text-[#2c2f33]">
|
||||
데이터가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1000px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InfilChart);
|
||||
@@ -0,0 +1,357 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
CHART_FORM,
|
||||
type ChartConfig,
|
||||
MainChart,
|
||||
UiModal,
|
||||
} from '@common/business_components';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
|
||||
import type { TRdiiAnls } from './aplcnYms';
|
||||
import * as fn from './aplcnYms';
|
||||
import { useRdiiChartModalStore } from '../chartModalStore';
|
||||
|
||||
type ModalOpenProp = {
|
||||
pplMngNo?: string; // 관로관리번호
|
||||
};
|
||||
|
||||
type LineBarChartData = {
|
||||
label: string;
|
||||
lineVal0: number;
|
||||
barVal0: number;
|
||||
};
|
||||
|
||||
const INITIAL_ANALYSIS_OPTIONS = [{ val: 0, text: '선택' }];
|
||||
|
||||
const RdiiChart = ({ pplMngNo = '' }: ModalOpenProp) => {
|
||||
const { isOpen: isModalOpen, setIsOpen: setIsModalOpen } = useRdiiChartModalStore();
|
||||
|
||||
const [aplcnYm, setAplcnYm] = useState('');
|
||||
const [rdiiAnlss, setRdiiAnlss] = useState<TRdiiAnls[]>([]);
|
||||
const [rdiiAnlsInfos, setRdiiAnlsInfos] = useState<{ text: string; val: number; hidden?: boolean }[]>(
|
||||
INITIAL_ANALYSIS_OPTIONS,
|
||||
);
|
||||
const [rdiiAnls, setRdiiAnls] = useState<TRdiiAnls | null>(null); // 강우유입수분석정보
|
||||
const [rainInfwtAnlsSn, setRainInfwtAnlsSn] = useState<number>(0);// 강우유입수분석 정보 선택시 사용
|
||||
const [chartData, setChartData] = useState<LineBarChartData[]>([]);
|
||||
const [rdiiQty, setRdiiQty] = useState<string>('0'); // 강우유입수량
|
||||
const [rainTotal, setRainTotal] = useState<string>('0'); // 적용강우량
|
||||
const [totalVolume, setTotalVolume] = useState<string>('0');// 총체적 m³/mm
|
||||
|
||||
const aplcnYms = fn.useAplcnYms('rdiiAnlsRslt'); // 상수사용월 데이터 가져오기
|
||||
const normalizedPplMngNo = pplMngNo ? `L${pplMngNo}` : '';
|
||||
|
||||
const chartForm = CHART_FORM.LINEBAR2;
|
||||
const chartConfig = {
|
||||
barVal0: {
|
||||
label: '강수량',
|
||||
color: '#33AAFF',
|
||||
},
|
||||
lineVal0: {
|
||||
label: 'RDII',
|
||||
color: '#FF0000',
|
||||
},
|
||||
} as ChartConfig;
|
||||
|
||||
const hasChartData = useMemo(() => chartData.length > 0, [chartData]);
|
||||
|
||||
const resetState = () => {
|
||||
setAplcnYm(''); // 상수사용월 초기화
|
||||
setRdiiAnlss([]); // 강우유입수분석 초기화
|
||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS); // 강우유입수분석정보 초기화
|
||||
setRdiiAnls(null); // 강우유입수분석정보 초기화
|
||||
setRainInfwtAnlsSn(0); // 강우유입수분석 일련번호 초기화
|
||||
setChartData([]);
|
||||
setRdiiQty('0'); // 강우유입수량 초기화
|
||||
setRainTotal('0');// 적용강우량 초기화
|
||||
setTotalVolume('0');// 총체적 m³/mm 초기화
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen) {
|
||||
resetState();
|
||||
}
|
||||
}, [isModalOpen]);
|
||||
|
||||
const handleAplcnYmChange = async (val: string | number) => {
|
||||
const selectedValue = String(val);
|
||||
|
||||
setAplcnYm(selectedValue);
|
||||
setRdiiAnlss([]);
|
||||
setRdiiAnls(null);
|
||||
setRainInfwtAnlsSn(0);
|
||||
setChartData([]);
|
||||
setRdiiQty('0');
|
||||
setRainTotal('0');
|
||||
setTotalVolume('0');
|
||||
|
||||
if (!selectedValue || selectedValue === '0') {
|
||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fn.selectRdiiAnls(selectedValue); // 상수사용월 변경시 분석 정보 리스트 가져오기
|
||||
const list = data ?? [];
|
||||
setRdiiAnlss(list);
|
||||
setRdiiAnlsInfos(fn.getOptionsRdiiAnlsInfo(list));
|
||||
} catch (error) {
|
||||
console.error('selectRdiiAnls 에러:', error);
|
||||
setRdiiAnlss([]);
|
||||
setRdiiAnlsInfos(INITIAL_ANALYSIS_OPTIONS);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRdiiAnlsSnChange = (val: string | number) => {
|
||||
const selectedValue = Number(val);
|
||||
|
||||
setRainInfwtAnlsSn(selectedValue);
|
||||
setChartData([]);
|
||||
setRdiiQty('0');
|
||||
setRainTotal('0');
|
||||
setTotalVolume('0');
|
||||
|
||||
if (selectedValue === 0) {
|
||||
setRdiiAnls(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAnls = rdiiAnlss.find((anls) => anls.rainInfwtAnlsSn === selectedValue) ?? null;
|
||||
setRdiiAnls(selectedAnls);
|
||||
};
|
||||
|
||||
const fetchFilteredData = async () => {
|
||||
if (!rainInfwtAnlsSn || !normalizedPplMngNo) {
|
||||
setChartData([]);
|
||||
setRdiiQty('0');
|
||||
setRainTotal('0');
|
||||
setTotalVolume('0');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [rsltData, rainData, qtyData] = await Promise.all([
|
||||
fn.selectRdiiAnlsRslt(rainInfwtAnlsSn, normalizedPplMngNo),
|
||||
fn.selectRainData(rainInfwtAnlsSn),
|
||||
fn.selectPplRdiiQty(rainInfwtAnlsSn, normalizedPplMngNo),
|
||||
]);
|
||||
|
||||
const rsltDataFormatted = fn.getChartDataRdiiAnlsRslt(rsltData ?? []);
|
||||
const rainDataFormatted = fn.getChartDataRainData(rainData ?? []);
|
||||
const rainMap = new Map(rainDataFormatted.map((item) => [item.label, item.barVal0]));
|
||||
|
||||
rsltDataFormatted.forEach((item) => {
|
||||
const matchedVal = rainMap.get(item.label);
|
||||
if (matchedVal !== undefined) {
|
||||
item.barVal0 = matchedVal;
|
||||
}
|
||||
});
|
||||
|
||||
setChartData(rsltDataFormatted);// 차트 데이터 설정
|
||||
|
||||
const avgRdii = Number(qtyData?.rdiih ?? 0);
|
||||
const appliedRainfall = Number(qtyData?.raintotal ?? 0);
|
||||
const rdiiPerRainfall =
|
||||
qtyData?.raintotal && qtyData?.raintotal !== 0
|
||||
? Math.round(avgRdii * Number(qtyData?.sustaintime ?? 0) / qtyData.raintotal)
|
||||
: 0;
|
||||
|
||||
setRdiiQty(Math.round(avgRdii * 24).toLocaleString('ko-KR'));
|
||||
setRainTotal(appliedRainfall.toLocaleString('ko-KR'));
|
||||
setTotalVolume(rdiiPerRainfall.toLocaleString('ko-KR'));
|
||||
} catch (error) {
|
||||
console.error('rdiiRsltChart 에러:', error);
|
||||
setChartData([]);
|
||||
setRdiiQty('0');
|
||||
setRainTotal('0');
|
||||
setTotalVolume('0');
|
||||
}
|
||||
};
|
||||
|
||||
const renderInfoInput = (label: string, value?: string | number | null) => (
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
|
||||
{label}
|
||||
</Label>
|
||||
<Input
|
||||
className="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white px-3 text-xl text-[#2b3440]"
|
||||
placeholder=""
|
||||
value={value ?? ''}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderBody = () => (
|
||||
<div className="text-black">
|
||||
<div className="mb-4 flex items-start justify-between">
|
||||
<div className="text-[18px] font-semibold text-[#111827]" />
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-[68vh] grid-cols-[minmax(0,36%)_minmax(0,64%)] gap-5">
|
||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
|
||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
|
||||
강우유입수분석 선택 조건
|
||||
</h3>
|
||||
|
||||
<div className="rounded-[6px] border border-[#d9dde3] bg-[#eff1f4] px-4 py-6">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
|
||||
상수사용월:
|
||||
</Label>
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||
label=""
|
||||
selectArray={aplcnYms}
|
||||
value={aplcnYm}
|
||||
onSelectValue={handleAplcnYmChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[minmax(0,32%)_minmax(0,68%)] items-center gap-3">
|
||||
<Label className="w-auto whitespace-nowrap text-[14px] font-semibold text-[#5c6470]">
|
||||
분석정보:
|
||||
</Label>
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
selectClass="h-[42px] rounded-[5px] border border-[#d7dde5] bg-white text-xl"
|
||||
label=""
|
||||
selectArray={rdiiAnlsInfos}
|
||||
value={rainInfwtAnlsSn}
|
||||
onSelectValue={handleRdiiAnlsSnChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{renderInfoInput('오수전환율:', rdiiAnls?.drtwSwtcRt ?? '')}
|
||||
{renderInfoInput('수용가-맨홀 매칭:', rdiiAnls?.atflMpngNm ?? '')}
|
||||
{renderInfoInput('관망도 파일명:', rdiiAnls?.atflInpNm ?? '')}
|
||||
{renderInfoInput('강우 파일명:', rdiiAnls?.atflRainNm ?? '')}
|
||||
{renderInfoInput('RTK계수 출처:', rdiiAnls?.srcNm ?? '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 mb-[40px] flex justify-center">
|
||||
<Button size="lg" onClick={fetchFilteredData}>
|
||||
조회
|
||||
</Button>
|
||||
|
||||
<Button size="lg" color="gray" onClick={resetState}>
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative rounded-[12px] border border-[#b1b1b1] bg-white p-5">
|
||||
<h3 className="mb-6 text-[20px] font-extrabold tracking-[-0.02em] text-black">
|
||||
강우유입수분석 결과
|
||||
</h3>
|
||||
|
||||
<div className="mb-6 flex gap-5">
|
||||
{/* 좌측 카드 */}
|
||||
<div className="flex min-h-[98px] min-w-0 basis-2/5 grow shrink-[1.4] items-center justify-between rounded-[16px] border border-[#5fa3ea] bg-white px-6 py-4">
|
||||
<div className="text-[16px] font-bold text-[#ef4444]">
|
||||
상수사용량 적용연월
|
||||
</div>
|
||||
<div className="text-[24px] font-extrabold leading-none text-[#111]">
|
||||
{aplcnYm ? moment(aplcnYm, 'YYYYMM').format('YYYY-MM') : '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측 카드 */}
|
||||
<div className="flex min-h-[98px] min-w-0 basis-3/5 grow shrink items-center gap-5 rounded-[16px] border border-[#5fa3ea] bg-white px-6 py-4">
|
||||
{/* 좌측: 제목 (세로 가운데 정렬) */}
|
||||
<div className="flex min-w-0 basis-[32%] items-center">
|
||||
<div className="text-[16px] font-bold text-[#ef4444] break-keep">
|
||||
강우유입수량 분석결과
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 지표 영역 */}
|
||||
<div className="flex min-w-0 basis-[68%] flex-col gap-[2px]">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="min-w-0 basis-[48%] text-right text-[15px] text-[#555]">
|
||||
평균RDII
|
||||
</div>
|
||||
<div className="min-w-0 basis-[32%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">
|
||||
{rdiiQty}
|
||||
</div>
|
||||
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">
|
||||
m³/d
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="min-w-0 basis-[48%] text-right text-[15px] text-[#555]">
|
||||
적용 강우량
|
||||
</div>
|
||||
<div className="min-w-0 basis-[32%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">
|
||||
{rainTotal}
|
||||
</div>
|
||||
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">
|
||||
mm
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="min-w-0 basis-[48%] text-right text-[15px] text-[#555]">
|
||||
강우량 대비 RDII
|
||||
</div>
|
||||
<div className="min-w-0 basis-[32%] text-right text-[18px] font-extrabold text-[#111] tabular-nums">
|
||||
{totalVolume}
|
||||
</div>
|
||||
<div className="min-w-0 basis-[20%] text-left text-[14px] text-[#111]">
|
||||
m³/mm
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-[10px] bg-white px-2 pb-2 pt-3">
|
||||
{hasChartData ? (
|
||||
<MainChart
|
||||
chartConfig={chartConfig}
|
||||
chartForm={chartForm}
|
||||
chartData={chartData}
|
||||
chartTitle={'RDII 그래프'}
|
||||
yFieldText2="강수량(mm)"
|
||||
yFieldText="유량(m³/h)"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-[464px] items-center justify-center text-[18px] text-[#2c2f33]">
|
||||
데이터가 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
w-[78vw]
|
||||
max-w-none
|
||||
min-w-[1100px]
|
||||
min-h-[72vh]
|
||||
overflow-visible
|
||||
z-[50]
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn="Y"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RdiiChart);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface ModalStore {
|
||||
isOpen: boolean;
|
||||
setIsOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useInfilChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
|
||||
export const useRdiiChartModalStore = create<ModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
setIsOpen: (open: boolean) => set({ isOpen: open }),
|
||||
}));
|
||||
+639
@@ -0,0 +1,639 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창
|
||||
*
|
||||
* 📄 FeatureClickEventPopup
|
||||
* 시설물(피처) 클릭시 오른쪽 팝업창 컴포넌트
|
||||
*
|
||||
* - 지도 시설물(피처) 클릭 시 팝업 표시 및 데이터 로딩
|
||||
* - 레이어 유형에 따른 동적 테이블 및 헤드리스트 설정
|
||||
* - 자체입력정보 조회 및 저장 기능 (하수관로, 우수토실, 하수펌프장, 하수맨홀)
|
||||
* - 코드 매핑 및 데이터 포맷팅 처리
|
||||
* - 상류관로 검색, 침입수/RDII 분석 차트 표시
|
||||
* - 모달을 통한 저장 확인 및 API 연동
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 하수관로 자체입력정보 조회: GET /api/v1/fcltppl/{fcltMngNo}/{mngInstCd} (sewerage-service)
|
||||
* - 우수토실 자체입력정보 조회: GET /api/v1/fcltstofcbr/{fcltMngNo}/{mngInstCd} (sewerage-service)
|
||||
* - 하수펌프장 자체입력정보 조회: GET /api/v1/fcltpump/{fcltMngNo}/{mngInstCd} (sewerage-service)
|
||||
* - 하수맨홀 자체입력정보 조회: GET /api/v1/fcltmhl/{fcltMngNo}/{mngInstCd} (sewerage-service)
|
||||
* - 하수관로 자체입력정보 저장: POST /api/v1/fcltppl (sewerage-service)
|
||||
* - 우수토실 자체입력정보 저장: POST /api/v1/fcltstofcbr (sewerage-service)
|
||||
* - 하수펌프장 자체입력정보 저장: POST /api/v1/fcltpump (sewerage-service)
|
||||
* - 하수맨홀 자체입력정보 저장: POST /api/v1/fcltmhl (sewerage-service)
|
||||
*/
|
||||
'use client'
|
||||
import { getData, postData } from "@/components/lib/api";
|
||||
import Modal from "@/components/ui/modal";
|
||||
import { VerticalTable } from "@/components/ui/verticalTable";
|
||||
import { useGisContext } from "@common/business_components/ui/map/Gis";
|
||||
import { Button } from "@common/components/ui";
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import Style from "ol/style/Style.js";
|
||||
import Fill from "ol/style/Fill.js";
|
||||
import Stroke from "ol/style/Stroke.js";
|
||||
import CircleStyle from "ol/style/Circle.js";
|
||||
import { getLayerContentComponent } from "../common";
|
||||
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
|
||||
import InfilChart from './chart/infilChart';
|
||||
import RdiiChart from './chart/rdiiChart';
|
||||
import { useInfilChartModalStore, useRdiiChartModalStore } from "./chartModalStore";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import { codeMapper } from "../common/codeMapper";
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { searchUpperPpls } from "./smh";
|
||||
|
||||
const linkedFacilityHighlightStyle = new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 18,
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 235, 59, 0.35)',
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(255, 193, 7, 0.45)',
|
||||
width: 2,
|
||||
}),
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: 'rgba(255, 235, 59, 0.2)',
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(255, 193, 7, 0.35)',
|
||||
width: 2.5,
|
||||
}),
|
||||
zIndex: 9999,
|
||||
});
|
||||
|
||||
|
||||
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
|
||||
const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; };
|
||||
const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태
|
||||
const [layerSpecificNm, setLayerSpecificNm] = useState<string>(""); // 레이어 이름 상태
|
||||
const [tableData, setTableData] = useState<any>(null); // 피처 데이터
|
||||
const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태
|
||||
const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태
|
||||
const [buttonChk, setButtonChk] = useState(true)
|
||||
const [fetchData, setFetchData] = useState<any>(); // api 호출여부
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
|
||||
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
|
||||
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
|
||||
// 🎯 InfilChart 및 RdiiChart 관련 상태 추가
|
||||
const { setIsOpen: setIsModalInfil } = useInfilChartModalStore();
|
||||
const { setIsOpen: setIsModalRdii } = useRdiiChartModalStore();
|
||||
const [isClicked, setIsClicked] = useState(true); // 클릭 여부 상태
|
||||
const [isLoading, setIsLoading] = useState(false); // 로딩 상태
|
||||
const [chmbrXcnts, setChmbrXcnts] = useState<string>(''); // 로딩 상태
|
||||
const [chmbrYdnts, setChmbrYdnts] = useState<string>(''); // 로딩 상태
|
||||
const linkedFacilityFeatureRef = useRef<any>(null);
|
||||
const linkedFacilityOriginalStyleRef = useRef<any>(null);
|
||||
|
||||
// 🎯 관로관리번호 추출
|
||||
const pplMngNo = featureData?.values_?.fclt_mng_no || "";
|
||||
|
||||
const featureId = typeof featureData.getId === 'function'
|
||||
? featureData.getId()
|
||||
: featureData.fid;
|
||||
|
||||
// 🎯 데이터 전처리 메모이제이션
|
||||
const processedData = useMemo(() => {
|
||||
if (!featureData?.values_) return null;
|
||||
|
||||
return {
|
||||
...featureData.values_,
|
||||
discharge_pipe_spec: `직경: ${featureData.values_.pipe_diameter || '-'} / 높이: ${featureData.values_.pipe_hg || '-'} / 길이: ${featureData.values_.pipe_lt || '-'}`
|
||||
};
|
||||
}, [featureData?.values_]);
|
||||
|
||||
const clearLinkedFacilityHighlight = () => {
|
||||
if (linkedFacilityFeatureRef.current) {
|
||||
linkedFacilityFeatureRef.current.setStyle(linkedFacilityOriginalStyleRef.current ?? undefined);
|
||||
linkedFacilityFeatureRef.current.changed?.();
|
||||
}
|
||||
|
||||
linkedFacilityFeatureRef.current = null;
|
||||
linkedFacilityOriginalStyleRef.current = null;
|
||||
};
|
||||
|
||||
// 하수처리구역에 연결된 하수처리시설 피처를 찾아 기존 아이콘 위에 노랑 하이라이트를 덧씌운다.
|
||||
const highlightLinkedFacility = (facilityCode: string | null | undefined) => {
|
||||
clearLinkedFacilityHighlight();
|
||||
|
||||
if (!gisMap || !facilityCode) return;
|
||||
|
||||
const normalizedFacilityCode = String(facilityCode).trim();
|
||||
const layers = gisMap.getLayers().getArray();
|
||||
|
||||
for (const layer of layers) {
|
||||
const source = layer?.getSource?.();
|
||||
if (!source?.getFeatures) continue;
|
||||
|
||||
const features = source.getFeatures();
|
||||
const targetFeature = features.find((feature: any) => {
|
||||
const featureId = feature?.getId?.();
|
||||
const isFacilityLayer = typeof featureId === 'string' && featureId.startsWith('tb_il_swage_prcs_fclt.');
|
||||
const featureFacilityCode = feature?.get?.('swtrmt_fclt_cd');
|
||||
|
||||
return isFacilityLayer && String(featureFacilityCode ?? '').trim() === normalizedFacilityCode;
|
||||
});
|
||||
|
||||
if (targetFeature) {
|
||||
const originalFeatureStyle = targetFeature.getStyle?.();
|
||||
const featureLayer = targetFeature.getLayer?.();
|
||||
const layerStyleFn = featureLayer?.getStyleFunction?.() ?? null;
|
||||
|
||||
linkedFacilityOriginalStyleRef.current = originalFeatureStyle;
|
||||
linkedFacilityFeatureRef.current = targetFeature;
|
||||
targetFeature.setStyle((feature: any, resolution: number) => {
|
||||
let styles: any[] = [];
|
||||
|
||||
if (typeof originalFeatureStyle === 'function') {
|
||||
const originalStyles = originalFeatureStyle(feature, resolution);
|
||||
if (Array.isArray(originalStyles)) styles.push(...originalStyles);
|
||||
else if (originalStyles) styles.push(originalStyles);
|
||||
} else if (originalFeatureStyle) {
|
||||
if (Array.isArray(originalFeatureStyle)) styles.push(...originalFeatureStyle);
|
||||
else styles.push(originalFeatureStyle);
|
||||
} else if (layerStyleFn) {
|
||||
const layerStyles = layerStyleFn(feature, resolution);
|
||||
if (Array.isArray(layerStyles)) styles.push(...layerStyles);
|
||||
else if (layerStyles) styles.push(layerStyles);
|
||||
}
|
||||
|
||||
styles.push(linkedFacilityHighlightStyle);
|
||||
return styles;
|
||||
});
|
||||
targetFeature.changed?.();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 피쳐데이터의 현재 레이어 이름 가져오는 함수
|
||||
const getSelectedLayers = () => {
|
||||
let result = null;
|
||||
const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기
|
||||
let featureLayer: any = null; // 피처가 속한 레이어를 찾기 위한 변수
|
||||
layers.some((layer: any) => {
|
||||
const source = layer.getSource();
|
||||
// if (source && featureData && featureData.fid) {
|
||||
// featureLayer = layer; // 피처가 속한 레이어 찾기
|
||||
// }
|
||||
if (source && source.hasFeature && featureData
|
||||
&& typeof featureData.getId === 'function' && source.hasFeature(featureData)) {
|
||||
featureLayer = layer; // 피처가 속한 레이어 찾기
|
||||
return true; // 반복 종료
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (featureData['ol_uid']) {
|
||||
// Gis 레이어 클릭 이벤트
|
||||
setIsClicked(true);
|
||||
const lyrTyNm = featureData.id_.split('.')[0];
|
||||
|
||||
switch (lyrTyNm) {
|
||||
case 'tb_ms_swage_ppl': {
|
||||
result = '하수관로';
|
||||
setHeadlist(MsSwagePplCol);
|
||||
setLayerSpecificNm(featureData.values_.fclt_mng_no);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
result = '하수맨홀';
|
||||
setHeadlist(MsSwageMhlCol);
|
||||
setLayerSpecificNm(featureData.values_.fclt_mng_no);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_pmstn': {
|
||||
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
|
||||
if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) {
|
||||
setIsDrtwSewagePump(true);
|
||||
setHeadlist(DrtwSewagePumpCol);
|
||||
}
|
||||
else {
|
||||
setIsDrtwSewagePump(false);
|
||||
setHeadlist(ExclnSewagePumpCol);
|
||||
}
|
||||
setLayerSpecificNm(featureData.values_.fclt_mng_no);
|
||||
result = '하수펌프장';
|
||||
break;
|
||||
}
|
||||
case 'tb_il_swage_prcs_fclt': {
|
||||
result = '하수처리시설';
|
||||
setHeadlist(SwtrmtFcltCol);
|
||||
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_prcs_zone': {
|
||||
result = '하수처리구역';
|
||||
setHeadlist(SwtrmtPrcsZoneCol);
|
||||
setLayerSpecificNm(featureData.values_.swtrmt_fclt_cd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_stofcbr': {
|
||||
result = '우수토실';
|
||||
setHeadlist(stofcbrCol);
|
||||
setLayerSpecificNm(featureData.values_.fclt_mng_no);
|
||||
break;
|
||||
}
|
||||
case 'om_dt_untrt_swg_rain': {
|
||||
result = '중점우수토실';
|
||||
setHeadlist(untrtSwgRainCol1);
|
||||
setHeadlist2(untrtSwgRainCol2);
|
||||
setLayerSpecificNm(featureData.values_.manage_no);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
result = featureLayer ? featureLayer.get('lyrNm') : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// SearchModal 클릭 이벤트
|
||||
setIsClicked(false);
|
||||
const lyrTyNm = featureData.lyrTypeNm;
|
||||
|
||||
switch (lyrTyNm) {
|
||||
case 'tb_ms_swage_ppl': {
|
||||
result = '하수관로';
|
||||
setHeadlist(MsSwagePplCol2)
|
||||
setLayerSpecificNm(featureData.swtrmtFcltCd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
result = '하수맨홀';
|
||||
setHeadlist(MsSwageMhlCol2)
|
||||
setLayerSpecificNm(featureData.fcltMngNo);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_pmstn': {
|
||||
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
|
||||
if (drtwPumpCode && featureData.usgCd.trim() === drtwPumpCode.val) {
|
||||
setIsDrtwSewagePump(false);
|
||||
setHeadlist(ExclnSewagePumpCol2)
|
||||
}
|
||||
else {
|
||||
setIsDrtwSewagePump(true);
|
||||
setHeadlist(DrtwSewagePumpCol2)
|
||||
}
|
||||
setLayerSpecificNm(featureData.fcltMngNo);
|
||||
result = '하수펌프장';
|
||||
break;
|
||||
}
|
||||
case 'tb_il_swage_prcs_fclt': {
|
||||
result = '하수처리시설';
|
||||
setHeadlist(SwtrmtFcltCol2)
|
||||
setLayerSpecificNm(featureData.swtrmtFcltCd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_prcs_zone': {
|
||||
result = '하수처리구역';
|
||||
setHeadlist(SwtrmtPrcsZoneCol2);
|
||||
setLayerSpecificNm(featureData.swtrmtFcltCd);
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_stofcbr': {
|
||||
result = '우수토실';
|
||||
setHeadlist(stofcbrCol2)
|
||||
setLayerSpecificNm(featureData.fcltMngNo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!featureId) return;
|
||||
|
||||
try {
|
||||
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
|
||||
setLayerNm(selectedLayerName); // 상태 업데이트
|
||||
|
||||
let data;
|
||||
if (featureData['ol_uid']) data = featureData.values_; // Gis 레이어 클릭 이벤트일 때
|
||||
else data = featureData; // SearchModal 클릭 이벤트일 때
|
||||
|
||||
// 하수맨홀인 경우 용도코드를 텍스트로 변환
|
||||
if (selectedLayerName === '하수맨홀' && data) {
|
||||
// mqlt_cd 값 변환 (재질코드)
|
||||
if (data.mqlt_cd) {
|
||||
const materialCodeItem = MS_SWAGE_MHL_CODES.find(
|
||||
code => code.val === data.mqlt_cd.trim()
|
||||
);
|
||||
if (materialCodeItem) data.mqlt_cd = `${materialCodeItem.text}`;
|
||||
}
|
||||
} else {
|
||||
// 중점우수토실인 경우 구분 값
|
||||
if (selectedLayerName === '중점우수토실' && data) {
|
||||
if (data.mesure_spot_se) {
|
||||
const mesureSpotSe = [{id:'0', text: '합류식'}, {id:'1', text: '분류식'}]
|
||||
const mesureSpotSeItem = mesureSpotSe.find(
|
||||
code => code.id === data.mesure_spot_se.trim()
|
||||
);
|
||||
if (mesureSpotSeItem) data.mesure_spot_se = mesureSpotSeItem.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🎯 기본 데이터 포맷팅
|
||||
const formattedData = formatDisplayData(data);
|
||||
|
||||
setFetchData(formattedData);
|
||||
setTableData(formattedData);
|
||||
|
||||
switch (selectedLayerName) {
|
||||
case '하수관로':
|
||||
setButtonChk(true)
|
||||
|
||||
// ✅ codeMapper 사용하여 API 결과도 변환
|
||||
setTableData(codeMapper({
|
||||
layerName: selectedLayerName,
|
||||
data: { ...formattedData }
|
||||
}));
|
||||
break;
|
||||
case '하수맨홀':
|
||||
setButtonChk(true)
|
||||
|
||||
// ✅ codeMapper 사용하여 API 결과도 변환
|
||||
setTableData(codeMapper({
|
||||
layerName: selectedLayerName,
|
||||
data: { ...formattedData }
|
||||
}));
|
||||
break;
|
||||
case '하수펌프장':
|
||||
setButtonChk(true)
|
||||
|
||||
setTableData(codeMapper({
|
||||
layerName: selectedLayerName,
|
||||
data: { ...formattedData }
|
||||
}));
|
||||
break;
|
||||
case '하수처리시설':
|
||||
setButtonChk(false)
|
||||
break;
|
||||
case '하수처리구역':
|
||||
setButtonChk(false)
|
||||
break;
|
||||
case '우수토실':
|
||||
setButtonChk(true)
|
||||
setIsMainSsc(false)
|
||||
break;
|
||||
case '중점우수토실':
|
||||
setButtonChk(false)
|
||||
setIsMainSsc(true)
|
||||
// 🎯 processedData도 포맷팅하여 설정
|
||||
const processedFormattedData = formatDisplayData(processedData);
|
||||
|
||||
setFetchData([processedFormattedData])
|
||||
setTableData(processedFormattedData);
|
||||
// setIsLoading(false); // 기본 렌더링 완료
|
||||
|
||||
setTableData(codeMapper({
|
||||
layerName: selectedLayerName,
|
||||
data: { ...processedFormattedData }
|
||||
}));
|
||||
|
||||
setChmbrXcnts(data.chmbr_xcnts);
|
||||
setChmbrYdnts(data.chmbr_ydnts);
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// 자체입력정보 API 엔드포인트 선택
|
||||
let apiEndpoint = '';
|
||||
const fcltMngNo = featureData?.values_?.fclt_mng_no || null;
|
||||
const mngInstCd = featureData?.values_?.mng_inst_cd || null;
|
||||
if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`;
|
||||
else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`;
|
||||
else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`;
|
||||
else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${fcltMngNo}/${mngInstCd}`;
|
||||
|
||||
// API에서 상세 데이터 가져오기
|
||||
if (apiEndpoint) {
|
||||
try {
|
||||
const result = await getData(apiEndpoint);
|
||||
|
||||
if (result) {
|
||||
const processedApiData = codeMapper({
|
||||
layerName: selectedLayerName,
|
||||
data: { ...result, ...tableData }
|
||||
});
|
||||
|
||||
setFetchData(processedApiData);
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error('API 데이터 로딩 중 오류:', apiError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('데이터 로딩 중 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [featureId, processedData]);
|
||||
|
||||
useEffect(() => {
|
||||
const layerTypeNm = featureData?.values_
|
||||
? featureData?.id_?.split('.')?.[0]
|
||||
: featureData?.lyrTypeNm;
|
||||
|
||||
if (layerTypeNm === 'tb_ms_swage_prcs_zone') {
|
||||
const facilityCode = featureData?.values_?.swtrmt_fclt_cd ?? featureData?.swtrmtFcltCd;
|
||||
highlightLinkedFacility(facilityCode);
|
||||
return () => {
|
||||
clearLinkedFacilityHighlight();
|
||||
};
|
||||
}
|
||||
|
||||
clearLinkedFacilityHighlight();
|
||||
|
||||
return () => {
|
||||
clearLinkedFacilityHighlight();
|
||||
};
|
||||
}, [featureData, gisMap]);
|
||||
|
||||
// 저장 시 모달 띄우기
|
||||
const handleSaveClick = () => {
|
||||
setIsModalOpen(true); // 모달 열기
|
||||
};
|
||||
|
||||
const handleModalSave = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
btnClickEvent(); // 저장 이벤트 실행
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setIsModalOpen(false); // 모달 닫기
|
||||
};
|
||||
|
||||
// 저장 버튼 이벤트
|
||||
const btnClickEvent = async () => {
|
||||
const bodyData: Record<string, any> = {}
|
||||
|
||||
if (buttonChk && contentRef.current) {
|
||||
const inputElements = contentRef.current.querySelectorAll('input');
|
||||
const selectElements = contentRef.current.querySelectorAll('select');
|
||||
|
||||
// 카멜케이스 변환 함수
|
||||
const toCamelCase = (str: string) =>
|
||||
str.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
|
||||
|
||||
// select 태그의 값 가져오기
|
||||
selectElements.forEach((select) => {
|
||||
const camelCaseId = toCamelCase(select.id);
|
||||
bodyData[camelCaseId] = select.value;
|
||||
});
|
||||
|
||||
// // input 태그의 값 가져오기
|
||||
inputElements.forEach((input) => {
|
||||
const camelCaseId = toCamelCase(input.id);
|
||||
bodyData[camelCaseId] = input.value;
|
||||
});
|
||||
|
||||
// 레이어명에 따라 API 엔드포인트 분기처리
|
||||
let apiEndpoint = '';
|
||||
let result = null;
|
||||
|
||||
if (layerNm === '하수관로') apiEndpoint = 'v1/fcltppl';
|
||||
else if (layerNm === '우수토실') apiEndpoint = 'v1/fcltstofcbr';
|
||||
else if (layerNm === '하수펌프장') apiEndpoint = 'v1/fcltpump';
|
||||
else if (layerNm === '하수맨홀') apiEndpoint = 'v1/fcltmhl';
|
||||
|
||||
bodyData['fcltMngNo'] = featureData.values_.fclt_mng_no.toString() || null;
|
||||
bodyData['mngInstCd'] = featureData.values_.mng_inst_cd.toString() || null;
|
||||
// 빈 키 제거
|
||||
if (bodyData[""] !== undefined) delete bodyData[""];
|
||||
|
||||
// 🎯 우수토실일 경우 prrtymngYn 값 변환
|
||||
if (layerNm === '우수토실' && bodyData.prrtymngYn) {
|
||||
if (bodyData.prrtymngYn === '중점우수토실') {
|
||||
bodyData.prrtymngYn = 'Y';
|
||||
} else if (bodyData.prrtymngYn === '우수토실') {
|
||||
bodyData.prrtymngYn = 'N';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
result = await postData(apiEndpoint, bodyData);
|
||||
|
||||
if (result) {
|
||||
alert('저장되었습니다');
|
||||
setFetchData(result.data || result);
|
||||
} else {
|
||||
alert('저장에 실패했습니다');
|
||||
console.error('API 응답 오류:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('저장 중 오류가 발생했습니다');
|
||||
console.error('API 요청 오류:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 레이어명에 따라 상세페이지 렌더링
|
||||
const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null;
|
||||
|
||||
return (
|
||||
<div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-5rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[50]">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<div className="card-header bg-primary flex items-center justify-between relative p-4">
|
||||
<div className="flex items-center space-x-4 flex-1">
|
||||
<h3 className="text-2xl font-medium text-white text-label whitespace-nowrap">{layerNm}
|
||||
{tableData?.usg_cd && (
|
||||
<>
|
||||
({tableData.usg_cd})
|
||||
</>
|
||||
)} 정보 <br />관리번호 : {layerSpecificNm}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* 하수관로일 때만 Infil/RDII 버튼 표시 */}
|
||||
{layerNm === '하수관로' && (
|
||||
<>
|
||||
<Button onClick={() => setIsModalInfil(true)}
|
||||
className="py-1 text-xl text-blue-600 border border-blue-600 rounded hover:bg-gray-700"
|
||||
color="white"
|
||||
size="sm"
|
||||
>
|
||||
침입수분석
|
||||
</Button>
|
||||
<Button onClick={() => setIsModalRdii(true)}
|
||||
className="py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700 mr-4"
|
||||
color="white"
|
||||
size="sm"
|
||||
>
|
||||
RDII분석
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{layerNm === '하수맨홀' && (
|
||||
<>
|
||||
<Button
|
||||
color="white"
|
||||
className="px-2 py-1 text-xl text-gray-600 border border-gray-600 rounded hover:bg-gray-700 mr-4"
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
searchUpperPpls(gisMap, layerSpecificNm).finally(() => setIsLoading(false));
|
||||
}}
|
||||
>
|
||||
상류관로 보기
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className="flex items-center space-x-4 text-white hover:text-gray-300 focus:outline-none"
|
||||
size="xs"
|
||||
>
|
||||
✕
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}>
|
||||
<VerticalTable
|
||||
headName='기본정보'
|
||||
tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined}
|
||||
tableData={[tableData]}
|
||||
headlist={headlist}
|
||||
pageSize={20}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
// 두 번째 테이블 조건부 props
|
||||
showSecondTable={layerNm === '중점우수토실' ? true : false}
|
||||
tableName2={layerNm === '중점우수토실' ? "시설현황" : undefined}
|
||||
tableData2={layerNm === '중점우수토실' && tableData ? [tableData] : []}
|
||||
headlist2={layerNm === '중점우수토실' ? headlist2 : undefined}
|
||||
pageSize2={20}
|
||||
total2={0}
|
||||
isEditable2={buttonChk}
|
||||
/>
|
||||
<Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal>
|
||||
{ContentComponent &&
|
||||
<ContentComponent
|
||||
featureData={fetchData}
|
||||
isMainSsc={isMainSsc}
|
||||
isDrtwSewagePump={isDrtwSewagePump}
|
||||
onSaveClick={handleSaveClick}
|
||||
chmbrXcnts={chmbrXcnts}
|
||||
chmbrYdnts={chmbrYdnts}
|
||||
gisObject={featureData.values_}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<InfilChart pplMngNo={pplMngNo} />
|
||||
<RdiiChart pplMngNo={pplMngNo} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default FeatureClickEventPopup;
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수펌프장)
|
||||
*
|
||||
* 📄 sewagePump
|
||||
* 하수펌프장 정보 컴포넌트
|
||||
*
|
||||
* - 빗물펌프장 및 오수펌프장 구분에 따른 자체 입력 정보 표시 및 편집 기능
|
||||
* - VerticalTable을 통한 입력 폼 렌더링
|
||||
* - 저장 버튼을 통한 데이터 저장 기능
|
||||
*/
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import React, { useState, useEffect } from 'react'
|
||||
// 빗물펌프장 자체 입력 정보
|
||||
const exclnInputColumn = [
|
||||
{ id: "swtrmtZoneNm", title: "배수구역", type: "input" },
|
||||
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
|
||||
{ id: "designFrqYr", title: "설계빈도(년)", type: "input", placeholder: "예: 10", isNumber: true, length: 4 },
|
||||
]
|
||||
// 오수펌프장 자체 입력 정보
|
||||
const drtwInputColumn = [
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
{ id: "mltvDtdvNm", title: "처리분구", type: "input" },
|
||||
]
|
||||
|
||||
|
||||
const sewagePump = ({ featureData, isDrtwSewagePump, onSaveClick }: { featureData: any, isDrtwSewagePump?: boolean, onSaveClick?: () => void }) => {
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
|
||||
// featureData가 변경될 때 formData 초기화
|
||||
useEffect(() => {
|
||||
if (featureData) {
|
||||
setFormData(featureData);
|
||||
}
|
||||
}, [featureData]);
|
||||
|
||||
// 폼 데이터가 변경될 때 호출될 핸들러
|
||||
const handleFormDataChange = (newFormData: any) => {
|
||||
setFormData(newFormData);
|
||||
};
|
||||
|
||||
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
|
||||
const handleSaveClick = () => {
|
||||
if (onSaveClick) {
|
||||
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
|
||||
onSaveClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticalTable
|
||||
headName='입력정보'
|
||||
tableData={featureData ? [formData] : []}
|
||||
headlist={isDrtwSewagePump ? drtwInputColumn : exclnInputColumn}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
children={undefined}
|
||||
cellClick={undefined}
|
||||
isEditable={true}
|
||||
onPageChange={function (page: number): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
showSaveButton={true}
|
||||
onSaveClick={handleSaveClick}
|
||||
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default sewagePump
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (하수맨홀)
|
||||
*
|
||||
* 📄 Smh
|
||||
* 하수맨홀 정보 컴포넌트
|
||||
*
|
||||
* - 하수맨홀의 자체입력정보 표시 및 편집 기능
|
||||
* - VerticalTable을 통한 입력 폼 렌더링
|
||||
* - 상류관로 검색 및 지도 포커스 기능
|
||||
* - 저장 버튼을 통한 데이터 저장 기능
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 상류관로 조회: GET /api/v1/fcltmhl/upperPpls/{fcltMngNo} (sewerage-service)
|
||||
*/
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
|
||||
// 상류관로 보기 함수 (FeatureClickEventPopup에서 사용)
|
||||
export async function searchUpperPpls(gisMap: any, fcltMngNo: any) {
|
||||
const BASE_URL = "/sewerage-service/api";
|
||||
const res = await fetch(`${BASE_URL}/v1/fcltmhl/upperPpls/${fcltMngNo}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
let data: any = null;
|
||||
if (res.ok) {
|
||||
try {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get("content-length");
|
||||
if (contentLength === "0") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === "") {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// JSON 파싱 시도
|
||||
const response = JSON.parse(text);
|
||||
data = response.data;
|
||||
} catch (error) {
|
||||
console.error("JSON parsing error:", error);
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const select: any = GisApp.Module.select;
|
||||
let foundFeature: any = null;
|
||||
|
||||
(data || []).forEach((item: any) => {
|
||||
const pplMngNo = item.ppl_mng_no?.trim?.();
|
||||
|
||||
gisMap
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.filter((e: any) => e.get("lyrTyNm") == "tb_ms_swage_ppl")
|
||||
.forEach((layer: any) => {
|
||||
const source = layer.getSource?.();
|
||||
if (source?.getFeatures) {
|
||||
const features = source.getFeatures();
|
||||
for (const f of features) {
|
||||
if (f.values_.fclt_mng_no?.trim?.() === pplMngNo) {
|
||||
foundFeature = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (foundFeature) select?.selectMultiFeature?.(foundFeature, true);
|
||||
});
|
||||
|
||||
if (!foundFeature) alert(`해당 맨홀의 상류관로를 찾을 수 없습니다.`);
|
||||
|
||||
const geometry = foundFeature?.getGeometry();
|
||||
if (geometry) {
|
||||
const extent = geometry.getExtent();
|
||||
gisMap.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 14,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const inputColumn = [
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
{ id: "mltvZoneNm", title: "배수구역", type: "input" },
|
||||
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
|
||||
{ id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] },
|
||||
];
|
||||
const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => {
|
||||
const [formData, setFormData] = useState<any>({});
|
||||
|
||||
// featureData가 변경될 때 formData 초기화
|
||||
useEffect(() => {
|
||||
if (featureData) {
|
||||
setFormData(featureData);
|
||||
}
|
||||
}, [featureData]);
|
||||
|
||||
// 폼 데이터가 변경될 때 호출될 핸들러
|
||||
const handleFormDataChange = (newFormData: any) => {
|
||||
setFormData(newFormData);
|
||||
};
|
||||
|
||||
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
|
||||
const handleSaveClick = () => {
|
||||
if (onSaveClick) {
|
||||
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
|
||||
onSaveClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<VerticalTable
|
||||
headName='입력정보'
|
||||
tableData={featureData ? [formData] : []}
|
||||
headlist={inputColumn}
|
||||
pageSize={20}
|
||||
total={0}
|
||||
children={undefined}
|
||||
cellClick={undefined}
|
||||
isEditable={true}
|
||||
onPageChange={function (page: number): void {
|
||||
throw new Error("Function not implemented.");
|
||||
}}
|
||||
showSaveButton={true}
|
||||
onSaveClick={handleSaveClick}
|
||||
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Smh;
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (시설물 클릭) - 오른쪽 팝업창 (빗물펌프)
|
||||
*
|
||||
* 📄 StormWaterPump
|
||||
* 빗물펌프 정보 컴포넌트
|
||||
*
|
||||
* - 빗물펌프의 자체입력정보 표시 및 편집 기능
|
||||
* - VerticalTable을 통한 입력 폼 렌더링
|
||||
*/
|
||||
import { VerticalTable } from '@/components/ui/verticalTable';
|
||||
import React from 'react'
|
||||
const inputColumn = [
|
||||
{ id: "swtrmtZoneNm", title: "처리구역", type: "input" },
|
||||
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
|
||||
]
|
||||
// 빗물펌프
|
||||
const StormWaterPump = ({ featureData }: { featureData: Record<string, any> }) => {
|
||||
const validKey = inputColumn.map((item) => item.id);
|
||||
const filteredData = featureData && Object.entries(featureData).filter(([key]) => validKey.includes(key)).reduce((obj: Record<string, any>, [key, value]) => { obj[key] = value; return obj; }, {});
|
||||
return (
|
||||
<VerticalTable
|
||||
headName='입력정보'
|
||||
tableData={[filteredData]}
|
||||
headlist={inputColumn}
|
||||
pageSize={0}
|
||||
total={0}
|
||||
children={undefined}
|
||||
cellClick={undefined}
|
||||
isEditable={true}
|
||||
onPageChange={function (page: number): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default StormWaterPump
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지
|
||||
*
|
||||
* 📄 Map Page
|
||||
* 지도 페이지 컴포넌트
|
||||
*
|
||||
* - OpenLayers 기반 GIS 지도 렌더링 및 상호작용
|
||||
* - 레이어 트리 데이터 관리 및 동적 레이어 표시/숨김
|
||||
* - 피처 검색 및 선택 기능 (SearchModal, ProcSearchBar)
|
||||
* - 피처 클릭 이벤트 처리 및 상세 정보 팝업 표시
|
||||
* - 수심(WaterDepth) 레이어 애니메이션 재생 기능
|
||||
* - Zustand를 통한 검색 모달 상태 관리
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 레이어 설정 조회: GET /api/v1/map/lyrStngs/{lyrGrp} (sewerage-service)
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import Gis from "@common/business_components/ui/map/Gis";
|
||||
import { getData } from "../../../components/lib/api";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import SearchModal from "./searchModal/searchModal";
|
||||
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
|
||||
import { WaterDepth, WATER_DEPTH_LAYER_TYPES } from "./waterDepth/waterDepth";
|
||||
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
|
||||
const Map = () => {
|
||||
const { isSearchModalOpen, setIsSearchModalOpen } = useSearchModalStore();
|
||||
|
||||
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
|
||||
const gisMaps = useRef<any | null>(null);
|
||||
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
|
||||
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
|
||||
const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
let data;
|
||||
const res = await fetch("/sewerage-service/api/v1/map/lyrStngs/SN_NETWORK", {
|
||||
method: "GET",
|
||||
});
|
||||
if (res.ok) {
|
||||
// 응답이 비어있는지 확인
|
||||
const contentLength = res.headers.get('content-length');
|
||||
if (contentLength === '0') data = null;
|
||||
|
||||
// 응답 텍스트를 먼저 가져와서 빈 문자열인지 확인
|
||||
const text = await res.text();
|
||||
if (!text || text.trim() === '') data = null;
|
||||
|
||||
// JSON 파싱 시도
|
||||
data = JSON.parse(text);
|
||||
}
|
||||
setTreeData(data);
|
||||
} catch (error) {
|
||||
console.error('JSON parsing error:', error);
|
||||
// JSON 파싱에 실패한 경우, 원본 텍스트 또는 null 반환
|
||||
throw new Error('Invalid JSON response from server');
|
||||
}
|
||||
};
|
||||
|
||||
// SearchModal, GisContent 등에서 호출
|
||||
const onFeatureSelect = (data: any) => {
|
||||
let map = gisMaps.current;
|
||||
if (!map) {
|
||||
console.warn("지도가 초기화되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 항상 같은 select 객체 사용
|
||||
const select = GisApp.Module.select as any;
|
||||
if (!select) return;
|
||||
|
||||
// 1. 기존 선택 초기화
|
||||
select.resetSelectedFeature();
|
||||
|
||||
// 2. data가 OpenLayers Feature 인스턴스인지 확인
|
||||
let foundFeature = null;
|
||||
if (
|
||||
data &&
|
||||
typeof data.getGeometry === "function" &&
|
||||
typeof data.setStyle === "function"
|
||||
) {
|
||||
// 이미 Feature 인스턴스라면 그대로 사용
|
||||
foundFeature = data;
|
||||
} else {
|
||||
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
|
||||
const featureId = data.fid || data.ftrIdn || data.id;
|
||||
map
|
||||
.getLayers()
|
||||
.getArray()
|
||||
.forEach((layer: any) => {
|
||||
const source = layer.getSource?.();
|
||||
if (source?.getFeatureById) {
|
||||
const feature = source.getFeatureById(String(featureId).trim());
|
||||
if (feature) foundFeature = feature;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 피처 선택
|
||||
if (foundFeature) {
|
||||
let geometry = foundFeature.getGeometry();
|
||||
if (geometry) {
|
||||
let extent = geometry.getExtent();
|
||||
|
||||
map.getView().fit(extent, {
|
||||
duration: 300,
|
||||
padding: [50, 50, 50, 50],
|
||||
maxZoom: 16,
|
||||
constrainResolution: true,
|
||||
nearest: true,
|
||||
});
|
||||
}
|
||||
select.selectFeature(foundFeature, true);
|
||||
} else {
|
||||
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
|
||||
}
|
||||
|
||||
setFeatureData(data);
|
||||
setIsOpen(true);
|
||||
setIsSearchModalOpen(false);
|
||||
};
|
||||
|
||||
const facilityTypes = [{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" }];
|
||||
// WaterDepth 표시 여부를 결정하는 함수
|
||||
const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
const waterDepthLayers = checkedNodesData.filter(node =>
|
||||
WATER_DEPTH_LAYER_TYPES.includes(node.data?.lyrTyNm)
|
||||
);
|
||||
|
||||
const hasWaterDepthLayer = waterDepthLayers.length > 0;
|
||||
setShowWaterDepth(hasWaterDepthLayer);
|
||||
}, []);
|
||||
|
||||
// 체크된 레이어 변경 처리 함수
|
||||
const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
|
||||
handleWaterDepthVisibility(checkedIds, checkedNodesData);
|
||||
}, [handleWaterDepthVisibility]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-container">
|
||||
<Gis
|
||||
treeData={treeData}
|
||||
isSearch={true}
|
||||
gisMaps={gisMaps}
|
||||
isMiniMap={false}
|
||||
searchView={SearchModal}
|
||||
detailView={FeatureClickEventPopup}
|
||||
onFeatureSelect={onFeatureSelect}
|
||||
featureData={featureData}
|
||||
setFeatureData={setFeatureData}
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
isSearchModalOpen={isSearchModalOpen}
|
||||
setIsSearchModalOpen={setIsSearchModalOpen}
|
||||
onCheckedLayersChange={handleCheckedLayersChange}
|
||||
searchBar={<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />}
|
||||
waterDepth={showWaterDepth ? WaterDepth : undefined}
|
||||
isPlaying={isPlaying}
|
||||
setIsPlaying={setIsPlaying as any}
|
||||
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
|
||||
checkInterval={2500}
|
||||
showZoomLevel={true}
|
||||
|
||||
defaultZoom={7.5}
|
||||
defaultLonLat={[127.75, 36]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭
|
||||
*
|
||||
* 📄 BasicTab Component
|
||||
* Next.js App Router (app/) 기반 검색 모달의 기본정보 탭 컴포넌트
|
||||
*
|
||||
* - 기본정보 검색을 위한 구분 선택 및 조건 입력 UI 제공
|
||||
* - 하수관로, 하수처리시설, 하수처리구역, 하수맨홀, 하수펌프장, 우수토실 등 다양한 유형별 컴포넌트 렌더링
|
||||
* - useSearchModalStore를 통해 상태 관리 및 사용자 상호작용 처리
|
||||
*/
|
||||
import React from 'react';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Label } from '@common/components';
|
||||
import SwagePpl from './swagePpl';
|
||||
import SwagePrcsFclt from './swagePrcsFclt';
|
||||
import SwagePrcsZone from './swagePrcsZone';
|
||||
import SwageMhl from './swageMhl';
|
||||
import SwagePmstn from './swagePmstn';
|
||||
import Stofcbr from './stofcbr';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
|
||||
|
||||
// 기본정보 '구분' 선택 목록
|
||||
export const BASIC_TYPES = [
|
||||
{ val: "tb_ms_swage_ppl", text: "하수관로" },
|
||||
{ val: "tb_il_swage_prcs_fclt", text: "하수처리시설" },
|
||||
{ val: "tb_ms_swage_prcs_zone", text: "하수처리구역" },
|
||||
{ val: "tb_ms_swage_mhl", text: "하수맨홀" },
|
||||
{ val: "tb_ms_swage_pmstn", text: "하수펌프장" },
|
||||
{ val: "tb_ms_stofcbr", text: "우수토실" },
|
||||
];
|
||||
|
||||
const BasicTab: React.FC = () => {
|
||||
const { basicType, setBasicType } = useSearchModalStore();
|
||||
|
||||
return (
|
||||
<div className="relative border rounded-lg pt-5 px-5 w-full min-h-[10vh]">
|
||||
{/* 구분 */}
|
||||
<div className="mb-3 flex items-center justify-between gap-4">
|
||||
<div className="flex items-center">
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">구분</Label>
|
||||
<SelectBox
|
||||
className="w-[150px] mr-3"
|
||||
selectClass="rounded-lg bg-white"
|
||||
label=""
|
||||
selectArray={BASIC_TYPES}
|
||||
onSelectValue={(value: string | number) =>
|
||||
setBasicType(String(value))
|
||||
}
|
||||
value={basicType}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 조회 조건 */}
|
||||
{basicType === "tb_ms_swage_ppl" && (<SwagePpl />)} {/* 하수관로 */}
|
||||
{basicType === "tb_il_swage_prcs_fclt" && (<SwagePrcsFclt />)} {/* 하수처리시설 */}
|
||||
{basicType === "tb_ms_swage_prcs_zone" && (<SwagePrcsZone />)} {/* 하수처리구역 */}
|
||||
{basicType === "tb_ms_swage_mhl" && (<SwageMhl />)} {/* 하수맨홀 */}
|
||||
{basicType === "tb_ms_swage_pmstn" && (<SwagePmstn />)} {/* 하수펌프장 */}
|
||||
{basicType === "tb_ms_stofcbr" && (<Stofcbr />)} {/* 우수토실 */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(BasicTab);
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 유지관리 정보 탭
|
||||
*
|
||||
* 📄 Operation Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 유지관리 정보 탭 컴포넌트
|
||||
*
|
||||
* - 업무구분 선택 (보수 및 갱신, 점검 및 진단, 청소 및 준설)
|
||||
* - 사업명 입력 및 수행기간 범위 선택
|
||||
* - 사업 유형 필터링 기능
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from 'react';
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
import useStore from '@/store/useStore';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import useSearchModal from './useSearchModal';
|
||||
import useOperationTab from './useOperationTab';
|
||||
|
||||
|
||||
const OperationTab: React.FC = () => {
|
||||
const { BIZ_TY_CD } = useStore();
|
||||
const {
|
||||
workType, setWorkType,
|
||||
var1, setVar1, var2, setVar2,
|
||||
yearFilter, setYearFilter,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
const { WORK_TYPES } = useOperationTab();
|
||||
|
||||
const BIZ_TY_CD_SELECT_ARRAY = BIZ_TY_CD.map(item => ({
|
||||
val: item.lwpoCd,
|
||||
text: item.lwpoCdNm,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col border rounded-lg pt-5 px-5 w-full min-h-[10vh]">
|
||||
<div className="flex items-center mb-3">
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">업무구분</Label>
|
||||
<SelectBox
|
||||
className="w-40 mr-8"
|
||||
selectClass="rounded-lg bg-white text-xl"
|
||||
label=""
|
||||
selectArray={WORK_TYPES.map((w) => ({
|
||||
val: w.val,
|
||||
text: w.text,
|
||||
}))}
|
||||
onSelectValue={(val) => setWorkType(String(val))}
|
||||
value={workType}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center mb-6">
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">사업명</Label>
|
||||
<Input
|
||||
className="w-[100px] !text-xl placeholder:text-center mr-8"
|
||||
placeholder="입력"
|
||||
value={var1}
|
||||
onChange={(e) => { setVar1(e.target.value); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<YearRangeSelectBox
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
|
||||
startLabel="수행기간"
|
||||
endLabel=""
|
||||
/>
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">사업 유형</Label>
|
||||
<SelectBox
|
||||
className="w-60 mr-8"
|
||||
selectClass="rounded-lg bg-white text-xl"
|
||||
label=""
|
||||
selectArray={[{ val: "0", text: "전체" }, ...BIZ_TY_CD_SELECT_ARRAY]}
|
||||
value={var2}
|
||||
onSelectValue={(val) => {
|
||||
setVar2(String(val));
|
||||
}}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default React.memo(OperationTab);
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭 (셀 클릭)
|
||||
*
|
||||
* 📄 Report Files Modal
|
||||
* Next.js App Router (app/) 기반 보고서 파일 모달 컴포넌트
|
||||
*
|
||||
* - 하수도정비 기본계획 보고서 파일 목록 표시
|
||||
* - 파일명 및 등록일자 정보 테이블 출력
|
||||
* - 페이지네이션 및 로딩 오버레이 지원
|
||||
* - 파일 클릭 이벤트 처리 및 상세 조회
|
||||
* - Zustand를 통한 모달 상태 및 데이터 관리
|
||||
* - 모달 열림/닫힘 시 데이터 초기화 기능
|
||||
*/
|
||||
'use client'
|
||||
import React, { useEffect } from 'react';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import useReportTab from './useReportTab';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import { UiTable } from '@common/business_components/ui';
|
||||
import { UiModal } from "@common/business_components";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
|
||||
type ModalOpenProp = { clsfSystmSn: string } // 자료관 기본계획 일련번호
|
||||
const ReportFilesModal: React.FC<ModalOpenProp> = ({ clsfSystmSn }: ModalOpenProp) => {
|
||||
// 하수도정비 기본계획 보고서 파일목록
|
||||
const REPORT_FILES_HEADLIST = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "fileTitle", title: "파일명" },
|
||||
{ id: "regDt", title: "등록일자" },
|
||||
];
|
||||
|
||||
const {
|
||||
curPage, setCurPage,
|
||||
isLoading,
|
||||
isResetPage,
|
||||
isResetSelection,
|
||||
setIsResetPage,
|
||||
setIsResetSelection
|
||||
} = useSearchModalStore();
|
||||
const {
|
||||
isOpen: isModalOpen,
|
||||
files,
|
||||
dcFiles: filesOrigin,
|
||||
setIsOpen: setIsModalOpen,
|
||||
setFiles,
|
||||
fetchDcFiles
|
||||
} = useReportTabStore();
|
||||
const { getFiles, handleCellClick } = useReportTab();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchDcFiles(clsfSystmSn);
|
||||
}, [clsfSystmSn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpen === true) {
|
||||
getFiles(); // 파일 목록 조회
|
||||
} else {
|
||||
setFiles([]); // 모달이 닫힐 때 파일 목록 초기화
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
}
|
||||
}, [filesOrigin, isModalOpen, curPage]);
|
||||
|
||||
const renderBody = () => (
|
||||
<>
|
||||
<div className="flex-1 px-2 pb-2">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={(() => {
|
||||
return (<>조회 결과 ({filesOrigin.length.toLocaleString('ko-KR')})건</>);
|
||||
})()
|
||||
}
|
||||
tableData={files}
|
||||
headlist={REPORT_FILES_HEADLIST}
|
||||
pageSize={10}
|
||||
total={filesOrigin.length}
|
||||
cellClick={handleCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
headerTitle=""
|
||||
bodyClass={`
|
||||
min-h-[50vh] max-h-[90dvh]
|
||||
max-w-[750px]
|
||||
h-auto w-auto
|
||||
overflow-auto
|
||||
z-[5000]
|
||||
flex items-start
|
||||
`}
|
||||
body={renderBody()}
|
||||
noFooterYn='Y'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportFilesModal);
|
||||
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭
|
||||
*
|
||||
* 📄 Report Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 보고서 탭 컴포넌트
|
||||
*
|
||||
* - 하수도정비 기본계획 보고서 목록 표시 및 필터링
|
||||
* - 시도/시군구/보고서 구분/승인년도 기반 검색 기능
|
||||
* - 테이블 데이터 표시 및 페이지네이션 지원
|
||||
* - 보고서 클릭 시 파일 목록 모달 열기
|
||||
* - Zustand를 통한 모달 상태 및 데이터 관리
|
||||
* - 로딩 오버레이 및 선택 초기화 기능
|
||||
*/
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Label } from '@common/components';
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { SelectBox, UiTable } from '@common/business_components/ui';
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import useReportTab from './useReportTab';
|
||||
import useStore from '@/store/useStore';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import ReportFilesModal from './reportFilesModal'; // 파일목록모달
|
||||
|
||||
|
||||
// 하수도정비 기본계획 보고서
|
||||
export const reportTableHeadlist = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "sido", title: "시도" },
|
||||
{ id: "sigungu", title: "시군구" },
|
||||
{ id: "approvalYear", title: "승인년도" },
|
||||
{ id: "plantZoneGubun", title: "구분" },
|
||||
{ id: "reportGubun", title: "보고서구분" },
|
||||
{ id: "fileName", title: "파일명" },
|
||||
{ id: "regDate", title: "등록일자" },
|
||||
];
|
||||
interface ReportTabProps {
|
||||
open?: boolean;
|
||||
isSearch?: boolean;
|
||||
ctpvNm?: string;
|
||||
sggNm?: string;
|
||||
}
|
||||
|
||||
const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sggNm }) => {
|
||||
|
||||
// const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
// const [tableData, setTableData] = useState<any[]>([]);
|
||||
// const [tableCount, setTableCount] = useState(0);
|
||||
// const [curPage, setCurPage] = useState(1);
|
||||
// const [yearFilter, setYearFilter] = useState({ start: "", end: "" });
|
||||
const [ctpvCd, setCtpvCd] = useState('0');
|
||||
const [sggCd, setSggCd] = useState('0');
|
||||
const [reportGubun, setReportGubun] = useState("0");
|
||||
const [clsfSystmSn, setClsfSystmSn] = useState(''); // 자료관 기본계획 일련번호
|
||||
|
||||
const {
|
||||
tableData,
|
||||
tableCount,
|
||||
isLoading,
|
||||
yearFilter,
|
||||
curPage,
|
||||
setTableData,
|
||||
setTableCount,
|
||||
setIsLoading,
|
||||
setYearFilter,
|
||||
setCurPage
|
||||
} = useSearchModalStore();
|
||||
const { ctpv, sgg } = useStore();
|
||||
const {
|
||||
isOpen: isReportFilesModalOpen,
|
||||
setIsOpen: setIsReportFilesModalOpen,
|
||||
dcTree: reportOriginData,
|
||||
fetchDcTree
|
||||
} = useReportTabStore(); // 모달 상태 관리 스토어
|
||||
const { REPORT_TYPES, flattenFolders, addFileName } = useReportTab();
|
||||
|
||||
useEffect(() => {
|
||||
fetchDcTree("기본계획"); // 하수정비기본계획 report
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// filterParams 초기화
|
||||
setCtpvCd('0');
|
||||
setSggCd('0');
|
||||
setReportGubun('0');
|
||||
setYearFilter({ start: "", end: "" });
|
||||
setShowTable(false);
|
||||
setTableData([]);
|
||||
setTableCount(0);
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const newCtpvCd = ctpv.find((n) => n.text === ctpvNm)?.val || '0';
|
||||
const newSggCd = sgg.find((n) => n.text === sggNm)?.val || '0';
|
||||
|
||||
setCtpvCd(newCtpvCd);
|
||||
setSggCd(newSggCd);
|
||||
}, [ctpvNm, sggNm]);
|
||||
|
||||
// ctpvCd, sggCd가 업데이트된 후에 fetchFilteredData 실행
|
||||
useEffect(() => {
|
||||
if (!isSearch && ctpvCd && sggCd) {
|
||||
fetchFilteredData();
|
||||
}
|
||||
}, [reportOriginData, ctpvCd, sggCd, isSearch]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
// showTable이 true일 때만 fetch (최초 렌더 방지)
|
||||
setIsResetPage(false);
|
||||
|
||||
if (showTable) fetchFilteredData();
|
||||
}, [curPage]);
|
||||
|
||||
const handleCtpvCdChange = (sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd('0');
|
||||
}
|
||||
|
||||
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
|
||||
const handleSearch = () => {
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
fetchFilteredData();
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(false); // 선택 초기화
|
||||
setShowTable(true);
|
||||
};
|
||||
|
||||
// 하수도정비기본계획 테이블 cellClick 핸들러
|
||||
const handleReportCellClick = (index: number | undefined) => {
|
||||
if (typeof index === "number" && tableData[index]) {
|
||||
const reportData = tableData[index];
|
||||
if (reportData.fileSn !== "") {
|
||||
setClsfSystmSn(reportData.clsfSystmSn); // 자료관 기본계획 일련번호 설정
|
||||
setIsReportFilesModalOpen(true); // 모달 열기
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 검색 API 호출 함수
|
||||
const fetchFilteredData = async () => {
|
||||
setIsLoading(true); // 로딩 시작
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
|
||||
try {
|
||||
const reportData = flattenFolders(reportOriginData);
|
||||
let filterReportData = reportData;
|
||||
|
||||
if (ctpvCd !== '0') { // 시도검색
|
||||
const sidoOption = ctpv.find((n) => n.val.trim() === ctpvCd.trim());
|
||||
filterReportData = filterReportData.filter(item => item.sido === sidoOption?.text);
|
||||
}
|
||||
if (sggCd !== '0') { // 시군구검색
|
||||
const sigunguOption = sgg.find((n) => String(n.val) === String(sggCd));
|
||||
filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text);
|
||||
}
|
||||
if (reportGubun !== "0") { // 보고서구분검색
|
||||
const reportGubunOption = REPORT_TYPES.find((n) => n.val === reportGubun);
|
||||
filterReportData = filterReportData.filter(item => item.reportGubun === reportGubunOption?.text);
|
||||
}
|
||||
|
||||
const { start, end } = yearFilter;
|
||||
if (start && !end) //승인년도검색
|
||||
filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" : null);
|
||||
else if (!start && end)
|
||||
filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear <= yearFilter.end + "년" : null);
|
||||
else if (start && end) {
|
||||
filterReportData = filterReportData.filter(item => {
|
||||
return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null;
|
||||
});
|
||||
}
|
||||
// if (gubun !== "") { // 구분검색 (추후 사용할 수도 있을 것 같아 남겨둠)
|
||||
// filterReportData = filterReportData.filter(item => (item.plantZoneGubun) ? String(item.plantZoneGubun).includes(String(gubun)) : null);
|
||||
// }
|
||||
|
||||
setTableCount(filterReportData.length || 0);
|
||||
|
||||
filterReportData = filterReportData.slice((curPage - 1) * 10, curPage * 10); // paging
|
||||
filterReportData = filterReportData?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 }));
|
||||
|
||||
addFileName(filterReportData).then(data => {
|
||||
filterReportData = data;
|
||||
setTableData(filterReportData || []);
|
||||
setIsResetSelection(false); // 데이터 설정 후 선택 초기화 해제
|
||||
});
|
||||
|
||||
setTableData(filterReportData || []);
|
||||
setShowTable(true);
|
||||
} catch (err) {
|
||||
console.error('API ERROR:', err);
|
||||
setTableData([]);
|
||||
setIsResetSelection(false); // 에러 시에도 선택 초기화 해제
|
||||
} finally {
|
||||
setIsLoading(false); // 로딩 종료
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`relative flex flex-col border rounded-lg pt-5 px-5 w-full ${isSearch ? "min-h-[10vh]" : "min-h-[6vh]"}`}>
|
||||
{isSearch && (
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => { handleCtpvCdChange(sidoId) }}
|
||||
onSigunguChange={setSggCd}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center mb-6">
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">보고서 구분</Label>
|
||||
<SelectBox className="w-44 mr-8"
|
||||
selectClass="rounded-lg bg-white"
|
||||
label=""
|
||||
selectArray={[...REPORT_TYPES]}
|
||||
value={reportGubun} onSelectValue={val => setReportGubun(String(val))}
|
||||
selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2'
|
||||
/>
|
||||
{/* 승인년도 필터 */}
|
||||
<YearRangeSelectBox
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
|
||||
startLabel="승인년도" endLabel=""
|
||||
/>
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[79vh] w-full overflow-hidden rounded-lg border">
|
||||
{/* 테이블2 영역 (가로/세로 스크롤 가능) */}
|
||||
<div className="flex-1 px-2 pb-2">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={(() => {
|
||||
return (<>조회 결과</>);
|
||||
})()
|
||||
}
|
||||
tableData={tableData}
|
||||
headlist={reportTableHeadlist}
|
||||
pageSize={10}
|
||||
total={tableCount}
|
||||
cellClick={handleReportCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* 모달 컴포넌트 */}
|
||||
{isReportFilesModalOpen && <ReportFilesModal clsfSystmSn={clsfSystmSn} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportTab);
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창
|
||||
*
|
||||
* 📄 Search Modal
|
||||
* Next.js App Router (app/) 기반 검색 모달 컴포넌트
|
||||
*
|
||||
* - 탭 기반 검색 인터페이스 (기본정보, 유지관리 정보, 보고서)
|
||||
* - 테이블 데이터 표시 및 페이지네이션 기능
|
||||
* - 시설물 조회 결과 및 유지관리 사업 현황 표시
|
||||
* - 엑셀 다운로드 및 지도에서 보기 기능
|
||||
* - Zustand를 통한 검색 모달 상태 관리
|
||||
* - 로딩 오버레이 및 선택 초기화 지원
|
||||
*/
|
||||
import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { UiModal, UITab, UiTable } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table';
|
||||
import BasicTab, { BASIC_TYPES } from './basicTab';
|
||||
import OperationTab from './operationTab';
|
||||
import ReportTab from './reportTab';
|
||||
import useSearchModal from './useSearchModal';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
|
||||
const SearchModal: React.FC = () => {
|
||||
const {
|
||||
showTable,
|
||||
isSearchModalOpen, setIsSearchModalOpen,
|
||||
activeTab, basicType, workType,
|
||||
tableData, detailTableData,
|
||||
tableCount, detailTableCount,
|
||||
curPage, detailCurPage,
|
||||
detailBizId,
|
||||
isLoading, isDetailLoading,
|
||||
isResetPage, isResetDetailPage,
|
||||
isResetSelection, isResetDetailSelection,
|
||||
setActiveTab,
|
||||
setCurPage, setDetailCurPage,
|
||||
setIsResetPage, setIsResetDetailPage,
|
||||
setDetailBizId,
|
||||
setIsResetDetailSelection,
|
||||
resetFilters
|
||||
} = useSearchModalStore();
|
||||
const { TABS, viewMultipleFeature, fetchFilteredData } = useSearchModal();
|
||||
const { handleBasicCellClick, basicExcelDownload } = useBasicTab();
|
||||
const {
|
||||
WORK_TYPES,
|
||||
OPERATION_TABLE_SIZE,
|
||||
OPERATION_TABLE_HEADLIST,
|
||||
MAINTENANACE_TABLE_HEADLIST,
|
||||
CLEANING_TABLE_HEADLIST,
|
||||
INSPECTION_TABLE_HEADLIST,
|
||||
handleOperationCellClick,
|
||||
handleOperationResultCellClick,
|
||||
operationExcelDownload,
|
||||
operationResultExcelDownload,
|
||||
handleOperationPageNation
|
||||
} = useOperationTab();
|
||||
|
||||
|
||||
const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => {
|
||||
if (activeTab === 'basic') { // 기본정보
|
||||
if (basicType === 'tb_ms_swage_ppl') return MsSwagePplCol2;// 하수관로
|
||||
if (basicType === 'tb_il_swage_prcs_fclt') return SwtrmtFcltCol2;// 하수처리시설
|
||||
if (basicType === 'tb_ms_swage_prcs_zone') return SwtrmtPrcsZoneCol2; // 하수처리구역
|
||||
if (basicType === 'tb_ms_swage_mhl') return MsSwageMhlCol2; // 하수맨홀
|
||||
if (basicType === 'tb_ms_swage_pmstn') return ExclnSewagePumpCol2; // 하수펌프장
|
||||
if (basicType === 'tb_ms_stofcbr') return stofcbrCol2; // 우수토실
|
||||
|
||||
return [];
|
||||
}
|
||||
if (activeTab === 'operation') return OPERATION_TABLE_HEADLIST; // 유지관리 정보
|
||||
|
||||
return [];
|
||||
}, [activeTab, basicType, workType]);
|
||||
|
||||
const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => {
|
||||
if (activeTab === 'operation') {
|
||||
if (workType === WORK_TYPES[0]?.val) return MAINTENANACE_TABLE_HEADLIST; // 보수 및 갱신
|
||||
if (workType === WORK_TYPES[1]?.val) return CLEANING_TABLE_HEADLIST; // 점검 및 진단
|
||||
if (workType === WORK_TYPES[2]?.val) return INSPECTION_TABLE_HEADLIST; // 청소 및 준설
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [activeTab, workType]);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters();
|
||||
}, [activeTab, basicType, workType]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
setIsResetDetailPage(true);
|
||||
setDetailBizId(0);
|
||||
if (showTable) fetchFilteredData();
|
||||
}, [curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetDetailSelection(false);
|
||||
if (showTable) handleOperationPageNation(Number(detailBizId));
|
||||
}, [detailCurPage]);
|
||||
|
||||
|
||||
const renderBody = () => (
|
||||
<>
|
||||
{/* 탭 영역 */}
|
||||
<UITab
|
||||
defaultValue={activeTab}
|
||||
tabList={TABS.map((tab: any) => ({
|
||||
value: tab.val,
|
||||
label: tab.text
|
||||
}))}
|
||||
onValueChange={(value: string) => setActiveTab(value)}
|
||||
/>
|
||||
{activeTab === 'basic' && (<BasicTab />)} {/* 기본정보 탭 */}
|
||||
{activeTab === 'operation' && (<OperationTab />)} {/* 유지관리 정보 탭 */}
|
||||
{activeTab === 'report' && (<ReportTab open={isSearchModalOpen} />)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
|
||||
|
||||
{/* 📋 결과 리스트 */}
|
||||
{/* 기본정보 탭 */}
|
||||
{activeTab === 'basic' && (
|
||||
<div className="h-[79dvh] w-full rounded-lg border mt-4 overflow-x-scroll">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable className='w-[120%]'
|
||||
headName={(() => {
|
||||
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
|
||||
return (<>{label} 조회 결과</>);
|
||||
})()
|
||||
}
|
||||
tableData={tableData}
|
||||
headlist={dynamicHeadlist}
|
||||
pageSize={10}
|
||||
total={tableCount}
|
||||
cellClick={handleBasicCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
curPage={curPage}
|
||||
headButton={
|
||||
<div className='flex'>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={basicExcelDownload}>파일 다운로드</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 유지관리 정보 탭 */}
|
||||
{activeTab === 'operation' && (
|
||||
<>
|
||||
<div className="h-[39dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border mt-4">
|
||||
{/* <div className="flex-1 px-2 pb-2"> */}
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={
|
||||
(() => {
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text
|
||||
return (<>{label} 사업 현황</>);
|
||||
})()
|
||||
}
|
||||
tableData={tableData}
|
||||
headlist={dynamicHeadlist}
|
||||
pageSize={OPERATION_TABLE_SIZE}
|
||||
total={tableCount}
|
||||
cellClick={handleOperationCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
curPage={curPage}
|
||||
headButton={
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-sm font-medium green" size="sm" onClick={operationExcelDownload}>파일 다운로드</Button>
|
||||
}
|
||||
/>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
|
||||
<div className="h-[39dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
|
||||
{/* 테이블2 영역 */}
|
||||
{/* <div className="flex-1 px-2 pb-2"> */}
|
||||
<LoadingOverlay isLoading={isDetailLoading} />
|
||||
<UiTable
|
||||
headName={
|
||||
(() => {
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text
|
||||
return (<>{label} 결과표</>);
|
||||
})()
|
||||
}
|
||||
tableData={detailTableData}
|
||||
headlist={dynamicHeadlist2}
|
||||
pageSize={OPERATION_TABLE_SIZE}
|
||||
total={detailTableCount}
|
||||
cellClick={handleOperationResultCellClick}
|
||||
onPageChange={setDetailCurPage}
|
||||
resetPageNo={isResetDetailPage}
|
||||
resetSelection={isResetDetailSelection}
|
||||
curPage={detailCurPage}
|
||||
headButton={
|
||||
<div className='flex'>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={operationResultExcelDownload}>파일 다운로드</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='overflow-x-auto overflow-y-hidden'>
|
||||
<UiModal
|
||||
open={isSearchModalOpen}
|
||||
onOpenChange={setIsSearchModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[1220px]
|
||||
max-h-[970px]
|
||||
overflow-y-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={renderBody()}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
<Toaster position="top-center"></Toaster>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SearchModal);
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 우수토실)
|
||||
*
|
||||
* 📄 Stofcbr Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 우수토실 탭 컴포넌트
|
||||
*
|
||||
* - 시도/시군구 선택 및 설치년도 범위 필터링
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { Button } from '@common/components';
|
||||
import React from 'react';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModal from './useSearchModal';
|
||||
|
||||
|
||||
export const stofcbrCqlFilters = () => {
|
||||
const {
|
||||
ctpvCd, sggCd,
|
||||
yearFilter,
|
||||
todayYear, todayMonth, todayDate,
|
||||
getDateString
|
||||
} = useSearchModalStore.getState();
|
||||
|
||||
const cqlFilters: string[] = [];
|
||||
// 시도, 시군구
|
||||
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
|
||||
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
|
||||
// 설치년도 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
cqlFilters.push(
|
||||
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
|
||||
);
|
||||
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const Stofcbr: React.FC = () => {
|
||||
const {
|
||||
ctpvCd, setCtpvCd,
|
||||
sggCd, setSggCd,
|
||||
yearFilter, setYearFilter,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center mb-2">
|
||||
{/* 시도, 시군구 */}
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd("0");
|
||||
}}
|
||||
onSigunguChange={setSggCd}
|
||||
/>
|
||||
{/* 설치년도 */}
|
||||
<YearRangeSelectBox
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
|
||||
startLabel="설치년도"
|
||||
endLabel=""
|
||||
/>
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Stofcbr);
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수맨홀)
|
||||
*
|
||||
* 📄 SwageMhl Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 하수맨홀 탭 컴포넌트
|
||||
*
|
||||
* - 시도/시군구 선택 및 설치년도 범위 필터링
|
||||
* - 용도 및 구경 범위 기반 검색 기능
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from 'react';
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
import { MS_SWAGE_MHL_USE_CODES } from '../components/common/table';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModal from './useSearchModal';
|
||||
|
||||
|
||||
export const swageMhlCqlFilters = () => {
|
||||
const {
|
||||
ctpvCd, sggCd,
|
||||
yearFilter,
|
||||
start1, end1,
|
||||
var1,
|
||||
todayYear, todayMonth, todayDate,
|
||||
getDateString
|
||||
} = useSearchModalStore.getState();
|
||||
|
||||
const cqlFilters: string[] = [];
|
||||
// 시도, 시군구
|
||||
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
|
||||
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
|
||||
// // 설치년도 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
cqlFilters.push(
|
||||
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
|
||||
);
|
||||
if (var1) cqlFilters.push(`usg_cd='${var1}'`);
|
||||
// 구경(mm) 범위
|
||||
if (start1 && end1) cqlFilters.push(`swage_mhL_diam_vl BETWEEN ${start1} AND ${end1}`);
|
||||
else if (start1) cqlFilters.push(`swage_mhL_diam_vl >= ${start1}`);
|
||||
else if (end1) cqlFilters.push(`swage_mhL_diam_vl <= ${end1}`);
|
||||
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const SwageMhl: React.FC = () => {
|
||||
const {
|
||||
var1, setVar1,
|
||||
start1, setStart1, end1, setEnd1,
|
||||
ctpvCd, setCtpvCd,
|
||||
sggCd, setSggCd,
|
||||
yearFilter, setYearFilter,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-2 flex items-center">
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd("0");
|
||||
}}
|
||||
onSigunguChange={setSggCd}
|
||||
/>
|
||||
{/* 설치년도 */}
|
||||
<YearRangeSelectBox
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={(range) =>
|
||||
setYearFilter({
|
||||
...yearFilter,
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
})
|
||||
}
|
||||
startLabel="설치년도"
|
||||
endLabel=""
|
||||
/>
|
||||
{/* 용도 */}
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">용도</Label>
|
||||
<SelectBox
|
||||
className="w-48 mr-8"
|
||||
selectClass="rounded-lg bg-white text-xl"
|
||||
label=""
|
||||
selectArray={[{ val: "0", text: "전체" }, ...MS_SWAGE_MHL_USE_CODES]}
|
||||
value={var1}
|
||||
onSelectValue={(val) => {
|
||||
setVar1(String(val));
|
||||
}}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
{/* 하수맨홀 구경 */}
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">구경(mm)</Label>
|
||||
<Input
|
||||
className="w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={start1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setStart1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="mx-2 text-xl font-semibold">~</span>
|
||||
<Input
|
||||
className="mr-8 w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={end1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setEnd1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SwageMhl);
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수펌프장)
|
||||
*
|
||||
* 📄 SwagePmstn Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 하수펌프장 탭 컴포넌트
|
||||
*
|
||||
* - 시도/시군구 선택 및 설치년도 범위 필터링
|
||||
* - 일일처리용량 및 양수능력 범위 기반 검색 기능
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
import React from 'react';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModal from './useSearchModal';
|
||||
|
||||
|
||||
export const swagePmstnCqlFilters = () => {
|
||||
const {
|
||||
ctpvCd, sggCd,
|
||||
yearFilter,
|
||||
start1, start2, end1, end2,
|
||||
todayYear, todayMonth, todayDate,
|
||||
getDateString
|
||||
} = useSearchModalStore.getState();
|
||||
|
||||
const cqlFilters: string[] = [];
|
||||
// 시도, 시군구
|
||||
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
|
||||
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
|
||||
// 설치년도 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
cqlFilters.push(
|
||||
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
|
||||
);
|
||||
// 일일처리용량 범위
|
||||
if (start1 && end1)
|
||||
cqlFilters.push(`daily_prcs_cpct BETWEEN ${start1} AND ${end1}`);
|
||||
else if (start1) cqlFilters.push(`daily_prcs_cpct >= ${start1}`);
|
||||
else if (end1) cqlFilters.push(`daily_prcs_cpct <= ${end1}`);
|
||||
// 양수능력 범위
|
||||
if (start2 && end2)
|
||||
cqlFilters.push(`excln_wpmp_ablt_vl BETWEEN ${start2} AND ${end2}`);
|
||||
else if (start2) cqlFilters.push(`excln_wpmp_ablt_vl >= ${start2}`);
|
||||
else if (end2) cqlFilters.push(`excln_wpmp_ablt_vl <= ${end2}`);
|
||||
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const SwagePmstn: React.FC = () => {
|
||||
const {
|
||||
start1, setStart1,
|
||||
end1, setEnd1,
|
||||
ctpvCd, setCtpvCd,
|
||||
sggCd, setSggCd,
|
||||
yearFilter, setYearFilter,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center mb-2">
|
||||
{/* 시도, 시군구 */}
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd("0");
|
||||
}}
|
||||
onSigunguChange={setSggCd}
|
||||
/>
|
||||
{/* 설치년도 */}
|
||||
<YearRangeSelectBox
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
|
||||
startLabel="설치년도"
|
||||
endLabel=""
|
||||
/>
|
||||
{/* 일일처리용량 */}
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">일일처리용량(㎥)</Label>
|
||||
<Input
|
||||
className="w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={start1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setStart1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="mx-2 text-xl font-semibold">~</span>
|
||||
<Input
|
||||
className="mr-8 w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={end1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setEnd1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 양수능력 */}
|
||||
{/* <Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">양수능력(m²/d)</Label>
|
||||
<Input
|
||||
className="w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={start2}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setStart2(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="mx-2 text-xl font-semibold">~</span>
|
||||
<Input
|
||||
className="mr-8 w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={end2}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setEnd2(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/> */}
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SwagePmstn);
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수관로)
|
||||
*
|
||||
* 📄 SwagePpl Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 하수관로 탭 컴포넌트
|
||||
*
|
||||
* - 시도/시군구 선택 및 설치년도 범위 필터링
|
||||
* - 용도 및 관경 범위 기반 검색 기능
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { SelectBox } from '@common/business_components/ui';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
import React from 'react';
|
||||
import { MS_SWAGE_PPL_USE_CODES } from '../components/common/table';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModal from './useSearchModal';
|
||||
|
||||
|
||||
// 하수관로용 CQL 필터 생성
|
||||
export const swagePplCqlFilters = () => {
|
||||
const {
|
||||
var1, var2,
|
||||
ctpvCd, sggCd,
|
||||
yearFilter,
|
||||
start1, start2, end1, end2,
|
||||
todayYear, todayMonth, todayDate,
|
||||
getDateString
|
||||
} = useSearchModalStore.getState();
|
||||
|
||||
const cqlFilters: string[] = [];
|
||||
if (var1) cqlFilters.push(`usg_cd='${var1}'`);
|
||||
if (var2) cqlFilters.push(`mqlt_cd='${var2}'`);
|
||||
if (ctpvCd !== '0' && sggCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${sggCd}%'`);
|
||||
else if (ctpvCd !== '0') cqlFilters.push(`pbadms_emd_cd LIKE '${ctpvCd?.trim()}%'`);
|
||||
// 설치년도 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
cqlFilters.push(
|
||||
`instl_ymd >= '${startDate}' and instl_ymd <= '${endDate}'`,
|
||||
);
|
||||
// 관경(mm) 범위
|
||||
if (start1 && end1) cqlFilters.push(`pdmt_vl BETWEEN ${start1} AND ${end1}`);
|
||||
else if (start1) cqlFilters.push(`pdmt_vl >= ${start1}`);
|
||||
else if (end1) cqlFilters.push(`pdmt_vl <= ${end1}`);
|
||||
// 연장(m) 범위
|
||||
if (start2 && end2) cqlFilters.push(`prlg_vl BETWEEN ${start2} AND ${end2}`);
|
||||
else if (start2) cqlFilters.push(`prlg_vl >= ${start2}`);
|
||||
else if (end2) cqlFilters.push(`prlg_vl <= ${end2}`);
|
||||
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const SwagePpl: React.FC = () => {
|
||||
const {
|
||||
var1, setVar1,
|
||||
start1, setStart1, end1, setEnd1,
|
||||
ctpvCd, setCtpvCd, sggCd, setSggCd,
|
||||
yearFilter, setYearFilter,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="mb-2 flex items-center">
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd("0");
|
||||
}}
|
||||
onSigunguChange={setSggCd}
|
||||
/>
|
||||
{/* 설치년도 */}
|
||||
<YearRangeSelectBox
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={(range) =>
|
||||
setYearFilter({
|
||||
...yearFilter,
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
})
|
||||
}
|
||||
startLabel="설치년도"
|
||||
endLabel=""
|
||||
/>
|
||||
{/* 용도 */}
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">용도</Label>
|
||||
<SelectBox
|
||||
className="w-32 mr-8"
|
||||
selectClass="rounded-lg bg-white text-xl"
|
||||
label=""
|
||||
selectArray={[{ val: "0", text: "전체" }, ...MS_SWAGE_PPL_USE_CODES]}
|
||||
value={var1}
|
||||
onSelectValue={(val) => {
|
||||
setVar1(String(val));
|
||||
}}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
{/* 재질 */}
|
||||
{/* <Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">재질</Label>
|
||||
<SelectBox
|
||||
className="w-60 mr-8"
|
||||
selectClass="rounded-lg bg-white text-xl"
|
||||
label=""
|
||||
selectArray={MS_SWAGE_PPL_CODES}
|
||||
value={var2}
|
||||
onSelectValue={(val) => {
|
||||
setVar2(String(val));
|
||||
}}
|
||||
selectContentClass="z-[101]"
|
||||
/> */}
|
||||
{/* 관경 */}
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">관경(mm)</Label>
|
||||
<Input
|
||||
className="w-[60px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={start1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setStart1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="mx-2 text-xl font-semibold">~</span>
|
||||
<Input
|
||||
className="mr-8 w-[60px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={end1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setEnd1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 연장 */}
|
||||
{/* <Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">연장(m)</Label>
|
||||
<Input
|
||||
className="w-[60px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={start2}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setStart2(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="mx-2 text-xl font-semibold">~</span>
|
||||
<Input
|
||||
className="mr-8 w-[60px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={end2}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setEnd2(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/> */}
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SwagePpl);
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수처리시설)
|
||||
*
|
||||
* 📄 SwagePrcsFclt Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 하수처리시설 탭 컴포넌트
|
||||
*
|
||||
* - 시도/시군구 선택 및 가동개시일자 범위 필터링
|
||||
* - 시설용량 범위 기반 검색 기능
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from 'react';
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModal from './useSearchModal';
|
||||
|
||||
|
||||
// 하수처리시설용 CQL 필터 생성
|
||||
export const swagePrcsFcltCqlFilters = () => {
|
||||
const {
|
||||
ctpvName, sggName,
|
||||
yearFilter,
|
||||
start1, end1,
|
||||
todayYear, todayMonth, todayDate,
|
||||
getDateString
|
||||
} = useSearchModalStore.getState();
|
||||
|
||||
const cqlFilters: string[] = [];
|
||||
// 가동개시일자 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
cqlFilters.push(
|
||||
`oprtng_strt_ymd >= '${startDate}' and oprtng_strt_ymd <= '${endDate}'`,
|
||||
);
|
||||
// 시설용량 범위
|
||||
if (start1 && end1)
|
||||
cqlFilters.push(`fclt_cpct BETWEEN ${start1} AND ${end1}`);
|
||||
else if (start1) cqlFilters.push(`fclt_cpct >= ${start1}`);
|
||||
else if (end1) cqlFilters.push(`fclt_cpct <= ${end1}`);
|
||||
|
||||
// 시도
|
||||
if (ctpvName) cqlFilters.push(`ctpv_nm ILIKE '%${ctpvName}%'`);
|
||||
if (sggName) cqlFilters.push(`sgg_nm ILIKE '%${sggName}%'`);
|
||||
// // 처리구역 면적
|
||||
// if (start2 && end2)
|
||||
// cqlFilters.push(`prcsZoneAreaSum BETWEEN ${start2} AND ${end2}`);
|
||||
// else if (start2) cqlFilters.push(`prcsZoneAreaSum >= ${start2}`);
|
||||
// else if (end2) cqlFilters.push(`prcsZoneAreaSum <= ${end2}`);
|
||||
// // 하수처리시설명
|
||||
// if (var1) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var1}%'`);
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const SwagePrcsFclt: React.FC = () => {
|
||||
const {
|
||||
start1, setStart1, end1, setEnd1,
|
||||
ctpvCd, setCtpvCd, setCtpvName,
|
||||
sggCd, setSggCd, setSggName,
|
||||
yearFilter, setYearFilter,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-2 flex items-center">
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd("0");
|
||||
}}
|
||||
onSigunguChange={setSggCd}
|
||||
setCtpvName={setCtpvName}
|
||||
setSggName={setSggName}
|
||||
/>
|
||||
{/* 가동개시일자 */}
|
||||
<Label className="mr-2 w-auto text-xl text-gray-600">가동개시일자</Label>
|
||||
<YearRangeSelectBox
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={(range) =>
|
||||
setYearFilter({
|
||||
...yearFilter,
|
||||
start: range.start,
|
||||
end: range.end,
|
||||
})
|
||||
}
|
||||
startLabel=""
|
||||
endLabel=""
|
||||
/>
|
||||
{/* 시설용량 */}
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">시설용량(㎥/d)</Label>
|
||||
<Input
|
||||
className="w-[60px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={start1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setStart1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="mx-2 text-xl font-semibold">~</span>
|
||||
<Input
|
||||
className="mr-8 w-[60px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={end1}
|
||||
onChange={(e) => {
|
||||
// 숫자만 입력 허용
|
||||
const val = e.target.value.replace(/[^0-9]/g, "");
|
||||
setEnd1(val);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SwagePrcsFclt);
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 기본정보 탭 (구분: 하수처리구역)
|
||||
*
|
||||
* 📄 SwagePrcsZone Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 하수처리구역 탭 컴포넌트
|
||||
*
|
||||
* - 시도/시군구 선택 및 하수처리시설명/구역명 기반 검색 기능
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - Zustand를 통한 상태 관리 및 필터 값 저장
|
||||
* - Enter 키 입력 시 자동 조회 지원
|
||||
*/
|
||||
import React from 'react';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import { Button, Input, Label } from '@common/components';
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useSearchModal from './useSearchModal';
|
||||
|
||||
|
||||
// 하수처리구역용 CQL 필터 생성
|
||||
export const swagePrcsZoneCqlFilters = () => {
|
||||
const {
|
||||
ctpvCd, sggCd,
|
||||
var1, var2
|
||||
} = useSearchModalStore.getState();
|
||||
const cqlFilters: string[] = [];
|
||||
if (ctpvCd !== '' && ctpvCd !== '0') cqlFilters.push(`ctpv_cd='${ctpvCd?.trim()}'`);
|
||||
if (sggCd !== '' && sggCd !== '0') cqlFilters.push(`sgg_cd='${sggCd}'`);
|
||||
if (var1) cqlFilters.push(`swtrmt_zone_nm ILIKE '%${var1}%'`);
|
||||
if (var2) cqlFilters.push(`swtrmt_fclt_nm ILIKE '%${var2}%'`);
|
||||
return cqlFilters;
|
||||
};
|
||||
|
||||
const swagePrcsZone: React.FC = () => {
|
||||
const {
|
||||
var1, setVar1, var2, setVar2,
|
||||
ctpvCd, setCtpvCd,
|
||||
sggCd, setSggCd,
|
||||
} = useSearchModalStore();
|
||||
const { handleSearch } = useSearchModal();
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mb-2 flex items-center">
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd("0");
|
||||
}}
|
||||
onSigunguChange={setSggCd}
|
||||
/>
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">하수처리시설명</Label>
|
||||
<Input
|
||||
className='!text-xl w-[120px] placeholder:text-center mr-8'
|
||||
placeholder="입력"
|
||||
value={var2}
|
||||
onChange={(e) => {
|
||||
setVar2(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">하수처리구역명</Label>
|
||||
<Input
|
||||
className="mr-8 w-[100px] !text-xl placeholder:text-center"
|
||||
placeholder="입력"
|
||||
value={var1}
|
||||
onChange={(e) => {
|
||||
setVar1(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault(); // Enter 키 기본 동작 방지
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* 조회 버튼 */}
|
||||
<div className="flex-1 flex justify-end">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(swagePrcsZone);
|
||||
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 📄 useBasicTab Hook
|
||||
* 검색 모달 기본정보 탭 훅
|
||||
*
|
||||
* - 필터링된 시설물 데이터 조회 및 포맷팅
|
||||
* - 테이블 셀 클릭 시 지도 피처 선택 및 줌 기능
|
||||
* - 엑셀 다운로드 기능 (한글 헤더 지원)
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - Zustand를 통한 상태 관리 및 로딩 처리
|
||||
* - 데이터 매핑 및 코드 변환 (시도/시군구, 용도 등)
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 관망도 시설물 목록 조회: GET /api/v1/fclts (sewerage-service)
|
||||
*/
|
||||
import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
|
||||
import {
|
||||
MS_SWAGE_PPL_USE_CODES, MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_SHAPE, MS_SWAGE_MHL_CODES, MS_SWAGE_MHL_USE_CODES,
|
||||
MsSwagePplExcelCol, SwtrmtFcltExcelCol, SwtrmtPrcsZoneExcelCol, MsSwageMhlExcelCol, ExclnSewagePumpExcelCol, stofcbrExcelCol
|
||||
} from '../components/common/table';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { BASIC_TYPES } from './basicTab';
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import useStore from '@/store/useStore';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { confirmToast } from '@/components/ui/toast'
|
||||
import { toast } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { swagePplCqlFilters } from './swagePpl';
|
||||
import { swagePrcsFcltCqlFilters } from './swagePrcsFclt';
|
||||
import { swagePrcsZoneCqlFilters } from './swagePrcsZone';
|
||||
import { swageMhlCqlFilters } from './swageMhl';
|
||||
import { swagePmstnCqlFilters } from './swagePmstn';
|
||||
import { stofcbrCqlFilters } from './stofcbr';
|
||||
|
||||
|
||||
export default function useBasicTab() {
|
||||
const {
|
||||
basicType,
|
||||
tableData,
|
||||
curPage,
|
||||
todayYear, todayMonth, todayDate,
|
||||
setIsSearchModalOpen,
|
||||
setTableData,
|
||||
setTableCount,
|
||||
setIsLoading,
|
||||
} = useSearchModalStore();
|
||||
const { ctpv, sgg } = useStore();
|
||||
|
||||
|
||||
const fetchBasicFilteredData = async () => {
|
||||
let cqlFilters: string[] = [];
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: basicType,
|
||||
page: `${curPage - 1}`,
|
||||
size: '10',
|
||||
sort: 'string',
|
||||
});
|
||||
|
||||
if (basicType === 'tb_ms_swage_ppl') cqlFilters = swagePplCqlFilters(); // 하수관로
|
||||
else if (basicType === 'tb_il_swage_prcs_fclt') cqlFilters = swagePrcsFcltCqlFilters(); // 하수처리시설
|
||||
else if (basicType === 'tb_ms_swage_prcs_zone') cqlFilters = swagePrcsZoneCqlFilters(); // 하수처리구역
|
||||
else if (basicType === 'tb_ms_swage_mhl') cqlFilters = swageMhlCqlFilters(); // 하수맨홀
|
||||
else if (basicType === 'tb_ms_swage_pmstn') cqlFilters = swagePmstnCqlFilters(); // 하수펌프장
|
||||
else if (basicType === 'tb_ms_stofcbr') cqlFilters = stofcbrCqlFilters(); // 우수토실
|
||||
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
|
||||
|
||||
const res = await getData(`v1/fclts?${queryParams.toString()}`);
|
||||
|
||||
// 🎯 각 아이템을 개별적으로 포맷팅하면서 일련번호 추가
|
||||
const dataWithRowNumber = (res.data || []).map((item: any, index: number) => {
|
||||
// 🎯 먼저 각 아이템을 포맷팅
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
let ctpvCd = item?.ctpvNm ?? formattedItem?.ctpvNm ?? '';
|
||||
let sggCd = item?.sggNm ?? formattedItem?.sggNm ?? '';
|
||||
|
||||
if (!ctpvCd || !sggCd) {
|
||||
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
|
||||
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
|
||||
if (!sggCd && code) sggCd = code.slice(0, 5);
|
||||
}
|
||||
|
||||
const matchedMqltCode = basicType === 'tb_ms_swage_ppl'
|
||||
? MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 하수관로 재질
|
||||
: basicType === 'tb_ms_swage_mhl'
|
||||
? MS_SWAGE_MHL_CODES.find(code => code.val === item?.mqltCd?.trim()) // 맨홀 재질
|
||||
: undefined;
|
||||
const matchedMhlUsgCode = MS_SWAGE_MHL_USE_CODES.find(code => code.val === item?.usgCd); // 맨홀 용도
|
||||
const matchedPplShpCode = MS_SWAGE_PPL_SHAPE.find(code => code.val === item?.fcltyShpCd); // 하수관로 시설물형태코드
|
||||
|
||||
|
||||
const ctpvNm = ctpv.find(code => code.val.trim() === ctpvCd)?.text || item?.ctpvNm;
|
||||
const sggNm = sgg.find(code => code.val.trim() === sggCd)?.text || item?.sggNm;
|
||||
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
|
||||
|
||||
// 추가 처리 및 일련번호 추가
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
|
||||
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item?.scsYn, // scsYn이 null이면 'N'으로 변경
|
||||
mqltCd: matchedMqltCode ? matchedMqltCode.text : item?.mqltCd,
|
||||
usgCd: matchedMhlUsgCode ? matchedMhlUsgCode.text : (usgCdObj ? usgCdObj.text : item?.usgCd),
|
||||
ctpvNm: ctpvNm ?? '-',
|
||||
sggNm: sggNm ?? '-',
|
||||
swtrmt_zone_nm: item.swtrmtZoneNm || item.swtrmt_zone_nm || '-',
|
||||
fcltyShpCd: matchedPplShpCode? matchedPplShpCode.text : item?.fcltyShpCd,
|
||||
};
|
||||
});
|
||||
|
||||
setTableData(dataWithRowNumber);
|
||||
// console.log(tableData);
|
||||
setTableCount(res.count || []);
|
||||
};
|
||||
|
||||
// 기본정보 테이블 cellClick 핸들러
|
||||
const handleBasicCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false); // SearchModal 닫기
|
||||
}
|
||||
};
|
||||
|
||||
// 기본정보 Excel 다운로드 핸들러
|
||||
const basicExcelDownload = async () => {
|
||||
if (tableData.length === 0) {
|
||||
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.dismiss("global-confirm-toast");
|
||||
|
||||
confirmToast(
|
||||
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
|
||||
onDownload ?? (() => { }),
|
||||
"text-xl font-medium text-gray-800"
|
||||
);
|
||||
|
||||
async function onDownload() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
lyrTypeNm: basicType,
|
||||
size: '10000',
|
||||
});
|
||||
|
||||
let cqlFilters: string[] = [];
|
||||
let headerMapping: { [key: string]: string } = {};
|
||||
let headerRange: string = "A1:F1";
|
||||
|
||||
switch (basicType) {
|
||||
case 'tb_ms_swage_ppl': {
|
||||
// 하수관로
|
||||
cqlFilters = swagePplCqlFilters()
|
||||
headerMapping = MsSwagePplExcelCol;
|
||||
headerRange = "A1:Q1";
|
||||
break;
|
||||
}
|
||||
case 'tb_il_swage_prcs_fclt': {
|
||||
// 하수처리시설
|
||||
cqlFilters = swagePrcsFcltCqlFilters();
|
||||
headerMapping = SwtrmtFcltExcelCol;
|
||||
headerRange = "A1:J1";
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_prcs_zone': {
|
||||
// 하수처리구역
|
||||
cqlFilters = swagePrcsZoneCqlFilters();
|
||||
headerMapping = SwtrmtPrcsZoneExcelCol;
|
||||
headerRange = "A1:D1";
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
// 하수맨홀
|
||||
cqlFilters = swageMhlCqlFilters();
|
||||
headerMapping = MsSwageMhlExcelCol;
|
||||
headerRange = "A1:J1";
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_swage_pmstn': {
|
||||
// 하수펌프장
|
||||
cqlFilters = swagePmstnCqlFilters();
|
||||
headerMapping = ExclnSewagePumpExcelCol;
|
||||
headerRange = "A1:J1";
|
||||
break;
|
||||
}
|
||||
case 'tb_ms_stofcbr': {
|
||||
// 우수토실
|
||||
cqlFilters = stofcbrCqlFilters();
|
||||
headerMapping = stofcbrExcelCol;
|
||||
headerRange = "A1:H1";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cqlFilters.length > 0) queryParams.append('cqlFiter', cqlFilters.join(' and '));
|
||||
|
||||
const res = await getData(`v1/fclts?${queryParams.toString()}`);
|
||||
|
||||
const mhlUsgCdMap = new Map(MS_SWAGE_MHL_USE_CODES.map(code => [code.val?.trim(), code.text]));
|
||||
const pplUsgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val?.trim(), code.text]));
|
||||
const pplMqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val?.trim(), code.text]));
|
||||
const pplShpCdMap = new Map(MS_SWAGE_PPL_SHAPE.map(code => [code.val?.trim(), code.text])); // 관로 시설물형태코드
|
||||
const ctpvMap = new Map(ctpv.map(code => [code.val?.trim(), code.text]));
|
||||
const sggMap = new Map(sgg.map(code => [code.val?.trim(), code.text]));
|
||||
|
||||
const mappedContent = (res.data || []).map((item: any) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
let ctpvCd = item?.ctpvCd ?? formattedItem?.ctpvCd ?? '';
|
||||
let sggCd = item?.sggCd ?? formattedItem?.sggCd ?? '';
|
||||
|
||||
if (!ctpvCd || !sggCd) {
|
||||
const code = String(item?.pbadmsEmdCd ?? formattedItem?.pbadmsEmdCd ?? '')?.trim();
|
||||
if (!ctpvCd && code) ctpvCd = code.slice(0, 2);
|
||||
if (!sggCd && code) sggCd = code.slice(0, 5);
|
||||
}
|
||||
|
||||
// trim 한 번만 수행
|
||||
const trimmedUsgCd = item?.usgCd?.trim();
|
||||
// const trimmedMqltCd = item?.mqltCd?.trim();
|
||||
|
||||
switch (basicType) {
|
||||
case 'tb_ms_swage_ppl': {
|
||||
// 하수관로
|
||||
return {
|
||||
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
|
||||
instMngNo: formattedItem.instMngNo ?? item.instMngNo ?? '-',
|
||||
ctpvNm: ctpvMap.get(ctpvCd) ?? item.ctpvNm ?? '-',
|
||||
sggNm: sggMap.get(sggCd) ?? item.sggNm ?? '-',
|
||||
swtrmt_zone_nm: formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '-',
|
||||
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
|
||||
usgCd: pplUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
|
||||
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '-',
|
||||
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '-',
|
||||
tmptDpth: formattedItem.tmptDpth ?? item.tmptDpth ?? '-',
|
||||
tmptMhlMngNo: formattedItem.tmptMhlMngNo ?? item.tmptMhlMngNo ?? '-',
|
||||
scendDpth: formattedItem.scendDpth ?? item.scendDpth ?? '-',
|
||||
scendMhlMngNo: formattedItem.scendMhlMngNo ?? item.scendMhlMngNo ?? '-',
|
||||
vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-',
|
||||
wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-',
|
||||
fcltyShpCd: pplShpCdMap.get(item?.fcltyShpCd?.trim()) || item.fcltyShpCd || formattedItem.fcltyShpCd || '-',
|
||||
mqltCd: pplMqltCdMap.get(item?.mqltCd?.trim()) || item.mqltCd || formattedItem.mqltCd || '-',
|
||||
};
|
||||
}
|
||||
case 'tb_il_swage_prcs_fclt': {
|
||||
// 하수처리시설
|
||||
return {
|
||||
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-',
|
||||
fcltCpct: formattedItem.fcltCpct ?? item.fcltCpct ?? '-',
|
||||
rgnExpln: formattedItem.rgnExpln ?? item.rgnExpln ?? '-',
|
||||
oprtngStrtYmd: formattedItem.oprtngStrtYmd ?? item.oprtngStrtYmd ?? '-',
|
||||
laeNm: formattedItem.laeNm ?? item.laeNm ?? '-',
|
||||
sareaNm: formattedItem.sareaNm ?? item.sareaNm ?? '-',
|
||||
rvrNm: formattedItem.rvrNm ?? item.rvrNm ?? '-',
|
||||
dschgWtstNm: formattedItem.dschgWtstNm ?? item.dschgWtstNm ?? '-',
|
||||
swtrmtPpltnCnt: formattedItem.swtrmtPpltnCnt ?? item.swtrmtPpltnCnt ?? '-',
|
||||
rprsMsrcCn: formattedItem.rprsMsrcCn ?? item.rprsMsrcCn ?? '-'
|
||||
};
|
||||
}
|
||||
case 'tb_ms_swage_prcs_zone': {
|
||||
// 하수처리구역
|
||||
return {
|
||||
swtrmtZoneNm: formattedItem.swtrmtZoneNm ?? item.swtrmtZoneNm ?? '-',
|
||||
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
|
||||
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
|
||||
swtrmtFcltNm: formattedItem.swtrmtFcltNm ?? item.swtrmtFcltNm ?? '-'
|
||||
};
|
||||
}
|
||||
case 'tb_ms_swage_mhl': {
|
||||
// 하수맨홀
|
||||
return {
|
||||
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
|
||||
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
|
||||
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
|
||||
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
|
||||
usgCd: mhlUsgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '-',
|
||||
swageMhlDiamVl: formattedItem.swageMhlDiamVl ?? item.swageMhlDiamVl ?? '-',
|
||||
wdthLen: formattedItem.wdthLen ?? item.wdthLen ?? '-',
|
||||
vrtcLen: formattedItem.vrtcLen ?? item.vrtcLen ?? '-',
|
||||
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
|
||||
dpthlVl: formattedItem.dpthlVl ?? item.dpthlVl ?? '-'
|
||||
};
|
||||
}
|
||||
case 'tb_ms_swage_pmstn': {
|
||||
// 하수펌프장
|
||||
return {
|
||||
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
|
||||
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
|
||||
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
|
||||
swagePmstnNm: formattedItem.swagePmstnNm ?? item.swagePmstnNm ?? '-',
|
||||
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
|
||||
dailyPrcsCpct: formattedItem.dailyPrcsCpct ?? item.dailyPrcsCpct ?? '-',
|
||||
maxImpndCpct: formattedItem.maxImpndCpct ?? item.maxImpndCpct ?? '-',
|
||||
altdVl: formattedItem.altdVl ?? item.altdVl ?? '-',
|
||||
wtlvVl: formattedItem.wtlvVl ?? item.wtlvVl ?? '-',
|
||||
exclnWpmpAbltVl: formattedItem.exclnWpmpAbltVl ?? item.exclnWpmpAbltVl ?? '-'
|
||||
};
|
||||
}
|
||||
case 'tb_ms_stofcbr': {
|
||||
// 우수토실
|
||||
return {
|
||||
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '-',
|
||||
mapshtNo: formattedItem.mapshtNo ?? item.mapshtNo ?? '-',
|
||||
ctpvNm: ctpvMap.get(ctpvCd) ?? formattedItem.ctpvNm ?? '-',
|
||||
sggNm: sggMap.get(sggCd) ?? formattedItem.sggNm ?? '-',
|
||||
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '-',
|
||||
worflwWallHgt: formattedItem.worflwWallHgt ?? item.worflwWallHgt ?? '-',
|
||||
swageMhlMngNo: formattedItem.swageMhlMngNo ?? item.swageMhlMngNo ?? '-',
|
||||
drtnAngl: formattedItem.drtnAngl ?? item.drtnAngl ?? '-'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
|
||||
const excelData = mappedContent; // 배열로 감싸서 전달
|
||||
|
||||
// 파일명 생성 (분석번호 기반)
|
||||
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
|
||||
const fileName = `${label} 조회 결과_${todayYear}${todayMonth}${todayDate}`;
|
||||
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: excelData as JSON[],
|
||||
fileName: fileName,
|
||||
sheetNameArray: [fileName],
|
||||
headerRange: headerRange,
|
||||
headerMapping: headerMapping
|
||||
});
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error("Excel 다운로드 중 오류 발생:", error);
|
||||
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return { fetchBasicFilteredData, handleBasicCellClick, basicExcelDownload };
|
||||
};
|
||||
@@ -0,0 +1,452 @@
|
||||
/**
|
||||
* 📄 useOperationTab Hook
|
||||
* 검색 모달 유지관리 정보 탭 훅
|
||||
*
|
||||
* - 필터링된 사업 현황 데이터 조회 및 포맷팅
|
||||
* - 사업 현황 테이블 셀 클릭 시 상세 결과표 조회
|
||||
* - 결과표 셀 클릭 시 지도 피처 선택 및 줌 기능
|
||||
* - 엑셀 다운로드 기능 (사업 현황 및 결과표)
|
||||
* - CQL 필터 생성을 통한 쿼리 조건 설정
|
||||
* - Zustand를 통한 상태 관리 및 로딩 처리
|
||||
* - 데이터 매핑 및 코드 변환 (사업 유형, 용도, 재질 등)
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 유지관리 정보 목록 조회: GET /api/v1/bizInfo (sewerage-service)
|
||||
* - 유지관리 정보(사업정보) 관련 시설 목록 조회: GET /api/v1/bizInfo/fclts (sewerage-service)
|
||||
*/
|
||||
import { downloadExcelWithKoreanHeaders } from '@common/business_components/ui';
|
||||
import { MS_SWAGE_PPL_CODES, MS_SWAGE_PPL_USE_CODES } from '../components/common/table';
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import { confirmToast } from '@/components/ui/toast'
|
||||
import { toast } from "react-hot-toast";
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import useStore from '@/store/useStore';
|
||||
|
||||
|
||||
export default function useOperationTab() {
|
||||
const OPERATION_TABLE_SIZE = 5;
|
||||
|
||||
const {
|
||||
workType,
|
||||
tableData, detailTableData,
|
||||
curPage, detailCurPage,
|
||||
detailBizId,
|
||||
var1, var2,
|
||||
yearFilter,
|
||||
todayYear, todayMonth, todayDate,
|
||||
setIsSearchModalOpen,
|
||||
setTableData, setDetailTableData,
|
||||
setTableCount, setDetailTableCount,
|
||||
setDetailCurPage,
|
||||
setIsLoading, setIsDetailLoading,
|
||||
setIsResetDetailPage,
|
||||
setIsResetDetailSelection,
|
||||
setDetailBizId,
|
||||
getDateString
|
||||
} = useSearchModalStore();
|
||||
const { BIZ_TY_CD } = useStore();
|
||||
|
||||
const WORK_TYPES = [ // 유지관리 정보 '구분' 선택 목록
|
||||
{ val: "['200', '201', '204']", text: "보수 및 갱신" },
|
||||
{ val: "['100']", text: "점검 및 진단" },
|
||||
{ val: "['203']", text: "청소 및 준설" },
|
||||
];
|
||||
const OPERATION_TABLE_HEADLIST = [ // 유지관리 정보 - 사업 현황
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
{ id: "bizInfoNm", title: "사업명" },
|
||||
{ id: "flfmtBgngYmd", title: "수행시작일자" },
|
||||
{ id: "flfmtEndYmd", title: "수행종료일자" },
|
||||
{ id: "flfmtYr", title: "수행년도" },
|
||||
{ id: "bizTypeCd", title: "사업유형" },
|
||||
{ id: "bizAmt", title: "사업금액" },
|
||||
];
|
||||
const MAINTENANACE_TABLE_HEADLIST = [ // 유지관리 정보 - 보수 및 갱신 결과표
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
{ id: "fcltMngNo", title: "관리번호" },
|
||||
{ id: "swtrmt_zone_nm", title: "처리구역" },
|
||||
{ id: "usgCd", title: "용도" },
|
||||
// { id: "lgalDrblyr", title: "내용연수" },
|
||||
// { id: "acqsPrmpc", title: "취득금액" },
|
||||
{ id: "mqltCd", title: "재질" },
|
||||
{ id: "pdmtVl", title: "관경(mm)" },
|
||||
{ id: "prlgVl", title: "연장(m)" },
|
||||
{ id: "instlYmd", title: "설치일자" },
|
||||
// { id: "instlYmd", title: "사업구분" },
|
||||
];
|
||||
const CLEANING_TABLE_HEADLIST = [ // 유지관리 정보 - 청소 및 준설 결과표
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
{ id: "fcltMngNo", title: "관리번호" },
|
||||
{ id: "swtrmt_zone_nm", title: "처리구역" },
|
||||
{ id: "usgCd", title: "용도" },
|
||||
// { id: "lgalDrblyr", title: "내용연수" },
|
||||
// { id: "acqsPrmpc", title: "취득금액" },
|
||||
{ id: "mqltCd", title: "재질" },
|
||||
{ id: "pdmtVl", title: "관경(mm)" },
|
||||
{ id: "prlgVl", title: "연장(m)" },
|
||||
{ id: "instlYmd", title: "설치일자" },
|
||||
];
|
||||
const INSPECTION_TABLE_HEADLIST = [ // 유지관리 정보 - 점검 및 진단 결과표
|
||||
{ id: 'rowNumber', title: '일련번호' },
|
||||
{ id: "fcltMngNo", title: "관리번호" },
|
||||
{ id: "swtrmt_zone_nm", title: "처리구역" },
|
||||
{ id: "usgCd", title: "용도" },
|
||||
// { id: "riskEval", title: "위험평가" },
|
||||
// { id: "acqsPrmpc", title: "취득금액" },
|
||||
// { id: "lgalDrblyr", title: "내용연수" },
|
||||
// { id: "remLife", title: "잔존수명" },
|
||||
{ id: "mqltCd", title: "재질" },
|
||||
{ id: "pdmtVl", title: "관경(mm)" },
|
||||
{ id: "prlgVl", title: "연장(m)" },
|
||||
{ id: "instlYmd", title: "설치일자" },
|
||||
{ id: "strc_stts_grd", title: "구조적 상태등급" },
|
||||
{ id: "oper_stts_grd", title: "운영적 상태등급" },
|
||||
];
|
||||
|
||||
const fetchOperationFilteredData = async () => {
|
||||
const RequestDTO: Record<string, string> = {
|
||||
page: `${curPage - 1}`,
|
||||
size: `${OPERATION_TABLE_SIZE}`,
|
||||
};
|
||||
if (var1) RequestDTO.bizInfoNm = var1;
|
||||
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
|
||||
// 수행기간 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
RequestDTO.fromflfmtEndYmd = startDate;
|
||||
RequestDTO.toflfmtEndYmd = endDate;
|
||||
|
||||
// JSON.parse 사용으로 문자열 -> 배열 변환
|
||||
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
|
||||
|
||||
const params = new URLSearchParams(RequestDTO).toString();
|
||||
|
||||
const res = await getData(`v1/bizInfo?${params}`);
|
||||
|
||||
const mappedContent = (res.content || []).map((item: any, index: number) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가
|
||||
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
|
||||
};
|
||||
});
|
||||
|
||||
setTableData(mappedContent || []);
|
||||
setTableCount(res.totalElements || 0);
|
||||
setDetailTableData([]);
|
||||
setDetailTableCount(0);
|
||||
};
|
||||
|
||||
// 유지관리 정보 테이블 cellClick 핸들러
|
||||
const handleOperationCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
setIsResetDetailPage(true); // 페이지를 1로 초기화
|
||||
setIsResetDetailSelection(true); // 선택 초기화
|
||||
setIsResetDetailPage(true); // 디테일 페이지를 초기화
|
||||
|
||||
if (typeof index === "number") {
|
||||
setIsDetailLoading(true);
|
||||
setDetailBizId(tableData[index].bizId);
|
||||
setDetailCurPage(1); // 상세 페이지 초기화
|
||||
try {
|
||||
const RequestDTO: Record<string, string> = {
|
||||
page: `${detailCurPage - 1}`,
|
||||
size: `${OPERATION_TABLE_SIZE}`,
|
||||
bizId: tableData[index % OPERATION_TABLE_SIZE].bizId,
|
||||
};
|
||||
|
||||
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
|
||||
|
||||
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
|
||||
|
||||
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
|
||||
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
|
||||
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
|
||||
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
|
||||
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
|
||||
};
|
||||
});
|
||||
|
||||
setDetailTableData(mappedData);
|
||||
setDetailTableCount(res.count || 0);
|
||||
} catch (error) {
|
||||
console.error('유지관리 정보 cellClick 에러:', error);
|
||||
} finally {
|
||||
setIsDetailLoading(false);
|
||||
setIsResetDetailPage(false);
|
||||
setIsResetDetailSelection(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 유지관리 정보 디테일 테이블 cellClick 핸들러
|
||||
const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => {
|
||||
if (typeof index === "number") {
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[index];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fid = row.fid;
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showSearchResultsFromGeoJSON(layerNm, fid);
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false); // SearchModal 닫기
|
||||
}
|
||||
};
|
||||
|
||||
// 유지관리 정보 디테일 테이블 페이지네이션 핸들러
|
||||
const handleOperationPageNation = async (index?: number | undefined) => {
|
||||
if (typeof index === "number") {
|
||||
setIsDetailLoading(true);
|
||||
try {
|
||||
const RequestDTO: Record<string, string> = {
|
||||
page: `${detailCurPage - 1}`,
|
||||
size: `${OPERATION_TABLE_SIZE}`,
|
||||
bizId: String(detailBizId),
|
||||
};
|
||||
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
|
||||
|
||||
const mappedData = (res.data || []).map((item: any, detailIndex: number) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
const usgCdObj = MS_SWAGE_PPL_USE_CODES.find(code => code.val === item?.usgCd?.trim());
|
||||
const mqltCdObj = MS_SWAGE_PPL_CODES.find(code => code.val === item?.mqltCd?.trim());
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (detailCurPage - 1) * OPERATION_TABLE_SIZE + detailIndex + 1,
|
||||
usgCd: usgCdObj ? usgCdObj.text : item.usgCd,
|
||||
mqltCd: mqltCdObj ? mqltCdObj.text : item.mqltCd,
|
||||
swtrmt_zone_nm: item.swtrmt_zone_nm ? item.swtrmt_zone_nm : '-',
|
||||
};
|
||||
});
|
||||
|
||||
setDetailTableData(mappedData);
|
||||
setDetailTableCount(res.count || 0);
|
||||
} catch (error) {
|
||||
console.error('유지관리 정보 페이지네이션 에러:', error);
|
||||
} finally {
|
||||
setIsDetailLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 유지관리 정보 Excel 다운로드 핸들러 추가
|
||||
const operationExcelDownload = async () => {
|
||||
if (tableData.length === 0) {
|
||||
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.dismiss("global-confirm-toast");
|
||||
|
||||
confirmToast(
|
||||
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
|
||||
onAnalysis ?? (() => { }),
|
||||
"text-xl font-medium text-gray-800"
|
||||
);
|
||||
|
||||
async function onAnalysis() {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const RequestDTO: Record<string, string> = {
|
||||
size: '10000',
|
||||
};
|
||||
if (var1) RequestDTO.bizInfoNm = var1;
|
||||
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
|
||||
// 수행기간 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
RequestDTO.fromflfmtEndYmd = startDate;
|
||||
RequestDTO.toflfmtEndYmd = endDate;
|
||||
|
||||
// JSON.parse 사용으로 문자열 -> 배열 변환
|
||||
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
|
||||
|
||||
const params = new URLSearchParams(RequestDTO).toString();
|
||||
const res = await getData(`v1/bizInfo?${params}`);
|
||||
|
||||
// 헤더 매핑 정의
|
||||
const headerMapping: { [key: string]: string } = {
|
||||
'bizInfoNm': '사업명',
|
||||
'flfmtBgngYmd': '수행시작일자',
|
||||
'flfmtEndYmd': '수행종료일자',
|
||||
'flfmtYr': '수행년도',
|
||||
'bizTypeCd': '사업유형',
|
||||
'bizAmt': '사업금액(원)',
|
||||
};
|
||||
|
||||
const bizTyCdMap = new Map(BIZ_TY_CD.map(code => [code.lwpoCd, code.lwpoCdNm]));
|
||||
|
||||
const mappedContent = (res.content || []).map((item: any) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
return {
|
||||
bizInfoNm: formattedItem.bizInfoNm ?? item.bizInfoNm ?? '',
|
||||
flfmtBgngYmd: formattedItem.flfmtBgngYmd ?? item.flfmtBgngYmd ?? '',
|
||||
flfmtEndYmd: formattedItem.flfmtEndYmd ?? item.flfmtEndYmd ?? '',
|
||||
flfmtYr: formattedItem.flfmtYr ?? item.flfmtYr ?? '',
|
||||
bizTypeCd: bizTyCdMap.get(item.bizTypeCd) || item.bizTypeCd || formattedItem.bizTypeCd || '',
|
||||
bizAmt: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.bizAmt ?? item.bizAmt),
|
||||
};
|
||||
});
|
||||
|
||||
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
|
||||
const excelData = mappedContent; // 배열로 감싸서 전달
|
||||
|
||||
// 파일명 생성 (분석번호 기반)
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text
|
||||
const fileName = `${label} 사업 현황_${todayYear}${todayMonth}${todayDate}`;
|
||||
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: excelData as JSON[],
|
||||
fileName: fileName,
|
||||
sheetNameArray: [fileName],
|
||||
headerRange: "A1:F1",
|
||||
headerMapping: headerMapping
|
||||
});
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error("Excel 다운로드 중 오류 발생:", error);
|
||||
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 유지관리 정보 결과표 Excel 다운로드 핸들러 추가
|
||||
const operationResultExcelDownload = async () => {
|
||||
if (detailTableData.length === 0) {
|
||||
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.dismiss("global-confirm-toast");
|
||||
|
||||
confirmToast(
|
||||
"파일 다운로드에는 2분 내외의 시간이 소요될 수 있습니다.\n파일 다운로드를 진행하시겠습니까?",
|
||||
onAnalysis ?? (() => { }),
|
||||
"text-xl font-medium text-gray-800"
|
||||
);
|
||||
|
||||
async function onAnalysis() {
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const RequestDTO: Record<string, string> = {
|
||||
size: '100',
|
||||
bizId: String(detailBizId),
|
||||
};
|
||||
|
||||
const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`);
|
||||
|
||||
// 헤더 매핑 정의
|
||||
const headerMapping: { [key: string]: string } = {
|
||||
'fcltMngNo': '관리번호',
|
||||
'swtrmt_zone_nm': '처리구역',
|
||||
'usgCd': '용도',
|
||||
'mqltCd': '재질',
|
||||
'pdmtVl': '관경(mm)',
|
||||
'prlgVl': '연장(m)',
|
||||
'instlYmd': '설치일자',
|
||||
'strc_stts_grd': '구조적 상태등급',
|
||||
'oper_stts_grd': '운영적 상태등급',
|
||||
};
|
||||
|
||||
const usgCdMap = new Map(MS_SWAGE_PPL_USE_CODES.map(code => [code.val, code.text]));
|
||||
const mqltCdMap = new Map(MS_SWAGE_PPL_CODES.map(code => [code.val, code.text]));
|
||||
|
||||
// 🎯 단일 반복문으로 최적화 - 각 아이템마다 필요한 필드만 직접 매핑
|
||||
const mappedData = (res.data || []).map((item: any) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
|
||||
// trim 한 번만 수행
|
||||
const trimmedUsgCd = item?.usgCd?.trim();
|
||||
const trimmedMqltCd = item?.mqltCd?.trim();
|
||||
|
||||
// zone 값 한 번만 계산
|
||||
const zone = item.swtrmtZoneNm ?? formattedItem.swtrmt_zone_nm ?? item.swtrmt_zone_nm ?? '';
|
||||
|
||||
// 직접 매핑 - 반복문 없이 한 번에 객체 생성
|
||||
return {
|
||||
fcltMngNo: formattedItem.fcltMngNo ?? item.fcltMngNo ?? '',
|
||||
swtrmt_zone_nm: (zone === null || zone === '') ? '-' : zone,
|
||||
usgCd: usgCdMap.get(trimmedUsgCd) || item.usgCd || formattedItem.usgCd || '',
|
||||
mqltCd: mqltCdMap.get(trimmedMqltCd) || item.mqltCd || formattedItem.mqltCd || '',
|
||||
pdmtVl: formattedItem.pdmtVl ?? item.pdmtVl ?? '',
|
||||
prlgVl: formattedItem.prlgVl ?? item.prlgVl ?? '',
|
||||
instlYmd: formattedItem.instlYmd ?? item.instlYmd ?? '',
|
||||
strc_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.strc_stts_grd ?? item.strc_stts_grd),
|
||||
oper_stts_grd: ((v) => (v === 0 || v === '0') ? '0' : (v ?? ''))(formattedItem.oper_stts_grd ?? item.oper_stts_grd),
|
||||
};
|
||||
});
|
||||
|
||||
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
|
||||
const excelData = mappedData; // 배열로 감싸서 전달
|
||||
|
||||
// 파일명 생성 (분석번호 기반)
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text
|
||||
const fileName = `${label} 결과표_${todayYear}${todayMonth}${todayDate}`;
|
||||
|
||||
await downloadExcelWithKoreanHeaders({
|
||||
data: excelData as JSON[],
|
||||
fileName: fileName,
|
||||
sheetNameArray: [fileName],
|
||||
headerRange: "A1:I1",
|
||||
headerMapping: headerMapping
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error("Excel 다운로드 중 오류 발생:", error);
|
||||
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
WORK_TYPES,
|
||||
OPERATION_TABLE_HEADLIST,
|
||||
MAINTENANACE_TABLE_HEADLIST,
|
||||
CLEANING_TABLE_HEADLIST,
|
||||
INSPECTION_TABLE_HEADLIST,
|
||||
OPERATION_TABLE_SIZE,
|
||||
fetchOperationFilteredData,
|
||||
handleOperationCellClick,
|
||||
handleOperationResultCellClick,
|
||||
handleOperationPageNation,
|
||||
operationExcelDownload,
|
||||
operationResultExcelDownload
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 📄 useReportTab Hook
|
||||
* 검색 모달 보고서 탭 훅
|
||||
*
|
||||
* - 계층적 폴더 구조 평탄화 및 데이터 변환
|
||||
* - 파일 목록 조회 및 페이징 처리
|
||||
* - 파일 다운로드 기능
|
||||
* - 셀 클릭 이벤트 처리
|
||||
* - Zustand를 통한 상태 관리 및 로딩 처리
|
||||
* - 데이터 매핑 및 코드 변환 (시도/시군구, 보고서 유형 등)
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 보고서 파일 조회: GET /api/v1/file (dc-service)
|
||||
* - 파일 다운로드: GET /api/v1/fileDownload (dc-service)
|
||||
*/
|
||||
import { getDcData } from '@/components/lib/api';
|
||||
import { fileDownload } from '@/utils';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
|
||||
export type Folder = {
|
||||
clsfSystmSn: string;
|
||||
upClsfSystmSn: string;
|
||||
clsfSystmNm: string;
|
||||
children: Folder[];
|
||||
};
|
||||
export type FlattenedFolder = {
|
||||
[key: string]: string | number | undefined;
|
||||
};
|
||||
export type SelectDataType = {
|
||||
text: string;
|
||||
val: string;
|
||||
sido?: string; // 시도 코드 (선택 사항)
|
||||
}
|
||||
export type ReportDataFile = {
|
||||
no: number;
|
||||
fileTitle: string;
|
||||
regDt: string;
|
||||
fileSn: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export default function useReportTab() {
|
||||
const REPORT_TYPES = [
|
||||
{ val: '0', text: '전체' },
|
||||
{ val: '1', text: '보고서' },
|
||||
{ val: '2', text: '요약보고서' },
|
||||
{ val: '3', text: '부록' },
|
||||
{ val: '4', text: '기타' }
|
||||
];
|
||||
|
||||
const { curPage, setIsLoading } = useSearchModalStore();
|
||||
const { dcFiles: filesOrigin, files, setFiles } = useReportTabStore();
|
||||
|
||||
/**
|
||||
* Flatten a hierarchical folder structure into a flat array.
|
||||
* @param data - The hierarchical folder data to flatten.
|
||||
* @returns An array of flattened folders with their paths and levels.
|
||||
*/
|
||||
const flattenFolders = (data: Folder[]): FlattenedFolder[] => {
|
||||
const result: FlattenedFolder[] = [];
|
||||
const exceptNodeNm = ["부분", "통합", "전체"];
|
||||
const addNodeNm = ["보고서", "요약보고서", "부록", "기타"]; // level5
|
||||
const columnNms: { [key: number]: string } = {
|
||||
0: "root", 1: "sido", 2: "sigungu", 3: "approvalYear"
|
||||
, 4: "plantZoneGubun", 5: "reportGubun", 7: "fileName", 8: "regDate"
|
||||
};
|
||||
|
||||
const traverse = (
|
||||
node: Folder,
|
||||
path: { [key: string]: string | number },
|
||||
level: number
|
||||
) => {
|
||||
const isYear = (value: string): boolean => {
|
||||
return /^[0-9]{4}년$/.test(value);
|
||||
};
|
||||
|
||||
if (level === 2 && isYear(node.clsfSystmNm)) {
|
||||
level++;
|
||||
}
|
||||
if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) {
|
||||
level++; // 레벨을 증가시켜 하위값에 추가되도록 함.
|
||||
}
|
||||
|
||||
const newPath = {
|
||||
...path,
|
||||
[`${columnNms[level]}`]: node.clsfSystmNm,
|
||||
};
|
||||
|
||||
if (node.children.length === 0) {
|
||||
newPath.no = result.length + 1;
|
||||
newPath.clsfSystmSn = node.clsfSystmSn; // 파일 조회를 위한 고유번호
|
||||
result.push(newPath);
|
||||
} else {
|
||||
if (level === 4) {
|
||||
if (exceptNodeNm.includes(node.clsfSystmNm)) {
|
||||
level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함.
|
||||
}
|
||||
}
|
||||
node.children.forEach((child) => {
|
||||
traverse(child, newPath, level + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리
|
||||
data.forEach((root) => traverse(root, {}, 0));
|
||||
} else {
|
||||
data.forEach((root) => traverse(root, {}, 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const getDcFiles = async (clsfSystmSn: string): Promise<ReportDataFile[]> => {
|
||||
if (!clsfSystmSn) {
|
||||
console.error("clsfSystmSn is required to get files.");
|
||||
return [];
|
||||
}
|
||||
const queryString = 'clsfSystmSn=' + clsfSystmSn;
|
||||
const result = await getDcData(`/api/v1/file${queryString ? `?${queryString}` : ''}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
// 파일 이름 추가
|
||||
const addFileName = async (data: FlattenedFolder[]) => {
|
||||
const fileReportData = await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const fileArr: ReportDataFile[] = await getDcFiles(String(item.clsfSystmSn));
|
||||
let result: FlattenedFolder;
|
||||
if (fileArr.length > 0) {
|
||||
const f = fileArr[0];
|
||||
const fCount = fileArr.length;
|
||||
result = { ...item, fileName: f?.fileTitle + " 외 " + fCount + "건", regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath };
|
||||
} else {
|
||||
result = { ...item, fileName: "", regDate: "", fileSn: "", filePath: "" };
|
||||
}
|
||||
return result;
|
||||
})
|
||||
);
|
||||
return fileReportData;
|
||||
};
|
||||
|
||||
const getDcFileDownload = async (fileSn: string, fileName: string, filePath: string) => {
|
||||
if (filePath === "") {
|
||||
alert(`filePath 정보가 없으므로 다운로드가 불가합니다.\nfilesn: ${fileSn}`);
|
||||
return;
|
||||
}
|
||||
const queryString = 'filePath=' + encodeURIComponent(filePath);
|
||||
const responseBlob = await getDcData(`/api/v1/fileDownload${queryString ? `?${queryString}` : ''}`, "blob");
|
||||
fileDownload(responseBlob, fileName);
|
||||
}
|
||||
|
||||
// 파일 목록 조회
|
||||
const getFiles = async () => {
|
||||
setIsLoading(true); // 로딩 시작
|
||||
try {
|
||||
let data = filesOrigin;
|
||||
data = data?.slice((curPage - 1) * 10, curPage * 10); // paging
|
||||
data = data?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 }));
|
||||
setFiles(data);
|
||||
} catch (error) {
|
||||
console.error("파일 목록 조회 에러:", error);
|
||||
} finally {
|
||||
setIsLoading(false); // 로딩 종료
|
||||
}
|
||||
};
|
||||
|
||||
// cellClick 핸들러
|
||||
const handleCellClick = (index: number | undefined) => {
|
||||
if (typeof index === "number" && files[index]) {
|
||||
const fileName = files[index].fileTitle;
|
||||
const filePath = files[index].filePath;
|
||||
if (files[index].fileSn !== "") {
|
||||
getDcFileDownload(files[index].fileSn, fileName, filePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
REPORT_TYPES,
|
||||
flattenFolders,
|
||||
addFileName,
|
||||
getDcFiles,
|
||||
getDcFileDownload,
|
||||
getFiles,
|
||||
handleCellClick
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 📄 useSearchModal Hook
|
||||
* 검색 모달 훅
|
||||
*
|
||||
* - 탭 기반 검색 인터페이스 관리 (기본정보, 유지관리 정보, 보고서)
|
||||
* - 필터링된 데이터 조회 및 포맷팅
|
||||
* - 조회 버튼을 통한 데이터 검색 트리거
|
||||
* - 여러 시설 보기 기능
|
||||
* - Zustand를 통한 상태 관리 및 로딩 처리
|
||||
* - 데이터 매핑 및 코드 변환 (사업 유형 등)
|
||||
*
|
||||
* 🔗 사용 API
|
||||
* - 유지관리 정보 목록 조회: GET /api/v1/bizInfo (sewerage-service)
|
||||
*/
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import useStore from "@/store/useStore";
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import useSearchModalStore from "@/store/useSearchModalStore";
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
|
||||
export default function useSearchModal() {
|
||||
const {
|
||||
activeTab, workType,
|
||||
tableData, detailTableData,
|
||||
curPage,
|
||||
var1, var2,
|
||||
yearFilter,
|
||||
todayYear, todayMonth, todayDate,
|
||||
setIsSearchModalOpen,
|
||||
setShowTable,
|
||||
setTableData, setDetailTableData,
|
||||
setTableCount, setDetailTableCount,
|
||||
setCurPage,
|
||||
setIsLoading,
|
||||
setIsResetPage, setIsResetDetailPage,
|
||||
setIsResetSelection, setIsResetDetailSelection,
|
||||
getDateString
|
||||
} = useSearchModalStore();
|
||||
const { fetchBasicFilteredData } = useBasicTab();
|
||||
const { OPERATION_TABLE_SIZE } = useOperationTab();
|
||||
const { BIZ_TY_CD } = useStore();
|
||||
|
||||
const TABS = [
|
||||
{ val: "basic", text: "기본 정보" },
|
||||
{ val: "operation", text: "유지관리 정보" },
|
||||
{ val: "report", text: "하수도 정비 기본계획 보고서" },
|
||||
];
|
||||
|
||||
// 검색 API 호출 함수
|
||||
const fetchFilteredData = async () => {
|
||||
setIsLoading(true); // 로딩 시작
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
setIsResetDetailSelection(true); // 선택 초기화
|
||||
|
||||
try {
|
||||
if (activeTab === 'basic') {
|
||||
fetchBasicFilteredData();
|
||||
|
||||
} else if (activeTab === 'operation') {
|
||||
const RequestDTO: Record<string, string> = {
|
||||
page: `${curPage - 1}`,
|
||||
size: `${OPERATION_TABLE_SIZE}`,
|
||||
};
|
||||
if (var1) RequestDTO.bizInfoNm = var1;
|
||||
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
|
||||
// 수행기간 범위
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
'',
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
RequestDTO.fromflfmtEndYmd = startDate;
|
||||
RequestDTO.toflfmtEndYmd = endDate;
|
||||
|
||||
// JSON.parse 사용으로 문자열 -> 배열 변환
|
||||
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
|
||||
|
||||
const params = new URLSearchParams(RequestDTO).toString();
|
||||
|
||||
const res = await getData(`v1/bizInfo?${params}`);
|
||||
|
||||
const mappedContent = (res.content || []).map((item: any, index: number) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가
|
||||
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
|
||||
};
|
||||
});
|
||||
|
||||
setTableData(mappedContent || []);
|
||||
setTableCount(res.totalElements || 0);
|
||||
setDetailTableData([]);
|
||||
setDetailTableCount(0);
|
||||
}
|
||||
|
||||
setShowTable(true);
|
||||
setIsResetSelection(false);
|
||||
} catch (err) {
|
||||
console.error('API ERROR:', err);
|
||||
setTableData([]);
|
||||
setIsResetSelection(false);
|
||||
} finally {
|
||||
setIsLoading(false); // 로딩 종료
|
||||
setIsResetSelection(false);
|
||||
// setIsResetDetailSelection(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
|
||||
const handleSearch = () => {
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
fetchFilteredData();
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetDetailPage(true); // 디테일 페이지를 초기화
|
||||
setIsResetSelection(false); // 선택 초기화
|
||||
setIsResetDetailSelection(false); // 선택 초기화
|
||||
setShowTable(true);
|
||||
};
|
||||
|
||||
// 여러 시설 보기
|
||||
const viewMultipleFeature = async () => {
|
||||
if (activeTab === 'basic') {
|
||||
if (!(tableData.length === 0)) {
|
||||
setIsLoading(true);
|
||||
|
||||
const row = tableData[0];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fids = tableData.map((item) => item.fid);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
} else if (activeTab === 'operation') {
|
||||
if (!(detailTableData.length === 0)) {
|
||||
setIsLoading(true);
|
||||
|
||||
const row = detailTableData[0];
|
||||
const layerNm = row.lyrTypeNm;
|
||||
const fids = detailTableData.map((item) => item.fid);
|
||||
const core = GisApp.Module.layer as any;
|
||||
await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids);
|
||||
setIsLoading(false);
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { TABS, getDateString, handleSearch, viewMultipleFeature, fetchFilteredData };
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인페이지 내 침수심 범례
|
||||
*
|
||||
* 📄 WaterDepth Component
|
||||
* 침수심 범례 컴포넌트
|
||||
*
|
||||
* - 침수심 레이어의 범례 표시 및 애니메이션 재생 제어
|
||||
* - 진행률 바를 통한 애니메이션 상태 시각화
|
||||
* - 플레이/일시정지 버튼을 통한 사용자 상호작용
|
||||
* - 범례 항목별 색상과 레이블 표시
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { Label } from "@common/components/ui/label";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const WATER_DEPTH_LAYER_TYPES: string[] = [
|
||||
'tb_ms_flood_dngr_map', // 홍수위험지도
|
||||
'tb_ms_sbmrg_dngr_map' // 침수위험지도
|
||||
];
|
||||
|
||||
interface WaterDepthProps {
|
||||
onClose?: () => void;
|
||||
isPlaying: boolean;
|
||||
setIsPlaying: (isPlaying: boolean) => void;
|
||||
totalSteps?: number;
|
||||
currentStep?: number;
|
||||
}
|
||||
|
||||
export function WaterDepth({
|
||||
onClose,
|
||||
isPlaying,
|
||||
setIsPlaying,
|
||||
totalSteps = 0,
|
||||
currentStep = 0,
|
||||
}: WaterDepthProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (setIsPlaying) setIsPlaying(isPlaying);
|
||||
}, [isPlaying, setIsPlaying]);
|
||||
|
||||
const handlePlayToggle = () => {
|
||||
setIsPlaying(!isPlaying);
|
||||
};
|
||||
|
||||
// 진행률 계산 (0-100%)
|
||||
const progressPercentage = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 100;
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-lg shadow-lg border border-gray-200 w-[160px] px-4 py-3">
|
||||
<div className="flex items-center justify-between mx-2 my-4">
|
||||
<Label className="text-3xl text-gray-900 font-bold">침수심 범례</Label>
|
||||
<button type="button" className="mt-1 ml-2" onClick={handlePlayToggle}>
|
||||
{isPlaying ? (
|
||||
<img src="/images/map/toolbar/pause_button.png" alt="pause" className="w-6 h-6 object-contain" />
|
||||
) : (
|
||||
<img src="/images/map/toolbar/play_button_arrowhead.png" alt="play" className="w-6 h-6 object-contain" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<div className="w-full bg-gray-200 rounded-full h-1">
|
||||
<div
|
||||
className="bg-blue-900 h-1 rounded-full transition-all duration-300 ease-in-out"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[140px] mx-auto flex flex-col items-center">
|
||||
<div className="bg-blue-900 text-white text-2xl text-center py-1 mb-2 rounded w-full">침 수 심</div>
|
||||
<div className="space-y-1 w-full">
|
||||
<LegendItem color="rgb(255, 255, 127)" label="0.0~0.5m 미만" /> {/* 코드: N330 */}
|
||||
<LegendItem color="rgb(191, 255, 0)" label="0.5~1.0m 미만" /> {/* 코드: N331 */}
|
||||
<LegendItem color="rgb(0, 255, 255)" label="1.0~2.0m 미만" /> {/* 코드: N332 */}
|
||||
<LegendItem color="rgb(191, 127, 255)" label="2.0~5.0m 미만" /> {/* 코드: N333 */}
|
||||
<LegendItem color="rgb(255, 0, 127)" label="5.0m 이상" /> {/* 코드: N334 */}
|
||||
</div>
|
||||
<div className="border-t-2 border-blue-900 mt-1 mx-auto w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LegendItem({ color, label }: { color: string; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<span
|
||||
className="inline-block w-16 h-6"
|
||||
style={{ backgroundColor: color, border: "1px solid #ccc" }}
|
||||
/>
|
||||
<span className="text-xl text-gray-900 font-bold">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* ➡️ 경로 : I/I분석, 유지관리 정보 모든 페이지
|
||||
*
|
||||
* 📄 With Base Layout
|
||||
* Next.js App Router (app/) 기반 분석 페이지 기본 레이아웃 컴포넌트
|
||||
*
|
||||
* - 메뉴 클릭 이벤트 처리 및 라우팅
|
||||
* - 현재 메뉴 정보 Zustand를 통한 상태 관리
|
||||
* - 시도/시군구/사업 구분/사업 유형 데이터 초기 로드
|
||||
* - BaseLayout을 통한 공통 레이아웃 적용
|
||||
* - QueryClient를 통한 데이터 캐싱 및 관리
|
||||
* - 경로 변경 시 메뉴 정보 동기화
|
||||
*/
|
||||
"use client";
|
||||
|
||||
import React, { useEffect} from 'react';
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { BaseLayout } from "@common/components";
|
||||
import type { MenuItem } from "@/entities/menu";
|
||||
import { useMenuStore } from "@/shared/lib";
|
||||
import { PageLayout } from "@/shared/ui/page-layout";
|
||||
import useStore from '../store/useStore';
|
||||
|
||||
export default function WithBaseLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setCurrentMenuInfo = useMenuStore((state) => state.setCurrentMenuInfo);
|
||||
|
||||
const { fetchCtpv, fetchSgg, fetchBIZ_SE_CD, fetchBIZ_TY_CD } = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchCtpv();
|
||||
fetchSgg();
|
||||
fetchBIZ_SE_CD();
|
||||
fetchBIZ_TY_CD();
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = (item: MenuItem) => {
|
||||
setCurrentMenuInfo(item);
|
||||
router.push(item.urlPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseLayout
|
||||
queryClient={queryClient}
|
||||
currentPath={pathname ?? ""}
|
||||
handleMenuClick={handleMenuClick}
|
||||
>
|
||||
<PageLayout>{children}</PageLayout>
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
# 노후관로 관리
|
||||
|
||||
---
|
||||
|
||||
## 화면구성
|
||||
### GIS 기본화면
|
||||
-노후관로 조건에 맞지 않는 관로를 숨기지 않고, 기존 시설물 레이어는 원래대로 모두 보이게 두고 조건에 맞는 관로만 빨간 강조선을 추가하도록 한다.
|
||||
- 좌측 상단 (시설물 레이어 트리 및 목록 아이콘): 시설, 침수위험지도, 지반침하정보(우측에 목록 아이콘), 노후관로(우측에 목록 아이콘)
|
||||
- 좌측 하단 (레이어별 설정 항목 및 적용 버튼): 경과년도(년) [ 디폴트 30년 ]
|
||||
- 우측 영역(배경 지도 및 시설레이어 표출): 하수관로 중 경과년도가 30년 이상된 관로들을 시각적으로 강조 표시
|
||||
|
||||
### 관로정보 상세화면
|
||||
- GIS 화면에서 관로 위치 클릭시 우측 정렬로 세로형 영역으로 관로정보 상세화면 팝업
|
||||
- 1차년도에서 개발한 관로정보 표출 : 시설관리번호, 기관관리번호, 설치일자, 도엽번호 등 전체 항목 그대로 표출
|
||||
|
||||
### 노후관로정보 목록화면
|
||||
- 윈도우형 팝업
|
||||
- GIS 기본화면 좌측 상단의 레이어 트리에서 노후관로 목록 아이콘 클릭시 윈도우 팝업으로 가운데 영역으로 노후관로정보 목록화면 팝업
|
||||
- 상단 조회조건( 조회조건 영역 ) : 경과년도, 시도, 시군구, 버튼 2개 (조회, 다운로드)
|
||||
- 하단 가로형 그리드 (노후관로 현황) : NO, 시설관리번호, 설치일자, 관경(mm), 연장(m), 용도, 재질, 경과년도
|
||||
|
||||
---
|
||||
|
||||
## 화면동작
|
||||
|
||||
### 초기화 및 기본 설정
|
||||
|
||||
```구조적표기
|
||||
// 화면 로드 시 초기 데이터 및 환경 설정
|
||||
ON_LOAD:
|
||||
CALL GIS_지도_레이어_초기화()
|
||||
SET 기본_경과년도 = 20 // 회의록(260311) 반영 디폴트 설정
|
||||
SET 현재_조회_조건 = { "경과년도": 20, "관종": "전체" }
|
||||
|
||||
// 초기 데이터 로드
|
||||
CALL 노후관로_데이터_조회(현재_조회_조건)
|
||||
CALL 지도_상_노후관로_표출(조회결과)
|
||||
```
|
||||
|
||||
### 좌측 사이드바 제어 (분석 설정)
|
||||
|
||||
```구조적표기
|
||||
// 노후관로 조회 조건 변경 및 적용
|
||||
IF 노후관로_조회_버튼.CLICK THEN
|
||||
SET 입력_경과년도 = 사이드바.경과년도_INPUT.Value
|
||||
SET 선택_관종 = 사이드바.관종_SELECT.Value
|
||||
|
||||
// 공간 및 속성 필터링 실행
|
||||
CALL 노후관로_필터링_프로세스(입력_경과년도, 선택_관종)
|
||||
REFRESH_GIS_LAYER(노후관로_레이어)
|
||||
SHOW MESSAGE "설정된 조건으로 노후관로가 필터링되었습니다."
|
||||
END IF
|
||||
|
||||
// 목록 아이콘 클릭 (모달 호출)
|
||||
IF 레이어트리.노후관로_목록아이콘.CLICK THEN
|
||||
DISPLAY 노후관로_목록_모달 = TRUE
|
||||
CALL 노후관로_목록_그리드_로드()
|
||||
END IF
|
||||
```
|
||||
|
||||
#### 노후관로 상세화면 (우측 패널)
|
||||
|
||||
```구조적표기
|
||||
// 지도 상의 특정 관로 클릭 시
|
||||
IF GIS.노후관로_객체.CLICK THEN
|
||||
SET 선택관로_ID = 클릭객체.ID
|
||||
|
||||
// 상세 속성 정보 조회 및 매핑
|
||||
CALL 관로_상세속성_API_호출(선택관로_ID)
|
||||
SET 우측패널.기본정보 = [관로ID, 시설명, 주소, 관리기관]
|
||||
SET 우측패널.제원정보 = [관경, 연장, 관종, 준공일자, 경과년도]
|
||||
SET 우측패널.상태정보 = [최종조사일, 조사결과등급]
|
||||
|
||||
DISPLAY 우측_상세_패널 = TRUE
|
||||
END IF
|
||||
```
|
||||
|
||||
#### 노후관로 목록 및 데이터 출력 (모달 윈도우)
|
||||
```구조적표기
|
||||
// 엑셀 다운로드 (회의록 요구사항 반영)
|
||||
IF 모달.다운로드_버튼.CLICK THEN
|
||||
// 엑셀 파일에는 관로의 '모든 기본정보'를 포함
|
||||
SET 다운로드_컬럼 = [관로ID, 주소, 관경, 연장, 관종, 준공일, 경과년도, ...전체필드]
|
||||
CALL 시스템_엑셀_추출_모듈(현재_조회_데이터, 다운로드_컬럼)
|
||||
END IF
|
||||
|
||||
// 목록 그리드 행 선택 시 로직
|
||||
IF 모달.그리드_행.DOUBLE_CLICK THEN
|
||||
SET 좌표 = 현재행.중심좌표
|
||||
CALL GIS_지도_줌인(좌표)
|
||||
CALL 노후관로_상세정보_표출(현재행.ID)
|
||||
CLOSE_MODAL(노후관로_목록_모달)
|
||||
END IF
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
## html 디렉토리
|
||||
- 이 디렉토리의 목적은 코드를 화면 publishing과 로직 설정을 위한 폴더이다.
|
||||
- 작업시 html 디렉토리 이외의 수정이 되지 않도록 해야 한다.
|
||||
- 추가 *.md 파일의 요청대로 만들되 상위의 <Gis>를 건드리지 않고 apps\sn\app\html 에 추가파일을 만들어서 처리한다.
|
||||
- 기존의 package/common/component 와 package/common/business_component를 잘 활용한다.
|
||||
- <Gis> 의 시설물 layer는 api 호출 결과 값 설정된 대로 zoom level에 따라 모두 보여야 한다.
|
||||
## Encoding
|
||||
- UTF-8
|
||||
- 한글이 깨지지 않도록 주의할 것
|
||||
|
||||
## 주석 comment
|
||||
- 한글 주석을 삭제하지 말 것
|
||||
|
||||
## Monorepo structure
|
||||
apps/
|
||||
_template
|
||||
as
|
||||
cm
|
||||
cs
|
||||
dc
|
||||
dm
|
||||
dm_new
|
||||
fo
|
||||
sn
|
||||
st
|
||||
wr
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"@common/assets": "workspace:*",
|
||||
"@common/business_components": "workspace:*",
|
||||
"@common/components": "workspace:*",
|
||||
"@common/config": "workspace:*",
|
||||
"@fontsource-variable/noto-sans-kr": "^5.2.5",
|
||||
"@frontend-opensource/use-react-hooks": "^1.1.4",
|
||||
"@hookform/resolvers": "^4.1.3",
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@ NEXT_PUBLIC_CS_SERVER_API_URL = http://dev.onasys.co.kr:8000
|
||||
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
|
||||
NEXT_PUBLIC_GEOSERVER_URL = https://hasudo.geo.rsonesoft.com/geoserver
|
||||
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
#NEXT_PUBLIC_WR_SERVER_API_URL = http://192.168.1.222:8883/reuse-service/api/v1
|
||||
# NEXT_PUBLIC_WR_SERVER_API_URL = http://con.mind-one.co.kr:11083/reuse-service/api/v1
|
||||
NEXT_PUBLIC_WR_SERVER_API_URL = http://192.168.1.222:8883/reuse-service/api/v1
|
||||
NEXT_PUBLIC_SEWERAGE_API_URL = http://con.mind-one.co.kr:11083/sewerage-service # 개발 서버 API
|
||||
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service
|
||||
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr
|
||||
|
||||
@@ -21,6 +21,7 @@ Main technologies:
|
||||
|
||||
## 주석 comment
|
||||
- 한글 주석을 삭제하지 말 것
|
||||
- 신규 component나 함수 자동 생성시 주석을 추가할 것(기본 함수의 역할과 인수 설명)
|
||||
|
||||
## Monorepo structure
|
||||
apps/
|
||||
|
||||
@@ -386,7 +386,6 @@ const Map = () => {
|
||||
};
|
||||
}, [checkedLayers]);
|
||||
|
||||
console.log(checkedLayers);
|
||||
return (
|
||||
<div className="bg-container">
|
||||
<Gis
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 선택된 시설
|
||||
*/
|
||||
import React from "react";
|
||||
|
||||
type FcltItem = {
|
||||
@@ -20,52 +23,6 @@ export default function SelectedFcltList({
|
||||
onRemove(updatedList);
|
||||
};
|
||||
|
||||
const TableContent = (
|
||||
<>
|
||||
<thead className="bg-gray-100 text-gray-700 text-[13px]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left border-b">시설명</th>
|
||||
<th className="px-3 py-2 text-right border-b">
|
||||
하수처리량
|
||||
<br />
|
||||
(㎥/일)
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right border-b">
|
||||
재이용량
|
||||
<br />
|
||||
(㎥/일)
|
||||
</th>
|
||||
<th className="px-3 py-2 border-b">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item) => (
|
||||
<tr key={item.pbnstRuseFcltId} className="border-b last:border-none">
|
||||
<td className="px-3 py-2 text-[12px]">{item.ruseUtztnFcltNm}</td>
|
||||
<td className="px-3 py-2 text-right text-[12px]">
|
||||
{Number(item.splyExtraCpct || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-[12px]">
|
||||
{Number(item.ruseCpct || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(item.pbnstRuseFcltId)}
|
||||
className="text-red-500 hover:text-red-700 text-sm font-bold"
|
||||
aria-label="삭제"
|
||||
title="삭제"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
|
||||
if (data.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed right-40 top-[8rem] w-[40rem] max-h-[130rem] overflow-auto bg-white shadow-lg rounded p-4 border border-gray-300 z-50">
|
||||
<h3 className="bg-cyan-700 text-white text-xl font-bold mb-2 p-2">
|
||||
@@ -73,7 +30,53 @@ export default function SelectedFcltList({
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-gray-800 border border-gray-200">
|
||||
{TableContent}
|
||||
<thead className="bg-gray-100 text-gray-700 text-[13px]">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left border-b">시설명</th>
|
||||
<th className="px-3 py-2 text-right border-b">
|
||||
하수처리량
|
||||
<br />
|
||||
(㎥/일)
|
||||
</th>
|
||||
<th className="px-3 py-2 text-right border-b">
|
||||
재이용량
|
||||
<br />
|
||||
(㎥/일)
|
||||
</th>
|
||||
<th className="px-3 py-2 border-b">삭제</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length > 0 ? (
|
||||
data.map((item) => (
|
||||
<tr key={item.pbnstRuseFcltId} className="border-b last:border-none">
|
||||
<td className="px-3 py-2 text-[12px]">{item.ruseUtztnFcltNm}</td>
|
||||
<td className="px-3 py-2 text-right text-[12px]">
|
||||
{Number(item.splyExtraCpct || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right text-[12px]">
|
||||
{Number(item.ruseCpct || 0).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
<button
|
||||
onClick={() => handleDelete(item.pbnstRuseFcltId)}
|
||||
className="text-red-500 hover:text-red-700 text-sm font-bold"
|
||||
aria-label="삭제"
|
||||
title="삭제"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-3 py-6 text-center text-[12px] text-gray-500">
|
||||
선택된 시설이 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
/**
|
||||
* 📄 CoMatRvw 수요매칭 검토
|
||||
* 📄 CoMatRvw 수요처 매칭 검토
|
||||
* Next.js App Router (app/) 기반 페이지 컴포넌트 예시
|
||||
*
|
||||
* - 대민에서 신청하는 수요처의 수요등록 정보를 운영담당자가 수요 매칭 검토를 진행한다.
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import { useWrStore } from "@/app/store";
|
||||
import { btnClass } from "@/app/store/common/code";
|
||||
import { usageProcData } from "@/app/store/matDmd/coMatRvw/headList";
|
||||
import { usageProcData } from "@/app/store/matDmd/coMatRvw/headList"; // 용도에 따른 처리공정
|
||||
import {
|
||||
DmdCoReqSchema,
|
||||
type DmdCoReqType,
|
||||
@@ -112,19 +112,29 @@ export default function CoMatRvw() {
|
||||
if (coMatRvwRes && coMatRvwRes[index]) {
|
||||
setSelectData(coMatRvwRes[index]);
|
||||
setSelectionVersion((v) => v + 1); // 행을 클릭할 때마다 버전을 1씩 증가
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" }); // 화면 스크로 처리
|
||||
}
|
||||
};
|
||||
const useUsgCdWatch = useWatch({ control: form.control, name: "useUsgCd" });
|
||||
useEffect(() => {
|
||||
if (useUsgCdWatch) {
|
||||
console.log("useUsgCdWatch:" , useUsgCdWatch);
|
||||
const normalizedUseUsgCd =
|
||||
code.sewageOutFclttUse?.find((item) => item.val === useUsgCdWatch)?.val ??
|
||||
code.sewageOutFclttUse?.find((item) => item.text === useUsgCdWatch)?.val ??
|
||||
useUsgCdWatch;
|
||||
|
||||
if (normalizedUseUsgCd !== useUsgCdWatch) {
|
||||
form.setValue("useUsgCd", normalizedUseUsgCd);
|
||||
}
|
||||
|
||||
form.setValue(
|
||||
"demandCst.usgPrcsProcs",
|
||||
usageProcData?.find((item) => item.usage === useUsgCdWatch)?.process ??
|
||||
usageProcData?.find((item) => item.useUsgCd === normalizedUseUsgCd)?.process ??
|
||||
""
|
||||
);
|
||||
}
|
||||
}, [useUsgCdWatch, form, selectionVersion]);
|
||||
}, [useUsgCdWatch, form, selectionVersion, code.sewageOutFclttUse]);
|
||||
|
||||
useEffect(() => {
|
||||
if (coMatRvwRes && coMatRvwRes.length > 0) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 수요처관리
|
||||
*/
|
||||
import { useWrStore } from "@/app/store";
|
||||
import { btnClass } from "@/app/store/common/code";
|
||||
import { excelCoSttnParsing } from "@/app/store/matDmd/coSttn/excel";
|
||||
@@ -188,6 +191,47 @@ export default function CoSttn() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록/수정 : SdmInsertForm에서 저장 버튼을 눌렀을 때, 폼 데이터가 부모로 넘어오는 콜백
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
const handleRtnInputData = async (data: DemandCompType) => {
|
||||
const con =
|
||||
objSearch.sgg && objSearch.sgg !== "0"
|
||||
? objSearch.sgg
|
||||
: objSearch.city;
|
||||
|
||||
if (updateData?.dmdInfoId) { // 수정
|
||||
const message = await updateDemandCompany(data);
|
||||
alert(message);
|
||||
coSttnInit("1", "13", String(con));
|
||||
setIsDmdModal({
|
||||
isMoal: false,
|
||||
data,
|
||||
});
|
||||
setIsResetSelection(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const dmd = await getFetch(
|
||||
fetchType.WR,
|
||||
`/reuseFc/request/${data.dmdInfoId}`
|
||||
);
|
||||
if (dmd) {
|
||||
alert(`사업자번호 ${dmd.dmdInfoId}는 이미 등록되었습니다. 다시등록해주세요`);
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await regDemandCompany(data); // 등록
|
||||
alert(message);
|
||||
coSttnInit("1", "13", String(con));
|
||||
setIsDmdModal({
|
||||
isMoal: false,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentsDiv>
|
||||
@@ -284,12 +328,12 @@ export default function CoSttn() {
|
||||
headlist={coSttn.coSttHeadList}
|
||||
pageSize={13}
|
||||
total={coSttnResCount || 0}
|
||||
cellClick={ async(index: number | undefined) => {
|
||||
const uuid = await getMyUUID();
|
||||
const updateData = coSttnRes[index as number];
|
||||
if (updateData && typeof uuid === "string") {
|
||||
updateData.mdfr = uuid;
|
||||
}
|
||||
cellClick={async(index: number | undefined) => {
|
||||
const uuid = await getMyUUID();
|
||||
const updateData = coSttnRes[index as number];
|
||||
if (updateData && typeof uuid === "string") {
|
||||
updateData.mdfr = uuid;
|
||||
}
|
||||
setUpdateData(coSttnRes[index as number]);
|
||||
}}
|
||||
onPageChange={(page) => {
|
||||
@@ -321,38 +365,7 @@ export default function CoSttn() {
|
||||
rtnValue={(searchText: string, pageCnt: number) => {
|
||||
getDemandCompList(searchText, pageCnt);
|
||||
}}
|
||||
rtnInputData={async (data) => {
|
||||
const con =
|
||||
objSearch.sgg && objSearch.sgg !== "0"
|
||||
? objSearch.sgg
|
||||
: objSearch.city;
|
||||
if (updateData?.dmdInfoId) {
|
||||
const message = await updateDemandCompany(data as DemandCompType);
|
||||
alert(message);
|
||||
coSttnInit("1", "13", String(con));
|
||||
setIsDmdModal({
|
||||
isMoal: false,
|
||||
data: data as DemandCompType,
|
||||
});
|
||||
setIsResetSelection(true);
|
||||
} else {
|
||||
const dmd = await getFetch(
|
||||
fetchType.WR,
|
||||
`/reuseFc/request/${data.dmdInfoId}`
|
||||
);
|
||||
if (dmd) {
|
||||
alert(`사업자번호 ${dmd.dmdInfoId}는 이미 등록되었습니다. 다시등록해주세요`);
|
||||
return;
|
||||
}
|
||||
const message = await regDemandCompany(data as DemandCompType);
|
||||
alert(message);
|
||||
coSttnInit("1", "13", String(con));
|
||||
setIsDmdModal({
|
||||
isMoal: false,
|
||||
data: data as DemandCompType,
|
||||
});
|
||||
}
|
||||
}}
|
||||
rtnInputData={handleRtnInputData}
|
||||
bodyClassNm={"w-full mx-auto bg-white rounded-lg shadow-md p-6"}
|
||||
updateList={updateData}
|
||||
openNetwork={true}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { App } from "../src/app";
|
||||
import { setCookie } from "../src/shared/lib/cookie";
|
||||
|
||||
setCookie("site-id-key", process.env.NEXT_PUBLIC_SITE_ID || "WR");
|
||||
export const metadata: Metadata = {
|
||||
title: "물재이용매칭시스템",
|
||||
description: "물재이용매칭 시스템입니다",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default function Loading() {
|
||||
return <div>페이지 불러오는 중입니다...</div>;
|
||||
// return <div>페이지 불러오는 중입니다...</div>;
|
||||
return <></>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { fetchType, getFetchWithCount } from "@/components/lib/api";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import { getCookie } from "@common/components/lib";
|
||||
import toast from "react-hot-toast";
|
||||
// import toast from "react-hot-toast";
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export type authState = {
|
||||
auth: {
|
||||
@@ -59,10 +60,11 @@ export const authActionsExport: (set: any, get: any) => authActions = (
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
"권한 정보를 가져오는 중 오류가 발생했습니다. 관리자에게 문의하세요."
|
||||
);
|
||||
// toast.error(
|
||||
// "권한 정보를 가져오는 중 오류가 발생했습니다. 관리자에게 문의하세요."
|
||||
// );
|
||||
console.error("Error fetching menu authorities:", e);
|
||||
redirect("/");
|
||||
}
|
||||
},
|
||||
getLaeSeCdCheck: async () => {
|
||||
|
||||
@@ -77,6 +77,7 @@ export const getCommonUpCd = async (set: any) => {
|
||||
pplQulity: lwCdCode["pplQulity"] ?? [],
|
||||
pplLenght: lwCdCode["pplLenght"] ?? [],
|
||||
dmdUszCd: lwCdCode["dmdUszCd"] ?? [],
|
||||
sewageOutFclttUse: lwCdCode["sewageOutFclttUse"] ?? [],
|
||||
ruseMngMtd: lwCdCode["ruseMngMtd"] ?? [],
|
||||
rusePrcsMthdCd: lwCdCode["rusePrcsMthdCd"] ?? [],
|
||||
erctUswtrPrcsMeth: lwCdCode["erctUswtrPrcsMeth"] ?? [],
|
||||
@@ -233,6 +234,7 @@ export type comCodeState = {
|
||||
pplQulity: SelectDataType[];
|
||||
pplLenght: SelectDataType[];
|
||||
dmdUszCd: SelectDataType[];
|
||||
sewageOutFclttUse: SelectDataType[];
|
||||
buildingType: {
|
||||
RST: string;
|
||||
RNB: string;
|
||||
@@ -274,6 +276,7 @@ export const comCodeStateExport: comCodeState = {
|
||||
pplLenght: lwCds["pplLenght"] ?? [],
|
||||
yn: yn,
|
||||
dmdUszCd: lwCds["dmdUszCd"] ?? [],
|
||||
sewageOutFclttUse: lwCds["sewageOutFclttUse"] ?? [],
|
||||
buildingType: buildingType,
|
||||
ruseMngMtd: lwCds["ruseMngMtd"] ?? [],
|
||||
ruseSeCd: ruseSeCd,
|
||||
|
||||
@@ -39,7 +39,8 @@ export const comFcltStateExport: comFcltState = {
|
||||
cdtnv: 0,
|
||||
crmty: 0,
|
||||
rmrk: "",
|
||||
reqDt: ""
|
||||
reqDt: "",
|
||||
zip: "",
|
||||
},
|
||||
constCostRes:[],
|
||||
rateFeeRes:[],
|
||||
@@ -51,7 +52,7 @@ export type comFcltState = {
|
||||
searchFclts: pubHasuType[];
|
||||
demandCompList: DemandCompType[];
|
||||
checkDemand: DemandCompType;
|
||||
constCostRes:conCostFeeType[],
|
||||
constCostRes:conCostFeeType[], // 표준사업비
|
||||
rateFeeRes:rateFeeType[],
|
||||
|
||||
};
|
||||
@@ -149,6 +150,7 @@ export const comFcltSearchAction: (set: any) => comFcltActions = (set: any) => {
|
||||
return "수요처 정보 변경 중 오류가 발생했습니다.";
|
||||
}
|
||||
},
|
||||
// 요율정보 조회 : ONSEMIRO_NEW.EM_STD_COST_INC_EXP_RT 설치관리표준사업비 부대경비요율
|
||||
getRateFee: async (year: string) => {
|
||||
try {
|
||||
const url = `/prcsFclt/CstRt/${year}`;
|
||||
@@ -164,7 +166,9 @@ export const comFcltSearchAction: (set: any) => comFcltActions = (set: any) => {
|
||||
} catch (e) {
|
||||
console.log(" error ", e);
|
||||
}
|
||||
},getConstCostFee: async (year: string) => {
|
||||
},
|
||||
// 공사금액 조회 : ONSEMIRO_NEW.EM_STD_COST_REUSE 설치관리표준사업비 재이용시설 공사비
|
||||
getConstCostFee: async (year: string) => {
|
||||
try {
|
||||
const url = `/prcsFclt/CstRuse/${year}`;
|
||||
const constCost = await getFetch(fetchType.COMMON, url);
|
||||
@@ -176,7 +180,7 @@ export const comFcltSearchAction: (set: any) => comFcltActions = (set: any) => {
|
||||
}
|
||||
|
||||
});
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
console.log('getConstCostFee ', e)
|
||||
}
|
||||
|
||||
|
||||
@@ -187,13 +187,11 @@ export const rateFeeHeadListExport: CommonTableProps[] = [
|
||||
];
|
||||
|
||||
export const usageProcData = [
|
||||
{ usage: "청소", process: "총인처리시설+염소소독" },
|
||||
{ usage: "화장실용수", process: "총인처리시설+염소소독" },
|
||||
{ usage: "세척", process: "총인처리시설+UV소독" },
|
||||
{ usage: "살수", process: "총인처리시설+UV소독" },
|
||||
{ usage: "조경", process: "총인처리시설+UV소독" },
|
||||
{ usage: "농업", process: "총인처리시설+UV소독" },
|
||||
{ usage: "하천", process: "총인처리시설+UV소독" },
|
||||
{ usage: "지하수", process: "전처리시설+RO" },
|
||||
{ usage: "공업", process: "전처리시설+RO" },
|
||||
{ useUsgCd: "01", usage: "청소화장실용수", process: "총인처리시설+염소소독" },
|
||||
{ useUsgCd: "02", usage: "세척살수용수", process: "총인처리시설+UV소독" },
|
||||
{ useUsgCd: "03", usage: "조경용수", process: "총인처리시설+UV소독" },
|
||||
{ useUsgCd: "05", usage: "지하수충전", process: "전처리시설+RO" },
|
||||
{ useUsgCd: "06", usage: "농업용수", process: "총인처리시설+UV소독" },
|
||||
{ useUsgCd: "07", usage: "하천등유지용수", process: "총인처리시설+UV소독" },
|
||||
{ useUsgCd: "08", usage: "공업용수", process: "전처리시설+RO" },
|
||||
];
|
||||
|
||||
@@ -139,6 +139,7 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => {
|
||||
return "매칭 검토 저장 오류입니다.";
|
||||
}
|
||||
},
|
||||
// 수요처 매칭 검토 현황
|
||||
coMatRvwSearch: async (
|
||||
pageNo: number,
|
||||
pageCnt: number,
|
||||
@@ -160,7 +161,7 @@ export const coRvwActionsExport: (set: any) => coRvwActions = (set: any) => {
|
||||
url += `/0`;
|
||||
}
|
||||
|
||||
console.log(" url ", url);
|
||||
// console.log(" url ", url);
|
||||
const coMatRvwRes = await getFetchWithCount(fetchType.WR, url);
|
||||
|
||||
return set((state: coRvwState) => ({
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
/**
|
||||
* 주어진 코드와 수량 기준으로 보간 계산에 사용할 하한/상한 구간 데이터를 찾는다.
|
||||
*
|
||||
* 같은 코드(`fctCd`)를 가진 데이터만 추린 뒤,
|
||||
* `qtyCode` 값이 현재 수량(`qty`) 이하인 항목 중 가장 큰 값을 `lower`,
|
||||
* 초과인 항목 중 가장 작은 값을 `upper`로 반환한다.
|
||||
* 한쪽 경계만 존재하면 같은 값을 상/하한에 보정해 반환한다.
|
||||
*
|
||||
* @param data 전체 기준 데이터 목록
|
||||
* @param fctCd 찾고 싶은 시설/구분 코드 값
|
||||
* @param dataCode 코드 비교에 사용할 객체 속성명
|
||||
* @param qtyCode 수량 비교에 사용할 객체 속성명
|
||||
* @param qty 현재 비교 기준 수량
|
||||
* @returns 보간 계산에 사용할 하한(lower), 상한(upper) 데이터
|
||||
*/
|
||||
export const findBounds = (
|
||||
data: any[],
|
||||
fctCd: string,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Form,
|
||||
FormField,
|
||||
FormUi,
|
||||
SelectBox,
|
||||
UiTable,
|
||||
} from "@common/business_components/ui";
|
||||
import PopoverUi from "@common/business_components/ui/popover_ui";
|
||||
@@ -47,12 +48,13 @@ export default function ConCostForm({
|
||||
readonly: boolean;
|
||||
}) {
|
||||
const {
|
||||
constCostRes,
|
||||
rateFeeRes,
|
||||
constCostRes, // 표준사업비
|
||||
rateFeeRes, // 부대경비요율
|
||||
coMatRvwUpdate,
|
||||
ruseRvwCalUpdate,
|
||||
getRateFee,
|
||||
getConstCostFee,
|
||||
code,
|
||||
} = useWrStore((state) => state);
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
@@ -107,8 +109,16 @@ export default function ConCostForm({
|
||||
const totalConstCost = form.watch("totalConstCost");
|
||||
const cstrnCst = form.watch("demandCst.cstrnCst");
|
||||
const splyList = form.watch("splyCndtList");
|
||||
|
||||
/**
|
||||
* 표준공산비 산정 : "계산"버튼 click시
|
||||
*/
|
||||
const calculateConstCost = useCallback(() => {
|
||||
const usgPrcsProcs = usgPrcsProcsWatch;
|
||||
if (selectionVersion == 0) {
|
||||
alert("먼저 목록에서 수요신청 건을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
if (!dmndQtyWatch) {
|
||||
alert("요구수량(m³/일)을 확인해주세요 계산할수 없습니다. ");
|
||||
return;
|
||||
@@ -119,23 +129,30 @@ export default function ConCostForm({
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (constCostRes?.length === 0) {
|
||||
alert("표준공사비 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
if (rateFeeRes?.length === 0) {
|
||||
alert("표준공사비 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const dmndQtyYear = dmndQtyWatch;
|
||||
if (usgPrcsProcs && usgPrcsProcs?.length > 0) {
|
||||
const firstProc =
|
||||
getConstFctCode(usgPrcsProcs?.split("+")[0] || "") || "";
|
||||
const secondProc =
|
||||
getConstFctCode(usgPrcsProcs?.split("+")[1] || "") || "";
|
||||
const firstProc = getConstFctCode(usgPrcsProcs?.split("+")[0] || "") || ""; // 첫번째 공정
|
||||
const secondProc = getConstFctCode(usgPrcsProcs?.split("+")[1] || "") || ""; // 두번째 공정
|
||||
|
||||
// 첫번째 처리공정 코드에 해당하는 표준공사비 테이블이서 요구수량을 감싸는 하한/상한 구간을 찾는 부분이다.
|
||||
const { lower: firstLower, upper: firstUpper } = findBounds(
|
||||
constCostRes,
|
||||
constCostRes, // 표준공사비
|
||||
firstProc || "",
|
||||
"fctCd",
|
||||
"fctCpc",
|
||||
Number(dmndQtyYear)
|
||||
);
|
||||
const { lower: secondLower, upper: secondUpper } = findBounds(
|
||||
constCostRes,
|
||||
constCostRes, // 표준공사비
|
||||
secondProc || "",
|
||||
"fctCd",
|
||||
"fctCpc",
|
||||
@@ -158,7 +175,7 @@ export default function ConCostForm({
|
||||
const groupedBounds: { [key: string]: { lower?: any; upper?: any } } = {};
|
||||
for (const code of groupCodes) {
|
||||
groupedBounds[code] = findBounds(
|
||||
rateFeeRes,
|
||||
rateFeeRes, // 부대경비요율
|
||||
code,
|
||||
"incExpCd",
|
||||
"stdCost",
|
||||
@@ -175,8 +192,15 @@ export default function ConCostForm({
|
||||
form.setValue("demandCst.prcsFcltCpctCstrnCst", Math.round(result));
|
||||
form.setValue("demandCst.fcltSbsdCstrnCst", Math.round(fcltSbsdCstrnCst));
|
||||
form.setValue("totalConstCost", Math.round(realResult));
|
||||
alert("표준공산비 산정이 완료되었습니다.");
|
||||
}
|
||||
}, [constCostRes, rateFeeRes, usgPrcsProcsWatch, dmndQtyWatch]);
|
||||
|
||||
/**
|
||||
* 저장 버튼 click 시 실행
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
const onSubmit = async (value: DmdCoReqType) => {
|
||||
if (value.demandCst && !value.demandCst.prcsFcltCpctCstrnCst) {
|
||||
alert("공사비 계산 진행 후 저장을 눌러주새요");
|
||||
@@ -188,7 +212,7 @@ export default function ConCostForm({
|
||||
// ? splycd[0]?.ruseUtztnFcltCd
|
||||
// : "11000SW001R1";
|
||||
|
||||
const pk =lastSub?.ruseUtztnFcltCd??null
|
||||
const pk =lastSub?.ruseUtztnFcltCd??null; // 후보지
|
||||
|
||||
if (type === "mat") {
|
||||
if (value.demandCst) {
|
||||
@@ -295,10 +319,17 @@ export default function ConCostForm({
|
||||
}, []);
|
||||
|
||||
|
||||
/**
|
||||
* 후보지 조회
|
||||
* @param e
|
||||
*/
|
||||
const searchCandidate = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (selectionVersion == 0) {
|
||||
alert("먼저 목록에서 수요신청 건을 선택하세요.");
|
||||
return;
|
||||
}
|
||||
const dmdInfoId = form.getValues("dmdInfoId");
|
||||
console.log(dmdInfoId)
|
||||
|
||||
@@ -365,14 +396,17 @@ export default function ConCostForm({
|
||||
name="useUsgCd"
|
||||
render={({ field }) => (
|
||||
<FormUi label="용도">
|
||||
<Input
|
||||
placeholder=""
|
||||
type="text"
|
||||
onChange={(e) => {
|
||||
field.onChange(e.target.value);
|
||||
<SelectBox
|
||||
className="w-full"
|
||||
label=""
|
||||
selectClass={""}
|
||||
selectArray={code.sewageOutFclttUse || []}
|
||||
onSelectValue={(val: string | number) => {
|
||||
if (readonly) return;
|
||||
field.onChange(String(val));
|
||||
}}
|
||||
value={field.value ?? ""}
|
||||
readOnly={true}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</FormUi>
|
||||
)}
|
||||
|
||||
@@ -42,22 +42,24 @@ const FormField = <
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
// 필드명으로 필드의 상태를 가져옴.
|
||||
// errors, isDirty, dirtyFields, touchedFields, isSubmitted, isSubmitSuccessful, isValid, defaultValues, disabled
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formItemId: `${id}-form-item`, // 접근성 속성 연결용
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
@@ -86,6 +88,8 @@ const FormItem = React.forwardRef<
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
// React.forwardRef를 사용해서 부모가 Label DOM ref를 직접 받을 수 있게 하고,
|
||||
// 현재 FormField의 에러 상태와 form item id를 연결해 접근성(htmlFor)과 에러 스타일을 함께 처리한다.
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
|
||||
@@ -206,7 +206,7 @@ GisOlStyle.prototype = {
|
||||
element: overlayContainer,
|
||||
positioning: 'bottom-center',
|
||||
stopEvent: false,
|
||||
offset: overLayOffset ? overLayOffset : [80, 0], // 오프셋 설정 (위치 조정)
|
||||
offset: overLayOffset ? overLayOffset : [65, 0], // 오프셋 설정 (위치 조정)
|
||||
});
|
||||
this.overlays[overLayId] = overlay;
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export type DemandCompType = {
|
||||
crmty: number | "";
|
||||
rmrk: string | "";
|
||||
reqDt: string | "";
|
||||
zip: string | "";
|
||||
};
|
||||
|
||||
type SelectDataType = {
|
||||
@@ -50,9 +51,10 @@ interface FormSchema {
|
||||
sggCd: string;
|
||||
dmdCoNm: string;
|
||||
dmdInfoId: string;
|
||||
zip?: string;
|
||||
daddr: string;
|
||||
telno: string;
|
||||
bznmTotNo?: string;
|
||||
bznmTotNo?: string; // 종사업자등록번호
|
||||
rgtr?: string | "";
|
||||
mdfr?: string | null;
|
||||
dmdAplyPswd?: string;
|
||||
@@ -62,9 +64,9 @@ interface FormSchema {
|
||||
interface SdmRegFormProps {
|
||||
form: UseFormReturn<FormSchema>;
|
||||
onCheckboxChange?: (checked: boolean) => void;
|
||||
selectSidoData: SelectDataType[];
|
||||
selectSigunData: SelectDataType[];
|
||||
dataList: DemandCompType[];
|
||||
selectSidoData: SelectDataType[]; //시/도 데이터
|
||||
selectSigunData: SelectDataType[]; //시/군/구 데이터
|
||||
dataList: DemandCompType[]; //수요처 목록
|
||||
rtnValue: (searchText: string, pageCnt: number) => void;
|
||||
setIsDuplicate: (value: boolean) => void;
|
||||
updateList?: DemandCompType;
|
||||
@@ -97,7 +99,7 @@ export function SdmRegForm({
|
||||
const [orgBznmNo, setOrgBznmNo] = useState<string>("");
|
||||
const [updateCoNm, setUpdateNm] = useState<string>("");
|
||||
const [isZipModalOpen, setIsZipModalOpen] = useState<boolean>(false);
|
||||
const [zipId, setZipId] = useState<number | undefined>(undefined);
|
||||
const [zoneNo, setZoneNo] = useState<string>("");
|
||||
const [address, setAddress] = useState<string | undefined>("");
|
||||
const [detailedAddress, setDetailedAddress] = useState<string>("");
|
||||
|
||||
@@ -219,6 +221,10 @@ export function SdmRegForm({
|
||||
handleCheckboxChange(false);
|
||||
}
|
||||
|
||||
setZoneNo(rtnCompData?.zip || "");
|
||||
form.setValue("zip", rtnCompData?.zip || "");
|
||||
setAddress(rtnCompData?.daddr || "");
|
||||
setDetailedAddress("");
|
||||
form.setValue("daddr", rtnCompData?.daddr || "");
|
||||
form.setValue("telno", rtnCompData?.telno || "");
|
||||
|
||||
@@ -235,6 +241,47 @@ export function SdmRegForm({
|
||||
[form],
|
||||
);
|
||||
|
||||
const applyZipRegion = useCallback(
|
||||
(ctpvNm?: string, sggNm?: string) => {
|
||||
const normalizeRegionName = (value?: string) =>
|
||||
value?.replace(/\s+/g, "").trim() ?? "";
|
||||
|
||||
const sidoName = normalizeRegionName(ctpvNm);
|
||||
const sigunName = normalizeRegionName(sggNm);
|
||||
if (!sidoName) return;
|
||||
|
||||
const matchedSido = selectSidoData.find(
|
||||
(item) => normalizeRegionName(item.text) === sidoName,
|
||||
);
|
||||
if (!matchedSido) return;
|
||||
|
||||
setSidoValue(matchedSido.val);
|
||||
form.setValue("ctpvCd", matchedSido.val, { shouldValidate: true });
|
||||
initializeSigunData(matchedSido.val);
|
||||
|
||||
if (!sigunName) return;
|
||||
|
||||
const sigunCandidates = selectSigunData.filter(
|
||||
(item) => item.val.substring(0, 2) === matchedSido.val,
|
||||
);
|
||||
|
||||
const matchedSigun =
|
||||
sigunCandidates.find(
|
||||
(item) => normalizeRegionName(item.text) === sigunName,
|
||||
) ??
|
||||
sigunCandidates.find((item) => {
|
||||
const itemName = normalizeRegionName(item.text);
|
||||
return itemName.includes(sigunName) || sigunName.includes(itemName);
|
||||
});
|
||||
|
||||
if (!matchedSigun) return;
|
||||
|
||||
setSigunValue(matchedSigun.val);
|
||||
form.setValue("sggCd", matchedSigun.val, { shouldValidate: true });
|
||||
},
|
||||
[form, initializeSigunData, selectSidoData, selectSigunData],
|
||||
);
|
||||
|
||||
const bznNoChange = useCallback(
|
||||
(
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
@@ -275,6 +322,10 @@ export function SdmRegForm({
|
||||
handleCheckboxChange(true);
|
||||
}
|
||||
form.setValue("dmdInfoId", updateList?.dmdInfoId.slice(0, 10));
|
||||
setZoneNo(updateList?.zip || "");
|
||||
form.setValue("zip", updateList?.zip || "");
|
||||
setAddress(updateList?.daddr || "");
|
||||
setDetailedAddress("");
|
||||
setUpdateNm(updateList?.dmdCoNm);
|
||||
// setSigunValue(updateList?.sggCd)
|
||||
} else {
|
||||
@@ -295,6 +346,10 @@ export function SdmRegForm({
|
||||
}
|
||||
}, [address, detailedAddress, form]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("zip", zoneNo, { shouldValidate: true });
|
||||
}, [form, zoneNo]);
|
||||
|
||||
|
||||
const formFields: Array<{
|
||||
name: keyof FormSchema;
|
||||
@@ -427,10 +482,10 @@ export function SdmRegForm({
|
||||
type="text"
|
||||
className="!text-xl text-gray-600 w-64"
|
||||
readOnly={true}
|
||||
value={zipId?.toString() ?? ""}
|
||||
value={form.watch("zip") ?? ""}
|
||||
placeholder="우편번호"
|
||||
/>
|
||||
<Button className="px-4 py-2 rounded-md" onClick={() => setIsZipModalOpen(true)}>우편번호 검색</Button>
|
||||
<Button type="button" className="px-4 py-2 rounded-md" onClick={() => setIsZipModalOpen(true)}>우편번호 검색</Button>
|
||||
</div>
|
||||
{/* 주소 */}
|
||||
<Input
|
||||
@@ -581,9 +636,11 @@ export function SdmRegForm({
|
||||
open={isZipModalOpen}
|
||||
onOpenChange={setIsZipModalOpen}
|
||||
onSelect={(zip) => {
|
||||
setZipId(zip.zipId);
|
||||
setZoneNo(zip.zoneNo || "");
|
||||
setAddress(zip.zipAddr || "");
|
||||
setDetailedAddress("");
|
||||
form.setValue("zip", zip.zoneNo || "", { shouldValidate: true });
|
||||
applyZipRegion(zip.ctpvNm, zip.sggNm);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -32,6 +32,7 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up
|
||||
.min(1, "사업자등록번호를 입력해주세요")
|
||||
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다")
|
||||
.refine(() => isBznmTotNoDisabled ? !isDuplicate : true, "이미 등록된 사업자등록번호입니다"),
|
||||
zip: z.string().optional(),
|
||||
bznmTotNo: isBznmTotNoDisabled
|
||||
? z.string().optional()
|
||||
: z.string()
|
||||
@@ -73,6 +74,7 @@ const defaultValues: FormSchema = {
|
||||
sggCd: '',
|
||||
dmdCoNm: '',
|
||||
dmdInfoId: '',
|
||||
zip: '',
|
||||
bznmTotNo: '',
|
||||
daddr: '',
|
||||
telno: '',
|
||||
@@ -88,13 +90,13 @@ const defaultValues: FormSchema = {
|
||||
* @param selectSigunData 시/군/구 데이터
|
||||
* @param dataList 수요처 목록
|
||||
* @param rtnValue 검색 결과(텍스트) 콜백
|
||||
* @param rtnInputData 수요처 리스트 콜백
|
||||
* @param rtnInputData 수요처관리 콜백 : 등록/수정 처리
|
||||
* @returns
|
||||
*/
|
||||
export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData
|
||||
, bodyClassNm, updateList, readOnly, openNetwork}: thisProps) {
|
||||
|
||||
export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm, updateList,readOnly,openNetwork}: thisProps) {
|
||||
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
|
||||
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false); // 종사업자등록번호 해당없음
|
||||
const [isDuplicate, setIsDuplicate] = useState(false);
|
||||
const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate, updateList );
|
||||
|
||||
@@ -144,7 +146,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList
|
||||
|
||||
|
||||
/* await insertSdmData(formData); */
|
||||
rtnInputData(formData as unknown as DemandCompType);
|
||||
rtnInputData(formData as unknown as DemandCompType); // 수요처 등록/수정 처리
|
||||
|
||||
|
||||
//router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
|
||||
@@ -157,7 +159,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList
|
||||
<>
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-gray-900 underline">
|
||||
{readOnly==true? '수요처 정보':'수요처 등록'}
|
||||
{!updateList? '수요처 등록' :readOnly==true? '수요처 정보':'수요처 수정'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -194,4 +196,4 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList
|
||||
<Toaster position="top-center" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Button, Input, Label, cn } from "@common/components";
|
||||
/**
|
||||
* 주소검색
|
||||
*/
|
||||
import { Button, Input, cn } from "@common/components";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { UiModal } from "../dialog_ui";
|
||||
import { UiTable } from "../table";
|
||||
import type { Zip, ZipSearchParams } from "./zipService";
|
||||
import { findAddress } from "./findAddress";
|
||||
import {
|
||||
fetchPostcodeSuggestions,
|
||||
findAddress,
|
||||
parseZipSearchInput,
|
||||
type Zip,
|
||||
type ZipSearchParams,
|
||||
} from "./zipService";
|
||||
|
||||
interface SearchAddressModalProps {
|
||||
open: boolean;
|
||||
@@ -11,20 +19,35 @@ interface SearchAddressModalProps {
|
||||
onSelect: (zip: Zip) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주소 검색 값 감시
|
||||
* @param value
|
||||
* @param delay
|
||||
* @returns
|
||||
*/
|
||||
function useDebouncedValue<T>(value: T, delay = 250) {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [delay, value]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
|
||||
export function SearchAddressModal({ open, onOpenChange, onSelect }: SearchAddressModalProps) {
|
||||
const PAGE_SIZE = 10;
|
||||
const [curPage, setCurPage] = useState<number>(1);
|
||||
const [searchInput, setSearchInput] = useState<string>("");
|
||||
const debouncedSearchInput = useDebouncedValue(searchInput, 250);
|
||||
const [zipSearchResults, setZipSearchResults] = useState<Zip[]>([]);
|
||||
const [zipTotalElements, setZipTotalElements] = useState<number>(-1);
|
||||
const [searchQuery, setSearchQuery] = useState<ZipSearchParams>({
|
||||
roadNm: "",
|
||||
bldgNoMno: null,
|
||||
bldgNoSno: null,
|
||||
page: 1,
|
||||
size: PAGE_SIZE
|
||||
});
|
||||
const [zipTotalElements, setZipTotalElements] = useState<number>(-1); //주소 검색 결과 건수
|
||||
const [searchMessage, setSearchMessage] = useState<string>("");
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [showSuggestions, setShowSuggestions] = useState<boolean>(true);
|
||||
const [searchQuery, setSearchQuery] = useState<ZipSearchParams | undefined>(undefined);
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
const [isResetSelection] = useState<boolean>(false);
|
||||
const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined);
|
||||
const [selectedZip, setSelectedZip] = useState<Zip>();
|
||||
const [isSelectedZip, setIsSelectedZip] = useState<boolean>(false);
|
||||
@@ -33,15 +56,13 @@ export function SearchAddressModal({ open, onOpenChange, onSelect }: SearchAddre
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCurPage(1);
|
||||
setSearchInput("");
|
||||
setZipSearchResults([]);
|
||||
setZipTotalElements(-1);
|
||||
setSearchQuery({
|
||||
roadNm: "",
|
||||
bldgNoMno: null,
|
||||
bldgNoSno: null,
|
||||
page: 0,
|
||||
size: PAGE_SIZE
|
||||
});
|
||||
setSearchMessage("");
|
||||
setSuggestions([]);
|
||||
setShowSuggestions(true);
|
||||
setSearchQuery(undefined);
|
||||
setSelectedZip(undefined);
|
||||
setIsSelectedZip(false);
|
||||
setSelectedRowIndex(undefined);
|
||||
@@ -50,42 +71,119 @@ export function SearchAddressModal({ open, onOpenChange, onSelect }: SearchAddre
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const keyword = debouncedSearchInput.trim();
|
||||
if (!keyword) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
if (!showSuggestions) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
let alive = true;
|
||||
|
||||
const fetchSuggestions = async () => {
|
||||
try {
|
||||
const nextSuggestions = await fetchPostcodeSuggestions(keyword);
|
||||
if (!alive) return;
|
||||
setSuggestions(nextSuggestions);
|
||||
} catch {
|
||||
if (!alive) return;
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
void fetchSuggestions();
|
||||
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [debouncedSearchInput, open, showSuggestions]);
|
||||
|
||||
// Pagination Effect
|
||||
useEffect(() => {
|
||||
if (!open || zipTotalElements === -1) return;
|
||||
if (!open || zipTotalElements === -1 || !searchQuery) return; // 주소 검색 결과 건수 초기값이 -1일 때는 아직 검색 전 상태
|
||||
|
||||
// Correctly update query for pagination without mutation
|
||||
const nextPage = curPage - 1;
|
||||
if (searchQuery.page !== nextPage) {
|
||||
const newQuery = { ...searchQuery, page: nextPage };
|
||||
setSearchQuery(newQuery);
|
||||
const newQuery = { ...searchQuery, page: nextPage };
|
||||
setSearchQuery(newQuery);
|
||||
setSelectedZip(undefined);
|
||||
setIsSelectedZip(false);
|
||||
setSelectedRowIndex(undefined);
|
||||
|
||||
const fetchAddress = async () => {
|
||||
const fetchAddressByPage = async () => {
|
||||
try {
|
||||
const result = await findAddress(newQuery);
|
||||
setZipSearchResults(result?.content || []);
|
||||
};
|
||||
fetchAddress();
|
||||
setSearchMessage("");
|
||||
setZipSearchResults(result?.content || []); // 주소검색결과
|
||||
} catch {
|
||||
setZipSearchResults([]);
|
||||
setSearchMessage("주소 검색 중 오류가 발생했습니다.");
|
||||
}
|
||||
};
|
||||
void fetchAddressByPage();
|
||||
}
|
||||
}, [curPage, open, zipTotalElements]);
|
||||
}, [curPage, open, searchQuery, zipTotalElements]);
|
||||
|
||||
const handleCellClick = (index: any) => {
|
||||
setSelectedZip(zipSearchResults[index]);
|
||||
const handleCellClick = (index: number | undefined) => {
|
||||
if (index === undefined) return;
|
||||
const zip = zipSearchResults[index];
|
||||
if (!zip) return;
|
||||
setSelectedZip(zip);
|
||||
setIsSelectedZip(true);
|
||||
setSelectedRowIndex(index);
|
||||
onSelect(zip);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleSearch = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // 폼 제출 동작을 막음.
|
||||
e.stopPropagation(); // 이벤트 버블링을 막음. 이벤트가 부모 컴포넌트나 바깥쪽 핸들러로 올라가지 않도록 막음.
|
||||
setShowSuggestions(false);
|
||||
setSuggestions([]);
|
||||
|
||||
const query = { ...searchQuery, page: 0 };
|
||||
const parsed = parseZipSearchInput(searchInput, 1);
|
||||
if (!parsed.ok) {
|
||||
setSearchMessage(parsed.message);
|
||||
setZipSearchResults([]);
|
||||
setZipTotalElements(-1);
|
||||
setSelectedZip(undefined);
|
||||
setIsSelectedZip(false);
|
||||
setSelectedRowIndex(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const query: ZipSearchParams = {
|
||||
roadNm: parsed.value.roadNm,
|
||||
bldgNoMno: parsed.value.bldgNoMno,
|
||||
bldgNoSno: parsed.value.bldgNoSno,
|
||||
page: parsed.value.page - 1,
|
||||
size: PAGE_SIZE,
|
||||
};
|
||||
|
||||
setSearchMessage("");
|
||||
setSearchQuery(query);
|
||||
setCurPage(1);
|
||||
setIsResetPage(true);
|
||||
setSelectedZip(undefined);
|
||||
setIsSelectedZip(false);
|
||||
setSelectedRowIndex(undefined);
|
||||
|
||||
const result = await findAddress(query);
|
||||
setZipSearchResults(result?.content || []);
|
||||
setZipTotalElements(result?.totalElements || 0);
|
||||
try {
|
||||
const result = await findAddress(query);
|
||||
setZipSearchResults(result?.content || []);
|
||||
setZipTotalElements(result?.totalElements ?? 0); //주소 검색 결과 건수
|
||||
} catch {
|
||||
setZipSearchResults([]);
|
||||
setZipTotalElements(-1);
|
||||
setSearchMessage("주소 검색 중 오류가 발생했습니다.");
|
||||
}
|
||||
|
||||
setTimeout(() => setIsResetPage(false), 100);
|
||||
};
|
||||
@@ -97,6 +195,8 @@ export function SearchAddressModal({ open, onOpenChange, onSelect }: SearchAddre
|
||||
}
|
||||
};
|
||||
|
||||
const showSuggest = open && showSuggestions && searchInput.trim().length >= 1 && suggestions.length > 0;
|
||||
|
||||
return (
|
||||
<UiModal
|
||||
headerTitle="주소 검색"
|
||||
@@ -125,64 +225,44 @@ export function SearchAddressModal({ open, onOpenChange, onSelect }: SearchAddre
|
||||
onSubmit={handleSearch}
|
||||
className="mb-4 space-y-2"
|
||||
>
|
||||
<div className="card card-border !m-0 flex flex-col gap-4 p-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex flex-col">
|
||||
<Label className="mb-1 font-bold text-base">도로명</Label>
|
||||
<Input
|
||||
value={searchQuery.roadNm || ""}
|
||||
onChange={(e) =>
|
||||
setSearchQuery({
|
||||
...searchQuery,
|
||||
roadNm: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="예: 테헤란로"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Label className="mb-1 text-base">건물본번</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={searchQuery.bldgNoMno ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: null;
|
||||
setSearchQuery({
|
||||
...searchQuery,
|
||||
bldgNoMno: value,
|
||||
});
|
||||
}}
|
||||
placeholder="예: 123"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Label className="mb-1 text-base">건물부번</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={searchQuery.bldgNoSno ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: null;
|
||||
setSearchQuery({
|
||||
...searchQuery,
|
||||
bldgNoSno: value,
|
||||
});
|
||||
}}
|
||||
placeholder="예: 45"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
<div className="card card-border !m-0 flex flex-col gap-3 p-4">
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Input
|
||||
value={searchInput}
|
||||
onChange={(e) => {
|
||||
setSearchInput(e.target.value);
|
||||
setShowSuggestions(true);
|
||||
}}
|
||||
placeholder="예) 테헤란로 152-1"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
{showSuggest && (
|
||||
<div className="absolute left-0 right-0 top-[calc(100%+6px)] z-50 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg">
|
||||
{suggestions.map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between px-4 py-3 text-left text-sm hover:bg-gray-50"
|
||||
onClick={() => {
|
||||
setSearchInput(suggestion);
|
||||
setShowSuggestions(false);
|
||||
setSuggestions([]);
|
||||
}}
|
||||
>
|
||||
<span className="truncate text-gray-800">{suggestion}</span>
|
||||
<span className="ml-4 text-gray-400">↵</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button className="rounded-md shrink-0" type="submit">검색</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="rounded-md" type="submit">검색</Button>
|
||||
<div className="text-sm text-gray-600">
|
||||
{searchMessage || "도로명과 건물번호를 함께 입력해 주세요. 예) 테헤란로 152-1"}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,7 +277,6 @@ export function SearchAddressModal({ open, onOpenChange, onSelect }: SearchAddre
|
||||
cellClick={handleCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
index={selectedRowIndex}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
|
||||
// const Key = 'U01TX0FVVEgyMDI1MDgyODE0MTAxNzExNjEyMjA'
|
||||
// const addresssKey = 'devU01TX0FVVEgyMDI1MDgyNTEwMzkwNDExNjEwNTM='; //인증키 만료일은 2025-11-23
|
||||
|
||||
import type { Zip, ZipSearchParams } from "./zipService";
|
||||
|
||||
/**
|
||||
* 주소 API 응답 타입
|
||||
* https://business.juso.go.kr/addrlink/main.do 해당 사이트 참조
|
||||
*/
|
||||
export type AddressApiResponse = {
|
||||
results: {
|
||||
common: {
|
||||
errorMessage: string;
|
||||
countPerPage: string;
|
||||
totalCount: string;
|
||||
errorCode: string;
|
||||
currentPage: string;
|
||||
};
|
||||
juso: addressType[];
|
||||
};
|
||||
}
|
||||
|
||||
export type addressType = {
|
||||
detBdNmList: string;
|
||||
engAddr: string;
|
||||
rn: string;
|
||||
emdNm: string;
|
||||
zipNo: string;
|
||||
roadAddrPart2: string;
|
||||
emdNo: string;
|
||||
sggNm: string;
|
||||
jibunAddr: string;
|
||||
siNm: string;
|
||||
roadAddrPart1: string;
|
||||
bdNm: string;
|
||||
admCd: string;
|
||||
udrtYn: string;
|
||||
lnbrMnnm: string;
|
||||
roadAddr: string;
|
||||
lnbrSlno: string;
|
||||
buldMnnm: string;
|
||||
bdKdcd: string;
|
||||
liNm: string;
|
||||
rnMgtSn: string;
|
||||
mtYn: string;
|
||||
bdMgtSn: string;
|
||||
buldSlno: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 주소에 따른 검색
|
||||
* @param search : 검색어
|
||||
* @returns addressType[] 주소 배열
|
||||
*/
|
||||
export const findAddress = async (searchQuery: ZipSearchParams): Promise<any> => {
|
||||
if (!searchQuery) return [];
|
||||
|
||||
// 쿼리 파라미터 생성
|
||||
const params = new URLSearchParams();
|
||||
if (searchQuery.roadNm) params.append('roadNm', searchQuery.roadNm);
|
||||
if (searchQuery.bldgNoMno != null) params.append('bldgNoMno', searchQuery.bldgNoMno.toString());
|
||||
if (searchQuery.bldgNoSno != null) params.append('bldgNoSno', searchQuery.bldgNoSno.toString());
|
||||
if (searchQuery.page != null) params.append('page', searchQuery.page.toString());
|
||||
if (searchQuery.size != null) params.append('size', searchQuery.size.toString());
|
||||
|
||||
const url = `/cms-service/api/v1/zips?${params.toString()}`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const data = await response.json();
|
||||
if (data) return data
|
||||
else return [];
|
||||
} catch (error) {
|
||||
console.error('Error fetching address data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const searchFilter = (search: string) => {
|
||||
|
||||
|
||||
if (search.length > 0) {
|
||||
//특수문자 제거
|
||||
const expText = /[%=><]/;
|
||||
if (expText.test(search) == true) {
|
||||
alert("특수문자를 입력 할수 없습니다.");
|
||||
search = search.split(expText).join("");
|
||||
return false;
|
||||
}
|
||||
|
||||
//특정문자열(sql예약어의 앞뒤공백포함) 제거
|
||||
const sqlArray = [
|
||||
//sql 예약어
|
||||
"OR", "SELECT", "INSERT", "DELETE", "UPDATE", "CREATE", "DROP", "EXEC",
|
||||
"UNION", "FETCH", "DECLARE", "TRUNCATE"
|
||||
]
|
||||
|
||||
let regex;
|
||||
for (let i = 0; i < sqlArray.length; i++) {
|
||||
regex = new RegExp(sqlArray[i] as string, "gi");
|
||||
|
||||
if (regex.test(search)) {
|
||||
alert("\"" + sqlArray[i] + "\"와(과) 같은 특정문자로 검색할 수 없습니다.");
|
||||
search = search.replace(regex, "");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from "./SearchAddressModal";
|
||||
export * from "./findAddress";
|
||||
export * from "./zipService";
|
||||
|
||||
@@ -64,3 +64,275 @@ export interface Zip {
|
||||
}
|
||||
|
||||
export type ZipList = PageableList<Zip>;
|
||||
|
||||
export interface ParsedZipSearchParams {
|
||||
roadNm: string;
|
||||
bldgNoMno: number;
|
||||
bldgNoSno: number | null;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export type ParseZipSearchInputResult =
|
||||
| { ok: true; value: ParsedZipSearchParams; keyword: string }
|
||||
| { ok: false; message: string };
|
||||
|
||||
type SuggestionSeed = {
|
||||
roadNm: string;
|
||||
bldgNoMno: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 주소 검색 API를 호출해 우편번호/주소 목록을 조회한다.
|
||||
* @param searchQuery 조회 조건 객체
|
||||
* @param searchQuery.roadNm 검색할 도로명
|
||||
* @param searchQuery.bldgNoMno 건물 본번
|
||||
* @param searchQuery.bldgNoSno 건물 부번
|
||||
* @param searchQuery.page 0부터 시작하는 페이지 번호
|
||||
* @param searchQuery.size 페이지당 조회 건수
|
||||
* @returns 검색 결과 목록과 전체 건수를 포함한 페이징 응답
|
||||
*/
|
||||
export async function findAddress(searchQuery: ZipSearchParams): Promise<ZipList | undefined> {
|
||||
if (!searchQuery) return undefined;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (searchQuery.roadNm) params.append("roadNm", searchQuery.roadNm);
|
||||
if (searchQuery.bldgNoMno != null) params.append("bldgNoMno", searchQuery.bldgNoMno.toString());
|
||||
if (searchQuery.bldgNoSno != null) params.append("bldgNoSno", searchQuery.bldgNoSno.toString());
|
||||
if (searchQuery.page != null) params.append("page", searchQuery.page.toString());
|
||||
if (searchQuery.size != null) params.append("size", searchQuery.size.toString());
|
||||
|
||||
const url = `/cms-service/api/v1/zips?${params.toString()}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error("Network response was not ok");
|
||||
|
||||
return (await response.json()) as ZipList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 문자열에서 자동완성 조회용 도로명만 추출한다.
|
||||
* 마지막 토큰이 숫자 또는 숫자-숫자 형식이면 건물번호로 보고 제외한다.
|
||||
* @param input 사용자가 입력창에 입력한 원본 문자열
|
||||
* @returns 자동완성 조회에 사용할 도로명
|
||||
*/
|
||||
export function extractRoadNameForSuggestion(input: string): string {
|
||||
const normalized = input.replace(/\s+/g, " ").trim();
|
||||
if (!normalized) return "";
|
||||
|
||||
const parts = normalized.split(" ");
|
||||
const tail = parts[parts.length - 1] ?? "";
|
||||
|
||||
if (/^\d+(?:-\d+)?$/.test(tail) && parts.length > 1) {
|
||||
return parts.slice(0, -1).join(" ").trim();
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색어에 허용되지 않는 특수문자나 SQL 예약어가 포함되어 있는지 검사한다.
|
||||
* @param search 사용자가 입력한 검색 문자열
|
||||
* @returns 검색 가능하면 true, 차단 대상이면 false
|
||||
*/
|
||||
export function searchFilter(search: string): boolean {
|
||||
if (search.length === 0) return true;
|
||||
|
||||
const expText = /[%=><]/;
|
||||
if (expText.test(search)) {
|
||||
alert("특수문자를 입력 할수 없습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const sqlArray = [
|
||||
"OR",
|
||||
"SELECT",
|
||||
"INSERT",
|
||||
"DELETE",
|
||||
"UPDATE",
|
||||
"CREATE",
|
||||
"DROP",
|
||||
"EXEC",
|
||||
"UNION",
|
||||
"FETCH",
|
||||
"DECLARE",
|
||||
"TRUNCATE",
|
||||
];
|
||||
|
||||
for (const sqlWord of sqlArray) {
|
||||
const regex = new RegExp(sqlWord, "gi");
|
||||
if (regex.test(search)) {
|
||||
alert(`"${sqlWord}"와(과) 같은 특정문자로 검색할 수 없습니다.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 자동완성 후보 생성을 위해 주소 항목에서 도로명과 건물 본번을 추출한다.
|
||||
* 우선 응답 필드를 사용하고, 값이 없으면 표시 주소 문자열을 파싱해 보정한다.
|
||||
* @param item 주소 검색 결과 1건
|
||||
* @returns 자동완성 생성에 사용할 도로명/건물본번 또는 추출 실패 시 null
|
||||
*/
|
||||
export function extractSuggestionSeed(item: Zip): SuggestionSeed | null {
|
||||
const mnoByField = Number(item.bldgNoMno);
|
||||
const roadNmByField = (item.roadNm ?? "").trim();
|
||||
if (roadNmByField && Number.isInteger(mnoByField) && mnoByField > 0) {
|
||||
return { roadNm: roadNmByField, bldgNoMno: mnoByField };
|
||||
}
|
||||
|
||||
const firstSegment = (item.zipAddr ?? "").split(",")[0]?.trim() ?? "";
|
||||
if (!firstSegment) return null;
|
||||
|
||||
const parts = firstSegment.split(/\s+/);
|
||||
const bldgToken = parts[parts.length - 1] ?? "";
|
||||
const matched = bldgToken.match(/^(\d+)(?:-\d+)?$/);
|
||||
if (!matched) return null;
|
||||
|
||||
const bldgNoMno = Number(matched[1]);
|
||||
const roadNm = parts.slice(0, -1).join(" ").trim();
|
||||
if (!roadNm || !Number.isInteger(bldgNoMno) || bldgNoMno <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { roadNm, bldgNoMno };
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 입력 주소 문자열을 "도로명 + 건물번호" 형식으로 파싱해 API 조회 파라미터로 변환한다.
|
||||
* 예) "테헤란로 152-1" -> { roadNm: "테헤란로", bldgNoMno: 152, bldgNoSno: 1, page }
|
||||
* @param input 사용자가 입력한 주소 검색어
|
||||
* @param page 1부터 시작하는 화면 페이지 번호
|
||||
* @returns 파싱 성공 시 조회 파라미터와 정규화된 검색어, 실패 시 오류 메시지
|
||||
*/
|
||||
export function parseZipSearchInput(
|
||||
input: string,
|
||||
page = 1,
|
||||
): ParseZipSearchInputResult {
|
||||
const normalized = input.replace(/\s+/g, " ").trim();
|
||||
|
||||
if (!normalized) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "도로명과 건물번호를 입력해 주세요. 예) 테헤란로 152-1",
|
||||
};
|
||||
}
|
||||
|
||||
if (!searchFilter(normalized)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "검색어를 다시 확인해 주세요.",
|
||||
};
|
||||
}
|
||||
|
||||
const parts = normalized.split(" ");
|
||||
if (parts.length < 2) {
|
||||
if (/^\d+(?:-\d+)?$/.test(normalized)) {
|
||||
return { ok: false, message: "도로명을 입력해 주세요." };
|
||||
}
|
||||
if (/[가-힣]/.test(normalized) && !/\d/.test(normalized)) {
|
||||
return { ok: false, message: "건물번호를 입력해 주세요." };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
message: "도로명과 건물번호를 공백으로 구분해 주세요. 예) 테헤란로 152-1",
|
||||
};
|
||||
}
|
||||
|
||||
const bldgToken = parts[parts.length - 1] ?? "";
|
||||
const roadNm = parts.slice(0, -1).join(" ").trim();
|
||||
|
||||
if (!roadNm || !/[가-힣]/.test(roadNm)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "도로명을 입력해 주세요.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!/\d/.test(bldgToken)) {
|
||||
return { ok: false, message: "건물번호를 입력해 주세요." };
|
||||
}
|
||||
|
||||
if (!/^\d+(?:-\d+)?$/.test(bldgToken)) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "건물번호는 숫자 또는 숫자-숫자 형식으로 입력해 주세요. 예) 152 또는 152-1",
|
||||
};
|
||||
}
|
||||
|
||||
const [mnoStr, snoStr] = bldgToken.split("-");
|
||||
const bldgNoMno = Number(mnoStr);
|
||||
const bldgNoSno = snoStr ? Number(snoStr) : null;
|
||||
|
||||
if (!Number.isInteger(bldgNoMno) || bldgNoMno <= 0) {
|
||||
return { ok: false, message: "건물본번은 1 이상의 숫자로 입력해 주세요." };
|
||||
}
|
||||
|
||||
if (bldgNoSno !== null && (!Number.isInteger(bldgNoSno) || bldgNoSno <= 0)) {
|
||||
return { ok: false, message: "건물부번은 1 이상의 숫자로 입력해 주세요." };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
roadNm,
|
||||
bldgNoMno,
|
||||
bldgNoSno,
|
||||
page,
|
||||
},
|
||||
keyword: `${roadNm} ${bldgNoMno}${bldgNoSno ? `-${bldgNoSno}` : ""}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 도로명 주소 문자열에서 재검색에 사용할 "도로명 건물번호" 입력값만 추출한다.
|
||||
* 예) "테헤란로 152, OO빌딩" -> "테헤란로 152"
|
||||
* @param roadAddr 화면에 표시된 도로명 주소 문자열
|
||||
* @returns 입력란에 다시 넣을 수 있는 문자열, 추출 실패 시 빈 문자열
|
||||
*/
|
||||
export function extractSearchInputFromRoadAddr(roadAddr: string): string {
|
||||
const firstSegment = roadAddr.split(",")[0]?.trim() ?? "";
|
||||
if (!firstSegment) return "";
|
||||
|
||||
const parts = firstSegment.split(/\s+/);
|
||||
const bldgToken = parts[parts.length - 1] ?? "";
|
||||
if (!/^\d+(?:-\d+)?$/.test(bldgToken)) return "";
|
||||
|
||||
return `${parts.slice(0, -1).join(" ")} ${bldgToken}`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자가 입력 중인 주소 문자열을 기준으로 도로명 후보를 조회한다.
|
||||
* 건물번호가 함께 입력된 경우에는 마지막 숫자 토큰을 제외한 도로명만으로 후보를 찾는다.
|
||||
* @param input 사용자가 입력창에 입력한 원본 문자열
|
||||
* @returns 최대 5개의 "도로명 건물본번" 자동완성 문자열 목록
|
||||
*/
|
||||
export async function fetchPostcodeSuggestions(input: string): Promise<string[]> {
|
||||
const roadNm = extractRoadNameForSuggestion(input);
|
||||
if (!roadNm) return [];
|
||||
if (!searchFilter(roadNm)) return [];
|
||||
|
||||
const data = await findAddress({
|
||||
roadNm,
|
||||
page: 0,
|
||||
size: 50,
|
||||
});
|
||||
|
||||
const list = Array.isArray(data?.content) ? data.content : [];
|
||||
const suggestions: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const item of list) {
|
||||
const seed = extractSuggestionSeed(item);
|
||||
if (!seed) continue;
|
||||
|
||||
const label = `${seed.roadNm} ${seed.bldgNoMno}`;
|
||||
if (seen.has(label)) continue;
|
||||
|
||||
seen.add(label);
|
||||
suggestions.push(label);
|
||||
if (suggestions.length >= 5) break;
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user