diff --git a/apps/sn/.env.production b/apps/sn/.env.production index 805498398..7d7a547eb 100644 --- a/apps/sn/.env.production +++ b/apps/sn/.env.production @@ -10,4 +10,4 @@ NEXT_PUBLIC_GEOSERVER_URL = http://172.29.143.87:8080/geoserver NEXT_PUBLIC_LX_GEOSERVER_URL = http://172.29.143.87:8080/geoserver_swl NEXT_PUBLIC_DC_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000 NEXT_PUBLIC_NGII_URL = http://10.98.25.94:18080/openapi/Gettile.do?apiKey=BDE2BEF39DAFEC293AB6730150F222AC -NEXT_PUBLIC_SEWERAGE_API_URL = http://apigateway.iipbs-dev.svc.cluster.local:8000/sewerage-service/api +NEXT_PUBLIC_SEWERAGE_API_URL = https://apigateway.hasuon.or.kr/sewerage-service/api diff --git a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx index 8b38d232d..2fb4e7cae 100644 --- a/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx +++ b/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx @@ -149,18 +149,7 @@ const SearchModal: React.FC = ({ setIsResetDetailSelection(true); // 선택 초기화 setDetailBizId(0); // queryParams는 fetchFilteredData 내에서 매번 새로 생성되므로 별도 상태 초기화는 필요 없음 - }, [activeTab, basicType, workType, open]); - - useEffect(() => { - setActiveTab(TABS[0]?.val ?? "basic"); - setBasicType(BASIC_TYPES[0]?.val ?? ""); - setWorkType(WORK_TYPES[0]?.val ?? ""); - setTableCount(0); - setTableData([]); - setDetailTableCount(0); - setDetailTableData([]); - setDetailBizId(0); - }, [open]); + }, [activeTab, basicType, workType]); // 페이지 변경 시마다 데이터 fetch useEffect(() => { @@ -490,14 +479,16 @@ const SearchModal: React.FC = ({ }; // 기본정보 테이블 cellClick 핸들러 - const handleBasicCellClick = (index?: number | undefined, headId?: String) => { + const handleBasicCellClick = async (index?: number | undefined, headId?: String) => { if (typeof index === "number") { + setIsLoading(true); const row = tableData[index]; const layerNm = row.lyrTypeNm; const fid = row.fid; const core = GisApp.Module.layer as any; - core.showSearchResultsFromGeoJSON(layerNm, fid); + await core.showSearchResultsFromGeoJSON(layerNm, fid); + setIsLoading(false); onOpenChange(false); // SearchModal 닫기 } }; @@ -521,8 +512,7 @@ const SearchModal: React.FC = ({ RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"')); const res = await getData(`v1/bizInfo/fclts?${new URLSearchParams(RequestDTO).toString()}`); - console.log('운영정보 cellClick 데이터1:', res); - console.log('운영정보 cellClick 데이터2:', res.data); + console.log('운영정보 cellClick 데이터:', res.data); const mappedData = (res.data || []).map((item: any, detailIndex: number) => { const formattedItem = formatDisplayData(item); @@ -587,8 +577,22 @@ const SearchModal: React.FC = ({ } }; + const handleOperationResultCellClick = async (index?: number | undefined, headId?: String) => { + if (typeof index === "number") { + setIsLoading(true); + + const row = detailTableData[index]; + const layerNm = row.lyrTypeNm; + const fid = row.fid; + const core = GisApp.Module.layer as any; + await core.showSearchResultsFromGeoJSON(layerNm, fid); + setIsLoading(false); + onOpenChange(false); // SearchModal 닫기 + } + }; + // 기본정보 Excel 다운로드 핸들러 - const BasicExcelDownload = async () => { + const basicExcelDownload = async () => { if (tableData.length === 0) { alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); return; @@ -804,7 +808,7 @@ const SearchModal: React.FC = ({ }; // 운영정보 Excel 다운로드 핸들러 추가 - const OperationExcelDownload = async () => { + const operationExcelDownload = async () => { if (tableData.length === 0) { alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); return; @@ -898,7 +902,7 @@ const SearchModal: React.FC = ({ }; // 운영정보 Excel 다운로드 핸들러 추가 - const OperationResultExcelDownload = async () => { + const operationResultExcelDownload = async () => { if (detailTableData.length === 0) { alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요."); return; @@ -1001,12 +1005,26 @@ const SearchModal: React.FC = ({ } }; + const viewMultipleFeature = async () => { + if (!(tableData.length === 0)) { + setIsLoading(true); + + const row = tableData[0]; + const layerNm = row.lyrTypeNm; + const fids = tableData.map((item) => item.fid); + const core = GisApp.Module.layer as any; + await core.showMultipleSearchResultsFromGeoJSON(layerNm, fids); + setIsLoading(false); + onOpenChange(false); + } + } + const renderBody = () => ( <> {/* 탭 영역 */} ({ value: tab.val, label: tab.text @@ -1084,7 +1102,10 @@ const SearchModal: React.FC = ({ resetPageNo={isResetPage} resetSelection={isResetSelection} headButton={ - +
+ + +
} /> @@ -1112,7 +1133,7 @@ const SearchModal: React.FC = ({ resetPageNo={isResetPage} resetSelection={isResetSelection} headButton={ - + } /> @@ -1133,12 +1154,12 @@ const SearchModal: React.FC = ({ headlist={dynamicHeadlist2} pageSize={OPERATION_TABLE_SIZE} total={detailTableCount} - cellClick={undefined} + cellClick={handleOperationResultCellClick} onPageChange={setDetailCurPage} resetPageNo={isResetDetailPage} resetSelection={isResetDetailSelection} headButton={ - + } /> diff --git a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx index bf056f4e7..af178ac59 100644 --- a/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx +++ b/apps/sn/app/(with-base-layout)/ii/BaseLayout.tsx @@ -58,6 +58,8 @@ type BaseLayout = { onDeleteFile?: () => void; onAnalysis?: () => void; selectedRowData?: any; + isIr?: boolean; + isRdii?: boolean; } const BaseLayout = ({ @@ -87,6 +89,8 @@ const BaseLayout = ({ onExcelDownload, onDeleteFile, onAnalysis, + isIr = false, + isRdii = false, }: BaseLayout) => { // 컴포넌트 언마운트 시 confirmToast 정리 @@ -207,6 +211,24 @@ const BaseLayout = ({ {graphBtn && ( )} + {/* 침입수 분석 */} + {isIr && ( + + )} + {/* RDII 분석 */} + {isRdii && ( + + )} {/* 분석 버튼 */} {analysisBtn && ( diff --git a/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx new file mode 100644 index 000000000..e72f7f4a5 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/fileupload/page.tsx @@ -0,0 +1,99 @@ +'use client' + +// FileUpload.js +import React, { useState } from 'react'; +import axios from 'axios'; +const SEWERAGE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ""; + +function FileUpload() { + const [file, setFile] = useState(null); + const [text, setText] = useState(''); + + const handleFileChange = (e) => { + setFile(e.target.files[0]); + }; + + const handleTextChange = (e) => { + setText(e.target.value); + }; + + function getCookie(name:string) { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + if (match && match[2]) { + return decodeURIComponent(match[2]); + } + return null; + } + const handleFindUser = (e) => { + const auth = getCookie("Authorization"); + if (auth) console.log("쿠키 auth : " + auth); + fetch(SEWERAGE_URL+"/v1/inftwtAnls/users/65a00f65-8460-49af-98ec-042977e56f4b", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({authToken: auth}) + }).then((data) => { + console.log(data); + }); + }; + const handleFindUserOna = (e) => { + const auth = getCookie("Authorization"); + if (auth) console.log("쿠키 auth : " + auth); + fetch("/server/user-service/api/v1/users/65a00f65-8460-49af-98ec-042977e56f4b").then((data) => { + console.log(data); + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!file) { + alert('파일을 선택하세요.'); + return; + } + + const jsonData = { + aplcnYm: '202507', + enoCd: 'ABC' + }; + + // JSON → Blob 으로 변환 + MIME 지정 + const jsonBlob = new Blob([JSON.stringify(jsonData)], { + type: 'application/json' + }); + + const formData = new FormData(); + formData.append('data', jsonBlob); + formData.append('inpFile', file); + + try { + const response = await axios.post('http://localhost:8883/sewerage-service/api/v1/rdiiAnls/savewithfile', formData); + + alert('업로드 성공: ' + response.data.message); + } catch (err) { + console.error(err); + alert('업로드 실패'); + } + }; + + return ( +
+
+ + +
+
+ + +
+ + + +
+ ); +} + +export default FileUpload; diff --git a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx index 6af066e4d..97864aa20 100644 --- a/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/irsetting/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { postData } from '@/components/lib/api' +import { postMutiPart } from '@/components/lib/api' import FileUploadForm from '@/components/ui/fileUploadForm' import FormSection from '@/components/ui/formSection' import { DateMonthPicker, filePicked } from '@common/business_components/ui' @@ -52,6 +52,19 @@ const IrSetting = () => { const [sggCd, setSggCd] = useState('0'); // 시군구 코드 상태 const [sggName, setSggName] = useState(''); // 시군구 이름 상태 const [swtrmtFcltNm, setSwtrmtFcltNm] = useState(''); // 하수처리시설명 상태 + const [uploadedFilesData, setUploadedFilesData] = useState([ + initialFileInfo, // 0: 오수관망도 + initialFileInfo, // 1: 수용가-맨홀 매칭결과 + initialFileInfo, // 2: 오수량 시간패턴 + initialFileInfo // 3: 유량자료 등록 + ]); + // selectedDate를 yyyyMM 문자열 형식으로 변경 + const [selectedDate, setSelectedDate] = useState(() => { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + return `${year}${month}`; + }); const { data: userInfo } = useQuery({ queryKey: [ENDPOINT.USER_SERVICE.USERS], @@ -72,21 +85,6 @@ const IrSetting = () => { } }) - const [uploadedFilesData, setUploadedFilesData] = useState([ - initialFileInfo, // 0: 오수관망도 - initialFileInfo, // 1: 수용가-맨홀 매칭결과 - initialFileInfo, // 2: 오수량 시간패턴 - initialFileInfo // 3: 유량자료 등록 - ]); - - // selectedDate를 yyyyMM 문자열 형식으로 변경 - const [selectedDate, setSelectedDate] = useState(() => { - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - return `${year}${month}`; - }); - // 폼 초기화 함수 const resetAllForms = () => { form.reset({ // react-hook-form 초기화 @@ -121,7 +119,7 @@ const IrSetting = () => { index: number, // FileUploadForm의 fileIdx 값 selectedFile: File | null, cd: string | null, - id: string | null + id: string | null, ) => { setUploadedFilesData(prevData => { const newData = [...prevData]; @@ -137,17 +135,16 @@ const IrSetting = () => { const registFile = async (analysisData: any) => { - setIsLoadingResult(true); // 로딩 상태 시작 + setIsLoadingResult(true); console.log("Submitting combined analysis data to backend:", analysisData); - // 추후 API 엔드포인트에 맞게 URL 수정 필요 - const data = await postData('v1/inftwtAnls', analysisData); + const data = await postMutiPart('v1/inftwtAnls/savewithfile', analysisData); console.log("Response from backend:", data); if (data) { - setIsLoadingResult(false); // 로딩 상태 시작 + setIsLoadingResult(false); alert("분석 데이터가 성공적으로 제출되었습니다."); resetAllForms(); // 성공 시 폼 초기화 함수 호출 } else { - setIsLoadingResult(false); // 로딩 상태 시작 + setIsLoadingResult(false); alert("분석 데이터 제출 중 오류가 발생했습니다."); } }; @@ -306,10 +303,10 @@ const IrSetting = () => { : null; const rcptrInfoData = file5?.file ? parsedResults.find(r => r && r.fileName === file5.file?.name)?.parsedData - : null; + : null; // 새로운 JSON 데이터 생성 - const combinedAnalysisData = { + const jsonData = { ctpvCd: ctpvCd !== '0' ? String(ctpvCd).trim() : null, sggCd: sggCd !== '0' ? String(sggCd) : null, // 시군구코드 (선택값) swtrmtFcltNm: swtrmtFcltNm ?? null, @@ -318,8 +315,8 @@ const IrSetting = () => { anlsInfo: input2, // 분석정보 rgtr: tokenManager.getToken(AUTH_USER_ID) || null, // 등록자 (사용자 ID) rgtrNm: userInfo?.userName, // 등록자 (사용자 ID) - atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) - atflId: file1?.atflId, // 첨부파일ID (오수관망도) + //atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) + //atflId: file1?.atflId, // 첨부파일ID (오수관망도) atflInpNm: file1?.file?.name, // 오수관망도 파일명 atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명 atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명 @@ -330,9 +327,15 @@ const IrSetting = () => { tbSnRcptrInfo: rcptrInfoData, // 수용가 상수사용량 (없을 수 있음) }; - console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); + console.log("통합 분석 데이터 (JSON):", jsonData); + + const jsonBlob = new Blob([JSON.stringify(jsonData)], { + type: 'application/json' + }); + const combinedAnalysisData = new FormData(); + combinedAnalysisData.append('data', jsonBlob); + if (file1?.file) combinedAnalysisData.append('inpFile', file1.file); - // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. await registFile(combinedAnalysisData); setIsLoadingResult(false); } catch (error) { @@ -342,9 +345,8 @@ const IrSetting = () => { return; } } - // const handleAnalysisClick = () => { - // } - //const rcptrInfoShow = true; + + return ( <> @@ -366,36 +368,10 @@ const IrSetting = () => { setSwtrmtFcltNm={setSwtrmtFcltNm} setIsLoading={setIsLoadingResult} onAnalysis={handleAnalysisClick} + isIr={true} > {[
- - handleFileSelection(0, file, cd, id)} - showButton={false} - showPreview={true} - accept={".inp"} - fileIdx={0} /> - - - handleFileSelection(1, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={1} /> - - - handleFileSelection(2, file, cd, id)} - showButton={false} - showPreview={true} - accept={".xls, .xlsx, .xlsm, .xltx"} - fileIdx={2} /> - - + {/* 연월 선택 라이브러리로 수정 필요 */} { accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={4} /> - + + handleFileSelection(0, file, cd, id)} + showButton={false} + showPreview={true} + accept={".inp"} + fileIdx={0} /> + + +
+ handleFileSelection(1, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={1} /> + {/* */} +
+
+ + handleFileSelection(2, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={2} /> + + { @@ -445,7 +456,7 @@ const IrSetting = () => { placeholder="예: 0.75" /> - + setInput2(e.target.value)} diff --git a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx index 7354db6cd..8a27abb81 100644 --- a/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx +++ b/apps/sn/app/(with-base-layout)/ii/rdii/page.tsx @@ -1,6 +1,6 @@ "use client" -import { postData } from '@/components/lib/api' +import { postMutiPart } from '@/components/lib/api' import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" import BaseLayout from "../BaseLayout" @@ -157,13 +157,18 @@ export default function RTKForm() { }; const registFile = async (analysisData: any) => { - console.log("Submitting combined analysis data to backend"); - // 추후 API 엔드포인트에 맞게 URL 수정 필요 - const data = await postData('v1/rdiiAnls', analysisData); + setIsLoadingResult(true); + console.log("Submitting combined analysis data to backend", analysisData); + const data = await postMutiPart('v1/rdiiAnls/savewithfile', analysisData); + console.log("Response from backend:", data); if (data) { + setIsLoadingResult(false); alert("분석 데이터가 성공적으로 제출되었습니다."); resetAllForms(); // 성공 시 폼 초기화 함수 호출 - } else alert("분석 데이터 제출 중 오류가 발생했습니다."); + } else { + setIsLoadingResult(false); + alert("분석 데이터 제출 중 오류가 발생했습니다."); + } }; // 침입수 분석 실행 버튼 클릭 핸들러 @@ -182,7 +187,7 @@ export default function RTKForm() { } const formData = form.getValues(); - console.log(uploadedFilesData); + console.log('uploadedFilesData', uploadedFilesData); const file1 = uploadedFilesData[0]; // 오수관망도 const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 const file3 = uploadedFilesData[2]; // 강우자료 @@ -253,9 +258,9 @@ export default function RTKForm() { if (!file || !file.file) return null; // fileInfo 객체 및 file 객체 null 체크 // filePicked 함수에 File 객체와 headerNum 전달 - const jsonData = await filePicked(file.file, 0); + const ExcelToJsonData = await filePicked(file.file, 0); - if (jsonData === undefined) { + if (ExcelToJsonData === undefined) { console.warn(`Warning: filePicked returned undefined for ${file.file.name}`); return { fileName: file.file.name, @@ -265,9 +270,9 @@ export default function RTKForm() { }; } - // jsonData를 각 parser 함수로 처리 + // ExcelToJsonData를 각 parser 함수로 처리 // parser 함수가 filePicked가 반환하는 JSON 형식을 직접 처리한다고 가정 - const parsedDataBySpecificFunction = parser(jsonData); + const parsedDataBySpecificFunction = parser(ExcelToJsonData); return { fileName: file.file.name, @@ -279,11 +284,28 @@ export default function RTKForm() { ); // 각 파싱 결과 접근 - const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData; - const rainData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; + const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData ?? []; + const rainData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData ?? []; + + // 상수사용량 적용월과 강우자료의 날짜 일치 여부 검증 + let dateValidated = true; + + if (rainData) { + rainData + .filter(data => data && 'mtrngDt' in data) + .map(data => { + if (formData.date !== data.mtrngDt.replace(/-/g, '').slice(0, 6)) dateValidated = false; + }); + } + if (!dateValidated) { + setIsLoadingResult(false); + alert("'상수사용량 적용월'과 강우자료의 '날짜'가 일치하지 않습니다."); + return; + } + // 새로운 JSON 데이터 생성 - const combinedAnalysisData = { + const jsonData = { aplcnYm: formData.date, // 적용연월 (YYYYMM 형식) ctpvCd: ctpvCd !== '0' ? String(ctpvCd).trim() : null, // 시도코드 (선택값) sggCd: sggCd !== '0' ? String(sggCd) : null, // 시군구코드 (선택값) @@ -329,9 +351,15 @@ export default function RTKForm() { tbSnRainData: rainData, // 강우자료 }; - console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); + console.log("통합 분석 데이터 (JSON):", jsonData); + + const jsonBlob = new Blob([JSON.stringify(jsonData)], { + type: 'application/json' + }); + const combinedAnalysisData = new FormData(); + combinedAnalysisData.append('data', jsonBlob); + if (file1?.file) combinedAnalysisData.append('inpFile', file1.file); - // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. await registFile(combinedAnalysisData); setIsLoadingResult(false); } catch (error) { @@ -389,18 +417,20 @@ export default function RTKForm() { setSwtrmtFcltNm={setSwtrmtFcltNm} setIsLoading={setIsLoadingResult} onAnalysis={handleAnalysisClick} + isRdii={true} > {[
- + - +
+ *
+ *
-
+
handleFileSelection(0, file, cd, id)} @@ -430,7 +461,7 @@ export default function RTKForm() { accept={".inp"} fileIdx={0} />
-
+
handleFileSelection(1, file, cd, id)} @@ -438,8 +469,14 @@ export default function RTKForm() { showPreview={true} accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={1} /> + {/* */}
-
+
handleFileSelection(2, file, cd, id)} @@ -448,7 +485,7 @@ export default function RTKForm() { accept={".xls, .xlsx, .xlsm, .xltx"} fileIdx={2} />
- + { @@ -478,7 +515,7 @@ export default function RTKForm() { placeholder="0 ~ 1 사이의 값 입력" /> - + - + { @@ -32,7 +33,8 @@ const Page = () => { const [curPage, setCurPage] = useState(1) //현재 페이지 const [totalData, setTotalData] = useState(0) //총 데이터 수 const [isLoading, setIsLoading] = useState(true) //로딩 상태 - const [isModalOpen, setIsModalOpen] = useState(false) // 모달 상태 + const [isUploadModalOpen, setIsUploadModalOpen] = useState(false) // 등록 모달 상태 + const [isTableModalOpen, setIsTableModalOpen] = useState(false) // 테이블 모달 상태 const [resetSelection, setResetSelection] = useState(false); // 선택 초기화 상태 const [isResetPage, setIsResetPage] = useState(false); @@ -135,14 +137,14 @@ const Page = () => { const data = await postData('v1/sbmrgPrevnt', json) if (data) { alert('파일이 등록되었습니다.'); - setIsModalOpen(false); + setIsUploadModalOpen(false); handleSearch(); } } // 등록 버튼 클릭 핸들러 const handleRegisterClick = () => { - setIsModalOpen(true); + setIsUploadModalOpen(true); } @@ -211,7 +213,7 @@ const Page = () => {

사업정보 등록현황

- +
@@ -228,6 +230,7 @@ const Page = () => { setFileNm(fileName); setAtflId(id); setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn); + setIsTableModalOpen(true); } }} onPageChange={setCurPage} @@ -237,8 +240,8 @@ const Page = () => { {/* 파일 업로드 모달 */} { /> } /> + {/* 엑셀 데이터 테이블 모달 */} + } + useDirectRender={true} + /> ) } diff --git a/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx b/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx new file mode 100644 index 000000000..c4d1a9fd5 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/oper/floodPre/tableModal.tsx @@ -0,0 +1,248 @@ +'use client' +import { useState, useEffect } from 'react' +import LoadingOverlay from '@/components/ui/LoadingBar' +import { UITab, UiTable } from '@common/business_components/ui' +import * as XLSX from 'xlsx'; + +const TABS = [ + { val: "management", text: "관리SHEET" }, + { val: "Inspection results", text: "운영관리현황 점검결과" }, +]; + +interface TableModalProps { + fileNm: string; + atflId: string; +} + +const TableModal: React.FC = ({ fileNm, atflId }) => { + + const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "management"); + const [managementTableHeader, setManagementTableHeader] = useState<{ id: string; title: string }[]>([]) //테이블 헤더 + const [managementTableData, setManagementTableData] = useState([{}]) //테이블 데이터 + const [resultTableHeader, setResultTableHeader] = useState<{ id: string; title: string }[]>([]) //테이블 헤더 + const [resultTableData, setResultTableData] = useState([{}]) //테이블 데이터 + const [isLoading, setIsLoading] = useState(false) //로딩 상태 + + useEffect(() => { + isChckFile(); + }, []); + + // 파일 다운로드 + const isChckFile = async () => { + setIsLoading(true); + try { + if (fileNm === '' || fileNm === undefined) { + alert('파일을 선택해주세요') + setIsLoading(false); + return false + } + + const response = await fetch(`/attach-service/api/v1/download/${atflId}`); + if (!response.ok) { + throw new Error('파일을 가져오는 중 오류가 발생했습니다.'); + } + + const blob = await response.blob(); + + const file = new File([blob], fileNm, { + type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + + const targetSheets = ['관리SHEET', '운영관리현황 점검결과']; + const jsonDataBySheet = await filePickedSelectedSheets(file, 0, targetSheets); + + // 관리 SHEET + const managementHeader = jsonDataBySheet['관리SHEET']?.[7] || []; + // 1번째~47번째, 74번째~80번째 데이터만 필터링 + const filteredManagementHeader = Object.entries(managementHeader) + .filter(([key, value]) => { + const match = key.match(/^__EMPTY_(\d+)$/); + if (match) { + const index = parseInt(match[1] ?? '', 10); + return (index > 0 && index < 47) || (index > 73); + } + return false; + }) + .map(([key, value]) => ({ + id: key, + title: String(value).replace(/\r\n/g, '\n') + })); + + setManagementTableHeader(filteredManagementHeader); + console.log('managementTableHeader', filteredManagementHeader); + + const managementData = Array.isArray(jsonDataBySheet['관리SHEET']) + ? jsonDataBySheet['관리SHEET'].slice(8) + : []; + // console.log('변환 후:', excelToManagementData(managementData)); + const filteredManagementData = filterEmptyRows(managementData); + setManagementTableData(filteredManagementData); + console.log('managementTableData', filteredManagementData); + + // 운영관리현황 점검결과 + const resultHeader = jsonDataBySheet['운영관리현황 점검결과']?.[2] || []; + const dynamicInspectionHeader = Object.entries(resultHeader) + .map(([key, value]) => ({ + id: key, + title: String(value).replace(/\r\n/g, '\n') + })); + dynamicInspectionHeader.splice(1, 0, { + id: "__EMPTY_2", + title: '' + }); + setResultTableHeader(dynamicInspectionHeader); + console.log('resultTableHeader', dynamicInspectionHeader); + + const resultData = Array.isArray(jsonDataBySheet['운영관리현황 점검결과']) + ? jsonDataBySheet['운영관리현황 점검결과'].slice(3) + : []; + const filteredResultData = filterEmptyRows(resultData); + setResultTableData(filteredResultData); + console.log('resultTableData', filteredResultData); + } catch (error) { + console.error('파일 처리 중 오류 발생:', error); + alert('파일 처리 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + } + + function filterEmptyRows(data: any[]) { + return data + .filter(row => { + return Object.entries(row).some(([key, value]) => { + const match = key.match(/^__EMPTY_(\d+)$/); + if (!match || parseInt(match[1] ?? '', 10) < 2) return false; + + return value !== undefined && + value !== null && + value !== '' && + value !== '-' && + String(value).trim() !== ''; + }); + }) + .map((row) => { + const processedRow = { ...row }; + const keys = Object.keys(processedRow); + + keys.forEach((key, keyIndex) => { + // 마지막 속성(비고)이 아닌 경우에만 빈 값을 '-'으로 변경 + if (keyIndex !== keys.length - 1) { + const value = processedRow[key]; + if (value === undefined || + value === null || + value === '' || + String(value).trim() === '') { + processedRow[key] = '-'; + } + } + }); + + return processedRow; + }); + }; + + // 특정 시트만 선택적으로 JSON으로 변환하는 함수 + function filePickedSelectedSheets(file: File, headerNum: number = 0, sheetNames: string[]): Promise> { + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = function (e) { + const data = e.target?.result; + if (!data) { + console.error("Failed to read file: e.target.result is null"); + reject(new Error("Failed to read file")); + return; + } + + // Excel 파일 읽기 + const wb = XLSX.read(data, { type: 'binary', cellDates: true }); + const result: Record = {}; + + // 지정된 시트 이름만 찾아서 처리 + sheetNames.forEach(targetSheetName => { + // 대소문자 구분 없이 시트 이름 찾기 + const sheetName = wb.SheetNames.find(name => + name.toLowerCase() === targetSheetName.toLowerCase() + ); + + if (sheetName) { + const sheet = wb.Sheets[sheetName]; + if (sheet) { + console.log(`Found sheet: "${sheetName}"`); + const jsonData = XLSX.utils.sheet_to_json(sheet, { range: headerNum, raw: false }); + result[targetSheetName] = jsonData; + } + } else { + console.warn(`Sheet "${targetSheetName}" not found in workbook`); + result[targetSheetName] = []; + } + }); + + resolve(result); + }; + + reader.onerror = function () { + console.error("Error reading file"); + reject(new Error("Error reading file")); + }; + + // 파일 읽기 시작 + reader.readAsBinaryString(file); + }); + }; + + + return ( + <> + {/* 탭 영역 */} + ({ + value: tab.val, + label: tab.text + }))} + onValueChange={(value: string) => setActiveTab(value)} + /> + {/* 관리SHEET 탭 */} + {activeTab === 'management' && ( +
+
+ +
+ { }} + /> +
+
+
+ )} + {/* 운영관리현황 점검결과 탭 */} + {activeTab === 'Inspection results' && ( +
+
+ + { }} + /> +
+
+ )} + + ) +}; + +export default TableModal; \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx b/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx index efcea5461..7165188ca 100644 --- a/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx +++ b/apps/sn/app/(with-base-layout)/oper/oldPp/page.tsx @@ -205,7 +205,7 @@ const Page = () => {

사업정보 등록현황

- +
diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index df1d9f509..f70b0f3c0 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -1,8 +1,9 @@ import { useEffect, useState } from "react"; +import axios from 'axios'; const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; -export const getData = async (url: string, data?: object): Promise => { +export const getData = async (url: string): Promise => { const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, { method: "GET", }); @@ -48,13 +49,8 @@ export const postData = async (url: string, data: object): Promise => { }; export const postMutiPart = async (url: string, data: FormData): Promise => { - const options: RequestInit = { - method: "POST", - body: data, - }; - const res = await fetch(`${BASE_URL}/${url}`, options); - const resData = await res.json(); - return resData; + const res = await axios.post(`${BASE_URL}/${url}`, data); + return res.data; }; export const putData = async (url: string, data: object): Promise => { diff --git a/apps/sn/app/components/ui/fileUploadForm.tsx b/apps/sn/app/components/ui/fileUploadForm.tsx index 0aed37bc9..1272488a1 100644 --- a/apps/sn/app/components/ui/fileUploadForm.tsx +++ b/apps/sn/app/components/ui/fileUploadForm.tsx @@ -41,23 +41,84 @@ const FileUploadForm = ({ const [selectedYear, setSelectedYear] = useState(yearOptions[0]?.val || ''); const [selectedQuarter, setSelectedQuarter] = useState(quater[0]?.val || ''); - const internalHandleUploadResponse = (file: File) => async (res: HttpResponse) => { - const fileInfo = res.getHeader("X-File-Info"); - if (!fileInfo) { - if (onFileSelect) onFileSelect(file, null, null); + function registFile() { + console.log(files?.[0]?.name) + if (!currentFile) { + alert('파일을 선택해주세요.'); return; } - const { atflCd: newAtflCd, atflId: newAtflId } = JSON.parse(fileInfo) ?? {}; - const orgnlFileNm = file.name; - console.log(`FileUploadForm (idx: ${fileIdx ?? 'N/A'}) - orgnlFileNm:`, orgnlFileNm, 'New AtflCd:', newAtflCd, 'New AtflId:', newAtflId); - setFileName(orgnlFileNm); - setInternalAtflCd(newAtflCd); - setInternalAtflId(newAtflId); - - if (onFileSelect) { - onFileSelect(file, newAtflCd, newAtflId); + if (!selectedYear) { + alert('등록년도를 선택해주세요.'); + return; } - }; + if (!selectedQuarter) { + alert('분기를 선택해주세요.'); + return; + } + + const fileNm = currentFile.name; + if (onSubmit && files && currentFile) { + // Promise로 handleUpload 완료를 기다림 + handleUploadWithResponse(files, currentFile) + .then(({ atflCd, atflId }) => { + // 업로드 완료 후 onSubmit 실행 + onSubmit(fileNm, selectedYear, selectedQuarter, atflCd || '', atflId || ''); + + // UI 초기화 + setFiles(null); + setFileName(''); + setUploaderKey(prev => prev + 1); + }) + .catch((error) => { + console.error('파일 업로드 실패:', error); + alert('파일 업로드 중 오류가 발생했습니다.'); + }); + } + } + + // handleUpload를 Promise로 감싸는 헬퍼 함수 + function handleUploadWithResponse(files: FileList, file: File): Promise<{ atflCd: string | null, atflId: string | null }> { + return new Promise((resolve, reject) => { + const uploadResponseHandler = async (res: HttpResponse) => { + try { + const fileInfo = res.getHeader("X-File-Info"); + if (!fileInfo) { + resolve({ atflCd: null, atflId: null }); + return; + } + + const { atflCd: newAtflCd, atflId: newAtflId } = JSON.parse(fileInfo) ?? {}; + console.log(`FileUploadForm (idx: ${fileIdx ?? 'N/A'}) - Upload completed:`, newAtflCd, newAtflId); + + // 내부 상태도 업데이트 (다른 용도로 사용할 수 있음) + setInternalAtflCd(newAtflCd); + setInternalAtflId(newAtflId); + setFileName(file.name); + + if (onFileSelect) { + onFileSelect(file, newAtflCd, newAtflId); + } + + // Promise 완료 + resolve({ atflCd: newAtflCd, atflId: newAtflId }); + } catch (error) { + reject(error); + } + }; + + const uploadErrorHandler = (error: Error) => { + console.error('Upload failed:', error); + reject(error); + }; + + // 기존 handleUpload 유틸리티 사용 (수정하지 않음) + handleUpload(files, { + handleUploadStart: setUpload, + handleSuccess: setUpload, + handleResponse: uploadResponseHandler, + }); + }); + } return ( @@ -87,19 +148,12 @@ const FileUploadForm = ({ if (!firstFile) return; // 타입스크립트 undefined 가능성 제거 setFiles(selectedFiles); + setFileName(firstFile.name); setCurrentFile(firstFile); if (onFileSelect) { onFileSelect(firstFile, internalAtflCd, internalAtflId); } - - handleUpload(selectedFiles, { - handleUploadStart: setUpload, - handleSuccess: setUpload, - // handleResponse는 tus-js-client의 Upload 인스턴스 콜백과 연동 필요 - // 여기서는 handleUpload 유틸리티가 응답을 직접 처리한다고 가정 - handleResponse: internalHandleUploadResponse(firstFile), - }); }} customPreview={ showPreview ? ( @@ -116,30 +170,7 @@ const FileUploadForm = ({ diff --git a/apps/sn/app/components/ui/formSection.tsx b/apps/sn/app/components/ui/formSection.tsx index 384808aee..4f33fbbe5 100644 --- a/apps/sn/app/components/ui/formSection.tsx +++ b/apps/sn/app/components/ui/formSection.tsx @@ -6,12 +6,13 @@ interface FormSectionProps { titleNm: string form: any name: string + required?: boolean } -const FormSection = ({ children, titleNm, form, name }: FormSectionProps) => { +const FormSection = ({ children, titleNm, form, name, required }: FormSectionProps) => { return (
-
{titleNm}
+
{titleNm}{required && *}