Merge branch 'iipbs/sn/dev'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -149,18 +149,7 @@ const SearchModal: React.FC<SearchModalProps> = ({
|
||||
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<SearchModalProps> = ({
|
||||
};
|
||||
|
||||
// 기본정보 테이블 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<SearchModalProps> = ({
|
||||
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<SearchModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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<SearchModalProps> = ({
|
||||
};
|
||||
|
||||
// 운영정보 Excel 다운로드 핸들러 추가
|
||||
const OperationExcelDownload = async () => {
|
||||
const operationExcelDownload = async () => {
|
||||
if (tableData.length === 0) {
|
||||
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
|
||||
return;
|
||||
@@ -898,7 +902,7 @@ const SearchModal: React.FC<SearchModalProps> = ({
|
||||
};
|
||||
|
||||
// 운영정보 Excel 다운로드 핸들러 추가
|
||||
const OperationResultExcelDownload = async () => {
|
||||
const operationResultExcelDownload = async () => {
|
||||
if (detailTableData.length === 0) {
|
||||
alert("다운로드 할 Excel 파일이 없습니다.\n조회 후 다시 시도해주세요.");
|
||||
return;
|
||||
@@ -1001,12 +1005,26 @@ const SearchModal: React.FC<SearchModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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 = () => (
|
||||
<>
|
||||
{/* 탭 영역 */}
|
||||
<UITab
|
||||
defaultValue={TABS[0]?.val}
|
||||
defaultValue={activeTab}
|
||||
tabList={TABS.map((tab) => ({
|
||||
value: tab.val,
|
||||
label: tab.text
|
||||
@@ -1084,7 +1102,10 @@ const SearchModal: React.FC<SearchModalProps> = ({
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
headButton={
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={BasicExcelDownload}>파일 다운로드</Button>
|
||||
<div className='flex'>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={basicExcelDownload}>파일 다운로드</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -1112,7 +1133,7 @@ const SearchModal: React.FC<SearchModalProps> = ({
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
headButton={
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={OperationExcelDownload}>파일 다운로드</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-sm font-medium green" size="sm" onClick={operationExcelDownload}>파일 다운로드</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@@ -1133,12 +1154,12 @@ const SearchModal: React.FC<SearchModalProps> = ({
|
||||
headlist={dynamicHeadlist2}
|
||||
pageSize={OPERATION_TABLE_SIZE}
|
||||
total={detailTableCount}
|
||||
cellClick={undefined}
|
||||
cellClick={handleOperationResultCellClick}
|
||||
onPageChange={setDetailCurPage}
|
||||
resetPageNo={isResetDetailPage}
|
||||
resetSelection={isResetDetailSelection}
|
||||
headButton={
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={OperationResultExcelDownload}>파일 다운로드</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={operationResultExcelDownload}>파일 다운로드</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 && (
|
||||
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile}>삭제</Button>
|
||||
)}
|
||||
{/* 침입수 분석 */}
|
||||
{isIr && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="mx-4 py-2 text-white rounded-sm text-lg font-medium green"
|
||||
>
|
||||
<a href="/samples/ir_setting_sample.zip" download="침입수 분석 업로드 파일 양식.zip">양식 받기</a>
|
||||
</Button>
|
||||
)}
|
||||
{/* RDII 분석 */}
|
||||
{isRdii && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="mx-4 py-2 text-white rounded-sm text-lg font-medium green"
|
||||
>
|
||||
<a href="/samples/rdii_sample.zip" download="RDII 분석 업로드 파일 양식.zip">양식 받기</a>
|
||||
</Button>
|
||||
)}
|
||||
{/* 분석 버튼 */}
|
||||
{analysisBtn && (
|
||||
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={handleConfirm}>{analysisBtn}</Button>
|
||||
|
||||
@@ -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 (
|
||||
<form onSubmit={handleSubmit} className="text-2xl">
|
||||
<div>
|
||||
<label>텍스트 입력:</label>
|
||||
<input type="text" value={text} onChange={handleTextChange} />
|
||||
</div>
|
||||
<div>
|
||||
<label>파일 선택:</label>
|
||||
<input type="file" onChange={handleFileChange} />
|
||||
</div>
|
||||
<button type="submit" className="px-4 py-2 bg-gray-500 text-white rounded">업로드</button>
|
||||
<button type="button" className="px-4 py-2 bg-gray-400 text-white rounded"
|
||||
onClick={handleFindUser}>사용자조회</button>
|
||||
<button type="button" className="px-4 py-2 bg-gray-200 text-white rounded"
|
||||
onClick={handleFindUserOna}>사용자조회ona</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileUpload;
|
||||
@@ -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<string>('0'); // 시군구 코드 상태
|
||||
const [sggName, setSggName] = useState<string>(''); // 시군구 이름 상태
|
||||
const [swtrmtFcltNm, setSwtrmtFcltNm] = useState<string>(''); // 하수처리시설명 상태
|
||||
const [uploadedFilesData, setUploadedFilesData] = useState<UploadedFileInfo[]>([
|
||||
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<UploadedFileInfo[]>([
|
||||
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 (
|
||||
<>
|
||||
<LoadingOverlay isLoading={isLoadingResult} />
|
||||
@@ -366,36 +368,10 @@ const IrSetting = () => {
|
||||
setSwtrmtFcltNm={setSwtrmtFcltNm}
|
||||
setIsLoading={setIsLoadingResult}
|
||||
onAnalysis={handleAnalysisClick}
|
||||
isIr={true}
|
||||
>
|
||||
{[<div key="irsetting-layout-key" className="grid gap-4">
|
||||
<FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'>
|
||||
<FileUploadForm
|
||||
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)}
|
||||
showButton={false}
|
||||
showPreview={true}
|
||||
accept={".inp"}
|
||||
fileIdx={0} />
|
||||
</FormSection>
|
||||
<FormSection name='suyeonga' form={form} titleNm='▷ 수용가-맨홀 매칭결과'>
|
||||
<FileUploadForm
|
||||
key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)}
|
||||
showButton={false}
|
||||
showPreview={true}
|
||||
accept={".xls, .xlsx, .xlsm, .xltx"}
|
||||
fileIdx={1} />
|
||||
</FormSection>
|
||||
<FormSection name='pattern' form={form} titleNm='▷ 오수량 시간패턴'>
|
||||
<FileUploadForm
|
||||
key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)}
|
||||
showButton={false}
|
||||
showPreview={true}
|
||||
accept={".xls, .xlsx, .xlsm, .xltx"}
|
||||
fileIdx={2} />
|
||||
</FormSection>
|
||||
<FormSection name='inputdate' form={form} titleNm='▷ 상수사용량 적용월'>
|
||||
<FormSection name='inputdate' form={form} titleNm='▷ 상수사용량 적용월' required={true}>
|
||||
{/* 연월 선택 라이브러리로 수정 필요 */}
|
||||
<DateMonthPicker
|
||||
field={{
|
||||
@@ -417,7 +393,42 @@ const IrSetting = () => {
|
||||
accept={".xls, .xlsx, .xlsm, .xltx"}
|
||||
fileIdx={4} />
|
||||
</FormSection>
|
||||
<FormSection name='rtkSource' form={form} titleNm='▷ 오수전환율'>
|
||||
<FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도' required={true}>
|
||||
<FileUploadForm
|
||||
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)}
|
||||
showButton={false}
|
||||
showPreview={true}
|
||||
accept={".inp"}
|
||||
fileIdx={0} />
|
||||
</FormSection>
|
||||
<FormSection name='suyeonga' form={form} titleNm='▷ 수용가-맨홀 매칭결과' required={true}>
|
||||
<div className="flex items-center">
|
||||
<FileUploadForm
|
||||
key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)}
|
||||
showButton={false}
|
||||
showPreview={true}
|
||||
accept={".xls, .xlsx, .xlsm, .xltx"}
|
||||
fileIdx={1} />
|
||||
{/* <Button
|
||||
size={"sm"}
|
||||
className={`text-white rounded-sm text-lg font-medium green`}
|
||||
>
|
||||
<a href="/samples/manhole-consumer-matching-template.xlsx" download="수용가-맨홀 매칭결과 파일 양식.xlsx">양식 받기</a>
|
||||
</Button> */}
|
||||
</div>
|
||||
</FormSection>
|
||||
<FormSection name='pattern' form={form} titleNm='▷ 오수량 시간패턴' required={true}>
|
||||
<FileUploadForm
|
||||
key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)}
|
||||
showButton={false}
|
||||
showPreview={true}
|
||||
accept={".xls, .xlsx, .xlsm, .xltx"}
|
||||
fileIdx={2} />
|
||||
</FormSection>
|
||||
<FormSection name='rtkSource' form={form} titleNm='▷ 오수전환율' required={true}>
|
||||
<Input
|
||||
value={input1}
|
||||
onChange={(e) => {
|
||||
@@ -445,7 +456,7 @@ const IrSetting = () => {
|
||||
placeholder="예: 0.75"
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection name='analysis information' form={form} titleNm='▷ 분석 정보'>
|
||||
<FormSection name='analysis information' form={form} titleNm='▷ 분석 정보' required={true}>
|
||||
<Input
|
||||
value={input2}
|
||||
onChange={(e) => setInput2(e.target.value)}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
{[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처">
|
||||
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처" required={true}>
|
||||
<Input {...form.register("rtkSource")} className="h-12 text-center text-xl w-[225px]" />
|
||||
</FormSection>
|
||||
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)">
|
||||
<FormSection name="rdiiArea" form={form} titleNm="○ RDII 기여면적(ha)" required={true}>
|
||||
<Input {...form.register("rdiiArea")} className="h-12 text-center text-xl w-[225px]" />
|
||||
</FormSection></div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="mb-2 text-xl font-medium">○ 단위도 계수</Label>
|
||||
<span className="text-2xl text-red-500">*</span>
|
||||
<EditableFormTable
|
||||
form={form}
|
||||
name="unitFactors"
|
||||
@@ -411,6 +441,7 @@ export default function RTKForm() {
|
||||
</div>
|
||||
<div>
|
||||
<Label className="mb-[1.2rem] text-xl font-medium">○ 초기 차단 깊이</Label>
|
||||
<span className="text-2xl text-red-500">*</span>
|
||||
<EditableFormTable
|
||||
form={form}
|
||||
name="initialDepths"
|
||||
@@ -421,7 +452,7 @@ export default function RTKForm() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormSection name="suyeonga" form={form} titleNm="○ 오수관망도"><div className="w-[350px]">
|
||||
<FormSection name="suyeonga" form={form} titleNm="○ 오수관망도" required={true}><div className="w-[350px]">
|
||||
<FileUploadForm
|
||||
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(0, file, cd, id)}
|
||||
@@ -430,7 +461,7 @@ export default function RTKForm() {
|
||||
accept={".inp"}
|
||||
fileIdx={0} />
|
||||
</div></FormSection>
|
||||
<FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과"><div className="w-[350px]">
|
||||
<FormSection name="regopm" form={form} titleNm="○ 수용가-맨홀 매칭결과" required={true}><div className="flex items-center">
|
||||
<FileUploadForm
|
||||
key={`file-upload-1-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(1, file, cd, id)}
|
||||
@@ -438,8 +469,14 @@ export default function RTKForm() {
|
||||
showPreview={true}
|
||||
accept={".xls, .xlsx, .xlsm, .xltx"}
|
||||
fileIdx={1} />
|
||||
{/* <Button
|
||||
size={"sm"}
|
||||
className={`text-white rounded-sm text-lg font-medium green`}
|
||||
>
|
||||
<a href="/samples/manhole-consumer-matching-template.xlsx" download="수용가-맨홀 매칭결과 파일 양식.xlsx">양식 받기</a>
|
||||
</Button> */}
|
||||
</div></FormSection>
|
||||
<FormSection name="rain" form={form} titleNm="○ 강우자료"><div className="w-[350px]">
|
||||
<FormSection name="rain" form={form} titleNm="○ 강우자료" required={true}><div className="w-[350px]">
|
||||
<FileUploadForm
|
||||
key={`file-upload-2-${formResetKey}`} // 폼 초기화를 위한 key
|
||||
onFileSelect={(file, cd, id) => handleFileSelection(2, file, cd, id)}
|
||||
@@ -448,7 +485,7 @@ export default function RTKForm() {
|
||||
accept={".xls, .xlsx, .xlsm, .xltx"}
|
||||
fileIdx={2} />
|
||||
</div></FormSection>
|
||||
<FormSection name='rate' form={form} titleNm='○ 오수전환율'>
|
||||
<FormSection name='rate' form={form} titleNm='○ 오수전환율' required={true}>
|
||||
<Input
|
||||
value={form.watch("rate")} // form의 rate 값을 가져옴
|
||||
onChange={(e) => {
|
||||
@@ -478,7 +515,7 @@ export default function RTKForm() {
|
||||
placeholder="0 ~ 1 사이의 값 입력"
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용월'>
|
||||
<FormSection name='date' form={form} titleNm='○ 상수사용량 적용월' required={true}>
|
||||
<DateMonthPicker
|
||||
field={{
|
||||
value: form.watch("date"), // form의 date 값을 직접 사용
|
||||
@@ -490,7 +527,7 @@ export default function RTKForm() {
|
||||
startYear={1900} // 시작 연도 설정
|
||||
/>
|
||||
</FormSection>
|
||||
<FormSection name='rate' form={form} titleNm='○ 분석 정보'>
|
||||
<FormSection name='rate' form={form} titleNm='○ 분석 정보' required={true}>
|
||||
<Input
|
||||
{...form.register("analysisInfo")}
|
||||
className="h-12 text-center text-xl w-[225px] ml-5"
|
||||
|
||||
@@ -14,6 +14,7 @@ import { AUTH_USER_ID } from "@/shared/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ENDPOINT } from "@common/components/config";
|
||||
import { getUserInfo } from "@common/components/api";
|
||||
import TableModal from './tableModal';
|
||||
|
||||
|
||||
const Page = () => {
|
||||
@@ -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<boolean>(false); // 선택 초기화 상태
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(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 = () => {
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-medium">사업정보 등록현황</h3>
|
||||
<div>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={isChckFile} >파일 다운로드</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={isChckFile} >파일 다운로드</Button>
|
||||
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile} >삭제</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,6 +230,7 @@ const Page = () => {
|
||||
setFileNm(fileName);
|
||||
setAtflId(id);
|
||||
setSbmrgPrevntBizSn(tableData[index]?.sbmrgPrevntBizSn);
|
||||
setIsTableModalOpen(true);
|
||||
}
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
@@ -237,8 +240,8 @@ const Page = () => {
|
||||
</ListDiv>
|
||||
{/* 파일 업로드 모달 */}
|
||||
<UiModal
|
||||
open={isModalOpen}
|
||||
onOpenChange={setIsModalOpen}
|
||||
open={isUploadModalOpen}
|
||||
onOpenChange={setIsUploadModalOpen}
|
||||
bodyClass="w-auto max-w-[90vw] min-w-[600px] h-auto"
|
||||
noHeaderYn="N"
|
||||
noFooterYn="Y"
|
||||
@@ -251,6 +254,21 @@ const Page = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 엑셀 데이터 테이블 모달 */}
|
||||
<UiModal
|
||||
open={isTableModalOpen}
|
||||
onOpenChange={setIsTableModalOpen}
|
||||
bodyClass={`
|
||||
h-[100dvh]
|
||||
w-[90dvw]
|
||||
max-w-[100dvw]
|
||||
max-h-[100dvh]
|
||||
overflow-hidden
|
||||
z-[101]
|
||||
`}
|
||||
body={<TableModal fileNm={fileNm} atflId={atflId} />}
|
||||
useDirectRender={true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<TableModalProps> = ({ fileNm, atflId }) => {
|
||||
|
||||
const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "management");
|
||||
const [managementTableHeader, setManagementTableHeader] = useState<{ id: string; title: string }[]>([]) //테이블 헤더
|
||||
const [managementTableData, setManagementTableData] = useState<any>([{}]) //테이블 데이터
|
||||
const [resultTableHeader, setResultTableHeader] = useState<{ id: string; title: string }[]>([]) //테이블 헤더
|
||||
const [resultTableData, setResultTableData] = useState<any>([{}]) //테이블 데이터
|
||||
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<Record<string, any[]>> {
|
||||
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<string, any[]> = {};
|
||||
|
||||
// 지정된 시트 이름만 찾아서 처리
|
||||
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 (
|
||||
<>
|
||||
{/* 탭 영역 */}
|
||||
<UITab
|
||||
defaultValue={activeTab}
|
||||
tabList={TABS.map((tab) => ({
|
||||
value: tab.val,
|
||||
label: tab.text
|
||||
}))}
|
||||
onValueChange={(value: string) => setActiveTab(value)}
|
||||
/>
|
||||
{/* 관리SHEET 탭 */}
|
||||
{activeTab === 'management' && (
|
||||
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
|
||||
<div className="flex-1 px-2 pb-2">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<div className="w-[1000%]">
|
||||
<UiTable
|
||||
headName={'관리SHEET'}
|
||||
tableData={managementTableData}
|
||||
headlist={managementTableHeader}
|
||||
pageSize={10}
|
||||
total={0}
|
||||
cellClick={undefined}
|
||||
onPageChange={() => { }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 운영관리현황 점검결과 탭 */}
|
||||
{activeTab === 'Inspection results' && (
|
||||
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
|
||||
<div className="flex-1 px-2 pb-2">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={'운영관리현황 점검결과'}
|
||||
tableData={resultTableData}
|
||||
headlist={resultTableHeader}
|
||||
pageSize={10}
|
||||
total={0}
|
||||
className={"overflow-x-auto overflow-y-auto"}
|
||||
cellClick={undefined}
|
||||
onPageChange={() => { }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export default TableModal;
|
||||
@@ -205,7 +205,7 @@ const Page = () => {
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-medium">사업정보 등록현황</h3>
|
||||
<div>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={isChckFile}>파일 다운로드</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={isChckFile}>파일 다운로드</Button>
|
||||
<Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size="sm" onClick={isDeleteFile}>삭제</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<any> => {
|
||||
export const getData = async (url: string): Promise<any> => {
|
||||
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<any> => {
|
||||
};
|
||||
|
||||
export const postMutiPart = async (url: string, data: FormData): Promise<any> => {
|
||||
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<any> => {
|
||||
|
||||
@@ -41,23 +41,84 @@ const FileUploadForm = ({
|
||||
const [selectedYear, setSelectedYear] = useState<string>(yearOptions[0]?.val || '');
|
||||
const [selectedQuarter, setSelectedQuarter] = useState<string>(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 = ({
|
||||
<Button
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
console.log(files?.[0]?.name)
|
||||
if (!currentFile) {
|
||||
alert('파일을 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!selectedYear) {
|
||||
alert('등록년도를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!selectedQuarter) {
|
||||
alert('분기를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
const fileNm = currentFile.name;
|
||||
if (onSubmit) {
|
||||
// 부모에게 전달할 때는 내부 상태인 internalAtflCd, internalAtflId 사용
|
||||
onSubmit(fileNm, selectedYear, selectedQuarter, internalAtflCd || '', internalAtflId || '');
|
||||
setFiles(null); // 파일 업로드 후 파일 목록 초기화
|
||||
setFileName(''); // 파일명 초기화
|
||||
// 파일 업로드 후 FileUploader 컴포넌트를 재마운트하기 위해 key 값 변경
|
||||
setUploaderKey(prev => prev + 1);
|
||||
}
|
||||
}}
|
||||
onClick={() => registFile()}
|
||||
>
|
||||
등록
|
||||
</Button>
|
||||
|
||||
@@ -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 (
|
||||
<div className="grid grid-cols-[200px_1fr] items-center">
|
||||
<div className="text-xl font-medium">{titleNm}</div>
|
||||
<div className="text-xl font-medium">{titleNm}{required && <span className="text-2xl text-red-500">*</span>}</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user