From e0041cbde4b034d3edbcd39a6b612ca2f58796ea Mon Sep 17 00:00:00 2001 From: JIeunhuh Date: Tue, 27 May 2025 15:54:55 +0900 Subject: [PATCH 1/6] =?UTF-8?q?ngii=20=EC=84=A4=EC=A0=95=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/map/lib/gis.core.js | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index 4085e2907..1869b766a 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -14,6 +14,9 @@ import CircleStyle from 'ol/style/Circle'; import VectorLayer from 'ol/layer/Vector'; import { Point } from 'ol/geom'; import { boundingExtent } from 'ol/extent'; +import TileWMTS from 'ol/source/WMTS'; +import WMTS from 'ol/tilegrid/WMTS'; +import { get as getProjection } from 'ol/proj'; import olSourceImageWMS from 'ol/source/ImageWMS'; import { GeoJSON } from 'ol/format'; import GisOlEditLayer from './gis.edit'; @@ -32,20 +35,26 @@ GisApp.GEOSERVER_URL = '/geoserver/hasudo/ows'; GisApp.GEOSERVER_WMS_URL = '/geoserver/hasudo/wms'; //맵 정보 -GisApp.EMAP_URL = '/emap/'; -GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; -GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png'; -GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg'; GisApp.GOOGLE_API_KEY = 'AIzaSyCf1sgeELFalfPuqlykRKBMj6X9u_DQH6M'; GisApp.GOOGLE_ROADMAP_URL = `https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`; GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`; + +// TODO: NGII 정보 +GisApp.NGII_API_KEY = '305CC242F2920F46BB93BCC13A52DD8A'; // NGII에서 발급받은 API 키 +GisApp.NGII_WMTS_URL = `http://map.ngii.go.kr/openapi/wmts_ngiiMap.js?apikey=${GisApp.NGII_API_KEY}`; + +GisApp.EMAP_URL = '/emap/'; +GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; +GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png'; +GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg'; GisApp.OPENSTREET_URL = '//tile.openstreetmap.org/{z}/{x}/{y}.png'; GisApp.BASE_MAP_TYPES = []; GisApp.FROM_PROJ = 'EPSG:4326'; -GisApp.TO_PROJ = 'EPSG:3857'; +// GisApp.TO_PROJ = 'EPSG:3857'; +GisApp.TO_PROJ = 'EPSG:5179'; //ngii 좌표계 GisApp.TO_DATA_PROJ = 'EPSG:4326'; GisApp.CONTEXT_LAYER_PATH = '/images/map/layericon/'; GisApp.layerCode = {}; @@ -86,6 +95,7 @@ GisApp.core.prototype = { minZoom: 5, maxZoom: 22, }, + options: { /*1: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.3227711, centerY: 129.1836202, dataProj: 'EPSG:5186' }, 4: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.0140499, centerY: 126.7110757, dataProj: 'EPSG:5186' }, @@ -109,11 +119,15 @@ GisApp.core.prototype = { GisApp.Module.core = this; GisApp.Module.layer = new GisOlLayer(this.defaults); GisApp.Module.edit = new GisOlEditLayer(this.defaults); - + // TODO: vworld layer 정보 (getNGIIbaseMap)활성화 하면 ngii base map으로 변경됨 this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base'); this.baseLayers['base'] = this.getBaseMap('base'); this.baseLayers['satellite'] = this.getBaseMap('satellite'); this.baseLayers['3d'] = this.getBaseMap('3d'); + // this.baseLayers['3d'] = this.getNGIIBaseMap('3d'); + // this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base'); + // this.baseLayers['base'] = this.getNGIIBaseMap('base'); + // this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite'); }, layerType: { point: 'POINT', @@ -126,6 +140,46 @@ GisApp.core.prototype = { this.setCenter(Number(this.defaults.centerX), Number(this.defaults.centerY)); this.setZoom(15); }, + // NGII Base Map + getNGIIBaseMap: function (_id) { + const layers = { + base: 'korean_map', // 일반지도 + satellite: 'satellite_map', // 위성지도 + night: 'night_map', // 야간지도 + }; + // 선택된 레이어 + const layerName = layers[_id] || layers.base; + // EPSG:5179 좌표계 설정 + const projection = getProjection('EPSG:5179'); + const extent = [705680.0000, 1349270.0000,1388291.0000, 2581448.0000]; // NGII 좌표계 범위 + const resolutions = [ + 2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255, + ]; + const tileGrid = new WMTS({ + extent: extent, + resolutions: resolutions, + matrixIds: resolutions.map((_, index) => index.toString()), // WMTS matrix IDs + }); + + // WMTS 소스 생성 + const wmtsSource = new TileWMTS({ + url: GisApp.NGII_WMTS_URL, + layer: layerName, + matrixSet: 'EPSG:5179', + format: 'image/png', + projection: projection, + tileGrid: tileGrid, + style: 'default', + crossOrigin: 'anonymous', + }); + + // TileLayer 생성 + return new TileLayer({ + source: wmtsSource, + visible: true, + zIndex: 0, + }); + }, getBaseMap: function (_id) { let url = GisApp.VWORLD_URL; if (_id === 'satellite') { @@ -182,7 +236,7 @@ GisApp.core.prototype = { setCenter: function (x, y, transYn) { if (!x || !y) return; let view = this.map.getView(); - + if (!transYn) { // 값이 없으면 let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ); @@ -553,7 +607,7 @@ GisApp.core.prototype = { */ convertLayerInfosToLayerCode: function (array, key, parentName) { let initialValue = {}; - + for (let i = 0; i < array.length; i++) { let item = {}; Object.assign(item, array[i]); From 3f6026065a5da213826614a108d10b467ef646b7 Mon Sep 17 00:00:00 2001 From: yschoi Date: Tue, 27 May 2025 16:37:21 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=ED=8C=8C=EB=B9=84=EC=BD=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/lib/auth/lib/auth-wrapper.tsx | 3 +++ apps/sn/public/favicon.ico | Bin 0 -> 4286 bytes 2 files changed, 3 insertions(+) create mode 100644 apps/sn/public/favicon.ico diff --git a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx b/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx index 0b72cc768..5b5e18143 100644 --- a/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx +++ b/apps/sn/app/components/lib/auth/lib/auth-wrapper.tsx @@ -43,6 +43,9 @@ const AuthWrapper = ({ children }: { children: React.ReactNode }) => { const accessToken = tokenManager.getToken(CLAIM_NAME); const tokenId = tokenManager.getToken(AUTH_USER_ID); + //TODO 삭제 + console.log(accessToken) + const response = await axios.get( `${ENDPOINT.USER_SERVICE.USERS}/info/me`, { diff --git a/apps/sn/public/favicon.ico b/apps/sn/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d304b69814b2d79060a24f36f667a7f53b768103 GIT binary patch literal 4286 zcmb`K3s6*59LCQ|ikK6XsZsfun##0v95pR15s|b|G%-a`$DE!vmSyIHWMNFiN+Xg- zJ|hw%MPPwl7SPpQ9=pp!V|wiw5)a-aYr6|L^?2bA>?& z4ZjBt6!KrO`(7dX2q6X$4he4}#~ok2?iNDQ*{|$R#ginMY$Exjid2&tQcm`gRm6up z(AoOV=Cjs|WDBV!aDmI5Q$#)@4?C-St8uRFOQK05amA}Pr;=+I+{$(1bFLXn%I?6c z+)F#_Y$iQ!^r(I2yeZ@qQP%iB?o(LvJ~gLfjL%1tKW^cc{m5MRk*y?=7|0oNt9>SO zPEUtVdF8klskljvb9}ag$oH`q=|(ifjdUji$wFc#H`_;)(kt&dzYCG?xU+ROKJ&hD zo!V#4^B^CR2BKE4jpGxQYx&umv^be>HONgkiwBkS-g^GhOXdoAR6 zr`?YIaqUc^)|dPlG2@zlFz?m&+060h9kp)v${L>&wcftxcvpR>c-zVI_}g_V*Rl2^ zqhaYAxvD%y5^VH z{VN65eLFgGPh{WUl>O^CUY78Xp<(|Rx6Yf?SNxtLM&w0^OPPvuKJbaa%0$0}#OR^5 z#ueB5&!GRv_Wpmil}wzhUafPZ^{GO0Iz?=u9}d6FJC|p^CcYo1gaVobmeXy1ilu{c*Ls#1-ZbzE>R{Bi-p*56m(wUc00yp;#Bz z6CG!;MnK1Brl_7cXBmr@!e`NJ9%5}i*gx`|$zw{>e(_XOp6GoxSGb?e5&xV|W47qp zkSTi9>qNi$3^A%cO)RKO6FcZX)tDx5Y%AT0>`m-9=IcT5E{Iq>_veDGUnR{%{>K6` z7r95tZ@Oo2(K-grhX#Xw(Ek_xjr1R*-$uWM=o|GSm;Pe=n=?? z_QDy17p|BzXw7Hl%)gQOH!^=o1M_cS{`Jhif%#vgyM^apwcP_hL_UP|@nf(^_a-J5 zM&SjzCtB9R#~hJLzq`XPuOT&t^}*Fym|LyGtfK}@KbD8U$~4R~e}iQQ!;oAYSoDj@ zYvZL{5qHTb_O=>C8p*hrO|nGVUpkT8k|8!07@pd;EFv?F#0QG|4RR> zhm$d*d>3YveUIs7-(h;`PE0G^j%g)ZG1dMp{B3b~*%pf_*3HiR->`p|{sx#`mGLWU>9;ZeHW&P7c>d`;{~-EZ%G;S+IZl5c`d_5~ zedZq@P`Wdl`R6kKT;{)*`LAdG^O^rp`nxOF-s~Pt0|2cX+J(SnA;z}~F`!8Z&tE;8 z+)d*zyPLw=-L!yPIG`cnZdW Date: Wed, 28 May 2025 11:07:04 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20-=20excelToJson.ts=20=EC=97=90=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EB=B3=80=ED=99=98=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=83=80=EC=9E=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20-=20UITable=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83?= =?UTF-8?q?=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=20Table=EC=9D=B8=20EditableFormTable.tsx=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84=20-?= =?UTF-8?q?=20RDII=20=EB=B6=84=EC=84=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20JSON=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=83=9D=EC=84=B1,=20API=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/lib/utils/excelToJson.ts | 145 +++++- .../app/components/ui/EditableFormTable.tsx | 131 ++++++ apps/sn/app/ii/irsetting/page.tsx | 17 +- apps/sn/app/ii/rdii/page.tsx | 434 ++++++++++++++---- 4 files changed, 610 insertions(+), 117 deletions(-) create mode 100644 apps/sn/app/components/ui/EditableFormTable.tsx diff --git a/apps/sn/app/components/lib/utils/excelToJson.ts b/apps/sn/app/components/lib/utils/excelToJson.ts index c4a3a226d..2ed697139 100644 --- a/apps/sn/app/components/lib/utils/excelToJson.ts +++ b/apps/sn/app/components/lib/utils/excelToJson.ts @@ -1,35 +1,106 @@ export const TEST_UUID= '2d4b151d-1828-41e0-91d9-4cd28ea1cc7e' import { z } from "zod"; -// 수용가-맨홀 매칭결과 (아직 안함) +// "YYYY-MM-DDTHH:MM:SS" 형식으로 포맷팅 하는 함수 +function parseAndFormatDateTimeToISOCompatibleString( + dateInput: string, + timeInputForMDY: string | null, + inputType: 'YYYY-MM-DD_H:MM' | 'M/D/YYYY_H:MM' +): string | null { + let year: number, month: number, day: number, hours: number, minutes: number; + let datePartStr: string; + let timePartStr: string; + + if (inputType === 'YYYY-MM-DD_H:MM') { + const parts = String(dateInput).split(' '); + if (parts.length !== 2) { + console.warn(`Invalid dateTime format for ${inputType} (split space):`, dateInput); + return null; + } + datePartStr = parts[0] ?? ""; // "YYYY-MM-DD" + timePartStr = parts[1] ?? ""; // "H:MM" + + const dateParts = datePartStr.split('-'); + if (dateParts.length !== 3) { + console.warn(`Invalid date format for ${inputType} (datePart):`, datePartStr); + return null; + } + year = parseInt(dateParts[0] ?? '', 10); + month = parseInt(dateParts[1] ?? '', 10); + day = parseInt(dateParts[2] ?? '', 10); + } else if (inputType === 'M/D/YYYY_H:MM') { + datePartStr = String(dateInput); // "M/D/YYYY" + if (!timeInputForMDY) { + console.warn(`Time input is missing for ${inputType}:`, dateInput); + return null; + } + timePartStr = String(timeInputForMDY); // "H:MM" + + const dateParts = datePartStr.split('/'); + if (dateParts.length !== 3) { + console.warn(`Invalid date format for ${inputType} (datePart):`, datePartStr); + return null; + } + // Note: Order for M/D/YYYY is month, day, year + month = parseInt(dateParts[0] ?? '', 10); + day = parseInt(dateParts[1] ?? '', 10); + year = parseInt(dateParts[2] ?? '', 10); + } else { + console.warn("Invalid inputType for date parsing."); + return null; + } + + // 시간 파싱 (H:MM) - 공통 로직 + const timeParts = timePartStr.split(':'); + if (timeParts.length !== 2) { + console.warn(`Invalid time format (timePart):`, timePartStr); + return null; + } + hours = parseInt(timeParts[0] ?? '', 10); + minutes = parseInt(timeParts[1] ?? '', 10); + + if (isNaN(year) || isNaN(month) || isNaN(day) || isNaN(hours) || isNaN(minutes)) { + console.warn("Invalid parsed date or time numeric values:", { year, month, day, hours, minutes }); + return null; + } + + // "YYYY-MM-DDTHH:MM:SS" 형식으로 포맷팅 - 공통 로직 + const formattedYear = String(year).padStart(4, '0'); // 연도는 4자리로 보장 (M/D/YYYY의 경우) + const formattedMonth = String(month).padStart(2, '0'); + const formattedDay = String(day).padStart(2, '0'); + const formattedHours = String(hours).padStart(2, '0'); + const formattedMinutes = String(minutes).padStart(2, '0'); + // 초는 00으로 고정, 'T'를 사용하여 Date 생성자 호환성 확보 + return `${formattedYear}-${formattedMonth}-${formattedDay} ${formattedHours}:${formattedMinutes}:00`; +} + + +// 수용가-맨홀 매칭결과 (수정 필요) export const MatchingResultZod = z.object({ - anls_sn: z.number(), - mhl_mng_no: z.string(), // 일련번호 0 포함인지? number? string? - rcptr_no: z.string(), + anls_sn: z.number(), // 분석 일련번호 + mhl_mng_no: z.string(), // 맨홀 번호 (임시값이라 추후 수정 필요) + rcptr_no: z.string(), // 수용가 번호 }); - export type MatchingResultZodType = z.infer; - export const excelToMatchingResultData = (excelData: any) => { const MatchingResultData: MatchingResultZodType[] = excelData.map((item: any) => { return { - anls_sn: Number(item["NO"]), - mhl_mng_no: String(item["FTR_IDN"]), // 임시값이라 추후 수정 필요 + anls_sn: Number(item["NO"]), + mhl_mng_no: String(item["FTR_IDN"]), // (임시값이라 추후 수정 필요) rcptr_no: String(item["수용가"]), }; }).filter((el: any) => el !== undefined); return MatchingResultData; }; + // 시간패턴 export const timePatternZod = z.object({ - hr_id: z.number(), - rt: z.number().nullish(), + hr_id: z.number(), // 시간 + rt: z.number().nullish(), // 비율 }); - export type timePatternZodType = z.infer; - export const excelToTimePatternData = (excelData: any) => { const timePatternData: timePatternZodType[] = excelData.map((item: any) => { @@ -41,23 +112,59 @@ export const excelToTimePatternData = (excelData: any) => { return timePatternData; }; + // 유량자료 export const flowRateZod = z.object({ - ppl_mng_no: z.string(), - mtrng_dt: z.number(), // type 뭘로 해야할지 Date? timeStamp임 - oilq: z.number().nullish(), + ppl_mng_no: z.string(), // 관로번호 + mtrng_dt: z.string(), // 시간 + oilq: z.number().nullish(), // 유량(㎥/h) }); - export type flowRateZodType = z.infer; - -export const excelToflowRateData = (excelData: any) => { +export const excelToFlowRateData = (excelData: any) => { const flowRateData: flowRateZodType[] = excelData.map((item: any) => { + const dateTimeStr = String(item["시간"]); // "YYYY-MM-DD H:MM" 형식 (예: "2025-02-23 8:50") + + const formattedDateTime = parseAndFormatDateTimeToISOCompatibleString(dateTimeStr, null, 'YYYY-MM-DD_H:MM'); + + if (!formattedDateTime) return undefined; + + const dateObject = new Date(formattedDateTime); + const mtrngDtValue = !isNaN(dateObject.getTime()) ? dateObject.toISOString() : null; + + if (!mtrngDtValue) { + console.warn("Failed to create Date object for flowRateData:", formattedDateTime); + return undefined; + } return { ppl_mng_no: String(item["관로번호"]), - mtrng_dt: new Date(item["시간"]).toISOString(), // type 뭘로 해야할지 Date? timeStamp임 + mtrng_dt: mtrngDtValue, oilq: Number(item["유량(㎥/h)"]), }; }).filter((el: any) => el !== undefined); return flowRateData; +}; + + +// 강우자료 (수정 필요) +export const rainZod = z.object({ + mtrng_dt: z.string(), // 날짜 + 시간 + amrf: z.number().nullish(), // 강우량(mm) +}); +export type rainZodType = z.infer; +export const excelToRainData = (excelData: any) => { + const rainData: rainZodType[] = excelData.map((item: any) => { + const datePartStr = String(item["날짜"]); // "M/D/YYYY" 형식 (예: "5/9/2021") + const timePartStr = String(item["시간"]); // "H:MM" 형식 (예: "8:00") + + const formattedForRainZod = parseAndFormatDateTimeToISOCompatibleString(datePartStr, timePartStr, 'M/D/YYYY_H:MM'); + + if (!formattedForRainZod) return undefined; + + return { + mtrng_dt: formattedForRainZod, + amrf: Number(item["강우량(mm)"]), + }; + }).filter((el: any) => el !== undefined); + return rainData; }; \ No newline at end of file diff --git a/apps/sn/app/components/ui/EditableFormTable.tsx b/apps/sn/app/components/ui/EditableFormTable.tsx new file mode 100644 index 000000000..89fd8f005 --- /dev/null +++ b/apps/sn/app/components/ui/EditableFormTable.tsx @@ -0,0 +1,131 @@ +import { Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui"; +import type { UseFormReturn, FieldArrayWithId, Path, FieldValues } from "react-hook-form"; +import { Controller } from "react-hook-form"; + +// 편집 가능한 테이블 헤더 항목 인터페이스 +export interface EditableTableHeadItem { + id: keyof TItem | 'response'; // 'response'는 직접적인 키가 아닐 수 있지만 특별히 처리될 수 있음 + title: string; + editable?: boolean; // 편집 가능 여부 + width?: string; // 컬럼 너비 추가 + align?: 'left' | 'center' | 'right'; // 정렬 + cellClassName?: string; // 셀 클래스 이름 + inputClassName?: string; // 입력 필드 클래스 이름 + inputProps?: React.ComponentProps; // Input 컴포넌트에 전달할 props (예: type="number") +} + +// EditableFormTable 컴포넌트 props 타입 정의 +type EditableFormTableProps< + TFormValues extends FieldValues, + TFieldArrayName extends Path, + TFieldArrayItem extends FieldValues = TFormValues[TFieldArrayName] extends (infer U)[] ? U extends FieldValues ? U : never : never +> = { + form: UseFormReturn; // react-hook-form의 form 객체 + name: TFieldArrayName; // 필드 배열 이름 (예: "unitFactors") + fields: FieldArrayWithId[]; // useFieldArray에서 반환된 필드 배열 + headlist: EditableTableHeadItem[]; // 테이블 헤더 설정 + tableClassName?: string; // 테이블 클래스 이름 + headerClassName?: string; // 헤더 클래스 이름 + bodyClassName?: string; // 바디 클래스 이름 + rowClassName?: string; // 행 클래스 이름 +}; + +// EditableFormTable 컴포넌트 +export function EditableFormTable< + TFormValues extends FieldValues, + TFieldArrayName extends Path, + TFieldArrayItem extends FieldValues = TFormValues[TFieldArrayName] extends (infer U)[] ? U extends FieldValues ? U : never : never +>({ + form, + name, + fields, + headlist, + tableClassName = "w-full", + headerClassName = "font-bold", + bodyClassName = 'text-center', + rowClassName = "", +}: EditableFormTableProps) { + return ( +
+ + + + {headlist.map((head) => ( + + {head.title} + + ))} + + + + {fields.map((rowItem, rowIndex) => ( + + {headlist.map((head) => { + const fieldName = `${name}.${rowIndex}.${String(head.id)}` as Path; + const cellValue = (rowItem as any)[head.id as string]; + + return ( + + {head.editable ? ( + { + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value; + + if (newValue === "") { + field.onChange(""); // 빈 문자열 허용 + return; + } + + // 숫자와 '.'만 허용 (단일 '.' 허용) + const isValidChars = /^[0-9]*\.?[0-9]*$/.test(newValue); + if (!isValidChars) { + return; // 유효하지 않은 문자 또는 여러 개의 '.' 포함 시 변경 무시 + } + + // '.' 기호가 포함된 경우, 처음에는 올 수 없음 (예: '.5' 방지, '0.5' 유도) + // 이 조건은 사용자의 요구사항에 따라 조정될 수 있습니다. + // 현재 로직은 '.5'와 같은 입력을 허용하지 않습니다. + if (newValue.includes('.') && newValue.startsWith('.')) { + // 만약 '.'만 입력된 경우 허용하려면 아래 주석 해제 + // if (newValue === '.') { + // field.onChange(newValue); + // return; + // } + return; + } + + field.onChange(newValue); + }; + + return ( + + ); + }} + /> + ) : ( + {String(cellValue ?? '')} + )} + + ); + })} + + ))} + +
+
+ ); +} \ No newline at end of file diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index a299e9337..084fef559 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -8,7 +8,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { excelToflowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson' +import { excelToFlowRateData, excelToMatchingResultData, excelToTimePatternData } from '../../components/lib/utils/excelToJson' import BaseLayout from '../BaseLayout' const formSchema = z.object({ @@ -135,7 +135,7 @@ const IrSetting = () => { } }; - // 버튼 클릭 핸들러 + // 침입수 분석 실행 버튼 클릭 핸들러 const handleAnalysisClick = async () => { console.log(uploadedFilesData); const file1 = uploadedFilesData[0]; // 오수관망도 @@ -160,7 +160,7 @@ const IrSetting = () => { // 여기서는 File 객체를 받고, 내부적으로 ArrayBuffer로 변환하여 parser에 전달한다고 가정 { file: file2, parser: excelToMatchingResultData }, { file: file3, parser: excelToTimePatternData }, - { file: file4, parser: excelToflowRateData } + { file: file4, parser: excelToFlowRateData } // 오수관망도 파일은 파싱 대상이 아닐 수 있으므로, 필요에 따라 추가/제외 ]; @@ -207,11 +207,11 @@ const IrSetting = () => { sggCd: select3, // 시군구코드 swageTmplntCd: select4, // 하수처리장코드 swtrmtZoneCd: select5, // 하수처리구역코드 - aplcnYmd: selectedDate.toISOString(), // 적용연월 - dtrwSwtcRt: input1, // 오수전환율 + aplcnYmd: selectedDate.toISOString().substring(0, 7).replace("-", ""), // 적용연월 + dtrwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) anlsInfo: input2, // 분석정보 - atflCd: file1?.atflCd, // 첨부파일CD - atflId: file1?.atflId, // 첨부파일ID + atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) + atflId: file1?.atflId, // 첨부파일ID (오수관망도) tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 tbSnHrPttn: timePatternData, // 오수량 시간패턴 tbSnOilqData: flowRateData, // 유량자료 @@ -255,7 +255,6 @@ const IrSetting = () => { showPreview={true} accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={1} /> - {/* */} { showPreview={true} accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={2} /> - {/* */} { showPreview={true} accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={3} /> - {/* */}
diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index 94cb94f40..7abaa7814 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -1,57 +1,296 @@ "use client" -import { date, z } from "zod" -import { useForm, useFieldArray } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Input, Button, DatePicker } from "@common/components/ui" -import { UiTable } from "@common/business_components/ui" -import React, { useState } from "react" -import BaseLayout from "@/ii/BaseLayout" +import { postData } from '@/components/lib/api' +import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" +import BaseLayout from "@/ii/BaseLayout" +import { UiTable, filePicked } from "@common/business_components/ui" +import { Button, DatePicker, Input } from "@common/components/ui" +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { useFieldArray, useForm } from "react-hook-form" +import { z } from "zod" +import { excelToMatchingResultData, excelToRainData } from '../../components/lib/utils/excelToJson' +import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable'; +import { Label } from '@radix-ui/react-label' + // TODO: 컴포넌트 호출 ~ 필요 코드 수정해야댐 // zod 스키마 정의 const formSchema = z.object({ - region: z.string(), - rdiiArea: z.string(), - rtkSource: z.string(), - unitFactors: z.array(z.object({ + enoCd: z.string(), // 환경청코드 + ctpvCd: z.string(), // 시도코드 + sggCd: z.string(), // 시군구코드 + swageTmplntCd: z.string(), // 하수처리장코드 + swtrmtZoneCd: z.string(), // 하수처리구역코드 + rtkSource: z.string(), // RTK계수 출처 + rdiiArea: z.string(), // RDII 기여면적 + unitFactors: z.array(z.object({ // 단위도 계수 response: z.string(), R: z.string(), T: z.string(), K: z.string(), })), - initialDepths: z.array(z.object({ + initialDepths: z.array(z.object({ // 초기 차단 깊이 response: z.string(), Dmax: z.string(), Drec: z.string(), Do: z.string(), })), - suyeonga: z.string(), - rate: z.string(), - date: z.coerce.date(), - rain: z.string(), + // osumap: z.instanceof(File).optional(), // 오수관망도 + // matching: z.instanceof(File).optional(), // 수용가-맨홀 매칭결과 + aplcnYm: z.string(), // 상수사용량 적용기간 (YYYYMM 형식) + rate: z.string(), // 오수전환율 + // rain: z.instanceof(File).optional(), // 강우자료 + analysisInfo: z.string().optional(), // 분석 정보 + date: z.date(), + // date: z.coerce.date(), }) +type FormSchemaType = z.infer; + +// 기본 폼 값 정의 +const defaultFormValues: FormSchemaType = { + enoCd: "", + ctpvCd: "", + sggCd: "", + swageTmplntCd: "", + swtrmtZoneCd: "", + rtkSource: "", + rdiiArea: "", // 문자열로 기본값 설정 + unitFactors: [ + { response: "Short-Term", R: "", T: "", K: "" }, // 문자열로 기본값 설정 + { response: "Medium-Term", R: "", T: "", K: "" }, + { response: "Long-Term", R: "", T: "", K: "" }, + ], + initialDepths: [ + { response: "Short-Term", Dmax: "", Drec: "", Do: "" }, // 문자열로 기본값 설정 + { response: "Medium-Term", Dmax: "", Drec: "", Do: "" }, + { response: "Long-Term", Dmax: "", Drec: "", Do: "" }, + ], + // osumap: undefined, + // matchingResultFile: undefined, + aplcnYm: "", + rate: "", // 문자열로 기본값 설정 + // rainFile: undefined, + analysisInfo: "", + date: new Date(), +}; + +// 각 파일 업로드 폼의 데이터를 저장할 인터페이스 정의 +interface UploadedFileInfo { + file: File | null; + atflCd: string | null; + atflId: string | null; + // 필요하다면 파일 이름 등 추가 정보 저장 가능 + // fileName: string; +} + +// 초기 파일 정보 상태 +const initialFileInfo: UploadedFileInfo = { file: null, atflCd: null, atflId: null }; + export default function RTKForm() { - const form = useForm>({ + const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 + + // FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열 + // 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨 + // 예: 0: 오수관망도, 1: 수용가-맨홀, 2: 강우자료 + const [uploadedFilesData, setUploadedFilesData] = useState([ + initialFileInfo, // 0: 오수관망도 + initialFileInfo, // 1: 수용가-맨홀 매칭결과 + initialFileInfo, // 2: 강우자료 + ]); + + // form 인스턴스 생성 + const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: { - region: "", - rdiiArea: "", - rtkSource: "", - unitFactors: [ - { response: "Short-Term", R: "", T: "", K: "" }, - { response: "Medium-Term", R: "", T: "", K: "" }, - { response: "Long-Term", R: "", T: "", K: "" }, - ], - initialDepths: [ - { response: "Short-Term", Dmax: "", Drec: "", Do: "" }, - { response: "Medium-Term", Dmax: "", Drec: "", Do: "" }, - { response: "Long-Term", Dmax: "", Drec: "", Do: "" }, - ], + defaultValues: defaultFormValues + }); + // const form = useForm>({ + // resolver: zodResolver(formSchema), + // defaultValues: { + // enoCd: "", // 환경청코드 + // ctpvCd: "", // 시도코드 + // sggCd: "", // 시군구코드 + // swageTmplntCd: "", // 하수처리장코드 + // swtrmtZoneCd: "", // 하수처리구역코드 + // rtkSource: "", // RTK계수 출처 + // rdiiArea: 0, // RDII 기여면적 + // unitFactors: [ + // { response: "Short-Term", R: 0, T: 0, K: 0 }, + // { response: "Medium-Term", R: 0, T: 0, K: 0 }, + // { response: "Long-Term", R: 0, T: 0, K: 0 }, + // ], + // initialDepths: [ + // { response: "Short-Term", Dmax: 0, Drec: 0, Do: 0 }, + // { response: "Medium-Term", Dmax: 0, Drec: 0, Do: 0 }, + // { response: "Long-Term", Dmax: 0, Drec: 0, Do: 0 }, + // ], + // // osumap: undefined, + // // matching: undefined, + // aplcnYm: "", + // rate: 0, + // // rain: undefined, + // analysisInfo: "", + // } + // }); + + // 폼 초기화 함수 + const resetAllForms = () => { + form.reset(defaultFormValues); // 폼 값 초기화 + setUploadedFilesData([ // 파일 업로드 상태 초기화 (3개 파일) + initialFileInfo, + initialFileInfo, + initialFileInfo, + ]); + setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 + }; + + // FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수 + const handleFileSelection = ( + index: number, // FileUploadForm의 fileIdx 값 + selectedFile: File | null, + cd: string | null, + id: string | null + ) => { + setUploadedFilesData(prevData => { + const newData = [...prevData]; + newData[index] = { + file: selectedFile, + atflCd: cd, + atflId: id, + }; + return newData; + }); + console.log(`Page: File data received for form index ${index}`, { fileName: selectedFile?.name, cd, id }); + }; + + const registFile = async (analysisData: any) => { + console.log("Submitting combined analysis data to backend"); + // 추후 API 엔드포인트에 맞게 URL 수정 필요 + const data = await postData('v1/rdiiAnls', analysisData); + if (data.ok) { + alert("분석 데이터가 성공적으로 제출되었습니다."); + resetAllForms(); // 성공 시 폼 초기화 함수 호출 + } else { + alert("분석 데이터 제출 중 오류가 발생했습니다."); + // resetAllForms(); } - }) + }; + + // 침입수 분석 실행 버튼 클릭 핸들러 + const handleAnalysisClick = async () => { + const formData = form.getValues(); + console.log(uploadedFilesData); + const file1 = uploadedFilesData[0]; // 오수관망도 + const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 + const file3 = uploadedFilesData[2]; // 강우자료 + + console.log("오수관망도 정보:", file1?.file); + console.log("수용가-맨홀 매칭결과 정보:", file2); + console.log("강우자료:", file3); + + // 모든 필수 파일이 선택되었는지 확인 + if (!file1?.file || !file2?.file || !file3?.file) { + alert('모든 분석용 파일을 선택해주세요.'); + return; + } + + const filePickers = [ + { file: file2, parser: excelToMatchingResultData }, + { file: file3, parser: excelToRainData }, + ]; + + try { + const parsedResults = await Promise.all( + filePickers.map(async ({ file, parser }) => { + if (!file || !file.file) return null; // fileInfo 객체 및 file 객체 null 체크 + + // filePicked 함수에 File 객체와 headerNum 전달 + const jsonData = await filePicked(file.file, 0); + + if (jsonData === undefined) { + console.warn(`Warning: filePicked returned undefined for ${file.file.name}`); + return { + fileName: file.file.name, + atflCd: file.atflCd, + atflId: file.atflId, + parsedData: null // 또는 적절한 기본값 + }; + } + + // jsonData를 각 parser 함수로 처리 + // parser 함수가 filePicked가 반환하는 JSON 형식을 직접 처리한다고 가정 + const parsedDataBySpecificFunction = parser(jsonData); + + return { + fileName: file.file.name, + atflCd: file.atflCd, + atflId: file.atflId, + parsedData: parsedDataBySpecificFunction + }; + }) + ); + + // 각 파싱 결과 접근 + const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData; + const rainData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; + + // 새로운 JSON 데이터 생성 + const combinedAnalysisData = { + enoCd: "", // 환경청코드 + aplcnYm: formData.date.toISOString().substring(0, 7).replace("-", ""), // 적용연월 (YYYYMM 형식) + ctpvCd: "", // 시도코드 + sggCd: "", // 시군구코드 + swageTmplntCd: "", // 하수처리장코드 + swtrmtZoneCd: "", // 하수처리구역코드 + atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) + atflId: file1?.atflId, // 첨부파일ID (오수관망도) + cntbArea: parseFloat("rdiiArea") || null, // RDII 기여면적(ha) + srcNm: "", // 출처이름 (RTK계수 출처) + + shortPrdR: parseFloat(formData.unitFactors[0]?.R !== undefined ? String(formData.unitFactors[0]?.R) : "0") || null, // 단기간R + shortPrdT: parseFloat(formData.unitFactors[0]?.T !== undefined ? String(formData.unitFactors[0]?.T) : "0") || null, // 단기간T + shortPrdK: parseFloat(formData.unitFactors[0]?.K !== undefined ? String(formData.unitFactors[0]?.K) : "0") || null, // 단기간K + + mdPrdR: parseFloat(formData.unitFactors[1]?.R !== undefined ? String(formData.unitFactors[1]?.R) : "0") || null, // 중간기간R + mdPrdT: parseFloat(formData.unitFactors[1]?.T !== undefined ? String(formData.unitFactors[1]?.T) : "0") || null, // 중간기간T + mdPrdK: parseFloat(formData.unitFactors[1]?.K !== undefined ? String(formData.unitFactors[1]?.K) : "0") || null, // 중간기간K + + longPrdR: parseFloat(formData.unitFactors[2]?.R !== undefined ? String(formData.unitFactors[2]?.R) : "0") || null, // 장기간R + longPrdT: parseFloat(formData.unitFactors[2]?.T !== undefined ? String(formData.unitFactors[2]?.T) : "0") || null, // 장기간T + longPrdK: parseFloat(formData.unitFactors[2]?.K !== undefined ? String(formData.unitFactors[2]?.K) : "0") || null, // 장기간K + + shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax !== undefined ? String(formData.initialDepths[0]?.Dmax) : "0") || null, // 단기간최대깊이 + shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec !== undefined ? String(formData.initialDepths[0]?.Drec) : "0") || null, // 단기간회복비율 + shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do !== undefined ? String(formData.initialDepths[0]?.Do) : "0") || null, // 단기간시작깊이 + + mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax !== undefined ? String(formData.initialDepths[1]?.Dmax) : "0") || null, // 중간기간최대깊이 + mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec !== undefined ? String(formData.initialDepths[1]?.Drec) : "0") || null, // 중간기간회복비율 + mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do !== undefined ? String(formData.initialDepths[1]?.Do) : "0") || null, // 중간기간시작깊이 + + longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax !== undefined ? String(formData.initialDepths[2]?.Dmax) : "0") || null, // 장기간최대깊이 + longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? String(formData.initialDepths[2]?.Drec) : "0") || null, // 장기간회복비율 + longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? String(formData.initialDepths[2]?.Do) : "0") || null, // 장기간시작깊이 + + dtrwSwtcRt: parseFloat(formData.rate !== undefined ? String(formData.rate) : "0") || null, // 오수전환율 + anlsInfo: "", // 분석정보 + + tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 + tbSnRainData: rainData, // 강우자료 + }; + + console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); + + // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. + await registFile(combinedAnalysisData); + + } catch (error) { + console.error("파일 파싱 중 오류 발생:", error); + alert("파일 처리 중 오류가 발생했습니다."); + return; + } + } const { fields: unitFactorFields } = useFieldArray({ control: form.control, @@ -67,88 +306,107 @@ export default function RTKForm() { console.log("✅ 제출 데이터:", data) } - // 단위도 계수 테이블 설정 - const unitFactorHeadlist = [ - { id: 'response', title: 'Response' }, - { id: 'R', title: 'R' }, - { id: 'T', title: 'T' }, - { id: 'K', title: 'K' } - ] + // 단위도 계수 테이블 설정 (EditableTableHeadItem 타입 사용 및 id 수정) + const unitFactorHeadlist: EditableTableHeadItem[] = [ + { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 + { id: 'R', title: 'R', editable: true, inputProps: { type: 'text' } }, + { id: 'T', title: 'T', editable: true, inputProps: { type: 'text' } }, + { id: 'K', title: 'K', editable: true, inputProps: { type: 'text' } } + ]; + + // 초기 차단 깊이 테이블 설정 (EditableTableHeadItem 타입 사용) + const initialDepthHeadlist: EditableTableHeadItem[] = [ + { id: 'response', title: 'Response', editable: false, width: '150px' }, // width 속성 추가 + { id: 'Dmax', title: 'Dmax', editable: true, inputProps: { type: 'text' } }, + { id: 'Drec', title: 'Drec', editable: true, inputProps: { type: 'text' } }, + { id: 'Do', title: 'Do', editable: true, inputProps: { type: 'text' } } + ]; - // 초기 차단 깊이 테이블 설정 - const initialDepthHeadlist = [ - { id: 'response', title: 'Response' }, - { id: 'Dmax', title: 'Dmax' }, - { id: 'Drec', title: 'Drec' }, - { id: 'Do', title: 'Do' } - ] // 테이블 페이지 변경 핸들러 const handlePageChange = (page: number) => { console.log('Page changed:', page) } - const [selectedDate, setSelectedDate] = useState(new Date()) + return ( { }} graphBtn={false} > - {[
+ {[
+
+ + + + + +
- -
-
-
○ 단위도 계수
- ○ 단위도 계수 + - {null} - + tableClassName="w-[400px]" + />
-
○ 초기 차단 깊이
- ○ 초기 차단 깊이 + - {null} - + tableClassName="w-[400px]" + />
- + handleFileSelection(0, file, cd, id)} + showButton={false} + showPreview={true} + accept={""} + fileIdx={0} />
- + handleFileSelection(1, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={1} />
- { if (date) setSelectedDate(date) }} mode={"single"} /> + { + if (date) { + form.setValue("date", date, { shouldValidate: true }); + } + }} + mode={"single"} + /> + + + -
- + handleFileSelection(2, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={2} />
- + + +
- +
]} From f446dde591213e375c2e44e9ca38f83a63d38743 Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 28 May 2025 17:10:56 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20-=20SelectBox=20Width=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95=20-=20I/I=EB=B6=84=EC=84=9D=20JSON=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=EC=97=90=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20=EC=8B=9C=EB=8F=84=EC=8B=9C=EA=B5=B0?= =?UTF-8?q?=EA=B5=AC=20=EB=A7=A4=ED=95=91=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ui/SidoSigunguSelectBox.tsx | 34 ++--- .../app/components/ui/YearRangeSelectBox.tsx | 4 +- apps/sn/app/components/ui/formSection.tsx | 2 +- apps/sn/app/ii/irsetting/page.tsx | 9 +- apps/sn/app/ii/rdii/page.tsx | 123 +++++++----------- apps/sn/app/map/SearchModal.tsx | 18 ++- 6 files changed, 83 insertions(+), 107 deletions(-) diff --git a/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx b/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx index d1f542583..18bc22337 100644 --- a/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx +++ b/apps/sn/app/components/ui/SidoSigunguSelectBox.tsx @@ -15,7 +15,7 @@ interface SidoSigunguSelectBoxProps { } const sidoList: { id: number, name: string }[] = [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 26, name: '부산광역시' }, { id: 28, name: '인천광역시' }, { id: 30, name: '대전광역시' }, @@ -29,19 +29,19 @@ const sidoList: { id: number, name: string }[] = [ ]; const sigunguMap: Record = { 26: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, // ...부산산광역시의 시군구 추가 ], 28: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, // ...인천광역시의 시군구 추가 ], 30: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, // ...대전광역시의 시군구 추가 ], 41: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 41820, name: '가평군' }, { id: 41550, name: '안성시' }, { id: 41830, name: '양평군' }, @@ -54,19 +54,19 @@ const sigunguMap: Record = { // ...경기도의 시군구 추가 ], 42: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 42170, name: '동해시' }, { id: 42110, name: '춘천시' }, { id: 42720, name: '홍천군' }, // ...강원도의 시군구 추가 ], 44: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 44770, name: '서천군' }, // ...충청남도의 시군구 추가 ], 45: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 45130, name: '군산시' }, { id: 45190, name: '남원시' }, { id: 45730, name: '무주군' }, @@ -79,7 +79,7 @@ const sigunguMap: Record = { // ...전라북도의 시군구 추가 ], 46: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 46810, name: "강진군" }, { id: 46770, name: "고흥군" }, { id: 46720, name: "곡성군" }, @@ -95,7 +95,7 @@ const sigunguMap: Record = { // ...전라남도의 시군구 추가 ], 47: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 47130, name: "경주시" }, { id: 47830, name: "고령군" }, { id: 47190, name: "구미시" }, @@ -116,7 +116,7 @@ const sigunguMap: Record = { // ...경상북도의 시군구 추가 ], 48: [ - { id: 0, name: '선택' }, + { id: 0, name: '전체' }, { id: 48310, name: "거제시" }, { id: 48250, name: "김해시" }, { id: 48270, name: "밀양시" }, @@ -134,23 +134,23 @@ export function SidoSigunguSelectBox({ className = "", }: SidoSigunguSelectBoxProps) { // 선택된 시도에 따라 시군구 리스트를 가져옴 - const sigunguList = useMemo(() => sigunguMap[sido] || [{ id: 0, name: '선택' }], [sido]); + const sigunguList = useMemo(() => sigunguMap[sido] || [{ id: 0, name: '전체' }], [sido]); return (
({ val: opt.id, text: opt.name }))} value={sido} onSelectValue={val => onSidoChange(Number(val))} /> ({ val: opt.id, text: opt.name }))} + selectArray={sigunguList.map(opt => ({ val: opt.id, text: opt.name }))} value={sigungu} onSelectValue={val => onSigunguChange(Number(val))} /> diff --git a/apps/sn/app/components/ui/YearRangeSelectBox.tsx b/apps/sn/app/components/ui/YearRangeSelectBox.tsx index 312c03e58..6c691cec1 100644 --- a/apps/sn/app/components/ui/YearRangeSelectBox.tsx +++ b/apps/sn/app/components/ui/YearRangeSelectBox.tsx @@ -64,7 +64,7 @@ export function YearRangeSelectBox({ minYear = 1900, maxYear = new Date().getFullYear(), className = "", - selectClass = "rounded-lg bg-white" + selectClass = "w-24 rounded-lg bg-white" }: YearRangeSelectBoxProps) { // 시작년도 배열 const getStartYearArray = () => { @@ -90,7 +90,7 @@ export function YearRangeSelectBox({ return (
{ return ( -
+
{titleNm}
{ setSelect4(''); setSelect5(''); setFormResetKey(prevKey => prevKey + 1); // FileUploadForm 재마운트를 위해 key 변경 - // FileUploadForm 내부의 파일명 표시 등을 초기화하려면 - // FileUploadForm 컴포넌트 자체에 reset 로직 또는 key 변경을 통한 재마운트 전략이 필요할 수 있습니다. - // 현재 구조에서는 FileUploadForm의 내부 상태를 직접 초기화하기 어렵습니다. - // 가장 간단한 방법은 FileUploadForm에 key prop을 주고, 초기화 시 key를 변경하여 강제로 재렌더링하는 것입니다. - // 예: setFormResetKey(prev => prev + 1) }; // FileUploadForm의 onFileSelect prop으로 전달될 콜백 함수 @@ -212,6 +207,10 @@ const IrSetting = () => { anlsInfo: input2, // 분석정보 atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) atflId: file1?.atflId, // 첨부파일ID (오수관망도) + atflInpNm: file1?.file?.name, // 오수관망도 파일명 + atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명 + atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명 + atflOilqNm: file4?.file?.name, // 유량자료 파일명 tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 tbSnHrPttn: timePatternData, // 오수량 시간패턴 tbSnOilqData: flowRateData, // 유량자료 diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index 7abaa7814..b51219f71 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -4,18 +4,17 @@ import { postData } from '@/components/lib/api' import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" import BaseLayout from "@/ii/BaseLayout" -import { UiTable, filePicked } from "@common/business_components/ui" +import { filePicked } from "@common/business_components/ui" import { Button, DatePicker, Input } from "@common/components/ui" import { zodResolver } from "@hookform/resolvers/zod" +import { Label } from '@radix-ui/react-label' import { useState } from "react" import { useFieldArray, useForm } from "react-hook-form" import { z } from "zod" import { excelToMatchingResultData, excelToRainData } from '../../components/lib/utils/excelToJson' -import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable'; -import { Label } from '@radix-ui/react-label' +import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable' -// TODO: 컴포넌트 호출 ~ 필요 코드 수정해야댐 // zod 스키마 정의 const formSchema = z.object({ enoCd: z.string(), // 환경청코드 @@ -46,9 +45,8 @@ const formSchema = z.object({ date: z.date(), // date: z.coerce.date(), }) - +// zod 스키마 타입 추론 type FormSchemaType = z.infer; - // 기본 폼 값 정의 const defaultFormValues: FormSchemaType = { enoCd: "", @@ -82,8 +80,6 @@ interface UploadedFileInfo { file: File | null; atflCd: string | null; atflId: string | null; - // 필요하다면 파일 이름 등 추가 정보 저장 가능 - // fileName: string; } // 초기 파일 정보 상태 @@ -94,7 +90,6 @@ export default function RTKForm() { // FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열 // 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨 - // 예: 0: 오수관망도, 1: 수용가-맨홀, 2: 강우자료 const [uploadedFilesData, setUploadedFilesData] = useState([ initialFileInfo, // 0: 오수관망도 initialFileInfo, // 1: 수용가-맨홀 매칭결과 @@ -106,34 +101,6 @@ export default function RTKForm() { resolver: zodResolver(formSchema), defaultValues: defaultFormValues }); - // const form = useForm>({ - // resolver: zodResolver(formSchema), - // defaultValues: { - // enoCd: "", // 환경청코드 - // ctpvCd: "", // 시도코드 - // sggCd: "", // 시군구코드 - // swageTmplntCd: "", // 하수처리장코드 - // swtrmtZoneCd: "", // 하수처리구역코드 - // rtkSource: "", // RTK계수 출처 - // rdiiArea: 0, // RDII 기여면적 - // unitFactors: [ - // { response: "Short-Term", R: 0, T: 0, K: 0 }, - // { response: "Medium-Term", R: 0, T: 0, K: 0 }, - // { response: "Long-Term", R: 0, T: 0, K: 0 }, - // ], - // initialDepths: [ - // { response: "Short-Term", Dmax: 0, Drec: 0, Do: 0 }, - // { response: "Medium-Term", Dmax: 0, Drec: 0, Do: 0 }, - // { response: "Long-Term", Dmax: 0, Drec: 0, Do: 0 }, - // ], - // // osumap: undefined, - // // matching: undefined, - // aplcnYm: "", - // rate: 0, - // // rain: undefined, - // analysisInfo: "", - // } - // }); // 폼 초기화 함수 const resetAllForms = () => { @@ -246,34 +213,37 @@ export default function RTKForm() { swtrmtZoneCd: "", // 하수처리구역코드 atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) atflId: file1?.atflId, // 첨부파일ID (오수관망도) + atflInpNm: file1?.file?.name || "", // 오수관망도 파일명 + atflMpngNm: file2?.file?.name || "", // 수용가-맨홀 매칭결과 파일명 + atflRainNm: file3?.file?.name || "", // 강우자료 파일명 cntbArea: parseFloat("rdiiArea") || null, // RDII 기여면적(ha) srcNm: "", // 출처이름 (RTK계수 출처) - shortPrdR: parseFloat(formData.unitFactors[0]?.R !== undefined ? String(formData.unitFactors[0]?.R) : "0") || null, // 단기간R - shortPrdT: parseFloat(formData.unitFactors[0]?.T !== undefined ? String(formData.unitFactors[0]?.T) : "0") || null, // 단기간T - shortPrdK: parseFloat(formData.unitFactors[0]?.K !== undefined ? String(formData.unitFactors[0]?.K) : "0") || null, // 단기간K + shortPrdR: parseFloat(formData.unitFactors[0]?.R !== undefined ? formData.unitFactors[0]?.R : "0") || null, // 단기간R + shortPrdT: parseFloat(formData.unitFactors[0]?.T !== undefined ? formData.unitFactors[0]?.T : "0") || null, // 단기간T + shortPrdK: parseFloat(formData.unitFactors[0]?.K !== undefined ? formData.unitFactors[0]?.K : "0") || null, // 단기간K - mdPrdR: parseFloat(formData.unitFactors[1]?.R !== undefined ? String(formData.unitFactors[1]?.R) : "0") || null, // 중간기간R - mdPrdT: parseFloat(formData.unitFactors[1]?.T !== undefined ? String(formData.unitFactors[1]?.T) : "0") || null, // 중간기간T - mdPrdK: parseFloat(formData.unitFactors[1]?.K !== undefined ? String(formData.unitFactors[1]?.K) : "0") || null, // 중간기간K + mdPrdR: parseFloat(formData.unitFactors[1]?.R !== undefined ? formData.unitFactors[1]?.R : "0") || null, // 중간기간R + mdPrdT: parseFloat(formData.unitFactors[1]?.T !== undefined ? formData.unitFactors[1]?.T : "0") || null, // 중간기간T + mdPrdK: parseFloat(formData.unitFactors[1]?.K !== undefined ? formData.unitFactors[1]?.K : "0") || null, // 중간기간K - longPrdR: parseFloat(formData.unitFactors[2]?.R !== undefined ? String(formData.unitFactors[2]?.R) : "0") || null, // 장기간R - longPrdT: parseFloat(formData.unitFactors[2]?.T !== undefined ? String(formData.unitFactors[2]?.T) : "0") || null, // 장기간T - longPrdK: parseFloat(formData.unitFactors[2]?.K !== undefined ? String(formData.unitFactors[2]?.K) : "0") || null, // 장기간K + longPrdR: parseFloat(formData.unitFactors[2]?.R !== undefined ? formData.unitFactors[2]?.R : "0") || null, // 장기간R + longPrdT: parseFloat(formData.unitFactors[2]?.T !== undefined ? formData.unitFactors[2]?.T : "0") || null, // 장기간T + longPrdK: parseFloat(formData.unitFactors[2]?.K !== undefined ? formData.unitFactors[2]?.K : "0") || null, // 장기간K - shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax !== undefined ? String(formData.initialDepths[0]?.Dmax) : "0") || null, // 단기간최대깊이 - shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec !== undefined ? String(formData.initialDepths[0]?.Drec) : "0") || null, // 단기간회복비율 - shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do !== undefined ? String(formData.initialDepths[0]?.Do) : "0") || null, // 단기간시작깊이 + shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax !== undefined ? formData.initialDepths[0]?.Dmax : "0") || null, // 단기간최대깊이 + shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec !== undefined ? formData.initialDepths[0]?.Drec : "0") || null, // 단기간회복비율 + shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do !== undefined ? formData.initialDepths[0]?.Do : "0") || null, // 단기간시작깊이 - mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax !== undefined ? String(formData.initialDepths[1]?.Dmax) : "0") || null, // 중간기간최대깊이 - mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec !== undefined ? String(formData.initialDepths[1]?.Drec) : "0") || null, // 중간기간회복비율 - mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do !== undefined ? String(formData.initialDepths[1]?.Do) : "0") || null, // 중간기간시작깊이 + mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax !== undefined ? formData.initialDepths[1]?.Dmax : "0") || null, // 중간기간최대깊이 + mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec !== undefined ? formData.initialDepths[1]?.Drec : "0") || null, // 중간기간회복비율 + mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do !== undefined ? formData.initialDepths[1]?.Do : "0") || null, // 중간기간시작깊이 - longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax !== undefined ? String(formData.initialDepths[2]?.Dmax) : "0") || null, // 장기간최대깊이 - longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? String(formData.initialDepths[2]?.Drec) : "0") || null, // 장기간회복비율 - longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? String(formData.initialDepths[2]?.Do) : "0") || null, // 장기간시작깊이 + longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax !== undefined ? formData.initialDepths[2]?.Dmax : "0") || null, // 장기간최대깊이 + longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? formData.initialDepths[2]?.Drec : "0") || null, // 장기간회복비율 + longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? formData.initialDepths[2]?.Do : "0") || null, // 장기간시작깊이 - dtrwSwtcRt: parseFloat(formData.rate !== undefined ? String(formData.rate) : "0") || null, // 오수전환율 + dtrwSwtcRt: parseFloat(formData.rate !== undefined ? formData.rate : "0") || null, // 오수전환율 anlsInfo: "", // 분석정보 tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 @@ -332,10 +302,10 @@ export default function RTKForm() { {[
- + - +
@@ -360,7 +330,7 @@ export default function RTKForm() {
-
+
handleFileSelection(0, file, cd, id)} @@ -369,7 +339,7 @@ export default function RTKForm() { accept={""} fileIdx={0} />
-
+
handleFileSelection(1, file, cd, id)} @@ -378,21 +348,7 @@ export default function RTKForm() { accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={1} />
- - { - if (date) { - form.setValue("date", date, { shouldValidate: true }); - } - }} - mode={"single"} - /> - - - - -
+
handleFileSelection(2, file, cd, id)} @@ -401,8 +357,23 @@ export default function RTKForm() { accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={2} />
+ + + + + { + if (date) { + form.setValue("date", date, { shouldValidate: true }); + } + }} + mode={"single"} + className="ml-5" + /> + - +
diff --git a/apps/sn/app/map/SearchModal.tsx b/apps/sn/app/map/SearchModal.tsx index b817e5d2e..3fe733cfe 100644 --- a/apps/sn/app/map/SearchModal.tsx +++ b/apps/sn/app/map/SearchModal.tsx @@ -438,7 +438,7 @@ const SearchModal: React.FC = ({ open, onOpenChange, onFeature
= ({ open, onOpenChange, onFeature { + set시도(sidoId); + set시군구(0); + }} onSigunguChange={set시군구} /> @@ -851,7 +854,7 @@ const SearchModal: React.FC = ({ open, onOpenChange, onFeature
({ val: w.val, text: w.text }))} @@ -896,12 +899,15 @@ const SearchModal: React.FC = ({ open, onOpenChange, onFeature { + set시도(sidoId); + set시군구(0); + }} onSigunguChange={set시군구} /> ({ val: opt.val, text: opt.text }))} onSelectValue={val => set처리구역(String(val))} @@ -1010,7 +1016,7 @@ const SearchModal: React.FC = ({ open, onOpenChange, onFeature z-[101] `} body={renderBody()} - headerTitle="검색" + // headerTitle="검색" // 필요시 footerContent 등 추가 /> ); From ef23f658b1de7bc3c52ba89a695f4b56412a6fe3 Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 28 May 2025 18:08:09 +0900 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20UiTable2=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20default,=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=97=86=EC=9D=84=20=EC=8B=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/SearchModal.tsx | 1 + .../src/ui/table/index-2.tsx | 82 +++++++++++-------- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/apps/sn/app/map/SearchModal.tsx b/apps/sn/app/map/SearchModal.tsx index 3fe733cfe..d7ab3175d 100644 --- a/apps/sn/app/map/SearchModal.tsx +++ b/apps/sn/app/map/SearchModal.tsx @@ -414,6 +414,7 @@ const SearchModal: React.FC = ({ open, onOpenChange, onFeature console.log('필터링된 API 결과(데이터):', res.data.data); setTableData(res.data.data || []); setTableCount(res.data.count || []); + console.log('tableCount 상태 업데이트 후:', res.data.count); // tableCount 로그 추가 setShowTable(true); }) .catch(err => { diff --git a/packages/common/business_components/src/ui/table/index-2.tsx b/packages/common/business_components/src/ui/table/index-2.tsx index 58ae1bbd0..3a8caa90e 100644 --- a/packages/common/business_components/src/ui/table/index-2.tsx +++ b/packages/common/business_components/src/ui/table/index-2.tsx @@ -59,44 +59,54 @@ export function UiTable2({ tableData, headlist, pageSize, total, children, headN

{headName}

) : null} -
-
- - {/* 스크롤 관련 div 제거 */} - - {children ? ( - <>{children} - ) : ( - - {headlist?.map((head) => ( - + {(!tableData || tableData.length === 0) ? ( + // "데이터 없음" 메시지를 flex를 이용해 수직/수평 중앙 정렬 +
+ 조회된 데이터가 없습니다. +
+ ) : ( +
+ {/* 스크롤 관련 div 제거 */} + + {children ? ( + <>{children} + ) : ( + + {headlist?.map((head) => ( + - {head.title} - - ))} - - )} - - - {tableData.map((el: { [key: string]: string | number | boolean | object }, index) => ( - - {headlist?.map((head) => ( - handleCellClick(startIdx + index, head.id)} - key={head.id + index} - > - {typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')} - - ))} - - ))} - -
+ {head.title} + + ))} + + )} + + + {tableData.map((el: { [key: string]: string | number | boolean | object }, index) => ( + + {headlist?.map((head) => ( + handleCellClick(startIdx + index, head.id)} + key={head.id + index} + > + {typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')} + + ))} + + ))} + + + )}
{/* 페이지네이션을 sticky 하단에 고정 */} {total > 0 && ( From c8f633be07112bf29237b10737a548547a6b8566 Mon Sep 17 00:00:00 2001 From: JIeunhuh Date: Thu, 29 May 2025 13:19:29 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/BaseLayout.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx index 129063ba7..34537192b 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -65,16 +65,16 @@ const BaseLayout = ({ children, titleName, form, onSubmitEvent, graphBtn = false { }} selectClass='' />
- {searchBox?.searchChk ? : null} + {searchBox?.searchChk ? : null}

{titleName.firstCardTitle}

- {graphBtn ? : null} + {graphBtn ? : null} {/* 그래프 있을 시 */} - {graphBtn ? : null} + {graphBtn ? : null}
@@ -91,9 +91,9 @@ const BaseLayout = ({ children, titleName, form, onSubmitEvent, graphBtn = false

{titleName?.secondCardTitle}

- {graphBtn ? : null} + {graphBtn ? : null} {/* 그래프 있을 시 */} - {graphBtn ? : null} + {graphBtn ? : null}
{children?.[1]}