Merge remote-tracking branch 'origin/iipbs/sn/dev' into sn-ljpark2

This commit is contained in:
2025-07-11 09:29:38 +09:00
22 changed files with 830 additions and 264 deletions
+2 -2
View File
@@ -12,5 +12,5 @@ NEXT_PUBLIC_ACCESS_TOKEN = access-token
NEXT_PUBLIC_REFRESH_TOKEN = refresh-token
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 # 최용성 부장님 API
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883/sewerage-service/api # 박이정 차장님 API
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.128:8883/sewerage-service/api # 최용성 부장님 API
#NEXT_PUBLIC_SEWERAGE_API_URL=http://192.168.1.222:8883/sewerage-service/api # 박이정 차장님 API
+32 -37
View File
@@ -1,8 +1,8 @@
import FeatureClickEventPopup from './featureClickEventPopup';
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { UiModal, UiTable } from '@common/business_components/ui';
import { Button, DatePicker, Input, Label } from '@common/components';
import { UiModal, UiTable, DatePickerRange } from '@common/business_components/ui';
import { Button, Input, Label } from '@common/components';
import { UiTable2 } from '@common/business_components/ui/table/index-2';
import { UITab2 } from '@common/business_components/ui/tab/index-2';
import { SelectBox2 } from '@common/business_components/ui/select/index-2';
@@ -138,6 +138,21 @@ const SearchModal: React.FC<SearchModalProps> = ({
const todayMonth = (today.getMonth() + 1).toString().padStart(2, "0");
const todayDate = today.getDate().toString().padStart(2, "0");
const isDateInRanges = (date: Date) => {
const standYear = parseInt(fromDate.getFullYear().toString());
const dateRanges = [];
dateRanges.push({
start: new Date(standYear, 0, 1),
end: new Date(standYear, 11, 31)
});
return dateRanges.some(range => {
const start = new Date(range.start);
const end = new Date(range.end);
return date >= start && date <= end;
});
};
// UiTable2에 넘길 headlist를 BASIC_TYPES 값에 따라 분기 처리
let dynamicHeadlist = MsSwagePplCol2; // default 값
if (activeTab === 'operation') dynamicHeadlist = operationTableHeadlist; // 운영정보
@@ -992,43 +1007,23 @@ const SearchModal: React.FC<SearchModalProps> = ({
startLabel="수행기간"
endLabel=""
/> */}
<DatePicker
className='mr-4'
mode={'single'}
selected={fromDate}
onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }}
modifiers={{
disabled: { after: new Date() }
}}
zIndex={102}
<DatePickerRange
mode="single" //고정
selected={fromDate} // 처음에 선택할 날짜 - 타입은 date타입
onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} // 선택했을 때 이벤트 (주로 set)
className="w-full p-1 border rounded bg-white" // 스타일
disabled={(date) => !isDateInRanges(date)} // 선택못하게 하는 날짜 - 없으면 안넣어도 됨
defaultMonth={fromDate} // 처음에 시작할 날짜 selected랑 같이 같은 날짜 넣으면 됨
/>
<span>~</span>
<DatePicker
className='ml-4'
mode={'single'}
selected={toDate}
onSelect={(date: Date | undefined) => { if (date) setToDate(date) }}
modifiers={{
disabled: { after: new Date() }
}}
zIndex={102}
<span className='text-xl m-4'>~</span>
<DatePickerRange
mode="single" //고정
selected={toDate} // 처음에 선택할 날짜 - 타입은 date타입
onSelect={(date: Date | undefined) => { if (date) setFromDate(date) }} // 선택했을 때 이벤트 (주로 set)
className="w-full p-1 border rounded bg-white" // 스타일
disabled={(date) => !isDateInRanges(date)} // 선택못하게 하는 날짜 - 없으면 안넣어도 됨
defaultMonth={toDate} // 처음에 시작할 날짜 selected랑 같이 같은 날짜 넣으면 됨
/>
<div className="flex items-center">
{/* 경과년도 */}
{/* <Label className="mr-2 w-auto text-xl text-gray-600">
경과년도
</Label>
<Input
className="mr-8 w-[100px] !text-xl placeholder:text-center"
placeholder="입력"
value={yearFilter.경과년도}
onChange={(e) => {
// 숫자만 입력 허용
const val = e.target.value.replace(/[^0-9]/g, "");
setYearFilter({ ...yearFilter, 경과년도: val });
}}
/> */}
</div>
</div>
{/* 조회 버튼 중앙 정렬 */}
<div className="absolute bottom-4 right-4 mr-2">
@@ -7,9 +7,11 @@ import SwtrmtFclt from "../popupComponent/SwtrmtFclt";
type LayerComponentProps = {
featureData: any;
chkMainSsc?: boolean;
isMainSsc?: boolean;
isDrtwSewagePump?: boolean;
additionalData?: any;
onSaveClick?: () => void;
onFormDataChange?: (data: any) => void; // 추가
};
type LayerContentMap = {
[key: string]: {
@@ -55,6 +55,213 @@ export const MsSwagePplCol2 = [
// { id: 'usgCd', title: '용도코드' },
// { id: 'lyrTypeNm', title: '레이어타입명' },
]
// 하수관로 용도 코드표
export const MS_SWAGE_PPL_USE_CODES = [
{ val: "SBA000", text: "미분류" },
{ val: "SBA001", text: "합류관" },
{ val: "SBA002", text: "차집관" },
{ val: "SBA003", text: "우수관" },
{ val: "SBA004", text: "오수관" },
{ val: "SBA999", text: "기타" },
{ val: "그 외", text: "미분류" },
];
// 하수관로 재질 코드표
export const MS_SWAGE_PPL_CODES = [
{ val: "MOP000", text: "미분류" },
{ val: "MOP001", text: "닥타일주철관(DCIP)" },
{ val: "MOP002", text: "강관(SP)" },
{ val: "MOP003", text: "아연도강관(GS)" },
{ val: "MOP004", text: "염화비닐관(PVC)" },
{ val: "MOP005", text: "폴리에틸렌관(PE)" },
{ val: "MOP006", text: "스테인레스관(SSP)" },
{ val: "MOP007", text: "동관(COP)" },
{ val: "MOP008", text: "P.S.콘크리트관(PC)" },
{ val: "MOP009", text: "석면시멘트관(AS)" },
{ val: "MOP010", text: "회주철관(CIP)" },
{ val: "MOP011", text: "연관(LP)" },
{ val: "MOP012", text: "흉관(HP)" },
{ val: "MOP013", text: "철근콘크리트관(RC)" },
{ val: "MOP014", text: "강관(ST)" },
{ val: "MOP015", text: "주철관(CI)" },
{ val: "MOP016", text: "화학제품류관(PV)" },
{ val: "MOP017", text: "도관(VC)" },
{ val: "MOP018", text: "석축(SE)" },
{ val: "MOP019", text: "도복장 강관(SP)" },
{ val: "MOP020", text: "흉관(EP)" },
{ val: "MOP021", text: "아연도강관(GP)" },
{ val: "MOP022", text: "청동관(BR)" },
{ val: "MOP023", text: "고무몰딩(RM)" },
{ val: "MOP024", text: "주강(CS)" },
{ val: "MOP025", text: "VR관" },
{ val: "MOP026", text: "HDPE관" },
{ val: "MOP027", text: "시멘트라이닝주철관(C)" },
{ val: "MOP028", text: "수도용 PE관(PE)" },
{ val: "MOP029", text: "석면시멘트관(AC)" },
{ val: "MOP030", text: "폴리에틸렌분체라이닝강관(PEP)" },
{ val: "MOP031", text: "에폭시코팅강관(EPS)" },
{ val: "MOP032", text: "HI-3P" },
{ val: "MOP033", text: "철근콘크리트박스(RCB)" },
{ val: "MOP034", text: "콘크리트중선함(CD)" },
{ val: "MOP035", text: "후랙시불전선함(FP)" },
{ val: "MOP036", text: "전선함(FD)" },
{ val: "MOP038", text: "이중보온관" },
{ val: "MOP039", text: "폴리에틸렌피복강관(PLP)" },
{ val: "MOP041", text: "FRP" },
{ val: "MOP042", text: "강재" },
{ val: "MOP999", text: "기타" },
{ val: "MOP800", text: "2중벽PE관" },
{ val: "MOP801", text: "3중벽PE관" },
{ val: "MOP802", text: "가스배관용 강관" },
{ val: "MOP803", text: "강관(피복 Double, 용접접합)" },
{ val: "MOP804", text: "강관(피복 Single, 용접접합)" },
{ val: "MOP805", text: "강관(CP)" },
{ val: "MOP806", text: "강철" },
{ val: "MOP807", text: "강화플라스틱복합관" },
{ val: "MOP808", text: "경질염화비닐관(HI-VP)" },
{ val: "MOP809", text: "고강성PVC이중벽관" },
{ val: "MOP810", text: "고광도PVC이중벽관" },
{ val: "MOP811", text: "고밀도폴리에틸렌관(THP)" },
{ val: "MOP812", text: "고밀도폴리에틸렌파상형이중벽관" },
{ val: "MOP813", text: "공장제 박스관거" },
{ val: "MOP814", text: "관로없음(알케이블)" },
{ val: "MOP815", text: "나선형강관" },
{ val: "MOP816", text: "내충격수도관" },
{ val: "MOP817", text: "내충격용하수도용경질염화비닐관(HI-VG)" },
{ val: "MOP818", text: "내충격HI-VG2관" },
{ val: "MOP819", text: "내충격PVC하수관" },
{ val: "MOP820", text: "녹석(돌산호과 일종)" },
{ val: "MOP821", text: "동관(구리관, 용접)" },
{ val: "MOP822", text: "둥근잔돌" },
{ val: "MOP823", text: "무근콘크리트" },
{ val: "MOP824", text: "백관" },
{ val: "MOP825", text: "복합철판파형관" },
{ val: "MOP826", text: "분체라이닝관" },
{ val: "MOP827", text: "석축박스" },
{ val: "MOP828", text: "석축철콘" },
{ val: "MOP829", text: "수도용경질염화비관" },
{ val: "MOP830", text: "수도용경질염화비닐이음관" },
{ val: "MOP831", text: "수도용엑포시수지분체내외면코팅강관" },
{ val: "MOP832", text: "수지파형강관(PCS)" },
{ val: "MOP833", text: "수지CSP" },
{ val: "MOP834", text: "스테인레스 주름관" },
{ val: "MOP835", text: "스테인레스관(STS)" },
{ val: "MOP836", text: "스테인레스스틸파이프관" },
{ val: "MOP837", text: "아스콘" },
{ val: "MOP838", text: "아스팔트" },
{ val: "MOP839", text: "아스팔트도복장강관(ASP)" },
{ val: "MOP840", text: "아연" },
{ val: "MOP841", text: "아연도금(백관)" },
{ val: "MOP842", text: "알루미늄" },
{ val: "MOP843", text: "알루미늄합금" },
{ val: "MOP844", text: "압력배관용 탄소강관" },
{ val: "MOP845", text: "양철관" },
{ val: "MOP846", text: "양철주름관(CSP)" },
{ val: "MOP847", text: "연합인슈강판" },
{ val: "MOP848", text: "와사관(GSP)" },
{ val: "MOP849", text: "원심력철근콘크리트" },
{ val: "MOP850", text: "유리섬유관(GRP)" },
{ val: "MOP851", text: "유리섬유복합관(GRP)" },
{ val: "MOP852", text: "이음매없는동및동합금관" },
{ val: "MOP853", text: "일반배관용스테인리스강관" },
{ val: "MOP854", text: "전압 콘크리트관" },
{ val: "MOP855", text: "주름관" },
{ val: "MOP856", text: "주름주철파형강관" },
{ val: "MOP857", text: "지중 저압케이블 입상관" },
{ val: "MOP858", text: "진동및전압철근콘크리트관" },
{ val: "MOP859", text: "철근콘크리트" },
{ val: "MOP860", text: "철근콘크리트 블록" },
{ val: "MOP861", text: "철근콘크리트조립흙막이" },
{ val: "MOP862", text: "철판매입관" },
{ val: "MOP863", text: "청동" },
{ val: "MOP864", text: "코아식프리스트레스트콘크리트관" },
{ val: "MOP865", text: "콘크리트" },
{ val: "MOP866", text: "콘크리트 폴륨관" },
{ val: "MOP867", text: "콘크리트블록" },
{ val: "MOP868", text: "탄소강" },
{ val: "MOP869", text: "탄소강(카본스틸)" },
{ val: "MOP870", text: "탄소주강" },
{ val: "MOP871", text: "트러후" },
{ val: "MOP872", text: "파선형강관(RCF)" },
{ val: "MOP873", text: "파형강관(CSP)" },
{ val: "MOP874", text: "파형관" },
{ val: "MOP875", text: "파형플라스틱관" },
{ val: "MOP876", text: "폴리머관" },
{ val: "MOP877", text: "폴리에스테르수지콘크리트관" },
{ val: "MOP878", text: "폴리에틸렌 전선관" },
{ val: "MOP879", text: "폴리에틸렌 피복강관" },
{ val: "MOP880", text: "폴리에틸렌관 1호관" },
{ val: "MOP881", text: "폴리에틸렌관 2호관" },
{ val: "MOP882", text: "폴리에틸렌관(2중벽)" },
{ val: "MOP883", text: "폴리에틸렌관(3중벽)" },
{ val: "MOP884", text: "폴리에틸렌분체라이닝" },
{ val: "MOP885", text: "폴리에틸렌이중벽하수관" },
{ val: "MOP886", text: "폴리탄산에스테르" },
{ val: "MOP887", text: "프리스트레스트콘크리트실린더관" },
{ val: "MOP888", text: "플라스틱" },
{ val: "MOP889", text: "플라스틱주름관" },
{ val: "MOP890", text: "하수도용고강성경질염화비닐이중벽주름관" },
{ val: "MOP891", text: "현장타설관" },
{ val: "MOP892", text: "현장타설철근콘크리트관" },
{ val: "MOP893", text: "흑관" },
{ val: "MOP894", text: "흙" },
{ val: "MOP895", text: "AP" },
{ val: "MOP896", text: "API관" },
{ val: "MOP897", text: "ASP" },
{ val: "MOP898", text: "CMCIP" },
{ val: "MOP899", text: "CSP" },
{ val: "MOP900", text: "DRP" },
{ val: "MOP901", text: "DSP" },
{ val: "MOP902", text: "DTC관" },
{ val: "MOP903", text: "ELP" },
{ val: "MOP904", text: "EPP" },
{ val: "MOP905", text: "FC관" },
{ val: "MOP906", text: "H" },
{ val: "MOP907", text: "H6" },
{ val: "MOP908", text: "HCP" },
{ val: "MOP909", text: "HI_2P관" },
{ val: "MOP910", text: "HI-KP" },
{ val: "MOP911", text: "HL" },
{ val: "MOP912", text: "IP" },
{ val: "MOP913", text: "KCGP" },
{ val: "MOP914", text: "KP관" },
{ val: "MOP915", text: "P2S관" },
{ val: "MOP916", text: "PC박스(PCB)" },
{ val: "MOP917", text: "PCS" },
{ val: "MOP918", text: "PDP" },
{ val: "MOP919", text: "PE이중벽관" },
{ val: "MOP920", text: "PE적측벽1종관" },
{ val: "MOP921", text: "PFP관" },
{ val: "MOP922", text: "PG관" },
{ val: "MOP923", text: "PIP관(이중보온관)" },
{ val: "MOP924", text: "PLCSP" },
{ val: "MOP925", text: "PS" },
{ val: "MOP926", text: "PUB" },
{ val: "MOP927", text: "PVB" },
{ val: "MOP928", text: "PVC이중벽관(PVC2)" },
{ val: "MOP929", text: "PVC-DC" },
{ val: "MOP930", text: "PVC-DV" },
{ val: "MOP931", text: "SBK" },
{ val: "MOP932", text: "SCP" },
{ val: "MOP933", text: "SEB" },
{ val: "MOP934", text: "SIS관(이중보온관)" },
{ val: "MOP935", text: "SMC" },
{ val: "MOP936", text: "STW" },
{ val: "MOP937", text: "SUS" },
{ val: "MOP938", text: "THP나선관" },
{ val: "MOP939", text: "U관" },
{ val: "MOP940", text: "VG2" },
{ val: "MOP941", text: "WSP(와사피)" },
{ val: "MOP942", text: "Steel Grating" },
{ val: "MOP943", text: "모래" },
{ val: "MOP944", text: "인조석" },
{ val: "MOP945", text: "SPOL" },
{ val: "MOP946", text: "PCF관(강판보강 PE 나선형관)" },
{ val: "MOP947", text: "PVC3(PVC3중벽관)" },
{ val: "MOP948", text: "PE다중벽관" },
{ val: "MOP977", text: "전주용입상관" },
{ val: "MOP978", text: "MD관" },
{ val: "그 외", text: "미분류" },
];
// 하수처리시설
export const SwtrmtFcltCol = [
@@ -306,7 +513,6 @@ export const ExclnSewagePumpCol2 = [
{ id: 'exclnWpmpAblt', title: '우수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
// 오수펌프장
export const DrtwSewagePumpCol1 = [
// { id: 'fid', title: 'FID' },
@@ -348,6 +554,17 @@ export const DrtwSewagePumpCol2 = [
{ id: 'exclnWpmpAblt', title: '오수양수능력(m²/d)' },
// { id: 'instMngNo', title: '기관관리번호' },
]
// 펌프장 용도 코드표
export const DRTW_SEWAGE_PUMP_USE_CODES = [
{ val: "SBE000", text: "미분류" },
{ val: "SBE001", text: "빗물펌프장" },
{ val: "SBE002", text: "오수펌프장" },
{ val: "SBE003", text: "처리장내펌프장" },
{ val: "SBE900", text: "오수배제" },
{ val: "SBE901", text: "우수배제" },
{ val: "SBE999", text: "기타" },
{ val: "그 외", text: "미분류" },
];
// 우수토실
export const stofcbrCol = [
@@ -1,18 +1,18 @@
'use client'
import { VerticalTable } from '@/components/ui/verticalTable';
import React, { useState } from 'react'
import React, { useState, useEffect } from 'react'
import InputSection from '../InputSection';
import { handleInputChange } from '../common';
// 하수관로
const inputColumn = [
{ id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] },
{ id: "swtrmt_zone_cd", title: "처리구역", type: "input" },
{ id: "basu", title: "배수구역", type: "input" },
{ id: "mltv_dtdv_nm", title: "배수분구", type: "input" },
{ id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] },
{ id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] },
{ id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] },
{ id: "instlMbdyCd", title: "설치주제", type: "select", options: ["BTL", "재정사업"] },
{ id: "swtrmtZoneCd", title: "처리구역", type: "input" },
{ id: "mltvZoneNm", title: "배수구역", type: "input" },
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
{ id: "lineCd", title: "지선/간선", type: "select", options: ["지선", "간선"] },
{ id: "floCd", title: "흐름방식", type: "select", options: ["자연유하", "압송"] },
{ id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] },
];
const MsSwagePpl = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => {
@@ -20,29 +20,48 @@ const MsSwagePpl = ({ featureData, onSaveClick }: { featureData: any, onSaveClic
{ year: "2025", type: "개대체" },
{ year: "2025", type: "보수" },
]);
const [cleaningData, setCleaningData] = useState([
{ year: "2025", type: "청소" },
{ year: "2025", type: "준설" },
]);
const [inspectionData, setInspectionData] = useState([
{ year: "2025", type: "관로기술진단" },
{ year: "2025", type: "관로기술진단" },
]);
const dataMap = {
repair: { data: repairData, setData: setRepairData },
cleaning: { data: cleaningData, setData: setCleaningData },
inspection: { data: inspectionData, setData: setInspectionData },
};
const [formData, setFormData] = useState<any>({});
// featureData가 변경될 때 formData 초기화
useEffect(() => {
if (featureData) {
setFormData(featureData);
}
}, [featureData]);
// 폼 데이터가 변경될 때 호출될 핸들러
const handleFormDataChange = (newFormData: any) => {
setFormData(newFormData);
};
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
const handleSaveClick = () => {
if (onSaveClick) {
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
onSaveClick();
}
};
return (
<>
<VerticalTable
headName='자체 입력정보'
tableData={[]}
tableData={featureData ? [formData] : []}
headlist={inputColumn}
pageSize={20}
total={0}
@@ -53,7 +72,8 @@ const MsSwagePpl = ({ featureData, onSaveClick }: { featureData: any, onSaveClic
throw new Error("Function not implemented.");
}}
showSaveButton={true}
onSaveClick={onSaveClick}
onSaveClick={handleSaveClick}
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
/>
<InputSection
title="보수 및 갱신"
@@ -1,29 +1,32 @@
// 우수토실
import { UiModal, UiTable } from '@common/business_components/ui';
import { Button, TableHead, TableRow } from '@common/components/ui';
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table';
import { VerticalTable } from '@/components/ui/verticalTable';
import LoadingOverlay from '@/components/ui/LoadingBar';
const inputColumn = [
{ id: "place", title: "중점여부", type: "select", options: ["중점우수토실", "우수토실"] },
{ id: "guyeok", title: "처리구역", type: "input" },
{ id: "prrtymngYn", title: "중점여부", type: "select", options: ["중점우수토실", "우수토실"] },
{ id: "swtrmtZoneCd", title: "처리구역", type: "input" },
];
const Ssc = ({ featureData, chkMainSsc, additionalData, onSaveClick }: {
const Ssc = ({ featureData, isMainSsc, additionalData, onSaveClick }: {
featureData: any,
chkMainSsc?: boolean,
isMainSsc?: boolean,
additionalData?: any,
onSaveClick?: () => void
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isDataLoaded, setIsDataLoaded] = useState(false);
const [formData, setFormData] = useState<any>({});
console.log('featureData data received:', featureData); // 추가 데이터 확인용
console.log('Additional data received:', additionalData); // 추가 데이터 확인용
const openModal = () => {
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
if (!isDataLoaded) {
@@ -31,11 +34,25 @@ const Ssc = ({ featureData, chkMainSsc, additionalData, onSaveClick }: {
}
};
// 데이터 전처리: 합쳐진 필드 추가
// const preprocessedData = featureData ? [{
// ...featureData,
// flwmtr_combined: `${featureData.flwmtr_mesure_tp_etc || '-'} / ${featureData.flwmtr_knd || '-'}`
// }] : [];
// featureData가 변경될 때 formData 초기화
useEffect(() => {
if (featureData) {
setFormData(featureData);
}
}, [featureData]);
// 폼 데이터가 변경될 때 호출될 핸들러
const handleFormDataChange = (newFormData: any) => {
setFormData(newFormData);
};
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
const handleSaveClick = () => {
if (onSaveClick) {
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
onSaveClick();
}
};
const renderModalBody = () => (
<div className='grid grid-cols-1 gap-4 p-4'>
@@ -100,23 +117,26 @@ const Ssc = ({ featureData, chkMainSsc, additionalData, onSaveClick }: {
return (
<div>
<VerticalTable
headName='자체 입력정보'
tableData={[]}
headlist={inputColumn}
pageSize={0}
total={0}
children={undefined}
cellClick={undefined}
isEditable={true}
onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}
showSaveButton={true}
onSaveClick={onSaveClick}
/>
{!isMainSsc && (
<VerticalTable
headName='자체 입력정보'
tableData={featureData ? [formData] : []}
headlist={inputColumn}
pageSize={0}
total={0}
children={undefined}
cellClick={undefined}
isEditable={true}
onPageChange={function (page: number): void {
throw new Error('Function not implemented.');
}}
showSaveButton={true}
onSaveClick={handleSaveClick}
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
/>
)}
{chkMainSsc && (
{isMainSsc && (
<div>
<div className="card-header">
<h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3>
@@ -1,22 +1,46 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import React from 'react'
import React, { useState, useEffect } from 'react'
// 빗물펌프 자체 입력 정보
const exclnInputColumn = [
{ id: "swtrmt_zone_cd", title: "배수구역", type: "input" },
{ id: "mltv_dtdv_nm", title: "배수분구", type: "input" },
{ id: "freq", title: "설계빈도(년)", type: "input" },
{ id: "swtrmtZoneCd", title: "배수구역", type: "input" },
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
{ id: "dsgnFrqnc", title: "설계빈도(년)", type: "input" },
]
// 오수펌프 자체 입력 정보
const drtwInputColumn = [
{ id: "swtrmt_zone_cd", title: "배수구역", type: "input" },
{ id: "mltv_dtdv_nm", title: "배수분구", type: "input" },
]
// 하수펌프장
const sewagePump = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => {
const sewagePump = ({ featureData, isDrtwSewagePump, onSaveClick }: { featureData: any, isDrtwSewagePump: boolean, onSaveClick?: () => void }) => {
const [formData, setFormData] = useState<any>({});
// featureData가 변경될 때 formData 초기화
useEffect(() => {
if (featureData) {
setFormData(featureData);
}
}, [featureData]);
// 폼 데이터가 변경될 때 호출될 핸들러
const handleFormDataChange = (newFormData: any) => {
setFormData(newFormData);
};
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
const handleSaveClick = () => {
if (onSaveClick) {
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
onSaveClick();
}
};
return (
<VerticalTable
headName='자체 입력정보'
tableData={[]}
headlist={exclnInputColumn}
tableData={featureData ? [formData] : []}
headlist={isDrtwSewagePump ? drtwInputColumn : exclnInputColumn}
pageSize={0}
total={0}
children={undefined}
@@ -26,7 +50,8 @@ const sewagePump = ({ featureData, onSaveClick }: { featureData: any, onSaveClic
throw new Error('Function not implemented.');
}}
showSaveButton={true}
onSaveClick={onSaveClick}
onSaveClick={handleSaveClick}
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
/>
)
}
@@ -1,17 +1,39 @@
// 하수맨홀
import { VerticalTable } from '@/components/ui/verticalTable';
import React from 'react'
import React, { useState, useEffect } from 'react'
const inputColumn = [
{ id: "guyeok", title: "처리구역", type: "input" },
{ id: "basu", title: "배수구역", type: "input" },
{ id: "bungu", title: "배수분구", type: "input" },
{ id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] },
{ id: "swtrmtZoneCd", title: "처리구역", type: "input" },
{ id: "mltvZoneNm", title: "배수구역", type: "input" },
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
{ id: "layCd", title: "매설위치", type: "select", options: ["도로", "하상"] },
];
const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: () => void }) => {
const [formData, setFormData] = useState<any>({});
// featureData가 변경될 때 formData 초기화
useEffect(() => {
if (featureData) {
setFormData(featureData);
}
}, [featureData]);
// 폼 데이터가 변경될 때 호출될 핸들러
const handleFormDataChange = (newFormData: any) => {
setFormData(newFormData);
};
// 저장 버튼 클릭 시 최신 formData를 부모에게 전달
const handleSaveClick = () => {
if (onSaveClick) {
// 저장 전에 formData를 부모 컴포넌트의 handleSaveClick에 전달
onSaveClick();
}
};
return (
<VerticalTable
headName='자체 입력정보'
tableData={[]}
tableData={featureData ? [formData] : []}
headlist={inputColumn}
pageSize={20}
total={0}
@@ -22,7 +44,8 @@ const Smh = ({ featureData, onSaveClick }: { featureData: any, onSaveClick?: ()
throw new Error("Function not implemented.");
}}
showSaveButton={true}
onSaveClick={onSaveClick}
onSaveClick={handleSaveClick}
onFormDataChange={handleFormDataChange} // 폼 데이터 변경 콜백 추가
/>
)
}
@@ -1,15 +1,16 @@
'use client'
import { getData, putData } from "@/components/lib/api";
import { getData, postData } from "@/components/lib/api";
import Modal from "@/components/ui/modal";
import { VerticalTable } from "@/components/ui/verticalTable";
import { useGisContext } from "@common/business_components/ui/map/Gis";
import { Button } from "@common/components/ui";
import { useEffect, useRef, useState, useCallback, useMemo } from "react";
import { getLayerContentComponent } from "./components/common";
import { SwtrmtFcltCol, SwtrmtPrcsZoneCol, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, ExclnSewagePumpCol, MsSwageMhlCol, stofcbrCol, MS_SWAGE_MHL_CODES } from "./components/common/table";
import { SwtrmtFcltCol, SwtrmtPrcsZoneCol, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, ExclnSewagePumpCol, MsSwageMhlCol, stofcbrCol, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "./components/common/table";
import InfilChart from './components/popupComponent/chart/infilChart';
import RdiiChart from './components/popupComponent/chart/rdiiChart';
import { useInfilChartModalStore, useRdiiChartModalStore } from "./components/popupComponent/chartModalStore";
import { set } from "react-hook-form";
// TODO:
const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => {
@@ -23,7 +24,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
const [fetchData, setfetchData] = useState<any>(); // api 호출여부
const contentRef = useRef<HTMLDivElement>(null);
const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태
const [chkMainSsc, setChkMainSsc] = useState(false); // 중점우수토실 여부
const [isMainSsc, setIsMainSsc] = useState(false); // 중점우수토실 여부
const [isDrtwSewagePump, setIsDrtwSewagePump] = useState(false); // 오수펌프장 여부
const [isLoading, setIsLoading] = useState(false);
// 🎯 추가 데이터 로딩 상태 분리
const [isAdditionalDataLoading, setIsAdditionalDataLoading] = useState(false);
@@ -54,17 +56,10 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
try {
console.log(`추가 데이터 API 호출 시작: v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs`);
// 🎯 타임아웃 설정 (10초)
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('API 타임아웃')), 10000)
);
const result = getData(`v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs`);
const apiPromise = getData(`v1/onsemiro/swgRain/${adstDstrCd}/${manageNo}/mntrngs`);
const response = await Promise.race([apiPromise, timeoutPromise]) as any;
console.log('추가 데이터 API 호출 성공:', response);
return response?.content || [];
console.log('추가 데이터 API 호출 성공:', result);
return result || [];
} catch (error) {
console.error('추가 데이터 로딩 실패:', error);
setAdditionalDataError(error instanceof Error ? error.message : '알 수 없는 오류');
@@ -106,7 +101,16 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
else if (featureData.lyrTypeNm === 'tb_ms_stofcbr') result = '우수토실';
else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
else if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') result = '하수관로';
else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') result = '하수펌프장';
else if (featureData.lyrTypeNm === 'tb_ms_swage_pmstn') {
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
console.log('값 비교', featureData.values_.usg_cd.trim(), drtwPumpCode?.val);
if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) {
setIsDrtwSewagePump(true);
} else {
setIsDrtwSewagePump(false);
}
result = '하수펌프장';
}
else if (featureData.lyrTypeNm === 'tb_ms_swage_mhl') result = '하수맨홀';
}
else {
@@ -116,7 +120,16 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
else if (lyrTyNm === 'tb_ms_stofcbr') result = '우수토실';
else if (lyrTyNm === 'om_dt_untrt_swg_rain') result = '중점우수토실';
else if (lyrTyNm === 'tb_ms_swage_ppl') result = '하수관로';
else if (lyrTyNm === 'tb_ms_swage_pmstn') result = '하수펌프장';
else if (lyrTyNm === 'tb_ms_swage_pmstn') {
const drtwPumpCode = DRTW_SEWAGE_PUMP_USE_CODES.find(code => code.text === '오수펌프장');
console.log('값 비교', featureData.values_.usg_cd.trim(), drtwPumpCode?.val);
if (drtwPumpCode && featureData.values_.usg_cd.trim() === drtwPumpCode.val) {
setIsDrtwSewagePump(true);
} else {
setIsDrtwSewagePump(false);
}
result = '하수펌프장';
}
else if (lyrTyNm === 'tb_ms_swage_mhl') result = '하수맨홀';
else result = featureLayer.get('lyrNm');
}
@@ -170,7 +183,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
setHeadlist(untrtSwgRainCol1)
setHeadlist2(untrtSwgRainCol2)
setButtonChk(false)
setChkMainSsc(true)
setIsMainSsc(true)
setfetchData([processedData])
setTableData(processedData);
@@ -188,11 +201,11 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
setMntrngsData(false);
});
}
return;
break;
case '우수토실':
setHeadlist(stofcbrCol)
setButtonChk(false)
setChkMainSsc(false)
setButtonChk(true)
setIsMainSsc(false)
break;
case '하수관로':
setHeadlist(MsSwagePplCol)
@@ -215,6 +228,59 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
default:
break;
}
// API 엔드포인트 선택
let apiEndpoint = '';
let getFeatureId = featureId.toString().split('.').pop() || null;
if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${getFeatureId}`;
else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${getFeatureId}`;
else if (selectedLayerName === '오수펌프' || selectedLayerName === '빗물펌프') apiEndpoint = `v1/fcltpump/${getFeatureId}`;
else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${getFeatureId}`;
// API에서 상세 데이터 가져오기
if (apiEndpoint) {
try {
console.log(`API 호출: ${apiEndpoint}`);
const result = await getData(apiEndpoint);
if (result) {
console.log('API 데이터 로드 성공:', result);
// 하수맨홀인 경우 용도코드를 텍스트로 변환
if (selectedLayerName === '하수맨홀') {
const processedData = { ...result };
// mqlt_cd 값 변환 (재질코드)
if (processedData.mqlt_cd) {
const materialCodeItem = MS_SWAGE_MHL_CODES.find(
code => code.val === processedData.mqlt_cd.trim()
);
if (materialCodeItem) {
processedData.mqlt_cd = `${materialCodeItem.text}`;
}
}
setfetchData(processedData);
} // 🎯 우수토실인 경우 prrtymngYn 값 변환
else if (selectedLayerName === '우수토실' && data) {
const processedData = { ...result, ...data };
// prrtymngYn 값 변환
if (processedData.prrtymngYn === 'Y') processedData.prrtymngYn = '중점우수토실';
else if (processedData.prrtymngYn === 'N') processedData.prrtymngYn = '우수토실';
console.log('processedData', processedData);
setfetchData(processedData);
} else {
setfetchData(result);
}
}
} catch (apiError) {
console.error('API 데이터 로딩 중 오류:', apiError);
}
}
console.log('mntrngsData', mntrngsData);
} catch (error) {
console.error('데이터 로딩 중 오류:', error);
}
@@ -275,27 +341,50 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
bodyData[camelCaseId] = select.value;
});
// input 태그의 값 가져오기
// // input 태그의 값 가져오기
inputElements.forEach((input) => {
const camelCaseId = toCamelCase(input.id);
bodyData[camelCaseId] = input.value;
});
if (layerNm === '하수관로') bodyData['lyrTypeNm'] = 'tb_ms_swage_ppl'
else if (layerNm === '우수토실') bodyData['lyrTypeNm'] = 'tb_ms_stofcbr'
else if (layerNm === '중점우수토실') bodyData['lyrTypeNm'] = 'om_dt_untrt_swg_rain'
else if (layerNm === '오수펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn'
else if (layerNm === '빗물펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn'
else if (layerNm === '하수맨홀') bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl'
// 레이어명에 따라 API 엔드포인트 분기처리
let apiEndpoint = '';
let result = null;
bodyData['fid'] = featureId
if (layerNm === '하수관로') apiEndpoint = 'v1/fcltppl';
else if (layerNm === '우수토실') apiEndpoint = 'v1/fcltstofcbr';
else if (layerNm === '하수펌프장') apiEndpoint = 'v1/fcltpump';
else if (layerNm === '하수맨홀') apiEndpoint = 'v1/fcltmhl';
const data = await putData(`v1/fclts/${featureId}`, bodyData);
bodyData['fid'] = featureId.toString().split('.').pop() || null;
// 빈 키 제거
if (bodyData[""] !== undefined) delete bodyData[""];
if (data.status == 200) {
alert('저장되었습니다')
setTableData(data)
setfetchData(data)
// 🎯 우수토실일 경우 prrtymngYn 값 변환
if (layerNm === '우수토실' && bodyData.prrtymngYn) {
if (bodyData.prrtymngYn === '중점우수토실') {
bodyData.prrtymngYn = 'Y';
} else if (bodyData.prrtymngYn === '우수토실') {
bodyData.prrtymngYn = 'N';
}
}
console.log('bodyData', bodyData);
console.log('apiEndpoint', apiEndpoint);
try {
result = await postData(apiEndpoint, bodyData);
console.log('result', result);
if (result) {
alert('저장되었습니다');
setfetchData(result.data || result);
} else {
alert('저장에 실패했습니다');
console.error('API 응답 오류:', result);
}
} catch (error) {
alert('저장 중 오류가 발생했습니다');
console.error('API 요청 오류:', error);
}
}
}
@@ -363,7 +452,8 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
{ContentComponent &&
<ContentComponent
featureData={fetchData}
chkMainSsc={chkMainSsc}
isMainSsc={isMainSsc}
isDrtwSewagePump={isDrtwSewagePump}
additionalData={mntrngsData}
onSaveClick={handleSaveClick}
/>
+2
View File
@@ -9,6 +9,8 @@ export const postData = async (url: string, data: object): Promise<any> => {
body: JSON.stringify(data),
};
const res = await fetch(`${BASE_URL}/${url}`, options);
console.log('res', res);
console.log('res.status', res.status);
if (res.ok) {
const resData = await res.json();
return resData;
+21 -9
View File
@@ -29,6 +29,7 @@ type UiTableProps = {
showSecondTable?: boolean;
showSaveButton?: boolean;
onSaveClick?: () => void;
onFormDataChange?: (formData: { [key: string]: string | number | boolean }) => void; // 추가
};
type UiHeadProps = {
@@ -66,6 +67,7 @@ export function VerticalTable({
showSecondTable = false,
showSaveButton = false,
onSaveClick,
onFormDataChange,
}: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [formData, setFormData] = useState<{ [key: string]: string | number | boolean }>({});
@@ -78,17 +80,27 @@ export function VerticalTable({
useEffect(() => {
if (tableData.length > 0 && tableData[0]) {
setFormData(tableData[0] as { [key: string]: string | number | boolean });
// 깊은 복사를 통해 새로운 객체 생성
const newData = JSON.parse(JSON.stringify(tableData[0]));
setFormData(newData);
console.log("Form data initialized:", newData); // 디버깅용
}
}, [tableData]);
const handleInputChange = (id: string, value: string | number | boolean) => {
setFormData((prev) => ({
...prev,
console.log(`Input change: ${id} = ${value}`); // 디버깅용
const newFormData = {
...formData,
[id]: value,
}));
};
};
console.log("New form data:", newFormData); // 디버깅용
setFormData(newFormData);
// 폼 데이터가 변경될 때마다 부모에게 알림
if (onFormDataChange) {
onFormDataChange(newFormData);
}
};
const handlePageChange = (page: number) => {
setCur(page);
onPageChange(page);
@@ -100,8 +112,8 @@ export function VerticalTable({
<div className="card-header flex items-center justify-between">
<h3 className="text-2xl mx-3 font-medium text-label">{headName}</h3>
{showSaveButton && onSaveClick && (
<Button
onClick={onSaveClick}
<Button
onClick={onSaveClick}
className="px-4 mr-3 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
>
저장
@@ -174,7 +186,7 @@ export function VerticalTable({
head.type === "select" ? (
<select
id={head.id}
value={String(formData[head.id]) ?? "-"}
value={formData[head.id] !== undefined ? String(formData[head.id]) : ""}
onChange={(e) => handleInputChange(head.id, e.target.value)}
className="w-full border border-gray-300 rounded-sm px-2 py-1 text-xl border-radius-0"
>
@@ -188,7 +200,7 @@ export function VerticalTable({
<input
id={head.id}
type="text"
value={String(formData[head.id]) ?? ""}
value={formData[head.id] !== undefined ? String(formData[head.id]) : ""}
onChange={(e) => handleInputChange(head.id, e.target.value)}
className="w-full text-xl border-radius-0 border border-gray-300 rounded-sm px-2 py-1"
/>
@@ -74,17 +74,25 @@ const CustomLegend = (props: any) => {
);
};
// chartData의 모든 값 중 최대값 구하기
const getGlobalMax = (data: Record<string, number>[]) => {
if (!data || data.length === 0) return 0;
return Math.max(
...data.flatMap(item =>
Object.entries(item)
.filter(([key]) => key !== 'label')
.map(([, value]) => value as number)
)
);
};
export default function Chart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) {
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#FED16A", "#FFEEAD", "#D4A5A5"];
const getMaxValue = (data: any[], keyPrefix: string) => {
return Math.max(...data.map(item =>
Object.keys(item)
.filter(key => key.includes(keyPrefix))
.map(key => item[key])
).flat());
};
const barMaxValue = getMaxValue(chartData, 'barVal');
const globalMax = getGlobalMax(chartData);
const tickCount = 5;
// interval을 globalMax / tickCount로 계산
const interval = globalMax / tickCount;
const ticks = Array.from({ length: tickCount + 1 }, (_, i) => Math.round(interval * i));
return (
<>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
@@ -93,8 +101,8 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle='
</div>): null}
<div className="flex justify-center">
<ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<BarChart accessibilityLayer data={chartData} className="!h-[88%]">
<CartesianGrid vertical={false} className="!h-[85%]" />
<XAxis
dataKey="label"
tickLine={true}
@@ -106,12 +114,12 @@ export default function Chart({chartConfig=undefined, chartData=[], chartTitle='
<YAxis
tickLine={true}
tickMargin={10}
tickCount={6}
axisLine={true}
fontSize={12}
fontWeight={500}
domain={[0, Math.ceil(barMaxValue * 1)]}
label={yFieldText ? { value: yFieldText, className:"text-[13px] font-bold", position: 'insideLeft', angle: -90, offset: 20 } : undefined}
domain={[0, globalMax]}
ticks={ticks}
label={yFieldText ? { value: yFieldText, className:"text-[15px] font-bold", position: 'insideLeft', angle: -90, offset: -30 } : undefined}
/>
<ChartTooltip cursor={false} content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />}/>
{chartData && chartData.length > 0 && (
@@ -6,62 +6,100 @@ import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import type { DayPickerSingleProps } from "react-day-picker";
import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "@common/components/ui";
import {
Button,
Calendar,
Popover,
PopoverContent,
PopoverTrigger,
} from "@common/components/ui";
import { cn } from "../../lib/utils";
import { FormControl } from "../form";
import { ko } from "date-fns/locale";
import { useState } from "react";
type DatePickerProps = DayPickerSingleProps & { field: { value: Date; onChange: (date: Date | undefined) => void }, selectClass: string | undefined, readOnly?: boolean };
type DatePickerProps = DayPickerSingleProps & {
field: { value: Date; onChange: (date: Date | undefined) => void };
selectClass: string | undefined;
readOnly?: boolean;
};
export function DatePicker({
field,
selectClass,
readOnly = false,
...props
}: DatePickerProps) {
const [open, setOpen] = useState(false);
export function DatePicker({ field, selectClass, readOnly = false, ...props }: DatePickerProps) {
const [open, setOpen] = useState(false);
const handleSelect = (date: Date | undefined) => {
if (!readOnly) {
// readOnly가 아닐 때만 변경 허용
field.onChange(date); // React Hook Form 업데이트
setOpen(false);
}
};
const handleSelect = (date: Date | undefined) => {
if (!readOnly) { // readOnly가 아닐 때만 변경 허용
field.onChange(date); // React Hook Form 업데이트
setOpen(false);
}
};
return (
<Popover
open={open && !readOnly}
onOpenChange={readOnly ? undefined : setOpen}
>
<PopoverTrigger asChild>
<FormControl>
<Button
color="white"
variant={"outline"}
className={
!selectClass
? cn(
"w-[240px] pl-3 text-left font-normal",
!field?.value && "text-muted-foreground",
return (
<Popover open={open && !readOnly} onOpenChange={readOnly ? undefined : setOpen}>
<PopoverTrigger asChild>
<FormControl>
<Button
color="white"
variant={"outline"}
disabled={readOnly} // readOnly일 때 버튼 비활성화
className={!selectClass ? cn(
"w-[240px] pl-3 text-left font-normal",
!field?.value && "text-muted-foreground",
readOnly && "opacity-60 cursor-not-allowed" // readOnly 스타일 추가
) : cn(selectClass, readOnly && "opacity-60 cursor-not-allowed")}
>
{field?.value ? (
format(
field.value instanceof Date && !isNaN(field.value.getTime())
? field.value
: new Date()
//감리 지적사항으로 삭제 예정 리드온리는 리드온리 로만 보일수 있도록 수정
// disabled={readOnly} // readOnly일 때 버튼 비활성화
//readOnly && "opacity-60 cursor-not-allowed" 감리 지적사항으로
)
: cn(
selectClass,
// , readOnly && "opacity-60 cursor-not-allowed"
)
}
>
{field?.value ? (
format(
field.value instanceof Date && !isNaN(field.value.getTime())
? field.value
: new Date(),
, "PPP", { locale: ko })
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="h-4" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={field?.value}
onSelect={handleSelect}
initialFocus
locale={ko}
/>
</PopoverContent>
</Popover>
);
"PPP",
{ locale: ko },
)
) : (
<span>날짜를 선택해주세요 </span>
)}
<CalendarIcon className="h-4" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
className="w-full"
//-- 데이트 피커가 드롭박스 형태로 년월을 선택할수 있도록 하는 Props 로 활성화 되면 드롭박스로 선택
// captionLayout="dropdown-buttons"
// fromYear={2000}
// toYear={new Date().getFullYear()}
toDate={new Date()} // 금일까지만 표시됨 (금일 이후는 안보임)
showOutsideDays={false} // 금월 까지만 표출되도록
mode="single"
selected={field?.value}
onSelect={handleSelect}
initialFocus
locale={ko}
/>
</PopoverContent>
</Popover>
);
}
@@ -0,0 +1,46 @@
import "../date_picker/date-picker.css";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import type { DayPickerSingleProps } from "react-day-picker";
import { cn } from "../../lib/utils";
import { Button } from "@common/components/ui";
import { Calendar } from "@common/components/ui";
import { Popover, PopoverContent, PopoverTrigger } from "@common/components/ui";
type DatePickerRangeProps = {
placeholder?: string;
disabled?: boolean | ((date: Date) => boolean);
} & DayPickerSingleProps;
export function DatePickerRange({
className,
placeholder = "날짜 선택",
disabled,
...props
}: DatePickerRangeProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn("date-picker-trigger", className)}
disabled={typeof disabled === "boolean" ? disabled : false}
>
<CalendarIcon className="!size-[1.6rem]" />
{props.selected ? (
format(props.selected, "yyyy-MM-dd")
) : (
<span>{placeholder}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar initialFocus classNames={{ cell: "rounded-md" }} {...props}
disabled={typeof disabled === "function" ? disabled : undefined}
/>
</PopoverContent>
</Popover>
);
}
@@ -21,4 +21,5 @@ export * from "./table_div";
export * from "./title";
export * from "./sdmInsertForm";
export * from "./popover_ui";
export * from "./date_month_picker";
export * from "./date_month_picker";
export * from "./date_picker_range"
@@ -30,6 +30,7 @@ function Gis({
setIsOpen,
setFeatureData,
featureData,
imgVisble = false,
searchView: SearchView,
detailView: DetailView,
onFeatureSelect,
@@ -46,7 +47,7 @@ function Gis({
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
const mapInstanceRef = useRef(null);
const checkedLayerIds = useRef([]);
const layerNmRef = useRef('')
// 팝업 데이터를 저장할 상태와 오버레이 참조를 저장할 ref 추가
// const [facilityInfoPopupData, setFacilityInfoPopupData] = useState([]);
@@ -84,12 +85,32 @@ function Gis({
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
let featureId
const layerName = layerNmRef.current
if (layerName === '하수재이용시설') {
featureId = data.searchPk;
} else if (layerName === '빗물이용시설'|| layerName == '중수도시설') {
featureId = data.bdstInfoPk;
} else if (layerName === '하수처리시설') {
featureId = data.bscFctCd;
} else if (layerName === '온배수시설' ) {
featureId = data.bdstRuseFcltPk;
} else {
console.warn('해당 레이어에 대한 featureId 매핑 없음', layerNm);
featureId = data.fid || data.id || data.ftrIdn;
}
gisMap.getLayers().getArray().forEach(layer => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
if (source?.getFeatures) {
const allFeatures = source.getFeatures();
for (const f of allFeatures) {
const id = String(f.getId());
// console.log('id', id, featureId)
if (id && id.split('.')[1] === String(featureId)) {
foundFeature = f;
break;
}
}
}
});
}
@@ -360,11 +381,13 @@ function Gis({
setVisible={setTabVisible}
onCheckedChange={handleCheckedLayersChange}
onLayerIconClick={(nodeNm) => {
layerNmRef.current = nodeNm
setLayerNm(nodeNm); setIsOpen(true); const dummyFeature = {
getProperties: () => ({}), // 최소 구조
};
onFeatureSelect(dummyFeature);
}}
imageVisible={imgVisble}
/>
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
@@ -375,6 +398,7 @@ function Gis({
featureData={featureData || { getProperties: () => ({}) }}
seletectedLayerNm={layerNm}
onClose={closePopup} // 닫기 콜백 전달
onFeatureSelect={handleFeatureSelect}
/>
)}
{/* SearchView가 존재할 때만 렌더링 */}
@@ -10,7 +10,7 @@ import { DndProvider } from "react-dnd";
import { useGisContext } from './Gis';
import GisApp from './lib/gis.core';
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick }) {
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick,imageVisible=false }) {
const { gisCore, treeData } = useGisContext();
const [checked, setChecked] = useState([]);
const [expanded, setExpanded] = useState([]);
@@ -188,9 +188,9 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
image.src = canvas.toDataURL('image/png');
return image;
};
const handleLayerImgClick = (nodeNm) => {
console.log('click!', nodeNm)
onLayerIconClick(nodeNm)
const handleLayerImgClick = (nodeNm, nodeId) => {
if(!checked.includes(nodeId)) alert('시설을 선택해주세요')
else onLayerIconClick(nodeNm)
}
if (!treeData || treeData.length === 0) return null;
@@ -224,7 +224,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
<input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer" />
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">{node.text}</span>
{depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={()=>{handleLayerImgClick(node.text)}} alt={node.text}></img></span>}
{imageVisible ? depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={()=>{handleLayerImgClick(node.text, node.id)}} alt={node.text}></img></span> : null}
</div>
)}
sort={(a, b) => {
@@ -234,6 +234,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun
}, [updateList,form]);
useEffect(() => {
console.log( 'form.formState.errors ', form.formState.errors)
},[form.formState.errors])
const formFields: Array<{
@@ -42,12 +42,14 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,up
.min(8, "전화번호는 8자 이상 입력해주세요")
.max(11, "전화번호는 11자 이내로 입력해주세요")
.regex(/^[0-9]+$/, "전화번호는 숫자만 입력 가능합니다"),
rgtr: z.string().optional(),
rgtr: z.string().nullish(),
dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우
? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음
: z.string().optional()
.refine((val) => !val || (val.length >= 8 && val.length <= 16), "패스워드는 8자 이상 16자 이내로 입력해주세요"),
dmdAplyPswdConfirm: z.string()
dmdAplyPswdConfirm: updateList?.dmdInfoPk // 업데이트일경우
? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음
:z.string()
.min(1, "패스워드확인을 입력해주세요"),
}).refine((data) => {
// 패스워드 확인 검증
@@ -301,7 +301,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
resetAllValues();
// 리셋 후 트리거를 false로 설정
if (setRtnPartValue) {
setRtnPartValue('0');
setRtnPartValue('A');
}
}
}, [resetTrigger]);
@@ -1,57 +1,103 @@
// selectBox
import { Label } from "@common/components";
import React from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./lib/selectBox";
import React, { useEffect, useState } from "react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./lib/selectBox";
/** 차트 기본 스타일 */
interface SelectBox1 {
text:string,
val:string|number,
hidden?: boolean
text: string;
val: string | number;
hidden?: boolean;
}
const SelectBox = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
label:string,
selectClass:string|undefined,
className:string,
selectArray:SelectBox1[],
onSelectValue:(val:string|number)=>void,
value?: string|number,
readOnly?: boolean // readOnly 속성 추가
label: string;
selectClass: string | undefined;
className: string;
selectArray: SelectBox1[];
onSelectValue: (val: string | number) => void;
value?: string | number;
readOnly?: boolean; // readOnly 속성 추가
}
>(({ label, className, selectClass, selectArray, onSelectValue, value, readOnly = false }, ref)=>{
// hidden이 true인 첫 번째 항목을 placeholder로 사용
const hiddenItem = selectArray.find(item => item.hidden);
const placeHolder = hiddenItem?.text ?? "선택";
// hidden이 true인 항목만 드롭다운에서 제외
const visibleItems = selectArray.filter(item => !item.hidden);
>(
(
{
label,
className,
selectClass,
selectArray,
onSelectValue,
value,
readOnly = false,
},
ref,
) => {
// hidden이 true인 첫 번째 항목을 placeholder로 사용
const hiddenItem = selectArray.find((item) => item.hidden);
const placeHolder = hiddenItem?.text ?? "선택";
const [isOpen, setIsOpen] = useState(false);
// hidden이 true인 항목만 드롭다운에서 제외
const visibleItems = selectArray.filter((item) => !item.hidden);
// 현재 값이 hidden 항목이거나 빈 값인 경우 undefined로 설정하여 placeholder가 표시되도록 함
const currentValue = value !== undefined &&
hiddenItem &&
(String(value) === String(hiddenItem.val) || value === "" || value === "0" || value === 0)
? undefined
: (value !== undefined ? String(value) : undefined);
// 현재 값이 hidden 항목이거나 빈 값인 경우 undefined로 설정하여 placeholder가 표시되도록 함
const currentValue =
value !== undefined &&
hiddenItem &&
(String(value) === String(hiddenItem.val) ||
value === "" ||
value === "0" ||
value === 0)
? undefined
: value !== undefined
? String(value)
: undefined;
return (
<div className={className+" flex items-center"} ref={ref}>
{label && <Label className="whitespace-nowrap w-20 text-xl text-gray-600">{label}</Label>}
<Select onValueChange={onSelectValue} value={currentValue} disabled={readOnly}>
<SelectTrigger className={selectClass?selectClass:"flex-1 p-2 border rounded"}>
<SelectValue placeholder={placeHolder} />
</SelectTrigger>
<SelectContent>
{visibleItems.map((selectArray) => (
<SelectItem key={selectArray.val} value={String(selectArray.val)}>{selectArray.text}</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
})
return (
<div className={className + " flex items-center"} ref={ref}>
{label && (
<Label className="w-20 whitespace-nowrap text-xl text-gray-600">
{label}
</Label>
)}
<Select
open={isOpen}
onOpenChange={(open) => {
if (readOnly === true) {
// readOnly 일경우 Select 선택안하게 하기
setIsOpen(false);
} else {
setIsOpen(open);
}
}}
onValueChange={onSelectValue}
value={currentValue}
// disabled={readOnly} //물재이용 감리 지적사항으로 일단 주석처리
>
<SelectTrigger
className={selectClass ? selectClass : "flex-1 rounded border p-2"}
>
<SelectValue placeholder={placeHolder} />
</SelectTrigger>
<SelectContent>
{visibleItems.map((selectArray) => (
<SelectItem key={selectArray.val} value={String(selectArray.val)}>
{selectArray.text}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
},
);
export { SelectBox };
@@ -2,7 +2,7 @@ import "./date-picker.css";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import type { DayPicker } from "react-day-picker";
import type { DayPickerSingleProps } from "react-day-picker";
import { cn } from "../../lib/utils";
import { Button } from "../button";
@@ -12,15 +12,12 @@ import { Popover, PopoverContent, PopoverTrigger } from "../popover";
type DatePickerProps = {
placeholder?: string;
disabled?: boolean;
selected?: Date;
zIndex?: number; // z-index prop 추가
} & React.ComponentProps<typeof DayPicker>;
} & DayPickerSingleProps;
export function DatePicker({
className,
placeholder = "날짜 선택",
disabled,
zIndex, // zIndex prop 받기
...props
}: DatePickerProps) {
return (
@@ -39,11 +36,8 @@ export function DatePicker({
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
style={{ zIndex: zIndex || 'auto' }}
>
<Calendar classNames={{ cell: "rounded-md" }} {...props} />
<PopoverContent className="w-auto p-0">
<Calendar initialFocus classNames={{ cell: "rounded-md" }} {...props} />
</PopoverContent>
</Popover>
);