This commit is contained in:
2025-08-10 18:28:15 +09:00
111 changed files with 2692 additions and 11820 deletions
+2 -1
View File
@@ -37,4 +37,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
.env*
.env*
__ENV__.js
+44
View File
@@ -0,0 +1,44 @@
import { findUp } from "find-up";
import { config } from "dotenv";
import fs from "fs";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
async function parseDotenv(nodeEnv) {
const envFilePath = await findUp(`.env.${nodeEnv}`);
const parsedEnv = config({ path: envFilePath }).parsed || {};
return parsedEnv;
}
function writeEnv(parsedEnv) {
const scriptFilePath = `${fs.realpathSync(process.cwd())}/public/__ENV__.js`;
fs.writeFileSync(
scriptFilePath,
`window.__ENV__ = ${JSON.stringify(parsedEnv)}`,
);
}
yargs(hideBin(process.argv))
.command(
"next-env",
"Create Next.js runtime environment js",
function builder(y) {
return y.option("env", {
alias: "e",
type: "string",
description: "Environment name(ex: development, production)",
});
},
async function handler(args) {
const nodeEnv = args.e || args.env || "development";
const parsedEnv = await parseDotenv(nodeEnv); // dotenv 파싱
writeEnv(parsedEnv); // 환경 변수 스크립트 파일 생성
return parsedEnv;
},
)
.parse();
+6 -2
View File
@@ -1,11 +1,12 @@
{
"name": "features",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"dev": "next dev -p 13000",
"dev": "node cli.js next-env -e ${NODE_ENV:-development} && next dev -p 13000",
"build": "next build",
"start": "next start -p 13000",
"start": "node cli.js next-env -e ${NODE_ENV:-production} && next start -p 13000",
"lint": "next lint"
},
"dependencies": {
@@ -27,6 +28,8 @@
"axios": "^1.8.1",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"dotenv": "^17.2.1",
"find-up": "^7.0.0",
"js-cookie": "^3.0.5",
"next": "15.3.0",
"next-themes": "^0.4.6",
@@ -44,6 +47,7 @@
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.13.1",
"@types/webpack": "^5.28.5",
"@types/yargs": "^17.0.33",
"eslint": "^9",
"eslint-config-next": "15.1.7",
"postcss": "^8",
+4
View File
@@ -4,12 +4,16 @@ import "@fontsource-variable/noto-sans-kr";
import { AppProvider } from "./providers";
import { cookies } from "next/headers";
import Script from "next/script";
const App = async ({ children }: Readonly<{ children: React.ReactNode }>) => {
const theme = (await cookies()).get("site-theme-key");
return (
<html lang="ko" data-theme={theme?.value}>
<head>
<Script src="/__ENV__.js" strategy="beforeInteractive" />
</head>
<body>
<AppProvider>{children}</AppProvider>
</body>
@@ -1,6 +1,10 @@
import { PROXY_HOST } from "./env";
"use client";
export const BASE_URL = `${PROXY_HOST}/server`;
import { getCookie, getEnv } from "../lib";
const SITE_ID = getCookie("site-id-key") ?? "";
const PROXY_HOST_KEY = `NEXT_PUBLIC_PROXY_HOST_${SITE_ID}`;
export const BASE_URL = `${getEnv(PROXY_HOST_KEY)}/server`;
export const ENDPOINT = {
ATTACH_SERVICE: {
@@ -0,0 +1,14 @@
declare global {
interface Window {
__ENV__?: Record<string, string>;
}
}
export function getEnvConfig(): Record<string, string> {
if (typeof window === "undefined") return {};
return window.__ENV__ ?? {};
}
export function getEnv(key: string): string | undefined {
return getEnvConfig()[key];
}
@@ -3,3 +3,4 @@ export { useAlertStore } from "./use-alert-store";
export { useMenuStore } from "./use-menu-store";
export { useUserStore } from "./use-user-store";
export { usePrompt } from "./use-prompt";
export { getEnv } from "./env";
@@ -16,6 +16,7 @@ export interface Authorizations {
httpMethodCode: string;
sortSeq: number;
useYn: string;
siteId: string;
mdfr: string;
mdfcnDt: string;
}
@@ -27,6 +28,7 @@ export interface RoleAuthorizations {
urlPatternValue: string;
httpMethodCode: string;
sortSeq: number;
siteId: string;
createdAt: string;
}
@@ -40,6 +42,7 @@ export const authorizationFormSchema = z.object({
httpMethodCode: z.string(),
sortSeq: z.number(),
useYn: z.string(),
siteId: z.string(),
});
export type AuthorizationsList = PageableList<Authorizations>;
@@ -161,12 +161,7 @@ export const LinkTableColumnsSchema = z.object({
}, {
message: '최대 14자리까지 입력 가능합니다.',
}),
col_seq: z.coerce.number().nullable().optional().refine((val) => {
if (val == null) return true;
return val > 0;
}, {
message: "컬럼 매칭 순서는 0 이상이어야 합니다."
}),
col_seq: z.coerce.number().nullable().optional(),
mdfcn_dt: z.string().nullable().optional(),
mdfr_id: z.string().nullable().optional(),
});
@@ -11,6 +11,7 @@ export interface WithRoleIdRoleAuthorizationsParams extends PageableParams {
keywordType?: string;
keyword?: string;
roleId?: string;
siteId?: string;
}
export interface RoleAuthorizations {
+25 -4
View File
@@ -13,21 +13,22 @@ import {
TableRow,
} from "@common/components";
import { FormWrapper } from "@/widgets/form-wrapper";
import { CodeSelect } from "@/entities/code";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
import type {
AuthorizationFormData,
WithRoleAuthorizations,
} from "@/entities/authorization";
import { CodeSelect } from "@/entities/code";
import { SiteSelect } from "@/entities/site";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
type BannerFormProps = {
type ModuleFormProps = {
form: UseFormReturn<AuthorizationFormData>;
authorizationNo?: number;
handleSave: (data: AuthorizationFormData) => Promise<WithRoleAuthorizations>;
handleDelete?: () => Promise<void>;
};
function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
function ModuleForm({ form, authorizationNo, ...props }: ModuleFormProps) {
return (
<FormWrapper form={form} isModifyMode={!!authorizationNo} {...props}>
<div className="card">
@@ -99,6 +100,26 @@ function ModuleForm({ form, authorizationNo, ...props }: BannerFormProps) {
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead data-required>사이트</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="siteId"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormControl>
<SiteSelect
{...field}
onValueChange={field.onChange}
className="w-[20rem]"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead data-required>정렬순서</TableHead>
<TableCell className="border">
@@ -40,9 +40,12 @@ function MappingRoleDialog({
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
const inputRef = useRef<HTMLInputElement>(null);
const [keywordType, setKeywordType] = useState("");
const [siteId, setSiteId] = useState("");
const [searchParams, setSearchParams] = useState({
keyword: "",
keywordType: "authorizationName",
keywordType: "",
siteId: "",
});
const query = useQuery({
@@ -85,18 +88,20 @@ function MappingRoleDialog({
const formData = new FormData(e.target as HTMLFormElement);
const keyword = formData.get("keyword") as string;
setSearchParams({
...searchParams,
keyword,
keywordType,
siteId: siteId === "ALL" ? "" : siteId,
});
};
const handleReset = () => {
if (inputRef.current) {
inputRef.current.value = "";
}
if (inputRef.current) inputRef.current.value = "";
setKeywordType("");
setSiteId("");
setSearchParams({
...searchParams,
keyword: "",
keywordType: "",
siteId: "",
});
};
@@ -119,14 +124,22 @@ function MappingRoleDialog({
</DialogHeader>
<div className="flex items-center justify-between">
<form onSubmit={handleSubmit} className="flex items-center gap-2">
<SiteSelect enabledAllSite />
<Select>
<SiteSelect
value={siteId}
onValueChange={(value) => setSiteId(value)}
enabledAllSite
/>
<Select
value={keywordType}
onValueChange={(value) => setKeywordType(value)}
>
<SelectTrigger className="w-[12rem]">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="authorizationName">API명</SelectItem>
<SelectItem value="urlPatternValue">API URL</SelectItem>
<SelectItem value="urlPatternValue">URL주소</SelectItem>
<SelectItem value="httpMethodCode">전송방식</SelectItem>
</SelectContent>
</Select>
<Input
+1
View File
@@ -11,6 +11,7 @@ NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
# NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
+1
View File
@@ -14,6 +14,7 @@ NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_CS_TUS_UPLOAD = /attach-service/api/v1/tus/upload
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api # 박이정
+1
View File
@@ -12,6 +12,7 @@ NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_REUSE_API_URL=http://con.mind-one.co.kr:11080/api/v1
NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
+1 -1
View File
@@ -40,7 +40,7 @@ const getRuseFclt = async (
searchPk: item.bscFctNm,
};
})
.filter((hasu) => ruseList.find((ruse) => hasu.bscFctCd === ruse));
.filter((hasu:any) => ruseList.find((ruse:any) => hasu.bscFctCd === ruse));
return { data: data, count: res.count };
});
+1 -1
View File
@@ -11,7 +11,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
+1 -1
View File
@@ -11,7 +11,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
+1
View File
@@ -24,6 +24,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
+1 -2
View File
@@ -1,5 +1,4 @@
"use client"
import Base from "./base";
import "./globals.css";
import { StoreProvider } from "./store";
@@ -12,7 +11,7 @@ export default function RootLayout({
<html lang="en">
<body>
<StoreProvider>
<Base>{children}</Base>
{children}
</StoreProvider>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
'use client'
import { NoFooterLayout } from "@common/components";
import type { MenuItem } from "../../store/common";
import { useFoStore } from "../../store";
import { usePathname, useRouter } from "next/navigation";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const { getMenuAuth } = useFoStore((state) => state);
const pathname = usePathname();
const router = useRouter();
const handleClick = (id: MenuItem) => {
getMenuAuth(id.menuCd);
router.push(id.urlPath);
};
return (
<NoFooterLayout currentPath={pathname ?? ""} handleMenuClick={handleClick}>{children}</NoFooterLayout>
);
}
+303
View File
@@ -0,0 +1,303 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { useEffect, useRef, useState } from "react";
import { ProcSearchBar } from "@common/business_components/ui/map/searchBar";
// OpenLayers overlay/extent 유틸
import Overlay from "ol/Overlay";
import { getCenter as getExtentCenter } from "ol/extent";
import type { Coordinate } from "ol/coordinate";
interface FeatureData {
fid?: string;
ftrIdn?: string;
id?: string;
getId?: () => string | number | undefined; // 추가
// OpenLayers Feature 비슷한 최소 인터페이스
get?: (key: string) => any;
getGeometry?: () => {
getExtent: () => number[];
getType?: () => string;
getCoordinates?: () => Coordinate;
};
setStyle?: () => void;
}
interface MapInstance {
addOverlay?: (ov: Overlay) => void;
removeOverlay?: (ov: Overlay) => void;
getOverlays?: () => { getArray: () => Overlay[] };
getLayers: () => {
getArray: () => Array<{
get: (key: string) => any;
getSource?: () => { getFeatureById?: (id: string) => FeatureData | null };
}>;
};
getView: () => { fit: (extent: number[], options: object) => void };
}
const Map = () => {
const [treeData, setTreeData] = useState([]);
const gisMaps = useRef<MapInstance | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false);
const [featureData, setFeatureData] = useState<FeatureData | null>(null);
// === Overlay 관리용 Ref ===
const overlayRef = useRef<Overlay | null>(null);
const overlayContainerRef = useRef<HTMLDivElement | null>(null);
const facilityTypes = [
{ text: "하수처리시설", val: "tb_il_swage_prcs_fclt" },
];
const fetchData = async () => {
try {
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`;
const reqUrl = `${API_URL}/lyrStngs/FO_NETWORK`;
const response = await fetch(reqUrl, {
method: "GET",
headers: { accept: "application/json" },
});
if (!response.ok) throw new Error(`HTTP 오류! 상태: ${response.status}`);
const data = await response.json();
setTreeData(data);
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
useEffect(() => {
fetchData();
}, []);
// ===== 오버레이 콘텐츠 빌더 =====
const buildOverlayContent = (layerType: string, feature: FeatureData) => {
const props: Record<string, any> = {};
// 안전하게 몇 개 필드 추출(없으면 건너뜀)
const mayGet = (k: string) => (feature.get ? feature.get(k) : undefined);
const safe = (v: any) => (v === undefined || v === null ? "-" : String(v));
console.log(feature);
// 레이어 유형별 대표 필드 추출(없으면 "-")
if (layerType === "tb_il_swage_prcs_fclt") {
// 하수처리시설
props["하수처리시설명"] = mayGet("swtrmt_fclt_nm");
props["시설용량(m³/d)"] = mayGet("fclt_cpct");
props["주소"] = mayGet("rdnm_addr");
props["가동개시일"] = mayGet("oprtng_strt_ymd");
props["공법"] = mayGet("rprs_msrc_cn");
} else if (layerType === "tb_ms_swage_prcs_zone") {
// 처리구역(존)
props["하수처리구역명"] = mayGet("swtrmt_zone_nm");
props["하수처리시설명"] = mayGet("swtrmt_fclt_nm");
} else if (layerType === "om_wqlt_station") {
// 수질관측소
props["관측소명"] = mayGet("stn_ko") ?? mayGet("name");
props["주소"] = mayGet("addr") ?? mayGet("indicator");
}
const titleMap: Record<string, string> = {
tb_il_swage_prcs_fclt: "하수처리시설",
tb_ms_swage_prcs_zone: "하수처리구역",
om_wqlt_station: "관측소",
};
const title = titleMap[layerType] || "정보";
// 컨테이너 재사용(없으면 생성)
if (!overlayContainerRef.current) {
overlayContainerRef.current = document.createElement("div");
}
const el = overlayContainerRef.current;
el.className =
"ol-custom-overlay shadow-xl rounded-2xl bg-white/95 backdrop-blur p-3 border border-gray-200 min-w-[220px] max-w-[350px] ";
el.innerHTML = `
<div class="font-semibold text-lg text-gray-800 mb-2 text-center bg-green-400">${title}</div>
<table class="w-full text-base text-gray-800">
${Object.entries(props)
.map(
([k, v]) => `
<tr>
<td class="py-1 pr-2 text-gray-500 whitespace-nowrap">${k}</td>
<td class="py-1 font-medium">${safe(v)}</td>
</tr>`
)
.join("")}
</table>
`;
return el;
};
// ===== 오버레이 생성/표시 =====
const showOverlay = (
map: MapInstance,
feature: FeatureData,
layerType: string
) => {
// 이전 오버레이 제거
if (overlayRef.current) {
map.removeOverlay?.(overlayRef.current);
overlayRef.current = null;
}
// 콘텐츠 만들기
const element = buildOverlayContent(layerType, feature);
// 앵커 좌표 계산(포인트면 좌표, 아니면 extent 중심)
let position: Coordinate | undefined;
const geom = feature.getGeometry?.();
if (geom?.getType && geom.getType() === "Point" && geom.getCoordinates) {
position = geom.getCoordinates();
} else if (geom?.getExtent) {
position = getExtentCenter(geom.getExtent());
}
if (!position) return;
// 오버레이 생성 & 지도에 추가
const overlay = new Overlay({
element,
positioning: "bottom-center",
stopEvent: false,
offset: [0, -12],
});
overlay.setPosition(position);
map.addOverlay?.(overlay);
overlayRef.current = overlay;
};
// ===== 피처 선택 시 호출 =====
const onFeatureSelect = (data: FeatureData) => {
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
type SelectModule = {
resetSelectedFeature: () => void;
selectFeature: (feature: FeatureData, selected: boolean) => void;
};
const maybeSelect = GisApp?.Module?.select; // 타입 그대로 둠 (any | unknown | null 가능)
if (!maybeSelect) {
// null/undefined면 일찍 반환
return;
}
const select = maybeSelect as SelectModule; // 여기선 null 아님이 보장됨
if (!select) return;
// 1) 기존 선택 초기화
select.resetSelectedFeature();
// 2) 피처 탐색 + 레이어 유형 파악
let featureId = String(data.getId?.() ?? "");
let foundLayerType = featureId.split(".")[0];
let foundFeature: FeatureData | null = null;
const isOlFeatureInstance =
data &&
typeof data.getGeometry === "function" &&
typeof data.setStyle === "function";
if (isOlFeatureInstance) {
foundFeature = data;
// 어떤 레이어에서 온 건지 모르는 경우가 많아 기본값으로 시도
} else {
map
.getLayers()
.getArray()
.forEach((layer) => {
if (foundFeature) return; // 이미 찾았으면 skip
const source = layer.getSource?.();
const feature = source?.getFeatureById?.(featureId);
if (feature) {
foundFeature = feature;
// 레이어 이름(유형) 우선순위: name -> lyrTblNm -> id
foundLayerType =
layer.get("name") || layer.get("lyrTblNm") || layer.get("id");
}
});
}
// 3) 피처 선택/줌 + 오버레이
if (foundFeature && foundFeature.getGeometry) {
const geometry = foundFeature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
select.selectFeature(foundFeature, true);
// 표출 대상 레이어만 오버레이 표시
const normalized = (foundLayerType || "").toString().trim();
const TARGETS = [
"tb_il_swage_prcs_fclt",
"tb_ms_swage_prcs_zone",
"om_wqlt_station",
];
if (TARGETS.includes(normalized)) {
showOverlay(map, foundFeature, normalized);
} else {
// 대상 외 레이어 클릭 시 기존 오버레이 제거
if (overlayRef.current) {
map.removeOverlay?.(overlayRef.current);
overlayRef.current = null;
}
}
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
// 실패 시 기존 오버레이 제거
if (overlayRef.current) {
map.removeOverlay?.(overlayRef.current);
overlayRef.current = null;
}
}
// 우측 패널/검색 모달 상태
setFeatureData(data);
setIsOpen(false);
setIsSearchModalOpen(false);
};
// (선택) 다른 위치 클릭 시 오버레이 닫고 싶다면 Gis 쪽에서 지도 클릭 이벤트 받아 여기서 제거하는 훅을 연결하면 됨.
return (
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={undefined}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={undefined}
imgVisble={false}
searchBar={
<ProcSearchBar gisMaps={gisMaps} facilityTypes={facilityTypes} />
}
waterDepth={undefined}
/>
</div>
);
};
export default Map;
@@ -1,7 +1,7 @@
"use client"
import { useFoStore } from "@/app/store";
import { useEffect, useState } from "react";
import SearchInput from "../../components/common/SearchInput";
import SearchInput from "../../../components/common/SearchInput";
export default function DashBoardFclty({ onFacilityChange, sidoSigun, facilityNm, }: { onFacilityChange?: (facility: { bscFctCd: string, bscFctNm: string, fcltyCpc: number | '', trtFrfNm: string | '', efflWqltStndrdNm: string | '' }) => void, sidoSigun?: string, facilityNm?: string }) {
// 입력값 상태 관리
@@ -4,8 +4,8 @@ import type { FacilityWaterQltyProps, modalExcelType } from "@/app/store/facilit
import { downloadExcel, UITab, UiTable } from "@common/business_components/ui";
import { Button, TabsContent } from "@common/components/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import type { ReturnType } from "../../../components/common/FacilityTermModalHeader";
import FacilityTermModalHeader from "../../../components/common/FacilityTermModalHeader";
import type { ReturnType } from "../../../../components/common/FacilityTermModalHeader";
import FacilityTermModalHeader from "../../../../components/common/FacilityTermModalHeader";
import { HeadMakeColSpan } from "../HeadMakeColSpan";
type ModalProps ={
@@ -5,7 +5,7 @@ import type { FlowRateModalInterface, modalExcelType } from "@/app/store/flowRat
import { downloadExcel, UITab, UiTable } from "@common/business_components/ui";
import { Button, TabsContent } from "@common/components/ui";
import { useCallback, useEffect, useRef, useState } from "react";
import type { ReturnType } from "../../../components/common/FacilityTermModalHeader";
import type { ReturnType } from "../../../../components/common/FacilityTermModalHeader";
const tabList = [
{ value: 'tab_term_year', label: '연간' },
@@ -0,0 +1,13 @@
"use client"
import Base from "@/app/base";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<Base>{children}</Base>
);
}
@@ -1,5 +1,5 @@
import type { TableUpperProps } from "@/app/store/common";
import { TableHead, TableRow } from "@common/components/ui";
import type { TableUpperProps } from "../../store/dashboard";
const HeadMakeColSpan = ({ upHeadList, layoutNum }: { upHeadList: TableUpperProps[], layoutNum: number }) => {
return (
-152
View File
@@ -1,152 +0,0 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { useEffect, useRef, useState } from "react";
import { SearchBar } from "./searchBar/searchBar";
interface FeatureData {
fid?: string;
ftrIdn?: string;
id?: string;
getGeometry?: () => { getExtent: () => number[] };
setStyle?: () => void;
}
interface MapInstance {
getLayers: () => {
getArray: () => Array<{
getSource?: () => { getFeatureById?: (id: string) => FeatureData | null };
}>;
};
getView: () => { fit: (extent: number[], options: object) => void };
}
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const gisMaps = useRef<MapInstance | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState<FeatureData | null>(null); // 선택된 피쳐 데이터
const fetchData = async () => {
try {
const API_URL = `${process.env.NEXT_PUBLIC_SEWERAGE_API_URL}/v1/map`;
const reqUrl = `${API_URL}/lyrStngs/FO_NETWORK`;
const response = await fetch(reqUrl, {
method: "GET",
headers: {
accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP 오류! 상태: ${response.status}`);
}
const data = await response.json(); // 여기서 json 파싱
setTreeData(data); // 데이터 설정
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: FeatureData) => {
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as unknown as
| {
resetSelectedFeature: () => void;
selectFeature: (feature: FeatureData, selected: boolean) => void;
}
| undefined;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature: FeatureData | null = 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) => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
}
});
}
// 3. 피처 선택
if (foundFeature && foundFeature.getGeometry) {
const geometry = foundFeature.getGeometry();
if (geometry) {
const 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);
};
return (
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={undefined}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={undefined}
imgVisble={false}
searchBar={() => <SearchBar onFeatureSelect={onFeatureSelect} gisMaps={gisMaps} />}
waterDepth={undefined}
/>
</div>
);
};
export default Map;
+4 -1
View File
@@ -17,7 +17,10 @@
"next": "15.1.7",
"select": "^1.1.2",
"webpack": "^5.98.0",
"zustand": "^5.0.3"
"zustand": "^5.0.3",
"ol": "^7.3.0",
"ol-ext": "^4.0.11",
"proj4": "^2.9.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
+2 -1
View File
@@ -13,4 +13,5 @@ NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883/sewerage-service/api
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883/sewerage-service/api
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.66:8883/sewerage-service/api
+3 -3
View File
@@ -3,7 +3,7 @@
import React, { useEffect } from 'react';
import { usePathname, useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { BaseLayout } from "@common/components";
import { NoFooterLayout } from "@common/components";
import type { MenuItem } from "@/entities/menu";
import { useMenuStore } from "@/shared/lib";
import useStore from '../store/useStore';
@@ -33,12 +33,12 @@ export default function MapLayout({
};
return (
<BaseLayout
<NoFooterLayout
queryClient={queryClient}
currentPath={pathname ?? ""}
handleMenuClick={handleMenuClick}
>
{children}
</BaseLayout>
</NoFooterLayout>
);
}
@@ -373,7 +373,7 @@ export const MsSwageMhlCol = [
// { id: 'usg_cd', title: '용도코드' },
// { id: 'fclty_shp_cd', title: '시설물형태코드' },
// { id: 'mhl_knd_cd', title: '맨홀종류코드' },
{ id: 'swage_mh_diam', title: '하수맨홀구경(mm)' },
{ id: 'swage_mhL_diam_vl', title: '하수맨홀구경(mm)' },
{ id: 'mqlt_cd', title: '재질' },
{ id: 'wdth_len', title: '가로길이(m)' },
{ id: 'vrtc_len', title: '세로길이(m)' },
@@ -396,7 +396,7 @@ export const MsSwageMhlCol2 = [
// { id: 'usgCd', title: '용도코드' },
// { id: 'fcltyShpCd', title: '시설물형태코드' },
// { id: 'mhlKndCd', title: '맨홀종류코드' },
{ id: 'swageMhDiam', title: '하수맨홀구경(mm)' },
{ id: 'swageMhlDiamVl', title: '하수맨홀구경(mm)' },
{ id: 'mqltCd', title: '재질코드' },
{ id: 'wdthLen', title: '가로길이(m)' },
{ id: 'vrtcLen', title: '세로길이(m)' },
@@ -487,7 +487,7 @@ export const ExclnSewagePumpCol = [
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
{ id: 'altd_vl', title: '표고(EL.m)' },
{ id: 'wtlv_vl', title: '수위(m)' },
{ id: 'excln_wpmp_ablt', title: '우수양수능력(m²/d)' },
{ id: 'excln_wpmp_ablt_vl', title: '우수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
export const ExclnSewagePumpCol2 = [
@@ -505,10 +505,10 @@ export const ExclnSewagePumpCol2 = [
// { id: 'opngSttsCd', title: '개통상태코드' },
// { id: 'usgCd', title: '용도코드' },
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
{ id: 'max_impndCpct', title: '최대저수용량(㎥)' },
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
{ id: 'altdVl', title: '표고(EL.m)' },
{ id: 'wtlvVl', title: '수위(m)' },
{ id: 'exclnWpmpAblt', title: '우수양수능력(m²/d)' },
{ id: 'exclnWpmpAbltVl', title: '우수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
// 오수펌프장
@@ -529,7 +529,7 @@ export const DrtwSewagePumpCol = [
{ id: 'max_impnd_cpct', title: '최대저수용량(㎥)' },
{ id: 'altd_vl', title: '표고(EL.m)' },
{ id: 'wtlv_vl', title: '수위(m)' },
{ id: 'excln_wpmp_ablt', title: '오수양수능력(m²/d)' },
{ id: 'excln_wpmp_ablt_vl', title: '오수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
export const DrtwSewagePumpCol2 = [
@@ -547,10 +547,10 @@ export const DrtwSewagePumpCol2 = [
// { id: 'opngSttsCd', title: '개통상태코드' },
// { id: 'usgCd', title: '용도코드' },
{ id: 'dailyPrcsCpct', title: '일일처리용량(㎥)' },
{ id: 'max_impndCpct', title: '최대저수용량(㎥)' },
{ id: 'maxImpndCpct', title: '최대저수용량(㎥)' },
{ id: 'altdVl', title: '표고(EL.m)' },
{ id: 'wtlvVl', title: '수위(m)' },
{ id: 'exclnWpmpAblt', title: '오수양수능력(m²/d)' },
{ id: 'exclnWpmpAbltVl', title: '오수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
// 펌프장 용도 코드표
@@ -5,13 +5,9 @@ import moment from "moment";
export type TInftwtAnls = {
inftwtAnlsSn:number;
aplcnYm: string;
enoCd: string;
ctpvCd: string;
sggCd: string;
swageTmplntCd: string;
swageTmplntNm: string;
swtrmtZoneCd: string;
swtrmtZoneNm: string;
swtrmtFcltNm: string;
atflCd: string;
atflId: string;
atflInpNm: string;
@@ -32,13 +28,9 @@ export type TInftwtAnlsRslt = {
export type TRdiiAnls = {
rainInfwtAnlsSn:number;
aplcnYm: string;
enoCd: string;
ctpvCd: string;
sggCd: string;
swageTmplntCd: string;
swageTmplntNm: string;
swtrmtZoneCd: string;
swtrmtZoneNm: string;
swtrmtFcltNm: string;
atflCd: string;
atflId: string;
atflInpNm: string;
@@ -52,6 +52,7 @@ type BaseLayout = {
setCtpvCd: (ctpv: number) => void;
setSggCd: (sgg: number) => void;
setSggName?: (sggName: string) => void;
setSwtrmtFcltNm?: (swtrmtFcltNm: string) => void;
setIsLoading?: (isLoading: boolean) => void;
onExcelDownload?: () => void;
onDeleteFile?: () => void;
@@ -81,6 +82,7 @@ const BaseLayout = ({
setCtpvCd,
setSggCd,
setSggName,
setSwtrmtFcltNm,
setIsLoading,
onExcelDownload,
onDeleteFile,
@@ -184,6 +186,7 @@ const BaseLayout = ({
setSggName={setSggName}
selectedSwagePrcsFclt={selectedSwagePrcsFclt}
onSwagePrcsFcltChange={onSwagePrcsFcltChange}
setSwtrmtFcltNm={setSwtrmtFcltNm}
setIsLoading={setIsLoading}
/>
{selectBox?.searchChk && (
@@ -18,6 +18,7 @@ import { AUTH_USER_ID } from "@/shared/config";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@common/components/config";
import { getUserInfo } from "@common/components/api";
import LoadingOverlay from '@common/business_components/ui/loadingBar';
const formSchema = z.object({
@@ -55,6 +56,7 @@ const IrSetting = () => {
const [ctpvCd, setCtpvCd] = useState<number>(0); // 시도 코드 상태
const [sggCd, setSggCd] = useState<number>(0); // 시군구 코드 상태
const [sggName, setSggName] = useState<string>(''); // 시군구 이름 상태
const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>(''); // 하수처리시설명 상태
const { data: userInfo } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
@@ -115,6 +117,7 @@ const IrSetting = () => {
setInput1(''); // 입력 필드 초기화
setInput2(''); // 입력 필드 초기화
setSelect1(0);
setSwtrmtFcltNm('');
setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경
};
@@ -156,6 +159,7 @@ const IrSetting = () => {
// 침입수 분석 실행 버튼 클릭 핸들러
const handleAnalysisClick = async () => {
console.log('분석 시작');
setIsLoadingResult(true); // 로딩 상태 시작
// 기본 선택 항목들 검증 (환경청, 시도, 시군구, 하수처리장)
@@ -296,9 +300,7 @@ const IrSetting = () => {
const combinedAnalysisData = {
ctpvCd: (ctpvCd ? String(ctpvCd) : null),
sggCd: (sggCd ? String(sggCd) : null), // 시군구코드 (선택값)
swageTmplntCd: (select1 && select1 !== '0' && select1 !== 0)
? select1.toString().split('.').pop() || null // '.'으로 split해서 마지막 부분만 사용
: null, // 하수처리장코드 (선택값)
swtrmtFcltNm: swtrmtFcltNm ?? null,
aplcnYm: selectedDate, // 적용연월
drtwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환)
anlsInfo: input2, // 분석정보
@@ -330,6 +332,7 @@ const IrSetting = () => {
return (
<>
<LoadingOverlay isLoading={isLoadingResult} />
<BaseLayout
titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력' }}
form={form}
@@ -345,6 +348,7 @@ const IrSetting = () => {
setCtpvCd={setCtpvCd}
setSggCd={setSggCd}
setSggName={setSggName}
setSwtrmtFcltNm={setSwtrmtFcltNm}
setIsLoading={setIsLoadingResult}
onAnalysis={handleAnalysisClick}
>
+146 -145
View File
@@ -20,15 +20,15 @@ import { AUTH_USER_ID } from "@/shared/config";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@common/components/config";
import { getUserInfo } from "@common/components/api";
import LoadingOverlay from '@common/business_components/ui/loadingBar';
// zod 스키마 정의
const formSchema = z.object({
enoCd: z.string(), // 환경청코드
ctpvCd: z.string(), // 시도코드
sggCd: z.string(), // 시군구코드
swageTmplntCd: z.string(), // 하수처리장코드
swtrmtZoneCd: z.string(), // 하수처리구역코드
swtrmtFcltNm: z.string(), // 하수처리시설명
rtkSource: z.string(), // RTK계수 출처
rdiiArea: z.string(), // RDII 기여면적
unitFactors: z.array(z.object({ // 단위도 계수
@@ -56,11 +56,9 @@ const formSchema = z.object({
type FormSchemaType = z.infer<typeof formSchema>;
// 기본 폼 값 정의 (테스트 편의를 위해 초기값 설정, 추후 삭제 필요)
const defaultFormValues: FormSchemaType = {
enoCd: "",
ctpvCd: "",
sggCd: "",
swageTmplntCd: "",
swtrmtZoneCd: "",
swtrmtFcltNm: "",
rtkSource: "",
rdiiArea: "", // 문자열로 기본값 설정
unitFactors: [
@@ -114,10 +112,11 @@ export default function RTKForm() {
initialFileInfo, // 2: 강우자료
]);
const [select1, setSelect1] = useState<string | number>('0'); // 하수처리장 (swageTmplntCd)
const [select1, setSelect1] = useState<string | number>('0'); // 하수처리장 (swtrmtFcltCd)
const [ctpvCd, setCtpvCd] = useState<number>(0); // 시도 코드 상태
const [sggCd, setSggCd] = useState<number>(0); // 시군구 코드 상태
const [sggName, setSggName] = useState<string>(''); // 시군구 이름 상태
const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>(''); // 하수처리시설명 상태
const { data: userInfo } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
@@ -293,9 +292,7 @@ export default function RTKForm() {
aplcnYm: formData.date, // 적용연월 (YYYYMM 형식)
ctpvCd: ctpvCd ? String(ctpvCd) : null, // 시도코드 (선택값)
sggCd: sggCd ? String(sggCd) : null, // 시군구코드 (선택값)
swageTmplntCd: (select1 && select1 !== '0' && select1 !== 0)
? select1.toString().split('.').pop() || null // '.'으로 split해서 마지막 부분만 사용
: null, // 하수처리장코드 (선택값)
swtrmtFcltNm: swtrmtFcltNm ?? null,
atflCd: file1?.atflCd, // 첨부파일CD (오수관망도)
atflId: file1?.atflId, // 첨부파일ID (오수관망도)
atflInpNm: file1?.file?.name || "", // 오수관망도 파일명
@@ -324,9 +321,9 @@ export default function RTKForm() {
mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec || "0"),
mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do || "0"),
longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax || "0"),
longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec || "0"),
longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do || "0"),
longPrdMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax || "0"),
longPrdRcvryRt: parseFloat(formData.initialDepths[2]?.Drec || "0"),
longPrdBgngDpth: parseFloat(formData.initialDepths[2]?.Do || "0"),
drtwSwtcRt: parseFloat(formData.rate || "0"), // 오수전환율
anlsInfo: formData.analysisInfo, // 분석정보
@@ -386,140 +383,144 @@ export default function RTKForm() {
}
return (
<BaseLayout
selectBox={{ title: '분석 대상지역 선택' }}
titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }}
form={form} // react-hook-form 인스턴스 전달
onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit
graphBtn={false}
analysisBtn={'RDII분석 실행'}
ctpvCd={ctpvCd}
sggCd={sggCd}
sggName={sggName}
selectedSwagePrcsFclt={select1}
onSwagePrcsFcltChange={setSelect1}
setCtpvCd={setCtpvCd}
setSggCd={setSggCd}
setSggName={setSggName}
setIsLoading={setIsLoadingResult}
onAnalysis={handleAnalysisClick}
>
{[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section">
<div className="grid grid-cols-2 gap-4">
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처">
<Input {...form.register("rtkSource")} className="h-12 text-center text-xl w-[225px]" />
</FormSection>
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)">
<Input {...form.register("rdiiArea")} className="h-12 text-center text-xl w-[225px]" />
</FormSection></div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="mb-2 text-xl font-medium">○ 단위도 계수</Label>
<EditableFormTable
form={form}
name="unitFactors"
fields={unitFactorFields}
headlist={unitFactorHeadlist}
tableClassName="w-[400px]"
/>
<>
<LoadingOverlay isLoading={isLoadingResult} />
<BaseLayout
selectBox={{ title: '분석 대상지역 선택' }}
titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }}
form={form} // react-hook-form 인스턴스 전달
onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit
graphBtn={false}
analysisBtn={'RDII분석 실행'}
ctpvCd={ctpvCd}
sggCd={sggCd}
sggName={sggName}
selectedSwagePrcsFclt={select1}
onSwagePrcsFcltChange={setSelect1}
setCtpvCd={setCtpvCd}
setSggCd={setSggCd}
setSggName={setSggName}
setSwtrmtFcltNm={setSwtrmtFcltNm}
setIsLoading={setIsLoadingResult}
onAnalysis={handleAnalysisClick}
>
{[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section">
<div className="grid grid-cols-2 gap-4">
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처">
<Input {...form.register("rtkSource")} className="h-12 text-center text-xl w-[225px]" />
</FormSection>
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)">
<Input {...form.register("rdiiArea")} className="h-12 text-center text-xl w-[225px]" />
</FormSection></div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="mb-2 text-xl font-medium">○ 단위도 계수</Label>
<EditableFormTable
form={form}
name="unitFactors"
fields={unitFactorFields}
headlist={unitFactorHeadlist}
tableClassName="w-[400px]"
/>
</div>
<div>
<Label className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</Label>
<EditableFormTable
form={form}
name="initialDepths"
fields={initialDepthFields}
headlist={initialDepthHeadlist}
tableClassName="w-[400px]"
/>
</div>
</div>
<div>
<Label className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</Label>
<EditableFormTable
form={form}
name="initialDepths"
fields={initialDepthFields}
headlist={initialDepthHeadlist}
tableClassName="w-[400px]"
/>
<div className="grid grid-cols-2 gap-4">
<FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[350px]">
<FileUploadForm
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)}
showButton={false}
showPreview={true}
accept={".inp"}
fileIdx={0} />
</div></FormSection>
<FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[350px]">
<FileUploadForm
key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={1} />
</div></FormSection>
<FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[350px]">
<FileUploadForm
key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={2} />
</div></FormSection>
<FormSection name='rate' form={form} titleNm='○ 오수전환율'>
<Input
value={form.watch("rate")} // form의 rate 값을 가져옴
onChange={(e) => {
const newValue = e.target.value;
if (newValue === "") {
form.setValue("rate", newValue); // form의 rate 필드에 설정
return;
}
// 숫자와 '.'만 허용하고, '.'은 최대 하나인지 확인
const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue);
if (!isValidChars) return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함
// '.' 기호가 처음에 오면 안 됨
if (newValue.startsWith('.')) return;
// 완성된 숫자인 경우에만 범위 체크 (입력 중인 경우 허용)
if (!newValue.endsWith('.')) {
const numericValue = parseFloat(newValue);
if (isNaN(numericValue) || numericValue < 0 || numericValue > 1) return;
}
form.setValue("rate", newValue, { shouldValidate: true }); // form의 rate 필드에 설정
}}
className="h-12 text-center text-xl w-[225px] ml-5"
placeholder="0 ~ 1 사이의 값 입력"
/>
</FormSection>
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용월'>
<DateMonthPicker
field={{
value: form.watch("date"), // form의 date 값을 직접 사용
onChange: (value: string) => {
form.setValue("date", value, { shouldValidate: true }); // form의 date 필드에 직접 설정
}
}}
selectClass="ml-5 w-[225px]"
startYear={1900} // 시작 연도 설정
/>
</FormSection>
<FormSection name='rate' form={form} titleNm='○ 분석 정보'>
<Input
{...form.register("analysisInfo")}
className="h-12 text-center text-xl w-[225px] ml-5"
placeholder='세부 분석정보 입력'
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault(); // Enter 키 기본 동작 방지
handleAnalysisClick();
}
}}
/>
</FormSection>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[350px]">
<FileUploadForm
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)}
showButton={false}
showPreview={true}
accept={".inp"}
fileIdx={0} />
</div></FormSection>
<FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[350px]">
<FileUploadForm
key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={1} />
</div></FormSection>
<FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[350px]">
<FileUploadForm
key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={2} />
</div></FormSection>
<FormSection name='rate' form={form} titleNm='○ 오수전환율'>
<Input
value={form.watch("rate")} // form의 rate 값을 가져옴
onChange={(e) => {
const newValue = e.target.value;
if (newValue === "") {
form.setValue("rate", newValue); // form의 rate 필드에 설정
return;
}
// 숫자와 '.'만 허용하고, '.'은 최대 하나인지 확인
const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue);
if (!isValidChars) return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함
// '.' 기호가 처음에 오면 안 됨
if (newValue.startsWith('.')) return;
// 완성된 숫자인 경우에만 범위 체크 (입력 중인 경우 허용)
if (!newValue.endsWith('.')) {
const numericValue = parseFloat(newValue);
if (isNaN(numericValue) || numericValue < 0 || numericValue > 1) return;
}
form.setValue("rate", newValue, { shouldValidate: true }); // form의 rate 필드에 설정
}}
className="h-12 text-center text-xl w-[225px] ml-5"
placeholder="0 ~ 1 사이의 값 입력"
/>
</FormSection>
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용월'>
<DateMonthPicker
field={{
value: form.watch("date"), // form의 date 값을 직접 사용
onChange: (value: string) => {
form.setValue("date", value, { shouldValidate: true }); // form의 date 필드에 직접 설정
}
}}
selectClass="ml-5 w-[225px]"
startYear={1900} // 시작 연도 설정
/>
</FormSection>
<FormSection name='rate' form={form} titleNm='○ 분석 정보'>
<Input
{...form.register("analysisInfo")}
className="h-12 text-center text-xl w-[225px] ml-5"
placeholder='세부 분석정보 입력'
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault(); // Enter 키 기본 동작 방지
handleAnalysisClick();
}
}}
/>
</FormSection>
</div>
</div>
]}
</BaseLayout >
]}
</BaseLayout >
</>
)
}
@@ -8,11 +8,12 @@ import { z } from 'zod'
import BaseLayout from '../BaseLayout'
import { handleDelete } from '@/components/lib/utils'
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import useStore from '../../../store/useStore';
const formSchema = z.object({
swageTmplntCd: z.string().default('0'), // 하수처리장
swtrmtZoneCd: z.string().default('0'), // 하수처리구역
swtrmtFcltCd: z.string().default('0'), // 하수처리시설코드
swtrmtFcltNm: z.string().default(''), // 하수처리시설명
aplcnYmFrom: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경
const now = new Date();
const year = now.getFullYear();
@@ -32,11 +33,9 @@ type ResultFormValues = z.infer<typeof formSchema>
const tableColumn = [
{ id: 'rowNumber', title: '일련번호' },
{ id: 'enoNm', title: '환경청' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
{ id: 'swageTmplntNm', title: '하수처리장' },
// { id: 'swtrmtZoneNm', title: '처리구역' },
{ id: 'swtrmtFcltNm', title: '하수처리시설' },
{ id: 'aplcnYm', title: '상수사용연월' },
{ id: 'drtwSwtcRt', title: '오수 전환율' },
{ id: 'srcNm', title: 'RTK 계수 출처' },
@@ -53,8 +52,7 @@ const Result = () => {
const form = useForm<ResultFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
swageTmplntCd: '0',
swtrmtZoneCd: '0',
swtrmtFcltCd: '0',
aplcnYmFrom: (() => { // 현재 연월을 yyyyMM 형식으로 설정
const now = new Date();
const year = now.getFullYear();
@@ -83,6 +81,8 @@ const Result = () => {
const [sggCd, setSggCd] = useState<number>(0); // 시군구 코드 상태
const [sggName, setSggName] = useState<string>(''); // 시군구 이름 상태
const {ctpv, sgg} = useStore();
useEffect(() => {
// setCurPage(1);
handleSearchButtonClick();
@@ -109,22 +109,13 @@ const Result = () => {
const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기
console.log("formValues", formValues);
// swageTmplntCd 처리 로직 추가
const processedSwageTmplntCd = formValues.swageTmplntCd && formValues.swageTmplntCd !== '0'
? (() => {
const valString = String(formValues.swageTmplntCd);
const parts = valString.split('.');
return parts.length > 1 ? parts[1] : valString;
})()
: null;
let inftwtAnlsSearchDto = {
"aplcnYmFrom": formValues.aplcnYmFrom && formValues.aplcnYmFrom !== '' ? formValues.aplcnYmFrom : null,
"aplcnYmTo": formValues.aplcnYmTo && formValues.aplcnYmTo !== '' ? formValues.aplcnYmTo : null,
"ctpvCd": ctpvCd === 0 ? null : ctpvCd,
"sggCd": sggCd === 0 ? null : sggCd,
"swageTmplntCd": processedSwageTmplntCd,
"auth": AUTH,
const inftwtAnlsSearchDto = {
aplcnYmFrom: formValues.aplcnYmFrom?.trim() ? formValues.aplcnYmFrom : null,
aplcnYmTo: formValues.aplcnYmTo?.trim() ? formValues.aplcnYmTo : null,
ctpvCd: ctpvCd || null,
sggCd: sggCd || null,
swtrmtFcltNm: formValues.swtrmtFcltNm?.trim() ? formValues.swtrmtFcltNm : null,
auth: AUTH,
};
console.log("Search DTO:", inftwtAnlsSearchDto);
@@ -149,12 +140,15 @@ const Result = () => {
const dataWithRowNumber = (data.content || []).map((item: any, index: number) => {
// 🎯 먼저 각 아이템을 포맷팅
const formattedItem = formatDisplayData(item);
const ctpvOne = ctpv.find(code => code.id === item.ctpvCd);
const sggOne = sgg.find(code => code.id === item.sggCd);
// 추가 처리 및 일련번호 추가
return {
...formattedItem,
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn, // scsYn이 null이면 'N'으로 변경
ctpvNm: ctpvOne ? ctpvOne.name : item.ctpvCd,
sggNm: sggOne ? sggOne.name : item.sggCd,
};
})
.filter((item: any) => item.scsYn === 'Y');
@@ -211,7 +205,7 @@ const Result = () => {
};
// 파일명 생성 (분석번호 기반)
const fileName = `RDII분석결과_${selectedRowData.swageTmplntNm}_${selectedRowData.aplcnYm}_${selectedRowData.rainInfwtAnlsSn}`;
const fileName = `RDII분석결과_${selectedRowData.swtrmtFcltNm}_${selectedRowData.aplcnYm}_${selectedRowData.rainInfwtAnlsSn}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
@@ -286,8 +280,8 @@ const Result = () => {
ctpvCd={ctpvCd}
sggCd={sggCd}
sggName={sggName}
selectedSwagePrcsFclt={form.watch("swageTmplntCd")}
onSwagePrcsFcltChange={(val) => form.setValue("swageTmplntCd", String(val))}
selectedSwagePrcsFclt={form.watch("swtrmtFcltCd")}
onSwagePrcsFcltChange={(val) => form.setValue("swtrmtFcltCd", String(val))}
aplcnYmFrom={form.watch("aplcnYmFrom")}
setAplcnYmFrom={(val: string) => { form.setValue("aplcnYmFrom", val) }}
aplcnYmTo={form.watch("aplcnYmTo")}
@@ -295,6 +289,7 @@ const Result = () => {
setCtpvCd={setCtpvCd}
setSggCd={setSggCd}
setSggName={setSggName}
setSwtrmtFcltNm={(val: string) => { form.setValue("swtrmtFcltNm", val) }}
setIsLoading={setIsLoadingResult}
// Excel 다운로드 핸들러 추가
onExcelDownload={handleExcelDownload}
@@ -8,11 +8,11 @@ import { z } from 'zod'
import BaseLayout from '../BaseLayout'
import { handleDelete } from '@/components/lib/utils'
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
import useStore from '../../../store/useStore';
const formSchema = z.object({
swageTmplntCd: z.string().default('0'), // 하수처리장
swtrmtZoneCd: z.string().default('0'), // 하수처리구역
swtrmtFcltCd: z.string().default('0'), // 하수처리시설코드
swtrmtFcltNm: z.string().default(''), // 하수처리시설명
aplcnYmFrom: z.string().default(() => { // Date에서 string(yyyyMM 형식)으로 변경
const now = new Date();
const year = now.getFullYear();
@@ -32,11 +32,9 @@ type ResultFormValues = z.infer<typeof formSchema>
const tableColumn = [
{ id: 'rowNumber', title: '일련번호' },
{ id: 'enoNm', title: '환경청' },
{ id: 'ctpvNm', title: '시도' },
{ id: 'sggNm', title: '시군구' },
{ id: 'swageTmplntNm', title: '하수처리장' },
// { id: 'swtrmtZoneNm', title: '처리구역' },
{ id: 'swtrmtFcltNm', title: '하수처리시설' },
{ id: 'aplcnYm', title: '적용연월' },
{ id: 'drtwSwtcRt', title: '오수 전환율' },
{ id: 'anlsInfo', title: '침입수 분석정보' },
@@ -51,8 +49,8 @@ const Result = () => {
const form = useForm<ResultFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
swageTmplntCd: '0',
swtrmtZoneCd: '0',
swtrmtFcltCd: '0',
swtrmtFcltNm: '',
aplcnYmFrom: (() => { // 현재 연월을 yyyyMM 형식으로 설정
const now = new Date();
const year = now.getFullYear();
@@ -79,6 +77,7 @@ const Result = () => {
const [ctpvCd, setCtpvCd] = useState<number>(0); // 시도 코드 상태
const [sggCd, setSggCd] = useState<number>(0); // 시군구 코드 상태
const [sggName, setSggName] = useState<string>(''); // 시군구 이름 상태
const {ctpv, sgg} = useStore();
useEffect(() => {
// setCurPage(1);
@@ -104,23 +103,12 @@ const Result = () => {
const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기
console.log("formValues", formValues);
// swageTmplntCd 처리 로직 추가
const processedSwageTmplntCd = formValues.swageTmplntCd && formValues.swageTmplntCd !== '0'
? (() => {
const valString = String(formValues.swageTmplntCd);
const parts = valString.split('.');
return parts.length > 1 ? parts[1] : valString;
})()
: null;
let inftwtAnlsSearchDto = {
"aplcnYmFrom": formValues.aplcnYmFrom && formValues.aplcnYmFrom !== '' ? formValues.aplcnYmFrom : null,
"aplcnYmTo": formValues.aplcnYmTo && formValues.aplcnYmTo !== '' ? formValues.aplcnYmTo : null,
"ctpvCd": ctpvCd === 0 ? null : ctpvCd,
"sggCd": sggCd === 0 ? null : sggCd,
"swageTmplntCd": processedSwageTmplntCd,
"swtrmtFcltNm": formValues.swtrmtFcltNm?.trim() ? formValues.swtrmtFcltNm : null,
"auth": AUTH,
};
@@ -147,12 +135,15 @@ const Result = () => {
const dataWithRowNumber = (data.content || []).map((item: any, index: number) => {
// 🎯 먼저 각 아이템을 포맷팅
const formattedItem = formatDisplayData(item);
const ctpvOne = ctpv.find(code => code.id === item.ctpvCd);
const sggOne = sgg.find(code => code.id === item.sggCd);
// 추가 처리 및 일련번호 추가
return {
...formattedItem,
rowNumber: (curPage - 1) * 10 + index + 1, // 1부터 시작하는 일련번호 추가
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn // scsYn이 null이면 'N'으로 변경
scsYn: item.scsYn === null || item.scsYn === undefined ? 'N' : item.scsYn, // scsYn이 null이면 'N'으로 변경
ctpvNm: ctpvOne ? ctpvOne.name : item.ctpvCd,
sggNm: sggOne ? sggOne.name : item.sggCd,
};
})
.filter((item: any) => item.scsYn === 'Y');
@@ -209,7 +200,7 @@ const Result = () => {
};
// 파일명 생성 (분석번호 기반)
const fileName = `침입수분석결과_${selectedRowData.swageTmplntNm}_${selectedRowData.aplcnYm}_${selectedRowData.inftwtAnlsSn}`;
const fileName = `침입수분석결과_${selectedRowData.swtrmtFcltNm}_${selectedRowData.aplcnYm}_${selectedRowData.inftwtAnlsSn}`;
await downloadExcelWithKoreanHeaders({
data: excelData as JSON[],
@@ -285,8 +276,8 @@ const Result = () => {
ctpvCd={ctpvCd}
sggCd={sggCd}
sggName={sggName}
selectedSwagePrcsFclt={form.watch("swageTmplntCd")}
onSwagePrcsFcltChange={(val) => form.setValue("swageTmplntCd", String(val))}
selectedSwagePrcsFclt={form.watch("swtrmtFcltCd")}
onSwagePrcsFcltChange={(val) => form.setValue("swtrmtFcltCd", String(val))}
aplcnYmFrom={form.watch("aplcnYmFrom")}
setAplcnYmFrom={(val: string) => { form.setValue("aplcnYmFrom", val) }}
aplcnYmTo={form.watch("aplcnYmTo")}
@@ -294,8 +285,8 @@ const Result = () => {
setCtpvCd={setCtpvCd}
setSggCd={setSggCd}
setSggName={setSggName}
setSwtrmtFcltNm={(val: string) => { form.setValue("swtrmtFcltNm", val) }}
setIsLoading={setIsLoadingResult}
// Excel 다운로드 핸들러 추가
onExcelDownload={handleExcelDownload}
onDeleteFile={handleFileDelete}
selectedRowData={selectedRowData}
@@ -1,5 +1,5 @@
'use client'
import { deleteData, postData } from '@/components/lib/api'
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
import { searchData } from '@/components/lib/common/apiHelpers'
import { formatDate, handleDownload } from '@/components/lib/utils'
import LoadingOverlay from '@/components/ui/LoadingBar'
@@ -9,9 +9,16 @@ import { Button, DatePicker, Input, Label } from '@common/components/ui'
import { useEffect, useState } from 'react'
import { tableHeadlist } from '..'
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
import { tokenManager } from '@/entities/auth/lib/token-manager';
import { AUTH_USER_ID } from "@/shared/config";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@common/components/config";
import { getUserInfo } from "@common/components/api";
const Page = () => {
const AUTH = useCookieAuth();
// 조회조건
const [fromDate, setFromDate] = useState(() => { const date = new Date(); date.setDate(date.getDate() - 7); return date })
const [toDate, setToDate] = useState(new Date())
@@ -29,6 +36,12 @@ const Page = () => {
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
const [isResetPage, setIsResetPage] = useState<boolean>(false);
const { data: userInfo } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: () => getUserInfo(),
enabled: true,
});
useEffect(() => {
setCurPage(1)
fetchData()
@@ -57,7 +70,7 @@ const Page = () => {
fromRegDt: formatDate(fromDate),
toRegDt: formatDate(toDate),
sbmrgPrevntBizNm: pjNm,
rgtrNm: usr,
auth: AUTH,
page: curPage - 1,
size: '10'
})
@@ -109,14 +122,17 @@ const Page = () => {
// 현황 등록
const registFile = async (fileName: string, year: string, quater: string, cd: string, id: string) => {
const data = await postData('v1/sbmrgPrevnt', {
const json = {
atflNm: fileName,
mngYr: year,
mngQu: quater,
atflCd: cd,
atflId: id,
sbmrgPrevntBizNm: fileName
})
sbmrgPrevntBizNm: fileName,
rgtr: tokenManager.getToken(AUTH_USER_ID) || null
}
console.log('request DTO', json);
const data = await postData('v1/sbmrgPrevnt', json)
if (data) {
alert('파일이 등록되었습니다.');
setIsModalOpen(false);
+2 -4
View File
@@ -1,19 +1,17 @@
export const tableHeadlist = [
{ id: 'rowNumber', title: '일련번호' },
// { id: 'sbmrgPrevntBizSn', title: '일련번호' },
{ id: 'sbmrgPrevntBizNm', title: '파일명' },
{ id: 'mngYr', title: '년도' },
{ id: 'mngQu', title: '분기' },
{ id: 'regDt', title: '등록 일자' },
{ id: 'rgtr', title: '등록자' }
{ id: 'rgtrNm', title: '등록자' }
]
export const tableHeadlistOldPp = [
{ id: 'rowNumber', title: '일련번호' },
// { id: 'dcrpPplMtncBizSn', title: '일련번호' },
{ id: 'dcrpPplMtncBizNm', title: '파일명' },
{ id: 'mngYr', title: '년도' },
{ id: 'mngQu', title: '분기' },
{ id: 'regDt', title: '등록 일자' },
{ id: 'rgtr', title: '등록자' }
{ id: 'rgtrNm', title: '등록자' }
]
@@ -1,5 +1,5 @@
'use client'
import { deleteData, postData } from '@/components/lib/api'
import { deleteData, postData, useCookieAuth } from '@/components/lib/api'
import { searchData } from '@/components/lib/common/apiHelpers'
import { formatDate, handleDownload, yearOptions } from '@/components/lib/utils'
import LoadingOverlay from '@/components/ui/LoadingBar'
@@ -9,9 +9,16 @@ import { Button, DatePicker, Input, Label } from '@common/components/ui'
import { useEffect, useState } from 'react'
import { tableHeadlistOldPp } from '..'
import OperFileUploadForm from '../../../components/ui/operFileUploadForm'
import { tokenManager } from '@/entities/auth/lib/token-manager';
import { AUTH_USER_ID } from "@/shared/config";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@common/components/config";
import { getUserInfo } from "@common/components/api";
const Page = () => {
const AUTH = useCookieAuth();
const [fromDate, setFromDate] = useState<Date>(() => { const date = new Date(); date.setDate(date.getDate() - 7); return date });
const [toDate, setToDate] = useState<Date>(new Date());
const [usr, setUsr] = useState('')
@@ -27,6 +34,12 @@ const Page = () => {
const [resetSelection, setResetSelection] = useState<boolean>(false); // 선택 초기화 상태
const [isResetPage, setIsResetPage] = useState<boolean>(false);
const { data: userInfo } = useQuery({
queryKey: [ENDPOINT.USER_SERVICE.USERS],
queryFn: () => getUserInfo(),
enabled: true,
});
useEffect(() => {
setCurPage(1)
fetchData()
@@ -54,7 +67,7 @@ const Page = () => {
fromRegDt: formatDate(fromDate),
toRegDt: formatDate(toDate),
dcrpPplMtncBizNm: pjNm,
rgtrNm: usr,
auth: AUTH,
page: curPage - 1,
size: '10'
})
@@ -112,7 +125,9 @@ const Page = () => {
mngQu: quater,
atflCd: cd,
atflId: id,
dcrpPplMtncBizNm: fileNm
dcrpPplMtncBizNm: fileNm,
rgtr: tokenManager.getToken(AUTH_USER_ID) || null,
rgtrNm: userInfo?.userName,
})
if (data) {
alert('파일이 등록되었습니다.');
@@ -20,6 +20,7 @@ interface CtpvSggPrcsFcltSelectBoxProps {
className?: string;
selectedSwagePrcsFclt?: string | number;
onSwagePrcsFcltChange?: (value: string | number) => void;
setSwtrmtFcltNm?: (name: string) => void;
setIsLoading?: (isLoading: boolean) => void;
}
@@ -36,6 +37,7 @@ export function CtpvSggPrcsFcltSelectBox({
className = "",
selectedSwagePrcsFclt,
onSwagePrcsFcltChange,
setSwtrmtFcltNm,
setIsLoading,
}: CtpvSggPrcsFcltSelectBoxProps) {
@@ -50,6 +52,7 @@ export function CtpvSggPrcsFcltSelectBox({
setCtpvName("");
setSggName("");
}
if (setSwtrmtFcltNm) setSwtrmtFcltNm("");
})();
}, []);
@@ -118,6 +121,7 @@ export function CtpvSggPrcsFcltSelectBox({
onSelectValue={val => {
onSidoChange(Number(val));
if (setCtpvName) setCtpvName(ctpv.find(opt => Number(opt.id) === Number(val))?.name || "");
if (setSwtrmtFcltNm) setSwtrmtFcltNm("");
}}
selectContentClass="z-[101]"
/>
@@ -137,6 +141,7 @@ export function CtpvSggPrcsFcltSelectBox({
onSelectValue={val => {
onSigunguChange(Number(val))
if (setSggName) setSggName(sgg.find(opt => Number(opt.id) === Number(val))?.name || "");
if (setSwtrmtFcltNm) setSwtrmtFcltNm("")
}}
selectContentClass="z-[101]"
/>
@@ -149,7 +154,10 @@ export function CtpvSggPrcsFcltSelectBox({
label=""
selectArray={[defaultOption, ...prcsFcltOptions]}
value={selectedSwagePrcsFclt}
onSelectValue={(val) => onSwagePrcsFcltChange?.(val)}
onSelectValue={(val) => {
onSwagePrcsFcltChange?.(val)
if (setSwtrmtFcltNm) setSwtrmtFcltNm(prcsFcltOptions.find(opt => String(opt.val) === String(val))?.text || "")
}}
selectContentClass="z-[101]"
/>
</>
+1 -1
View File
@@ -24,5 +24,5 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
+1 -1
View File
@@ -12,7 +12,7 @@ NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_GEOSERVER_URL = http://rsone.iptime.org:10010/geoserver
NEXT_PUBLIC_LX_GEOSERVER_URL = http://con.mind-one.co.kr:11306/geoserver
NEXT_PUBLIC_NGII_URL = https://map.ngii.go.kr/openapi/Gettile.do?apiKey=04trYP9_xwLAfALjwZ-B8g
NEXT_PUBLIC_SEWERAGE_API_URL=http://con.mind-one.co.kr:11083/sewerage-service/api # 개발 서버 API
+1 -1
View File
@@ -11,7 +11,7 @@ NEXT_PUBLIC_CLAIM_NAME = Authorization
NEXT_PUBLIC_AUTH_USER_ID = token-id
NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC
NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api
+25
View File
@@ -0,0 +1,25 @@
"use client";
import type { MenuItem } from "@common/components/api";
import { NoFooterLayout } from "@common/components/ui";
import { usePathname,useRouter } from "next/navigation";
import { useWrStore } from "../store";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const { getMenuAuthrts } = useWrStore((state) => state);
const router = useRouter();
const pathname = usePathname();
const changeMenu = (path: MenuItem) => {
getMenuAuthrts(path.menuCd);
router.push(path.urlPath);
};
return (
<NoFooterLayout currentPath={pathname ?? ""} handleMenuClick={changeMenu}>
{children}
</NoFooterLayout>
);
}
@@ -2,11 +2,12 @@
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState, useCallback } from "react";
import { mapService } from "../service";
import { mapService } from "../../service";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { SearchBar } from "@common/business_components/ui/map/searchBar/searchBar";
import { useWrStore } from "@/app/store";
import { getFeatureCd } from "@/components/lib/func/\mapUtil";
import { getFeatureCd } from "@/components/lib/func/mapUtil";
import toast, { Toaster } from "react-hot-toast";
const Map = () => {
const { comMap } = useWrStore((state) => state);
@@ -23,7 +24,7 @@ const Map = () => {
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
const response = await mapService.getLyrStngs('WR_NETWORK');
setTreeData(response); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
@@ -173,7 +174,6 @@ const Map = () => {
setIsSearchModalOpen(false);
};
const getSelectedLayers = (isFeature: any) => {
const map = gisMaps.current;
if (!map) {
@@ -206,33 +206,52 @@ const Map = () => {
const renderDetailView = useCallback(
(props: any) => {
const isFeature = props.featureData.getProperties();
if (
Object.keys(isFeature).length === 0 &&
isFeature.constructor === Object
) {
const url = `/mapDetilePopUp?layerNm=${encodeURIComponent(props.seletectedLayerNm)}`;
const width = window.screen.availWidth;
const height = window.screen.availHeight;
const left = 0;
const top = 0;
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
} else {
const layerNm = getSelectedLayers(featureData);
const safeProps = featureData.getProperties();
const featureCd = getFeatureCd(safeProps, String(layerNm));
const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
const width = Math.floor(window.innerWidth * 0.4); // 화면 폭의 40%
const height = Math.floor(window.innerHeight - 450); // 150px 여백 확보; // calc(100% - 70px)
const left = (window.screen.availWidth - width) / 2;
const top = (window.screen.availHeight - height) / 2;
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
popupDetailRef.current = window.open(
url,
"지도 시설 상세정보 팝업",
popupOptions
);
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
const isFeature = props?.featureData;
console.log('isFeature 1', isFeature)
if( typeof isFeature.getProperties === "function" ){
if (
Object.keys(isFeature?.getProperties()).length === 0 &&
isFeature.constructor === Object
) {
const url = `/mapDetailPopUp?layerNm=${encodeURIComponent(props.seletectedLayerNm)}`;
const width = window.screen.availWidth;
const height = window.screen.availHeight;
const left = 0;
const top = 0;
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`;
popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
} else {
const layerNm = getSelectedLayers(isFeature);
const safeProps = isFeature.getProperties();
const featureCd = getFeatureCd(safeProps, String(layerNm));
const url = `/mapFeaturePopup?layerNm=${encodeURIComponent(String(layerNm))}&featureCd=${encodeURIComponent(featureCd)} `;
const width =
String(layerNm) === "하수재이용시설"
? Math.floor(window.innerWidth * 0.8)
: Math.floor(window.innerWidth * 0.4); // 화면 폭의 40%
const height =
String(layerNm) === "하수재이용시설"
? Math.floor(window.innerHeight - 150)
: Math.floor(window.innerHeight - 450); // 150px 여백 확보; // calc(100% - 70px)
const left = (window.screen.availWidth - width) / 2;
const top = (window.screen.availHeight - height) / 2;
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
popupDetailRef.current = window.open(
url,
"지도 시설 상세정보 팝업",
popupOptions
);
}
}else{
console.log(' data확인 필요')
}
},
[checkedLayers, checkedLayersData, featureData] // 필요 시 의존성 넣기
@@ -246,6 +265,23 @@ const Map = () => {
}
};
const onOpenPopup = (data:any) => {
if(data){
const url = `/mapDetailPopUp?layerNm=${encodeURIComponent(data)}`;
const width = window.screen.availWidth;
const height = window.screen.availHeight;
const left = 0;
const top = 0;
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`;
popupRef.current = window.open(url, "지도 레이어 팝업", popupOptions);
}else{
toast.error('시설 및 수요처가 선택되지 않았습니다.')
}
}
return (
<div className="bg-container">
<Gis
@@ -255,7 +291,7 @@ const Map = () => {
isMiniMap={false}
searchView={undefined}
detailView={renderDetailView}
onFeatureSelect={onFeatureFocus}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
@@ -280,8 +316,12 @@ const Map = () => {
/>
}
waterDepth={undefined}
onOpenPopup={(data:any) =>{
onOpenPopup(data)
}}
/>
</div>
<Toaster position="top-center" />
</div>
);
};
@@ -30,8 +30,8 @@ export type LyrStngProps = {
export const mapService = {
//레이어 설정 조회
getLyrStngs: async () => {
const response = await fetch(`${API_URL}/lyrStngs/WR_NETWORK`, {
getLyrStngs: async (lyrGrp : string) => {
const response = await fetch(`${API_URL}/lyrStngs/${lyrGrp}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
@@ -16,7 +16,7 @@ export default function MapDetailPopUp() {
const renderConditionalContent = () => {
const layerId = searchParams.get("layerNm");
switch (layerId) {
case "하수재이용시설":
case "tb_wr_bdst_ruse_fclt":
return (
<RuseFacility
rtnFeature={(feature) => {
@@ -24,7 +24,7 @@ export default function MapDetailPopUp() {
}}
/>
);
case "하수처리시설":
case "tb_il_swage_prcs_fclt":
return (
<HasuFacility
rtnFeature={(feature) => {
@@ -32,7 +32,7 @@ export default function MapDetailPopUp() {
}}
/>
);
case "빗물이용시설":
case "tb_wr_rnwtr_utztn_fclt":
return (
<RainFacility
rtnFeature={(feature) => {
@@ -40,7 +40,7 @@ export default function MapDetailPopUp() {
}}
/>
);
case "중수도시설":
case "tb_wr_rswtr_fclt":
return (
<RswtFacility
rtnFeature={(feature) => {
@@ -48,7 +48,7 @@ export default function MapDetailPopUp() {
}}
/>
);
case "온배수시설":
case "tb_wr_htdfl_fclt":
return (
<HtdflFacility
rtnFeature={(feature) => {
@@ -56,7 +56,7 @@ export default function MapDetailPopUp() {
}}
/>
);
case "수요처":
case "tb_wr_ruse_fclt_dmd_co":
return (
<CoSttnMap
rtnFeature={(feature) => {
@@ -64,7 +64,7 @@ export default function MapDetailPopUp() {
}}
/>
);
case "잠재수요처":
case "tb_wr_tmpr_dmd_co":
return (
<LdrmntCoMap
rtnFeature={(feature) => {
@@ -78,7 +78,7 @@ export default function MapDetailPopUp() {
};
return (
<>
<div>{renderConditionalContent()}</div>
<div className="h-full overflow-auto">{renderConditionalContent()}</div>
</>
);
}
@@ -0,0 +1,70 @@
"use client";
import React from "react";
import { Label } from "@common/components/ui/label";
export const DRGT_LAYER_TYPES: string[] = [
// 수정 필요
'drought1',
'drought2',
'drought3',
'drought4',
'drought5',
'drought6',
];
// 지정된 순서에 따른 lyr_sn 배열
export const EXPRS_ORDERED_LAYERS: string[] = [
// 수정 필요
'106', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도
'107', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도
'108', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도
'109', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 100년 빈도
'110', // 도시침수지도(tb_ms_flood_dngr_map) - 30년 빈도
'111', // 도시침수지도(tb_ms_flood_dngr_map) - 50년 빈도
];
export function DrgtLegend() {
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>
</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" />
</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">
<Label className="text-xl text-gray-900 font-bold">관심</Label>
<LegendItem color="rgba(222,235,247, 1)" label="1회 미만" /> {/* 관심 - 1회 미만 */}
<LegendItem color="rgba(46, 117, 182, 1)" label="1회 이상" /> {/* 관심 - 1회 이상 */}
<Label className="text-xl text-gray-900 font-bold">주의</Label>
<LegendItem color="rgba(255, 242, 204, 1)" label="1회 미만" /> {/* 주의 - 1회 미만 */}
<LegendItem color="rgba(255, 217, 102, 1)" label="1회 이상" /> {/* 주의 - 1회 이상 */}
<Label className="text-xl text-gray-900 font-bold">관심 + 주의</Label>
<LegendItem color="rgba(251, 229, 214, 1)" label="1회 미만" /> {/* 관심+주의 - 1회 미만 */}
<LegendItem color="rgba(237, 125, 49, 1)" label="1회 이상" /> {/* 관심+주의 - 1회 이상 */}
</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,24 @@
import { boundingExtent, getCenter } from 'ol/extent';
import Feature from 'ol/Feature';
import Geometry from 'ol/geom/Geometry';
export function medianCoordinate2D(feature: Feature<Geometry>) {
// Feature를 입력 받아 중심 좌표를 계산하는 함수
if (!feature) {
console.warn("유효한 피처가 없습니다.");
return;
}
console.log(feature)
const geometry = feature.getGeometry();
if (!geometry) {
console.warn("피처에 geometry가 없습니다.");
return;
}
const extent = geometry.getExtent();
const center = getCenter(extent);
const centerExtent = boundingExtent([center]);
return centerExtent;
}
@@ -0,0 +1,88 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState, useCallback } from "react";
import { useSearchParams } from "next/navigation";
import { mapService } from "../../(map-layout)/service";
import { DrgtLegend, DRGT_LAYER_TYPES, EXPRS_ORDERED_LAYERS } from "./drgtLegend";
import { SearchBar } from "./searchBar";
export default function MapDrgtPopUp() {
const searchParams = useSearchParams();
const ctpvCd = searchParams.get('ctpvCd') || '';
const sggCd = searchParams.get('sggCd') || '';
const fromDate = searchParams.get('fromDate') || '';
const toDate = searchParams.get('toDate') || '';
const gisMaps = useRef<any | null>(null);
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [showDrgtLegend, setShowDrgtLegend] = useState(false); // DrgtLegend 표시 여부
const [isPlaying, setIsPlaying] = useState(false);
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchData();
}, []);
function fetchData() {
(async () => {
try {
const response = await mapService.getLyrStngs('WR_DROUGHT');
setTreeData(response); // API 응답 저장
console.log("Drought layers fetched:", response);
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
})();
}
// WaterDepth 표시 여부를 결정하는 함수
const handleDrgtLegndVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
const drgtLegndLayers = checkedNodesData.filter(node =>
DRGT_LAYER_TYPES.includes(node.data?.lyrTyNm)
);
const hasDrgtLegndLayer = drgtLegndLayers.length > 0;
setShowDrgtLegend(hasDrgtLegndLayer);
}, []);
// 체크된 레이어 변경 처리 함수
const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
handleDrgtLegndVisibility(checkedIds, checkedNodesData);
}, [handleDrgtLegndVisibility]);
return (
<div className="bg-container">
<Gis
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={undefined}
onFeatureSelect={undefined}
setIsOpen={setIsOpen}
setFeatureData={undefined}
featureData={undefined}
setIsSearchModalOpen={undefined}
isOpen={isOpen}
isSearchModalOpen={undefined}
onCheckedLayersChange={handleCheckedLayersChange}
imgVisble={true}
searchBar={<SearchBar gisMaps={gisMaps} ctpvCode={ctpvCd} sggCode={sggCd} fromDate={fromDate} toDate={toDate} />}
waterDepth={showDrgtLegend ? DrgtLegend : undefined}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying as any}
targetLayerTypes={DRGT_LAYER_TYPES as any}
orderedLayers={EXPRS_ORDERED_LAYERS as any}
checkInterval={2500}
height={`h-[100vh]`}
droughtDates={{ startYmd: fromDate, endYmd: toDate }}
/>
</div>
);
}
@@ -0,0 +1,173 @@
"use client";
import React, { useState, useEffect } from "react";
import { SelectBox } from '@common/business_components/ui'
import { Label, Button } from '@common/components';
import { type SelectDataType } from "@common/business_components/ui";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import GeoJSON from "ol/format/GeoJSON";
import Feature from "ol/Feature";
import Geometry from "ol/geom/Geometry";
import { medianCoordinate2D } from "./median";
interface CtpvSggPrcsFcltSelectBoxProps {
gisMaps: React.RefObject<any>;
ctpvCode: string;
sggCode: string;
fromDate: string;
toDate: string;
}
const DEFAULT_OPTION: SelectDataType = { val: '0', text: '전체' };
export function SearchBar({ gisMaps, ctpvCode, sggCode }: CtpvSggPrcsFcltSelectBoxProps) {
const [ctpvCd, setCtpvCd] = useState<string>(ctpvCode);
const [sggCd, setSggCd] = useState<string>(sggCode);
const [ctpv, setCtpv] = useState<SelectDataType[]>([]);
const [sgg, setSgg] = useState<SelectDataType[]>([]);
useEffect(() => {
(async () => {
await fetchSido();
await fetchSigungu();
setCtpvCd(ctpvCode);
setSggCd(sggCode);
})();
}, []);
function onViewMapClick() {
if (sggCd && sggCd !== '0') {
fetchSggData();
return;
}
if (ctpvCd && ctpvCd !== '0') fetchCtpvData();
}
async function fetchCtpvData() {
try {
const ctpvData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_ctpv&outputFormat=application%2Fjson&cql_filter=mctpv_cd=%27${ctpvCd.trim()}%27&srsName=EPSG:5179`)
.then(res => res.json());
console.log('시도 데이터', ctpvData.features[0]);
const format = new GeoJSON();
const feature: Feature<Geometry> = format.readFeature(ctpvData.features[0], {
dataProjection: "EPSG:5179", // GeoJSON 좌표계
featureProjection: GisApp.TO_PROJ // 지도 좌표계
});
console.log('feature', feature);
const extent = medianCoordinate2D(feature);
console.log('GisApp.TO_PROJ', GisApp.TO_PROJ);
console.log('extent', extent)
handleFeatureFocus(extent);
}
catch (error) {
console.error("시도 data fetching Error:", error);
}
}
async function fetchSggData() {
try {
const sggData = await fetch(`/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo%3Atb_ms_sgg&outputFormat=application%2Fjson&cql_filter=sgg_cd=%27${sggCd}%27&srsName=EPSG:5179`)
.then(res => res.json());
console.log('시군구 데이터', sggData.features[0]);
const format = new GeoJSON();
const feature: Feature<Geometry> = format.readFeature(sggData.features[0], {
dataProjection: "EPSG:5179", // GeoJSON 좌표계
featureProjection: GisApp.TO_PROJ // 지도 좌표계
});
const extent = medianCoordinate2D(feature);
handleFeatureFocus(extent);
}
catch (error) {
console.error("시군구 data fetching Error:", error);
}
}
function handleFeatureFocus(extent: any) {
console.log('ctpv', ctpv);
console.log('sgg', sgg);
const map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 11
});
};
const fetchSido = async () => {
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_ctpv&outputFormat=application/json&propertyName=mctpv_korn_nm,mctpv_cd`;
const res = await fetch(url).then(res => res.json());
const list = res.features.map((f: any) => ({
text: f.properties.mctpv_korn_nm,
val: f.properties.mctpv_cd,
}));
console.log('시도 데이터', list);
setCtpv(list);
};
const fetchSigungu = async () => {
const url = `/geoserver/hasudo/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=hasudo:tb_ms_sgg&outputFormat=application/json&propertyName=sgg_cd,sgg_korn_nm`;
const res = await fetch(url).then(res => res.json());
const list = res.features.map((f: any) => ({
text: f.properties.sgg_korn_nm,
val: f.properties.sgg_cd,
}));
console.log('시군구 데이터', list);
setSgg(list);
};
return (
<div className="flex items-center gap-4 bg-white min-h-[48px] min-w-[430px] px-5 py-3 rounded-lg shadow">
<div className={`flex items-center justify-center mr-8`}>
<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"
label=""
selectArray={[DEFAULT_OPTION, ...ctpv]}
value={ctpvCd}
onSelectValue={val => {
setCtpvCd(String(val));
setSggCd(String(0));
}}
selectContentClass="z-[101]"
/>
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">시군구</Label>
<SelectBox
className="w-48"
selectClass="w-48 rounded-lg bg-white truncate"
label=""
selectArray={
[DEFAULT_OPTION,
...sgg.filter(opt => String(opt.val).slice(0, 2) === String(ctpvCd).trim())
]
}
value={sggCd}
onSelectValue={val => { setSggCd(String(val)) }}
selectContentClass="z-[101]"
/>
</div>
<div className='flex flex-1 justify-end '>
<Button size="sm" onClick={() => onViewMapClick()}>조회</Button>
</div>
</div>
);
}
@@ -1,6 +1,6 @@
"use client";
import { useWrStore } from "@/app/store";
import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal";
import { RuseDetailPopup } from "@/components/ui/map/detailPopup/ruseDetailPopup";
import CoDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/coDatailModal";
import SttnDetailModal from "@/components/ui/modal/sttn/sttnDetailModal/sttnDetailModal";
import { Title, UiTable } from "@common/business_components/ui";
@@ -11,7 +11,6 @@ export default function MapFeaturePopup() {
const [isFeatureCd, setIsFeatureCd] = useState<any>(null);
const {
pbnMap,
ruseMapDetail,
rainMapDetile,
rswtMapDetile,
htdfMapDetile,
@@ -46,10 +45,8 @@ export default function MapFeaturePopup() {
useEffect(() => {
if (isFeatureCd && layerNm) {
let detail = {};
if (layerNm === "하수재이용시설") {
detail = ruseMapDetail(isFeatureCd);
setIsDetail(detail);
setIsDetail(isFeatureCd);
} else if (layerNm === "하수처리시설") {
pbnMapDetaile(isFeatureCd, isFeatureCd).then((res) => {
setIsDetail(res);
@@ -78,9 +75,9 @@ export default function MapFeaturePopup() {
return (
<>
{isDetail ? (
<div className="fixed inset-0 w-screen h-screen m-0 p-0 overflow-auto bg-white">
<div className="fixed inset-0 w-screen h-screen m-0 p-0 bg-white">
{layerNm === "하수재이용시설" ? (
<PbnstSttnModal data={isDetail} />
<RuseDetailPopup bscFctCd={isDetail} />
) : layerNm === "하수처리시설" ? (
<div style={{ overflowX: "hidden" }}>
<div className="flex justify-between items-center mb-4">
@@ -125,6 +122,7 @@ export default function MapFeaturePopup() {
: "HTB"
}
formObj={isDetail}
isMap={true}
/>
)}
</div>
@@ -0,0 +1,139 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
type SearchType = {
bscFctNm: string;
bscFctCd: string;
};
type thisProps = {
label: string | undefined;
initValue: string | undefined;
setValue: (searchNm?: string) => void;
setRtnData: (rtnData: any) => void;
searchList?: SearchType[];
};
export default function SearchInputText({
label = undefined,
initValue = undefined,
setValue = () => {},
setRtnData = () => {},
searchList = [],
}: thisProps) {
const [isOpen, setIsOpen] = useState(false);
const [textValue, setTextValue] = useState(initValue || "");
const [highlightIndex, setHighlightIndex] = useState<number>(-1);
const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setTextValue(value);
setValue(value);
setHighlightIndex(-1);
setIsOpen(value !== "");
},
[setValue]
);
const handleSuggestionClick = useCallback(
(searchNm: string) => {
setValue(searchNm);
setTextValue(searchNm);
setIsOpen(false);
setRtnData(searchList?.find((item) => item.bscFctNm === searchNm) || {});
},
[searchList, setRtnData, setValue]
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (!isOpen || searchList.length === 0) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setHighlightIndex((prev) => (prev + 1) % searchList.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setHighlightIndex((prev) =>
prev <= 0 ? searchList.length - 1 : prev - 1
);
} else if (e.key === "Enter") {
e.preventDefault();
if (searchList.length === 1 && searchList[0]) {
handleSuggestionClick(searchList[0].bscFctNm);
} else if (highlightIndex >= 0 && highlightIndex < searchList.length && searchList[highlightIndex]) {
handleSuggestionClick(searchList[highlightIndex].bscFctNm);
}
}
},
[highlightIndex, isOpen, searchList, handleSuggestionClick]
);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
wrapperRef.current &&
!wrapperRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div ref={wrapperRef} className="relative">
<input
type="text"
value={textValue || ""}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`${label}을(를) 입력하세요`}
onClick={() => {
setTextValue("");
setValue("");
setIsOpen(false);
}}
/>
{isOpen && (
<div className="absolute top-full left-0 z-50 mb-2 max-h-72 w-full overflow-auto rounded-md border border-gray-300 bg-white shadow-lg">
<div className="max-h-72">
{searchList.length > 0 ? (
searchList.map((item, index) => (
<div
key={index}
className={`w-full cursor-pointer overflow-auto px-5 py-3 text-left !text-xl ${
highlightIndex === index
? "bg-blue-100"
: "hover:bg-gray-100"
}`}
onClick={() => handleSuggestionClick(item.bscFctNm)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleSuggestionClick(item.bscFctNm);
}
}}
>
<span className="float-left">{item.bscFctNm}</span>
</div>
))
) : (
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
검색 결과가 없습니다.
</div>
)}
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,315 @@
"use client";
import Gis from "@common/business_components/ui/map/Gis";
import { useEffect, useRef, useState } from "react";
import { mapService } from "./service";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { SearchBar } from "./searchBar/searchBar";
import SelectedFcltList from "./selectedFcltList";
export type SubsInfo = {
ruseUtztnFcltNm: string; // 재이용시설명
ruseUtztnFcltCd: string; // 재이용시설 코드
}
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(true); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [selectDataPk, setSelectDataPk] = useState<Record<string, any>>({});
const [selectedFclts, setSelectedFclts] = useState<any[]>([]);
//적용 버튼 클릭 이벤트
const onApply = () => {
if (selectedFclts.length > 1) {
alert("재이용시설 공급처는 1곳만 선택할 수 있습니다.");
return;
}
const subsInfoList: SubsInfo[] = selectedFclts.map((fclt) => ({
ruseUtztnFcltNm: fclt.ruseUtztnFcltNm,
ruseUtztnFcltCd: fclt.pbnstRuseFcltId,
}));
if (!window.opener || typeof window.opener.postMessage !== "function") {
alert("부모 창을 찾을 수 없습니다. 팝업이 'noopener'로 열렸거나, 보안 정책으로 차단되었을 수 있어요.");
return;
}
window.opener.postMessage(
{
type: "SET_SUBS_INFO",
payload: subsInfoList[0], // 또는 필요한 데이터
},
"*"
);
window.close();
};
//트리 레이어 데이터 조회
const fetchTreeData = async () => {
try {
const response = await mapService.getLyrStngs();
setTreeData(response); // API 응답 저장
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
//하수처리시설코드로 재이용시설 목록 조회
const reuseData = async (ruseFcltPk: any) => {
try {
const response = await mapService.getPbnstRuseFclts(ruseFcltPk);
const data = response.data || [];
setSelectedFclts((prev) => {
const newItems = data.filter(
(item: any) =>
item &&
Object.keys(item).length > 0 &&
!prev.some((f) => f.pbnstRuseFcltId === item.pbnstRuseFcltId)
);
console.log(newItems);
return [...prev, ...newItems];
});
} catch (error) {
console.error("데이터 불러오기 실패:", error);
}
};
// 컴포넌트 마운트 시 1회 실행
useEffect(() => {
fetchTreeData();
}, []);
// 팝업 Map.tsx useEffect 안
useEffect(() => {
const origin = window.location.origin;
if (window.opener) {
window.opener.postMessage({ type: "REQUEST_INIT_SUBS_INFO" }, origin);
}
}, []);
useEffect(() => {
const origin = window.location.origin;
const handler = (event: MessageEvent) => {
// 동일 출처만 허용 (개발 중엔 임시로 주석처리하고 *로 테스트해도 됨)
if (event.origin !== origin) return;
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.type === "INIT_SUBS_INFO") {
// payload: [{ ruseUtztnFcltNm, ruseUtztnFcltCd }, ...]
const list = Array.isArray(data.payload) ? data.payload : [];
// selectedFclts 형태에 맞게 변환해서 반영
// (지금 onApply에서 pbnstRuseFcltId를 쓰므로 매핑 맞춰줍니다)
setSelectedFclts(list.map((x) => ({
ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "",
pbnstRuseFcltId: x.ruseUtztnFcltCd ?? "", // <- 기존 코드가 기대하는 키
})));
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
// 레이어 생성 후 초기화 처리
const onCheckedLayersChange = () => {
//수요처 ID
const urlParams = new URLSearchParams(window.location.search);
const dmdInfoId = urlParams.get("dmdInfoId");
//수요처 위치로 화면 이동
const maps = gisMaps.current;
const layer = GisApp.Module.layer as any;
const layerNm = "tb_wr_ruse_fclt_dmd_co";
const featureId = `${layerNm}.${dmdInfoId}`;
let foundFeature: any = null;
maps
.getLayers()
.getArray()
.forEach((layer: any) => {
if (layer.get("lyrTyNm") == layerNm) {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) {
foundFeature = feature;
return;
}
}
}
});
if (foundFeature) {
const geometry = foundFeature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
if (geometry.getType() === "Point") {
maps.getView().setCenter(geometry.getCoordinates());
maps.getView().setZoom(11);
} else {
maps.getView().fit(extent, {
duration: 300,
padding: [30, 30, 30, 30],
maxZoom: 16,
});
}
}
layer.highLightFeature(
[foundFeature],
{
id: GisApp.hlLayerId,
zIndex: 10,
opacity: 1,
selectable: true,
},
{ width: 30 }
);
}
const formatNumber = (val: any) => {
const num = Number(val);
return isNaN(num) ? "-" : num.toLocaleString();
};
// 재이용시설 오버레이
maps
.getLayers()
.getArray()
.forEach((layer: any) => {
if (layer.get("lyrTyNm") === "tb_wr_bdst_ruse_fclt") {
const source = layer.getSource?.();
if (source?.getFeatures) {
const features = source.getFeatures();
features.forEach((feature: any) => {
// 필요한 고유 ID 또는 속성 가져오기 (예: ftrIdn 또는 feature.getId())
const featureId =
feature.getId() || feature.get("ftrIdn") || Math.random();
// HTML 또는 React 오버레이 컴포넌트 구성
const overlayContent = `
<div class="rounded-md border border-gray-300 bg-white shadow-md text-sm w-[120px]">
<!-- 헤더 -->
<div class="bg-cyan-700 text-white px-3 py-2 text-[11px] font-semibold text-center rounded-t-md">
${feature.get("swtrmt_fclt_nm") || "-"}
</div>
<!-- 테이블 -->
<table class="w-full text-sm text-gray-800 border-t border-gray-200">
<tbody>
<tr class="border-b border-gray-200">
<th class="bg-gray-100 text-gray-600 font-semibold text-[10px] text-left">용량(m³/일)</th>
<td class="text-right text-[10px] font-bold">${formatNumber(feature.get("fclt_cpct"))}</td>
</tr>
</tbody>
</table>
</div>
`;
feature.set("html", overlayContent);
feature.set("overLayId", `overLay.${featureId}`);
});
}
}
});
};
// 검색해서 재이용시설 선택하면 해당 시설로 화면 이동 처리
useEffect(() => {
if (!selectDataPk || Object.keys(selectDataPk).length === 0) return;
const maps = gisMaps.current;
let foundFeature: any = null;
const layerNm = "tb_wr_bdst_ruse_fclt";
const featureId = `${layerNm}.${selectDataPk.bscFctCd}`;
maps
.getLayers()
.getArray()
.forEach((layer: any) => {
if (layer.get("lyrTyNm") == layerNm) {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) {
foundFeature = feature;
return;
}
}
}
});
if (foundFeature) {
const geometry = foundFeature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
maps.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", selectDataPk);
}
}, [selectDataPk]);
// 재이용시설 클릭 시 선택 시설목록에 추가
const onFeatureSelect = (data: any) => {
// 처리시설코드
const swtrmtFcltCd = data.get("swtrmt_fclt_cd");
reuseData(swtrmtFcltCd);
};
return (
<>
<div className="bg-container">
<Gis
height='h-screen'
treeData={treeData}
isSearch={false}
gisMaps={gisMaps}
isMiniMap={false}
searchView={undefined}
detailView={undefined}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={onCheckedLayersChange}
imgVisble={false}
searchBar={
<SearchBar
setHasuPk={(hasuPk: any) => {
if (hasuPk) setSelectDataPk(hasuPk);
}}
onApply={onApply}
/>
}
waterDepth={undefined}
/>
</div>
<SelectedFcltList
data={selectedFclts}
onRemove={(updatedList) => setSelectedFclts(updatedList)}
/>
</>
);
};
export default Map;
@@ -0,0 +1,118 @@
"use client";
import { useWrStore } from "@/app/store";
import {
SelectBox,
type SelectDataType,
} from "@common/business_components/ui";
import { Button, Label } from "@common/components/ui";
import { useCallback, useEffect, useState } from "react";
import SearchInputText from "../SearchInputText/SearchInputText";
export function SearchBar({
setHasuPk,
onApply,
}: {
setHasuPk: (hasuPk: any) => void;
onApply: () => void;
}) {
const { map, code, codeInit, findRuseFclt } = useWrStore(
(state) => state
);
const [sidoValue, setSidoValue] = useState<string>("");
const [sigunValue, setSigunValue] = useState<string>("");
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([
{ text: "전체", val: "0" },
]);
useEffect(() => {
//codeInit();
}, [codeInit]);
const initializeSigunData = useCallback(
(sidoCd: string) => {
if (sidoCd === "0") {
setSigunValue("0");
setFilteredSigunData([{ text: "전체", val: "0" }]);
} else {
const newFilteredData = [
{ text: "전체", val: "0" },
...code.sggCd.filter(
(item: SelectDataType) => item.val.substring(0, 2) === sidoCd
),
];
setSigunValue("0");
setFilteredSigunData(newFilteredData);
}
},
[code.sggCd]
);
const onSelectValue = useCallback(
(val: string, type: string) => {
switch (type) {
case "ctpvCd":
initializeSigunData(val);
setSidoValue(val);
break;
case "sggCd":
setSigunValue(val);
}
},
[initializeSigunData]
);
return (
<div className="flex justify-center w-full px-4 py-3">
<div className="flex items-center w-full max-w-screen-lg rounded-xl bg-white shadow-md px-6 py-4 space-x-3">
<SelectBox
labelClass="w-[40px]"
className="w-[200px]"
label="시도"
selectClass="rounded-lg bg-white"
selectArray={code.cityCd || []}
onSelectValue={(val) => onSelectValue(val.toString(), "ctpvCd")}
value={sidoValue}
/>
<SelectBox
labelClass="w-[65px]"
className="w-[200px]"
label="시군구"
selectClass="rounded-lg bg-white"
selectArray={filteredSigunData || []}
onSelectValue={(val) => onSelectValue(val.toString(), "sggCd")}
value={sigunValue}
/>
<Label className="text-gray-600">하수재이용시설</Label>
<SearchInputText
label="시설명"
initValue={undefined}
setValue={(searchNm?: string) => {
let sggOrCity = "";
if (searchNm) {
if (sigunValue === "0" || sigunValue === "") {
sggOrCity = sidoValue;
} else {
sggOrCity = sigunValue;
}
findRuseFclt(sggOrCity, searchNm);
}
}}
setRtnData={(rtnData: any) => {
setHasuPk(rtnData);
}}
searchList={map.ruseFcltList || []}
/>
<Button
color="sky"
className="ml-2 text-white"
size="sm"
onClick={() => {
onApply();
}}
>
적용
</Button>
</div>
</div>
);
}
@@ -0,0 +1,81 @@
import React from "react";
type FcltItem = {
pbnstRuseFcltId: string;
ruseUtztnFcltNm: string;
splyExtraCpct?: number;
ruseCpct?: number;
[key: string]: any;
};
export default function SelectedFcltList({
data = [],
onRemove,
}: {
data: FcltItem[];
onRemove: (updatedList: FcltItem[]) => void;
}) {
const handleDelete = (code: string) => {
const updatedList = data.filter((item) => item.pbnstRuseFcltId !== code);
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">
선택된 시설
</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm text-gray-800 border border-gray-200">
{TableContent}
</table>
</div>
</div>
);
}
@@ -0,0 +1 @@
export * from './map'
@@ -0,0 +1,60 @@
/**
* 관망도 API
*/
import { DEV_URL_WR, DEV_URL_SN } from "@/components/lib/config";
export type LyrStngProps = {
lyr_sn: string
lyr_nm: string
upper_lyr_sn: string
drawOrdr: string
flterSql: string
gobjTy: string
lyrTpNm: string
useYn: string
minViewSize: string
iconNm: string
lblAtrbNm: string
fontNm: string
fontIntnColor: string
fontExtnColor: string
fontExtnWdth: string
lnWdth: string
lnColor: string
polgnColor: string
initEprssYn: string
clickPsblYn: string
}
export const mapService = {
//레이어 설정 조회
getLyrStngs: async () => {
const response = await fetch(`${DEV_URL_SN}/v1/map/lyrStngs/DM_NETWORK`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
},
getPbnstRuseFclts: async (ruseFcltPk:any) => {
const response = await fetch(`${DEV_URL_WR}/reuseFc/pubFc/${ruseFcltPk}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
}
@@ -9,7 +9,7 @@ import type { objSearchType } from "@/components/ui/modal/fcltReg/bdstRegModal/b
import { ContentsDiv, DateMonthPicker, downloadExcel, SearchDiv, SearchFormLeft, SearchFormRight, SelectBox, Title, UiModal, UiTable, type SelectDataType } from "@common/business_components/ui";
import ListDiv from "@common/business_components/ui/list_div";
import { Button, Label, ScrollArea } from "@common/components/ui";
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { toast, Toaster } from "react-hot-toast";
export default function DrgtSttn() {
@@ -26,90 +26,91 @@ export default function DrgtSttn() {
isOpen: boolean;
data: customDrgt[] | undefined;
}>({ data: undefined, isOpen: false });
const popupRef = useRef<Window | null>(null);
const cellClick = (index: number) => {
if (fromDate && toDate && drgtRes[index]) {
drgtModalSearch(fromDate, toDate, drgtRes[index].sggNm ?? '');
} else {
toast.error("조회기간을 선택해주세요.");
}
const cellClick = (index: number) => {
if (fromDate && toDate && drgtRes[index]) {
drgtModalSearch(fromDate, toDate, drgtRes[index].sggNm ?? '');
} else {
toast.error("조회기간을 선택해주세요.");
}
}
const excelClick = () => {
downloadExcel({
data: excelData ?? [],
fileName: "우리동네가뭄 현황",
sheetNameArray: ["Sheet1"],
headerRange: "A1:G1"
})
const excelClick = () => {
downloadExcel({
data: excelData ?? [],
fileName: "우리동네가뭄 현황",
sheetNameArray: ["Sheet1"],
headerRange: "A1:G1"
})
}
useEffect(() => {
if (!drgt.isInit) {
drgtInit();
}
}, [drgt.isInit, drgtInit]);
useEffect(() => {
if (!drgt.isInit) {
drgtInit();
useEffect(() => {
let resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes];
resData = resData.map((item) => {
if (item.sggNm === '세종시') {
return {
...item,
ctpvCd: '세종특별자치시',
sggNm: ''
}
}, [drgt.isInit, drgtInit]);
}
useEffect(() => {
let resData = Array.isArray(drgtRes) ? drgtRes : [drgtRes];
resData = resData.map((item) => {
if (item.sggNm === '세종시') {
return {
...item,
ctpvCd: '세종특별자치시',
sggNm: ''
}
}
return {
...item,
ctpvCd: code.cityCd.find(ct => (ct.text == item.sggNm || (code.sggCd.find(sgg => sgg.text == item.sggNm)?.val.substring(0, 2) == ct.val)))?.text,
sggNm: code.sggCd.find(sgg => sgg.text == item.sggNm)?.text
}
})
const data = Array.isArray(resData)
? resData.map((item: customDrgt) => {
return {
"시도": item.ctpvCd,
"시군구": item.sggNm,
"정상(횟수)": item.normal,
"관심(횟수)": item.attention,
"주의(횟수)": item.caution,
"관심+주의(횟수)": item.warning,
"가뭄순위": item.ranking,
}
}) : [];
setExcelData(data)
setTableData(resData)
}, [drgtRes])
useEffect(() => {
if (drgtMoRes && drgtMoRes.length > 0) {
setDrgtModal({ isOpen: true, data: drgtMoRes });
return {
...item,
ctpvCd: code.cityCd.find(ct => (ct.text == item.sggNm || (code.sggCd.find(sgg => sgg.text == item.sggNm)?.val.substring(0, 2) == ct.val)))?.text,
sggNm: code.sggCd.find(sgg => sgg.text == item.sggNm)?.text
}
})
const data = Array.isArray(resData)
? resData.map((item: customDrgt) => {
return {
"시도": item.ctpvCd,
"시군구": item.sggNm,
"정상(횟수)": item.normal,
"관심(횟수)": item.attention,
"주의(횟수)": item.caution,
"관심+주의(횟수)": item.warning,
"가뭄순위": item.ranking,
}
}, [drgtMoRes]);
}) : [];
setExcelData(data)
setTableData(resData)
}, [drgtRes])
const getSelCity = (val: string | number) => {
const cityValue = String(val);
useEffect(() => {
if (drgtMoRes && drgtMoRes.length > 0) {
setDrgtModal({ isOpen: true, data: drgtMoRes });
}
}, [drgtMoRes]);
if (cityValue === "0") {
setSelectSgg([{ val: '0', text: '전체' }]);
setSelectedSgg('0');
setObjSearch({ city: cityValue, sgg: '0' });
} else {
const data = getCity(cityValue, code.sggCd);
setSelectSgg(data);
const getSelCity = (val: string | number) => {
const cityValue = String(val);
const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? "");
setSelectedSgg(firstSggVal);
setObjSearch({ city: cityValue, sgg: firstSggVal });
}
};
if (cityValue === "0") {
setSelectSgg([{ val: '0', text: '전체' }]);
setSelectedSgg('0');
setObjSearch({ city: cityValue, sgg: '0' });
} else {
const data = getCity(cityValue, code.sggCd);
setSelectSgg(data);
const getSgg = (val: string | number) => {
setSelectedSgg(String(val));
setObjSearch({ ...objSearch, sgg: String(val) });
};
const firstSggVal = data.find(item => item.val === "0")?.val || (data[0]?.val ?? "");
setSelectedSgg(firstSggVal);
setObjSearch({ city: cityValue, sgg: firstSggVal });
}
};
const getSgg = (val: string | number) => {
setSelectedSgg(String(val));
setObjSearch({ ...objSearch, sgg: String(val) });
};
useEffect(() => {
if (fromDate === undefined && toDate === undefined) {
@@ -121,6 +122,19 @@ export default function DrgtSttn() {
setToDate(formatted);
}
}, [fromDate, toDate])
function onViewMapClick() {
const params = new URLSearchParams({
ctpvCd: String(objSearch.city),
sggCd: String(objSearch.sgg),
fromDate: fromDate || '',
toDate: toDate || ''
});
const popupOptions = `width=${window.screen.availWidth},height=${window.screen.availHeight},left=0,top=0,resizable=yes,scrollbars=yes`;
popupRef.current = window.open(`/mapDrgtPopUp?${params.toString()}`, "가뭄 지도 보기 팝업", popupOptions);
}
return (
<>
<ContentsDiv>
@@ -136,7 +150,7 @@ export default function DrgtSttn() {
selectClass={""}
selectArray={code.cityCd || []}
onSelectValue={getSelCity}
value={objSearch?.city ||code.cityCd[0]?.val ||""}
value={objSearch?.city || code.cityCd[0]?.val || ""}
/>
<SelectBox
className="col-span-2 w-1/6 h-full text-2xl ml-8"
@@ -144,7 +158,7 @@ export default function DrgtSttn() {
selectClass={""}
selectArray={selectSgg || [{ val: '0', text: '전체' }]}
onSelectValue={getSgg}
value={selectedSgg ||'0'}
value={selectedSgg || '0'}
/>
<div className="col-span-5 w-auto">
<div className="flex flex-row items-center">
@@ -156,7 +170,7 @@ export default function DrgtSttn() {
onChange: (date: string | undefined) => {
const year = date ? date.slice(0, 4) : '';
let month = date?.slice(4);
if (month&&month?.length < 2) {
if (month && month?.length < 2) {
month = month?.padStart(2, "0");
}
const formatted = `${year}${month}`;
@@ -172,7 +186,7 @@ export default function DrgtSttn() {
onChange: (date: string | undefined) => {
const year = date ? date.slice(0, 4) : '';
let month = date?.slice(4);
if (month&&month.length < 2) {
if (month && month.length < 2) {
month = month?.padStart(2, "0");
}
const formatted = `${year}${month}`;
@@ -195,23 +209,23 @@ export default function DrgtSttn() {
let sidoText: string = '';
let sggText: string | string[] = '';
if (objSearch?.city.toString().length > 0) {
sidoText = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? '';
}
if (objSearch?.sgg.toString().length > 1) {
sggText = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? '';
} else {
sggText = code.sggCd.filter((item) => item.val.substring(0, 2) === objSearch?.city).map((item) => item.text);
if (objSearch?.city.toString().length > 0) {
sidoText = code.cityCd.find((item) => item.val === objSearch?.city)?.text ?? '';
}
if (objSearch?.sgg.toString().length > 1) {
sggText = code.sggCd.find((item) => item.val === objSearch?.sgg)?.text ?? '';
} else {
sggText = code.sggCd.filter((item) => item.val.substring(0, 2) === objSearch?.city).map((item) => item.text);
}
if (sidoText === '전체') { sidoText = '' }
}
if (sidoText === '전체') { sidoText = '' }
if (sidoText === '세종특별자치시') {
sidoText = '세종시';
sggText = sidoText;
}
if (sidoText === '세종특별자치시') {
sidoText = '세종시';
sggText = sidoText;
}
const searchArr:string[] = [];
const searchArr: string[] = [];
if (sidoText) searchArr.push(sidoText);
if (Array.isArray(sggText)) {
searchArr.push(...sggText);
@@ -234,7 +248,7 @@ export default function DrgtSttn() {
<Button
size={"sm"}
className={btnClass}
onClick={() => { }}
onClick={onViewMapClick}
>
지도보기
</Button>
-32
View File
@@ -1,32 +0,0 @@
import { cmsPostFectch } from "@/components/lib/api";
export const isLogin = async () => {
const headers = {
accept: "application/json",
"Content-Type": "application/json",
"X-Site-Id": "WR",
"Access-Control-Expose-Headers" : ["Authorization","refresh-token","token-id"]
};
const param = { userId: "tuser10003", password: "test1234!" };
await cmsPostFectch("/user-service/login",param,headers);
};
// export type LoginState = {
// login: {} as any;
// };
// export type loginType = LoginState & LoginActions;
// export const loginState: LoginState = {
// login: {},
// };
// export type LoginActions = {
// isLogin: () => void;
// };
// export const loginActionExport: (set: any) => LoginActions = (set: any) => {
// return {
// isLogin: async () => {},
// };
// };
+42 -1
View File
@@ -1,4 +1,4 @@
import { fetchType, getFetch } from "@/components/lib/api";
import { fetchType, getFetch, postFectch } from "@/components/lib/api";
import type { CommonTableProps } from "../../common/type/header";
import type { WaterReuseFacility } from "../../ruseFcltMng/fcltSttn/pbnstSttn/type";
import {
@@ -16,6 +16,7 @@ export type ruseMapState = {
ruseMapCount: number;
isInit: boolean;
fclts: string[];
ruseDetailList: customPbnst[];
ruseMapHeadList: CommonTableProps[];
ruseMapNodeList: CommonTableProps[];
};
@@ -31,14 +32,17 @@ type ruseMapActions = {
fclts: string[]
) => void;
ruseMapDetail: (ruseFcltId: string) => Promise<customPbnst | undefined>;
getRuseMaplList: (bscFctCd: string) => void;
};
export type ruseMapType = ruseMapState & ruseMapActions;
export const ruseMapStateExport: ruseMapState = {
ruseMap: {
isInit: false,
ruseMapRes: [],
ruseDetailList: [],
fclts: [],
ruseMapCount: 1,
ruseMapHeadList: pbnSttHeadListExport,
@@ -107,5 +111,42 @@ export const ruseMapActionsExport: (set: any) => ruseMapActions = (
throw errorHasu; // 호출부에서 catch 가능하도록
}
},
getRuseMaplList: async (bscFctCd: string) => {
try {
const ruseDetailList = await getFetch(
fetchType.WR,
`/reuseFc/pubFc/${bscFctCd}`
);
const year = new Date().getFullYear();
const data = [bscFctCd];
const rusePrfList = await postFectch(
`/reuseFc/pubDetailList/${year}/0/1/10`,
data,
fetchType.WR
);
const dataList = ruseDetailList.map((ruse) => {
const matchRuse = rusePrfList?.data.find(
(prf) => prf.pbnstRuseFcltId === ruse.pbnstRuseFcltId
);
return {
...ruse,
prfList: matchRuse.prfList,
};
});
console.log("/dataList ", dataList);
set((state: ruseMapState) => {
return {
...state,
ruseMap: {
...state.ruseMap,
ruseDetailList: dataList,
},
};
});
} catch (errorHasu) {
console.log(errorHasu);
throw errorHasu; // 호출부에서 catch 가능하도록
}
},
};
};
+93
View File
@@ -0,0 +1,93 @@
import { getFetch } from "@/components/lib/api";
import type { FacilityInfo, SubsInfo } from "./type";
import { DEV_URL_FO } from "@/components/lib/config";
const checkWrRegList = async () => {
const wrRegList = await getFetch("wr", `/reuseFc/pubFc`);
return wrRegList;
};
const getRuseFclt = async (
page: string,
sggOrCity: string,
searchTxt?: string
) => {
const checkWrRegListRes = await checkWrRegList();
let url = `/prcsFclt/ruseInfo/${page ? page : 1}/5/A/${sggOrCity ? sggOrCity : "0"}/0/1/SW`;
const ruseList = checkWrRegListRes.filter((item: string) => item !== "");
if (searchTxt) {
url = url + `/${searchTxt}`;
}
const response = await fetch(DEV_URL_FO + url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(ruseList),
});
const hasuList = await response.json().then((res: any) => {
const data = res.data
.map((item: any) => {
return {
...item,
fctCpc: Number(item.fctCpc) || 0,
searchNm: item.fctNm,
searchPk: item.bscFctNm,
};
})
.filter((hasu: any) => ruseList.find((ruse: any) => hasu.bscFctCd === ruse));
return { data: data, count: res.count };
});
return hasuList;
};
export type DmMapActions = {
findRuseFclt(sggOrCity: string, searchTxt: string): void;
setSubsInfo(subsInfo: SubsInfo[]): void;
};
export type DmMapType = {
map: {
ruseFcltList: FacilityInfo[];
subsInfo: SubsInfo[];
};
};
export const dmMapState: DmMapType = {
map: {
ruseFcltList: [],
subsInfo: []
},
};
export type DmMapStore = DmMapType & DmMapActions;
export const DmMapReducer = (set: any): DmMapActions => {
return {
findRuseFclt: async (sggOrCity: string, searchTxt: string) => {
const ruseFcltList = await getRuseFclt("1", sggOrCity, searchTxt);
set((state: DmMapType) => ({
...state,
map: {
...state.map,
ruseFcltList: ruseFcltList.data,
},
}));
},
setSubsInfo: async (subsInfo: SubsInfo[]) => {
set((state: DmMapType) => ({
...state,
map: {
...state.map,
subsInfo: subsInfo,
},
}));
},
};
};
+39
View File
@@ -0,0 +1,39 @@
export type FacilityInfo= {
bscFctCd: string; // 기본시설 코드
bscFctNm: string; // 기본시설명
rvbsnCd: string; // 유역 코드
rvbsn: string; // 유역명
lgbsn: string; // 대권역명
mdmbsn: string; // 중권역명
rgnCd: string; // 관할청 코드
rgnNm: string; // 관할청 명칭
adstDstrCd: string; // 행정구역 코드
sido: string; // 시도
gugun: string; // 구군
addr: string; // 주소
fctCpc: string; // 시설용량
efflWqltStndrdCd: string; // 방류수 수질기준 코드
efflWqltStndrdNm: string; // 방류수 수질기준명
trtFrfNm: string; // 처리공법명
trtDstr: string; // 처리구역
rnDt: string; // 사용개시일 (예: YYYYMMDD)
cmplDt: string; // 준공일자 (예: YYYYMMDD)
trstOprEtpNm: string; // 위탁운영기관명
bscFctClsNm: string; // 기본시설 구분명
sitArea: string; // 부지면적
trtDstrArea: string; // 처리구역 면적
swrTrtPopl: string; // 하수처리 인구
mnstr: string; // 총괄 유역
efflWtrrgn: string; // 방류수역
rvNm: string; // 하천명
bscFctClsCd: string; // 기본시설 구분 코드
rvbsnNm: string; // 유역명
tel: string; // 전화번호
rswtrYn: string; // 재이용수 여부 ("Y" 또는 "N")
ruse: boolean; // 재이용 여부 (boolean)
}
export type SubsInfo={
ruseUtztnFcltNm: string; // 재이용시설명
ruseUtztnFcltCd: string; // 재이용시설 코드
}
@@ -231,6 +231,30 @@ export const fcBizSchema = z.object({
mdfr: z.string().nullish(),
});
const PrfSchema = z.object({
pbnstRuseFcltId: z.string(),
ym: z.string(), // 예: "202310"
wshwtr: z.number(),
coolUswtr: z.number(),
cleanUswtr: z.number(),
drwtUswtr: z.number(),
dltnUswtr: z.number(),
ithlRswtr: z.number(),
ithlEtcUswtr: z.number(),
cleanToilUswtr: z.number(),
resn: z.number(),
lndwtr: z.number(),
hydUswtr: z.number(),
rvrKeepUswtr: z.number(),
farmUswtr: z.number(),
ugwtChr: z.number(),
indstUswtr: z.number(),
regDt: z.date().default(new Date()).nullish(),
rgtr: z.string().nullish(),
mdfcnDt: z.date().default(new Date()).nullish(),
mdfr: z.string().nullish(),
});
export const pbnstRegZod = z.object({
pbnstRuseFcltId: z.string().default(""),
swtrmtFcltId: z.string().default(""),
@@ -258,11 +282,14 @@ export const pbnstRegZod = z.object({
fcBizs: z.array(fcBizSchema).default([]),
fcOpers://z.array(fcOperSchema).default([]),
z .array(fcOperSchema)
.min(1, { message: "재이용시설의 운영정보를 최소 하나를 등록해주세요" })
.min(1, { message: "재이용시설의 운영정보를 최소 하나를 등록해주세요" }),
prfList: z.array(PrfSchema).nullish(),
});
export const pbnstRegArrayZod = z.array(pbnstRegZod);
export const pbnstRegArrayZod = z.array(pbnstRegZod);
export type PbnstRegArrrayType = z.infer<typeof pbnstRegArrayZod>;
export type fcBizType = z.infer<typeof fcBizSchema>;
+10 -1
View File
@@ -161,7 +161,6 @@ import {
type rswtStatState,
type rswtStatType,
} from "./stats/rswtStat";
//import { isLogin } from "./common/login";
import { LoginApi } from "@/src/entities/auth";
import {
authActionsExport,
@@ -217,6 +216,12 @@ import {
type ldrmntMapState,
type ldrmntMapType,
} from "./map/ldrmntCoMap";
import {
DmMapReducer,
dmMapState,
type DmMapType,
type DmMapStore,
} from "./map/splyPop";
export type WrStore = comCodeType &
pubFacType &
@@ -252,6 +257,7 @@ export type WrStore = comCodeType &
ruseMapType &
coSttnMapType &
ldrmntMapType &
DmMapStore &
authType;
export const StoreZus = (
@@ -289,6 +295,7 @@ export const StoreZus = (
ruseMapState &
coSttnMapState &
ldrmntMapState &
DmMapType &
authState = {
...comCodeStateExport,
...pubFacStateExport,
@@ -325,6 +332,7 @@ export const StoreZus = (
...ruseMapStateExport,
...coSttnMapStateExport,
...ldrmntMapStateExport,
...dmMapState,
}
) => {
return createStore<WrStore>()((set) => {
@@ -372,6 +380,7 @@ export const StoreZus = (
...ruseMapActionsExport(set),
...coSttnMapActionsExport(set),
...ldrmntMapActionsExport(set),
...DmMapReducer(set),
};
});
};
+72 -51
View File
@@ -1,28 +1,39 @@
//8880
import { AUTH_USER_ID, CLAIM_NAME } from "@/src/shared/config";
import { DEV_URL_FO, DEV_URL_WR, LOCAL_URL } from "../config/";
import { getCookie } from "@common/components/lib/cookie";
export const fetchType = { COMMON: "common", WR: "wr", LOCAL: "LOCAL" , CS: "CS" };
export const fetchType = {
COMMON: "common",
WR: "wr",
LOCAL: "LOCAL",
CS: "CS",
};
export const getFetch = async (type: string, url: string) => {
const div =
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
? ''
: DEV_URL_WR;
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
? ""
: DEV_URL_WR;
const tokenId = getCookie(AUTH_USER_ID);
const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "GET",
headers: {
accept: "application/json",
AUTH_USER_ID: tokenId ?? "",
CLAIM_NAME: Authorization ?? "",
},
});
if (!response.ok) {
console.log(`오류 발생: ${response.status}`);
console.log(`오류 발생: ${response.status}`);
}
const contentType = response.headers.get("content-type") ?? "";
@@ -30,7 +41,10 @@ export const getFetch = async (type: string, url: string) => {
// JSON 응답 처리
const res = await response.json();
return res.data;
} else {
} else if(contentType.includes('application/octet-stream')) {
// 파일 응답 처리 (ex. PDF, Excel 등)
console.log('contentType ' ,contentType)
// 파일 응답 처리 (ex. PDF, Excel 등)
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
@@ -41,30 +55,36 @@ export const getFetch = async (type: string, url: string) => {
a.download = filename;
a.click();
window.URL.revokeObjectURL(blobUrl);
return null; // 다운로드이므로 반환값 없음
}
}else{
throw Error('오류 발생')
}
};
export const getFetchWithCount = async (type: string, url: string) => {
const div =
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
? ''
: DEV_URL_WR;
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
? ""
: DEV_URL_WR;
const tokenId = getCookie(AUTH_USER_ID);
const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "GET",
headers: {
accept: "application/json",
AUTH_USER_ID: tokenId ?? "",
CLAIM_NAME: Authorization ?? "",
},
});
if (!response.ok) {
console.log(`오류 발생: ${response.status}`);
console.log(`오류 발생: ${response.status}`);
}
const contentType = response.headers.get("content-type") ?? "";
@@ -73,10 +93,11 @@ export const getFetchWithCount = async (type: string, url: string) => {
// JSON 응답 처리
const res = await response.json();
return res;
} else {
} else if(contentType.includes('application/octet-stream')) {
// 파일 응답 처리 (ex. PDF, Excel 등)
console.log('contentType ' ,contentType)
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
@@ -86,19 +107,25 @@ export const getFetchWithCount = async (type: string, url: string) => {
a.download = filename;
a.click();
window.URL.revokeObjectURL(blobUrl);
return null; // 다운로드이므로 반환값 없음
}
}else{
throw Error('오류 발생')
}
};
export const postFectch = async (url: string, params: any, type?: string) => {
const div = type === fetchType.COMMON ? DEV_URL_FO : DEV_URL_WR;
const tokenId = getCookie(AUTH_USER_ID);
const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "POST",
headers: {
accept: "application/json",
"Content-Type": "application/json",
AUTH_USER_ID: tokenId ?? "",
CLAIM_NAME: Authorization ?? "",
},
body: JSON.stringify(params),
});
@@ -108,39 +135,25 @@ export const postFectch = async (url: string, params: any, type?: string) => {
return await response.json();
};
export const cmsPostFectch = async (url: string, params: any,headers:any) => {
const response = fetch(url, {
method: "POST",
headers:headers ,
body: JSON.stringify(params),
}).then((res) =>
{
console.log(' res ' , res)
console.log(' access-token ' , res.headers.get('access-token'))
console.log(' refresh-token ' , res.headers.get('refresh-token'))
console.log(' token-id ' , res.headers.get('token-id'))
})
return await response;
};
export const putFectch = async (url: string, params: any,type?:string) => {
export const putFectch = async (url: string, params: any, type?: string) => {
const div =
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
? ''
: DEV_URL_WR;
? ""
: DEV_URL_WR;
const tokenId = getCookie(AUTH_USER_ID);
const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
AUTH_USER_ID: tokenId ?? "",
CLAIM_NAME: Authorization ?? "",
},
body: JSON.stringify(params),
});
@@ -150,25 +163,33 @@ export const putFectch = async (url: string, params: any,type?:string) => {
return response;
};
export const deleteFectch = async (url: string, params?: any, type?: string) => {
export const deleteFectch = async (
url: string,
params?: any,
type?: string
) => {
const div =
type === fetchType.COMMON
? DEV_URL_FO
: type === fetchType.LOCAL
? LOCAL_URL
: type === fetchType.CS
? ''
: DEV_URL_WR;
? ""
: DEV_URL_WR;
const tokenId = getCookie(AUTH_USER_ID);
const Authorization = getCookie(CLAIM_NAME);
const response = await fetch(div + url, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
AUTH_USER_ID: tokenId ?? "",
CLAIM_NAME: Authorization ?? "",
},
body: params?JSON.stringify(params):undefined,
body: params ? JSON.stringify(params) : undefined,
});
if (!response.ok) {
throw new Error(`오류 발생: ${response.status}`);
}
return await response.text();
};
return await response.text();
};
+2
View File
@@ -4,6 +4,7 @@ import {
SERVER_API_CS_URL,
SERVER_API_FO_URL,
SERVER_API_WR_URL,
SERVER_SEWERAGE_API_URL,
} from "@/src/shared/config";
//const DEV_PORT = "11080";
@@ -19,6 +20,7 @@ import {
export const LOCAL_URL_WR = PROXY_HOST + `/api/v1`;
export const DEV_URL_WR = SERVER_API_WR_URL;
export const DEV_URL_FO = SERVER_API_FO_URL;
export const DEV_URL_SN = SERVER_SEWERAGE_API_URL;
export const LOCAL_URL = PROXY_HOST;
export const CS_URL = SERVER_API_CS_URL;
export const CS_TUS_UPLOAD = SERVER_API_CS_TUS_URL;
@@ -50,6 +50,10 @@ export default function ConCostForm({
const ref = useRef<boolean>(false);
const [checkFinish, setCheckFinish] = useState<boolean>(false);
const [lastSub, setLastSub] = useState<{
ruseUtztnFcltNm: string;
ruseUtztnFcltCd: string;
} | null>(null);
useEffect(() => {
if (!ref.current) {
@@ -65,6 +69,18 @@ export default function ConCostForm({
setCheckFinish(false);
}, [selectionVersion]);
//공급처 팝업 값 셋팅
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === "SET_SUBS_INFO") {
setLastSub(event.data.payload);
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [setLastSub]);
const zipDowonLoad = () => {
fetch(`/2024_constarctFeeProgram.zip`, {}).then(async (response) => {
const blobRet = await response.blob();
@@ -80,7 +96,7 @@ export default function ConCostForm({
const dmndQtyWatch = form.watch("dmndQty");
const totalConstCost = form.watch("totalConstCost");
const cstrnCst = form.watch("demandCst.cstrnCst");
const splyList = form.watch("splyCndtList");
const calculateConstCost = useCallback(() => {
const usgPrcsProcs = usgPrcsProcsWatch;
if (!dmndQtyWatch) {
@@ -157,8 +173,11 @@ export default function ConCostForm({
return;
}
const splycd = form.getValues("splyCndtList")
const pk = splycd&&splycd[0]?.ruseUtztnFcltCd?splycd[0]?.ruseUtztnFcltCd:'11000SW001R1'
const splycd = form.getValues("splyCndtList");
const pk =
splycd && splycd[0]?.ruseUtztnFcltCd
? splycd[0]?.ruseUtztnFcltCd
: "11000SW001R1";
if (type === "mat") {
if (value.demandCst) {
@@ -183,11 +202,7 @@ export default function ConCostForm({
onClick={() => {
toast.dismiss(t.id);
coMatRvwUpdate(
pk,
form.getValues("dmdInfoId"),
data
);
coMatRvwUpdate(pk, form.getValues("dmdInfoId"), data);
}}
>
확인
@@ -199,7 +214,6 @@ export default function ConCostForm({
</span>
));
} else {
coMatRvwUpdate(pk, form.getValues("dmdInfoId"), data);
}
}
@@ -216,12 +230,63 @@ export default function ConCostForm({
console.log(" form ", form.formState.errors);
}, [form.formState]);
useEffect(() => {
const origin = window.location.origin;
const handler = (event: MessageEvent) => {
// 보안: 동일 출처만 수신
if (event.origin !== origin) return;
// 구조 안전 체크
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.type === "REQUEST_INIT_SUBS_INFO") {
const initSubs =
(splyList ?? [])
.filter(Boolean)
.map((x: any) => ({
ruseUtztnFcltNm: x.ruseUtztnFcltNm ?? "",
ruseUtztnFcltCd: x.ruseUtztnFcltCd ?? "",
}));
// 응답: 옵션 객체 형태로 전송 (TS 에러 방지)
const target = event.source as Window | null;
target?.postMessage(
{ type: "INIT_SUBS_INFO", payload: initSubs },
{ targetOrigin: origin }
);
}
};
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, [splyList]);
useEffect(() => {
form.register("dmdInfoSn");
form.register("dmdInfoId");
form.register("ruseUtztnFcltNm");
}, []);
const searchCandidate = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const dmdInfoId = form.getValues("dmdInfoId");
console.log(dmdInfoId)
const url = `/splyPopup?dmdInfoId=${encodeURIComponent(dmdInfoId)}`;
const width = window.screen.availWidth;
const height = window.screen.availHeight;
const left = 0;
const top = 0;
const popupOptions = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`;
window.open(url, "candidatePopup", popupOptions);
};
return (
<>
<hr className="mt-8" />
@@ -369,8 +434,8 @@ export default function ConCostForm({
console.log("dmndQty ", e);
//field.onChange(Number(e.target.value) * 365 );
}}
// value={Number(field.value) * 365 || 0}
value={Number(field.value) || 0}
// value={Number(field.value) * 365 || 0}
value={Number(field.value) || 0}
readOnly
/>
</FormUi>
@@ -501,30 +566,34 @@ export default function ConCostForm({
)}
/>
</div>
{/* <div className="col-span-3 mt-4">
<div className="col-span-3 mt-4">
<div className="flex items-center justify-between">
<Label>공급처 선정</Label>
<Button size={"sm"} onClick={(e) => {}} className="bg-none">
<Button
size={"sm"}
onClick={(e) => {
console.log(e);
setLastSub(null) // 빌드오류때문에 넣은거 삭제필요
searchCandidate(e);
//splyList => 해당객체가 대민에서 선택한 후보처 리스트
//setLastSub => 해당 스테이트가 선택한 마지막 후보처를 넣어야하는 스테이트
}}
className="bg-none"
>
후보지 조회
</Button>
</div>
<FormField
control={form.control}
name=""
render={({ field }) => (
<FormUi label="">
<Input
placeholder=""
type="text"
onChange={(e) => {
field.onChange(e.target.value);
}}
value={field.value ?? ""}
/>
</FormUi>
)}
/>
</div> */}
<FormUi label="">
<Input
placeholder=""
type="text"
value={lastSub?.ruseUtztnFcltNm || ""}
readOnly
/>
</FormUi>
</div>
<div className="col-span-3 mt-4">
<FormField
control={form.control}
@@ -536,9 +605,8 @@ export default function ConCostForm({
type="number"
step="0.01"
onChange={(e) => {
if (readonly) return;
if(readonly) return;
field.onChange(Number(e.target.value));
}}
value={field.value ?? 0}
@@ -34,11 +34,13 @@ export default function HtdflRegForm({
isModal,
readonly = false,
setIsModal,
isMap
}: {
htdflForm: customHtdflBdst;
isModal: boolean;
readonly?: boolean;
setIsModal?: (modal: boolean) => void;
isMap?: boolean;
}) {
const { code, htdflBdstUpdate, htdflBdstReg } = useWrStore((state) => state);
const [frontPHdata, setFrontPHdata] = useState("");
@@ -140,14 +142,16 @@ export default function HtdflRegForm({
const [divStyle, setDivStyle] = useState("h-[53vh]");
useEffect(() => {
if (isModal === true) {
setDivStyle("h-[53vh] max-h-[37vh]");
if(isMap){
setDivStyle("h-[100vh]")
}else if (isModal === true) {
setDivStyle("h-[53vh] max-h-[37vh]")
} else if (readonly === true) {
setDivStyle("h-[62vh]");
setDivStyle("h-[62vh]")
} else {
setDivStyle("h-[57vh]")
}
}, [isModal, readonly]);
}, [isModal, readonly ,isMap]);
useEffect(() => {
console.log("form errors:", form.formState.errors);
@@ -30,11 +30,13 @@ export default function RainRegForm({
isModal,
readonly = false,
setIsModal,
isMap
}: {
rainForm: customRainBdst;
isModal: boolean;
setIsModal?: (modal: boolean) => void;
readonly?: boolean;
isMap?: boolean;
}) {
const { code, rainBdstReg, rainBdstUpdate } = useWrStore(
(state) => state
@@ -54,14 +56,16 @@ export default function RainRegForm({
const [divStyle, setDivStyle] = useState("h-[53vh]");
useEffect(() => {
if (isModal === true) {
if(isMap=== true){
setDivStyle("h-[100vh]")
}else if (isModal === true) {
setDivStyle("h-[53vh] max-h-[37vh]")
} else if (readonly === true) {
setDivStyle("h-[62vh]")
} else {
setDivStyle("h-[57vh]")
}
}, [isModal, readonly]);
}, [isModal, readonly ,isMap]);
const cleanRainData = (raw: any): z.infer<typeof rainZod> => {
const buildings =
@@ -34,25 +34,30 @@ export default function RswtrRegForm({
isModal,
setIsModal,
readonly = false,
isMap
}: {
rswtForm: customRswtBdst;
isModal: boolean;
setIsModal?: (isModal: boolean) => void;
readonly?: boolean;
isMap?: boolean;
}) {
const { code, rswtBdstUpdate, rswtBdstReg } = useWrStore(
(state) => state
);
const [divStyle, setDivStyle] = useState("h-[53vh]");
useEffect(() => {
if (isModal === true) {
if(isMap){
setDivStyle("h-[100vh]")
}else if (isModal === true) {
setDivStyle("h-[53vh] max-h-[37vh]")
} else if (readonly === true) {
setDivStyle("h-[62vh]")
} else {
setDivStyle("h-[57vh]")
}
}, [isModal, readonly]);
}, [isModal, readonly ,isMap]);
// const defaultValues: z.infer<typeof rswtZod> = rswtZod.parse({});
const form = useForm<z.infer<typeof rswtZod>>({
@@ -0,0 +1,10 @@
export default function coSttnDetailPopup(){
return(
<>
</>
)
}
@@ -0,0 +1,63 @@
import { useWrStore } from "@/app/store";
import type { customPbnst } from "@/app/store/ruseFcltMng/fcltReg/pbnstReg/type";
import { PbnstSttnModal } from "@/components/ui/modal/sttn/pbnstSttnModal/pbnstSttnModal";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@common/components/ui";
import { useEffect, useRef, useState } from "react";
export function RuseDetailPopup({ bscFctCd }: { bscFctCd: string }) {
const { ruseMap, getRuseMaplList } = useWrStore((state) => state);
const { ruseDetailList } = ruseMap;
const [fcltTab, setFcltTab] = useState<string>("");
const [isDetail, setIsDetail] = useState<customPbnst>({} as customPbnst);
const initRef = useRef<boolean>(false);
useEffect(() => {
if (!initRef.current) {
getRuseMaplList(bscFctCd);
initRef.current = true;
}
}, [bscFctCd]);
useEffect(() => {
const detail = ruseDetailList.find(
(list) => list.pbnstRuseFcltId === fcltTab
);
if (detail) setIsDetail(detail);
}, [fcltTab, ruseDetailList]);
useEffect(() => {
if (ruseDetailList && ruseDetailList[0]) {
setFcltTab(ruseDetailList[0].pbnstRuseFcltId);
}
}, [ruseDetailList]);
if (!(ruseDetailList && ruseDetailList[0])) return;
return (
<div>
<Tabs value={fcltTab} onValueChange={setFcltTab}>
<TabsList className="mt-4">
{ruseDetailList.map((item, index) => {
return (
<TabsTrigger
key={item.pbnstRuseFcltId + index}
className="!text-xl"
value={item.pbnstRuseFcltId}
>
{item.ruseUtztnFcltNm}
</TabsTrigger>
);
})}
</TabsList>
<TabsContent className="flex-1 px-4" value={fcltTab}>
<PbnstSttnModal data={isDetail} isMap={true} />
</TabsContent>
</Tabs>
</div>
);
}
@@ -34,7 +34,7 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titleVisible?: boolean }) {
export function PbnstSttnModal({ data, titleVisible,isMap}: { data: customPbnst; titleVisible?: boolean ,isMap?:boolean}) {
const { pbnstSttn, prfmnc, pbnstPrfmncSttn } = useWrStore((state) => state);
const [fcltTab, setFcltTab] = useState<string>("fclt");
const [ruseTab, setRuseTab] = useState<string>("totalPrf");
@@ -223,17 +223,24 @@ export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titl
}
};
useEffect(() => {
if (pbnstSttn.pbnstSttnRes && data) {
const prfList = pbnstSttn.pbnstSttnRes.find(
(item) => item.pbnstRuseFcltId === data.pbnstRuseFcltId
)?.prfList;
if(isMap){
if(data.prfList &&data.prfList.length>0){
getTablePrfData(data.prfList);
}
}else{
if (prfList) {
getTablePrfData(prfList);
if (pbnstSttn.pbnstSttnRes && data) {
const prfList = pbnstSttn.pbnstSttnRes.find(
(item) => item.pbnstRuseFcltId === data.pbnstRuseFcltId
)?.prfList;
if (prfList) {
getTablePrfData(prfList);
}
}
}
}, [pbnstSttn.pbnstSttnRes, data]);
}, [pbnstSttn.pbnstSttnRes, data,isMap]);
useEffect(() => {
@@ -252,7 +259,7 @@ export function PbnstSttnModal({ data, titleVisible }: { data: customPbnst; titl
<Title title={"하수재이용 시설 상세정보"} />
</>
)}
<ScrollArea className={"h-[70vh]"}>
<ScrollArea className={ isMap==true? "h-[100vh]":"h-[70vh]" }>
<PbnstMngForm
form={form}
formType={"sttn"}
@@ -15,25 +15,26 @@ import RswtrRegForm from "../../../form/fcltReg/rswtrReg/rswtrRegForm";
export default function SttnDetailModal({
type,
formObj,
isMap ,
}: {
type: string;
formObj: any;
isMap?: boolean;
}) {
const { rainSttn, rswtSttn, rainSttnChkSearch } = useWrStore(
const { rainSttn, rswtSttn, rainSttnChkSearch } = useWrStore(
(state) => state
);
const [detailData, setDetailData] = useState({
head: [] as CommonTableProps[],
node:[] as CommonTableProps[],
node: [] as CommonTableProps[],
data: {} as any,
});
useEffect(() => {
if (type === "RNB" && formObj?.reuseBuilding[0]) {
rainSttnChkSearch(formObj.reuseBuilding[0].bdstRuseFcltSn);
} else if (type === "RST") {
// rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltSn)
// rswtChkPolAndGuide(formObj.reuseBuilding[0].bdstRuseFcltSn)
}
}, [type, formObj]);
@@ -60,61 +61,89 @@ export default function SttnDetailModal({
});
setDetailData({
head: rainSttn.rainSttnDetailHeadList,
node:rainSttn.rainSttnDetailNodeList,
node: rainSttn.rainSttnDetailNodeList,
data: data,
});
} else if (type === "RST" && rswtSttn.rswtSttnRes) {
// const tableTotalData = rswtChkTableData(rswtSttn.rswtSttnRes as unknown as rswtChkZodType[]);
// setDetailData({
// head: rswtSttn.rswtSttHeadList,
// node:rswtSttn.rswtSttNodeList,
// // data: tableTotalData,
// });
// const tableTotalData = rswtChkTableData(rswtSttn.rswtSttnRes as unknown as rswtChkZodType[]);
// setDetailData({
// head: rswtSttn.rswtSttHeadList,
// node:rswtSttn.rswtSttNodeList,
// // data: tableTotalData,
// });
}
}, [rainSttn.rainChkRes]);
useEffect(() =>{
setDetailData({data:[] , head:[], node:[]})
},[])
useEffect(() => {
setDetailData({ data: [], head: [], node: [] });
}, []);
return (
<div>
<Title title={ type === "RNB" ? "빗물이용시설 상세정보"
: type === "RST" ? "중수도시설 상세정보"
: "온배수시설 상세정보"}></Title>
<ScrollArea className={detailData.data.length>0?"h-[80vh]":"h-[70vh]"} >
<Title
title={
type === "RNB"
? "빗물이용시설 상세정보"
: type === "RST"
? "중수도시설 상세정보"
: "온배수시설 상세정보"
}
></Title>
<ScrollArea
className={isMap ?"h-[100vh]":
detailData.data.length > 0
? "h-[80vh]"
: "h-[70vh]"
}
>
{type === "RNB" ? (
<RainRegForm rainForm={formObj as customRainBdst} isModal={false} readonly={true}/>
<RainRegForm
rainForm={formObj as customRainBdst}
isModal={false}
readonly={true}
isMap={isMap}
/>
) : type === "RST" ? (
<RswtrRegForm rswtForm={formObj as customRswtBdst} isModal={false} readonly={true}/>
) : (<HtdflRegForm htdflForm={formObj as customHtdflBdst} isModal={false} readonly={true}/>)}
<RswtrRegForm
rswtForm={formObj as customRswtBdst}
isModal={false}
readonly={true}
isMap={isMap}
/>
) : (
<HtdflRegForm
htdflForm={formObj as customHtdflBdst}
isModal={false}
readonly={true}
isMap={isMap}
/>
)}
{detailData.data.length>0?
<UiTable
headName={
type === "RNB" ? "빗물이용시설 상세정보"
: type === "RST" ? "중수도시설 상세정보"
: "온배수시설 상세정보"
}
tableData={detailData.data || []}
headlist={detailData.head || []}
pageSize={0}
total={0}
cellClick={undefined}
onPageChange={function (page: number): void {
console.log('page ', page)
throw new Error("Function not implemented.");
}}
>
<HeadMakeColSpan upHeadList={detailData.node} />
</UiTable>:<></>}
{detailData.data.length > 0 ? (
<UiTable
headName={
type === "RNB"
? "빗물이용시설 상세정보"
: type === "RST"
? "중수도시설 상세정보"
: "온배수시설 상세정보"
}
tableData={detailData.data || []}
headlist={detailData.head || []}
pageSize={0}
total={0}
cellClick={undefined}
onPageChange={function (page: number): void {
console.log("page ", page);
throw new Error("Function not implemented.");
}}
>
<HeadMakeColSpan upHeadList={detailData.node} />
</UiTable>
) : (
<></>
)}
</ScrollArea>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More