From 67659bd16ffcc162f033802bbcd2762f27e37ff3 Mon Sep 17 00:00:00 2001 From: leedj312 Date: Mon, 2 Jun 2025 11:02:43 +0900 Subject: [PATCH 01/44] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=82=BD=EC=9E=85(=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/file-upload/lib/file_upload.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx index 3fe1aafd8..b73bcb886 100644 --- a/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx +++ b/packages/common/business_components/src/ui/file-upload/lib/file_upload.tsx @@ -303,9 +303,7 @@ export const FileUploaderItem = forwardRef< >
{ - - + onClick={() =>{onFileClick?.(index) }}> {children}
From aedb173ba7d27c31ad73d28d35b2761cf7976941 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Mon, 2 Jun 2025 11:15:19 +0900 Subject: [PATCH 02/44] =?UTF-8?q?=EC=88=98=EC=9A=94=EC=B2=98=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B3=B5=ED=86=B5=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EB=B9=BC=EB=8A=94=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/form_ui/index.tsx | 10 ++- .../business_components/src/ui/index.ts | 2 +- .../src/ui/sdmSearchForm/index.tsx | 85 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 packages/common/business_components/src/ui/sdmSearchForm/index.tsx diff --git a/packages/common/business_components/src/ui/form_ui/index.tsx b/packages/common/business_components/src/ui/form_ui/index.tsx index b834d5515..de4b37f61 100644 --- a/packages/common/business_components/src/ui/form_ui/index.tsx +++ b/packages/common/business_components/src/ui/form_ui/index.tsx @@ -1,5 +1,5 @@ import { Checkbox } from "@common/components/ui"; -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { FormControl, FormItem, FormLabel, FormMessage } from "../form"; export const BZNM_TOT_NO = '종사업자등록번호'; @@ -17,7 +17,11 @@ interface FormElementProps extends React.HTMLAttributes { const FormUi = React.forwardRef( ({ children, label, onCheckboxChange, isDisabled }, ref) => { - const [isChecked, setIsChecked] = useState(false); + const [isChecked, setIsChecked] = useState(isDisabled); + + useEffect(() => { + setIsChecked(isDisabled); + }, [isDisabled]); const handleCheckboxChange = useCallback((checked: boolean) => { setIsChecked(checked); @@ -50,7 +54,7 @@ const FormUi = React.forwardRef( {React.isValidElement(children) && isDisabled ? React.cloneElement(children, { - disabled: true, + disabled: isDisabled, ...(children.props as FormElementProps) }) : children} diff --git a/packages/common/business_components/src/ui/index.ts b/packages/common/business_components/src/ui/index.ts index 90bf029c0..606852d1e 100644 --- a/packages/common/business_components/src/ui/index.ts +++ b/packages/common/business_components/src/ui/index.ts @@ -19,4 +19,4 @@ export * from "./tab"; export * from "./table"; export * from "./table_div"; export * from "./title"; - +export * from "./sdmSearchForm"; \ No newline at end of file diff --git a/packages/common/business_components/src/ui/sdmSearchForm/index.tsx b/packages/common/business_components/src/ui/sdmSearchForm/index.tsx new file mode 100644 index 000000000..89edc233c --- /dev/null +++ b/packages/common/business_components/src/ui/sdmSearchForm/index.tsx @@ -0,0 +1,85 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +type thisProps = { + label: string | undefined; + initValue: string | undefined; + setRtnData : (rtnData: any[]) => void; +} + +export default function SdmSearchForm({label=undefined, initValue=undefined, setRtnData=()=>{}} :thisProps) { + + /* const { getDemandCompList, Sdm } = useDmStore((state) => state); + + const { + demandCompList + } = Sdm; */ + + const [isOpen, setIsOpen] = useState(false); + const [textValue, setTextValue] = useState(initValue || ''); + const wrapperRef = useRef(null); + const handleInputChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value; + setTextValue(value); + if (value) { + setIsOpen(true); + } else { + setIsOpen(false); + } + }, []); + + /* const handleSuggestionClick = useCallback((dmdCoNm: string) => { + setTextValue(dmdCoNm); + }, []); */ + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( +
+ setIsOpen(true)} */ + className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + placeholder={`${label}을(를) 입력하세요`} + /> + {isOpen && ( +
+
+ {/* {demandCompList && demandCompList.length > 0 ? demandCompList.map((item : DemandCompType, index : number) => ( +
handleSuggestionClick(item.dmdCoNm)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSuggestionClick(item.dmdCoNm); + } + }} + > + {item.dmdInfoPk.substring(0,10)} {item.dmdCoNm} +
+ )) : ( */} +
+ 검색 결과가 없습니다. +
+ {/* )) : ( */} +
+
+ )} +
+ ) +} \ No newline at end of file From 1eef53fe7dcd6605137b2db80f738df4bf586c72 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Mon, 2 Jun 2025 11:16:48 +0900 Subject: [PATCH 03/44] =?UTF-8?q?=EC=88=98=EC=9A=94=EC=B2=98=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B3=B5=ED=86=B5=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C=20=EB=B9=BC=EB=8A=94=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/business_components/src/ui/sdmSearchForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/business_components/src/ui/sdmSearchForm/index.tsx b/packages/common/business_components/src/ui/sdmSearchForm/index.tsx index 89edc233c..a724d1d97 100644 --- a/packages/common/business_components/src/ui/sdmSearchForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmSearchForm/index.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -type thisProps = { +export type thisProps = { label: string | undefined; initValue: string | undefined; setRtnData : (rtnData: any[]) => void; From feb5711ad438c3640d9e6ab6bccfc393778f2613 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Mon, 2 Jun 2025 11:25:31 +0900 Subject: [PATCH 04/44] =?UTF-8?q?export=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/business_components/src/ui/sdmSearchForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/business_components/src/ui/sdmSearchForm/index.tsx b/packages/common/business_components/src/ui/sdmSearchForm/index.tsx index a724d1d97..62009826e 100644 --- a/packages/common/business_components/src/ui/sdmSearchForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmSearchForm/index.tsx @@ -6,7 +6,7 @@ export type thisProps = { setRtnData : (rtnData: any[]) => void; } -export default function SdmSearchForm({label=undefined, initValue=undefined, setRtnData=()=>{}} :thisProps) { +export const SdmSearchForm = ({label=undefined, initValue=undefined, setRtnData=()=>{}} :thisProps) => { /* const { getDemandCompList, Sdm } = useDmStore((state) => state); From b96207cced396af35ee526e0883c1f2a0b2b16ab Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Tue, 3 Jun 2025 17:01:43 +0900 Subject: [PATCH 05/44] =?UTF-8?q?=EC=88=98=EC=9A=94=EC=B2=98=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=A7=8C=EB=93=9C=EB=8A=94=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/index.ts | 2 +- .../src/ui/sdmInsertForm/form.tsx | 246 ++++++++++++++++++ .../src/ui/sdmInsertForm/index.tsx | 164 ++++++++++++ .../sdmNameBox.tsx} | 46 ++-- 4 files changed, 439 insertions(+), 19 deletions(-) create mode 100644 packages/common/business_components/src/ui/sdmInsertForm/form.tsx create mode 100644 packages/common/business_components/src/ui/sdmInsertForm/index.tsx rename packages/common/business_components/src/ui/{sdmSearchForm/index.tsx => sdmInsertForm/sdmNameBox.tsx} (68%) diff --git a/packages/common/business_components/src/ui/index.ts b/packages/common/business_components/src/ui/index.ts index 606852d1e..0986f7953 100644 --- a/packages/common/business_components/src/ui/index.ts +++ b/packages/common/business_components/src/ui/index.ts @@ -19,4 +19,4 @@ export * from "./tab"; export * from "./table"; export * from "./table_div"; export * from "./title"; -export * from "./sdmSearchForm"; \ No newline at end of file +export * from "./sdmInsertForm/sdmNameBox"; \ No newline at end of file diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx new file mode 100644 index 000000000..6d3219468 --- /dev/null +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -0,0 +1,246 @@ +import { FormField } from "../form"; +import { SelectBox } from "../select"; +import { Input } from "@common/components"; +import { FormUi } from "../form_ui"; +import { useCallback, useState } from "react"; +import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; +import { SdmSearchForm } from "./sdmNameBox"; +import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; + +type SelectDataType = { + text: string; + val: string; +}; + +interface FormSchema { + ctpvCd: string; + sggCd: string; + dmdCoNm: string; + dmdInfoPk: string; + daddr: string; + telno: string; + bznmTotNo?: string; + rgtr?: string; +} + +interface SdmRegFormProps { + form: UseFormReturn; + onCheckboxChange?: (checked: boolean) => void; + selectSidoData: SelectDataType[]; + selectSigunData: SelectDataType[]; + dataList: DemandCompType[]; + rtnValue: (searchText: string, pageCnt: number) => void; + setIsDuplicate: (value: boolean) => void; +} + +type FieldComponent = (field: ControllerRenderProps) => JSX.Element; + +export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigunData, dataList, rtnValue, setIsDuplicate }: SdmRegFormProps) { + const [sidoValue, setSidoValue] = useState(''); + const [sigunValue, setSigunValue] = useState(''); + const [filteredSigunData, setFilteredSigunData] = useState([{ text: "전체", val: '0' }]); + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); + + const initializeSigunData = useCallback((sidoCd: string) => { + if (sidoCd === '0') { + setSigunValue('0'); + setFilteredSigunData([{ text: "전체", val: '0' }]); + } else { + const newFilteredData = [ + { text: "전체", val: '0' }, + ...selectSigunData.filter((item: SelectDataType) => item.val.substring(0, 2) === sidoCd) + ]; + setSigunValue('0'); + setFilteredSigunData(newFilteredData); + } + }, [selectSigunData]); + + const onSelectValue = useCallback(( + val: string, + type: string, + field: ControllerRenderProps | undefined + ) => { + switch (type) { + case "ctpvCd": + initializeSigunData(val); + setSidoValue(val); + field?.onChange(val); + form.setValue('dmdInfoPk', ''); + form.setValue('bznmTotNo', ''); + break; + case "sggCd": + setSigunValue(val); + field?.onChange(val); + form.setValue('dmdInfoPk', ''); + form.setValue('bznmTotNo', ''); + break; + } + }, [initializeSigunData, form]); + + const handleCheckboxChange = useCallback((checked: boolean) => { + setIsBznmTotNoDisabled(checked); + onCheckboxChange?.(checked); + }, [onCheckboxChange]); + + const setRtnValue = useCallback((searchText: string, pageCnt: number) => { + rtnValue(searchText, pageCnt); + }, [rtnValue]); + + const setRtnData = useCallback((item: DemandCompType) => { + const rtnCompData = item; + form.setValue('dmdCoNm', rtnCompData?.dmdCoNm || ''); + form.setValue('dmdInfoPk', rtnCompData?.dmdInfoPk.substring(0,10) || ''); + form.setValue('bznmTotNo', rtnCompData?.dmdInfoPk.substring(10,14)==='0000' ? '' : rtnCompData?.dmdInfoPk.substring(10,14)); + + const sidoCd = rtnCompData?.ctpvCd.trim() || ''; + setSidoValue(sidoCd); + form.setValue('ctpvCd', sidoCd); + initializeSigunData(sidoCd); + + setTimeout(() => { + const sigunCd = rtnCompData?.sggCd.trim() || ''; + setSigunValue(sigunCd); + form.setValue('sggCd', sigunCd); + }, 100); + + if(rtnCompData?.dmdInfoPk.substring(10,14) === '0000') { + handleCheckboxChange(true); + } else { + handleCheckboxChange(false); + } + + form.setValue('daddr', rtnCompData?.daddr || ''); + form.setValue('telno', rtnCompData?.telno || ''); + + // 선택을 했을 때 중복 상태를 true로 설정 + setIsDuplicate(true); + }, [form, handleCheckboxChange, initializeSigunData, setIsDuplicate]); + + const setRtnText = useCallback((text: string) => { + form.setValue('dmdCoNm', text); + }, [form]); + + const formFields: Array<{ + name: keyof FormSchema; + label: string; + component: FieldComponent; + }> = [ + { + name: "ctpvCd", + label: "시도", + component: (field) => ( + onSelectValue(val.toString(), 'ctpvCd', field)} + className="" + value={sidoValue} + /> + ) + }, + { + name: "sggCd", + label: "시군구", + component: (field) => ( + onSelectValue(val.toString(), 'sggCd', field)} + className="" + value={sigunValue} + /> + ) + }, + { + name: "dmdCoNm", + label: "회사명", + component: (field) => ( + + ) + }, + { + name: "dmdInfoPk", + label: "사업자등록번호", + component: (field) => ( + + ) + }, + { + name: "bznmTotNo", + label: "종사업자등록번호", + component: (field) => ( + + + + ) + }, + { + name: "daddr", + label: "사업자주소", + component: (field) => ( + + ) + }, + { + name: "telno", + label: "전화번호", + component: (field) => ( + + ) + } + ]; + + return ( + <> + {formFields.map(({ name, label, component }) => ( + ( + name === 'bznmTotNo' ? component(field) : ( + + {component(field)} + + ) + )} + /> + ))} + + ); +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx new file mode 100644 index 000000000..4a972180a --- /dev/null +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -0,0 +1,164 @@ +"use client"; + +/* import { useDmStore } from "@/app/store"; */ +import { SdmRegForm } from "./form"; +import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; +import { Button } from "@common/components/ui"; +import { zodResolver } from "@hookform/resolvers/zod"; +/* import { useRouter } from "next/navigation"; */ +import { useCallback, useEffect, useState } from "react"; +import { FormProvider, useForm, type SubmitHandler } from "react-hook-form"; +import { toast, Toaster } from "sonner"; +/* import { Toaster, toast } from "react-hot-toast"; */ +import * as z from "zod"; +import type { SelectDataType } from "../search_input_select"; + +type thisProps = { + selectSidoData: SelectDataType[]; + selectSigunData: SelectDataType[]; + dataList: DemandCompType[]; + rtnValue: (searchText: string, pageCnt: number) => void; + rtnInputData: (data: DemandCompType) => void; +} + +const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => z.object({ + ctpvCd: z.string().min(1, "시/도를 선택해주세요"), + sggCd: z.string().min(1, "시/군/구를 선택해주세요"), + dmdCoNm: z.string() + .min(1, "회사명을 입력해주세요") + .max(20, "회사명은 20자 이내로 입력해주세요"), + dmdInfoPk: z.string() + .min(1, "사업자등록번호를 입력해주세요") + .regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다") + .refine(() => !isDuplicate, "이미 등록된 사업자등록번호입니다"), + bznmTotNo: isBznmTotNoDisabled + ? z.string().optional() + : z.string() + .length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다"), + daddr: z.string() + .min(1, "주소를 입력해주세요") + .min(6, "주소는 6자 이상 입력해주세요") + .max(40, "주소는 40자 이내로 입력해주세요"), + telno: z.string() + .min(8, "전화번호는 8자 이상 입력해주세요") + .max(12, "전화번호는 12자 이내로 입력해주세요") + .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), + rgtr: z.string().optional(), +}); + +type FormSchema = ReturnType; +export type FormData = z.infer; + +const defaultValues: FormData = { + ctpvCd: '', + sggCd: '', + dmdCoNm: '', + dmdInfoPk: '', + bznmTotNo: '', + daddr: '', + telno: '', + rgtr: "3fa85f64-5717-4562-b3fc-2c963f66afa6", +}; + +export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData}: thisProps) { + /* const { insertSdmData } = useDmStore((state) => state); + const router = useRouter(); */ + + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); + const [isDuplicate, setIsDuplicate] = useState(false); + const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues, + mode: "onChange", + criteriaMode: "all", + shouldFocusError: false, + shouldUnregister: false + }); + + // 체크박스 상태가 변경될 때마다 bznmTotNo 필드의 에러를 초기화 + useEffect(() => { + if (isBznmTotNoDisabled) { + form.clearErrors('bznmTotNo'); + form.setValue('bznmTotNo', ''); + } + }, [isBznmTotNoDisabled, form]); + + const setRtnValue = useCallback((searchText: string, pageCnt: number) => { + rtnValue(searchText, pageCnt); + }, [rtnValue]); + + const onSubmit: SubmitHandler> = async (data) => { + try { + // form 에러 체크 + const formErrors = form.formState.errors; + if (Object.keys(formErrors).length > 0) { + const errorMessage = Object.values(formErrors)[0]?.message || '입력값을 확인해주세요.'; + toast.error(errorMessage); + return; + } + + const dmdInfoPk = data.dmdInfoPk + (data.bznmTotNo || '0000'); + const formData = { + ...data, + dmdInfoPk, + }; + + console.log(formData); + /* await insertSdmData(formData); */ + rtnInputData(formData as DemandCompType); + + const queryParams = new URLSearchParams({ + dmdCoNm: data.dmdCoNm, + ctpvCd: data.ctpvCd, + sggCd: data.sggCd, + dmdInfoPk, + bznmTotNo: data.bznmTotNo || '', + daddr: data.daddr, + telno: data.telno, + }); + + //router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`); + toast.success('수요처가 성공적으로 등록되었습니다.'); + } catch (error) { + console.error('Error submitting form:', error); + toast.error('수요처 등록에 실패했습니다.'); + } + }; + + return ( + <> +
+

+ 수요처등록 +

+
+ +
+ +
+ +
+ +
+ +
+
+ + + ); +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/sdmSearchForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx similarity index 68% rename from packages/common/business_components/src/ui/sdmSearchForm/index.tsx rename to packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx index 62009826e..18d048692 100644 --- a/packages/common/business_components/src/ui/sdmSearchForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx @@ -1,18 +1,24 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; +import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; export type thisProps = { label: string | undefined; initValue: string | undefined; - setRtnData : (rtnData: any[]) => void; + dataList : DemandCompType[]; + rtnValue : (searchText: string, pageCnt: number) => void; + rtnData : (item: DemandCompType) => void; + rtnText : (text: string) => void; } -export const SdmSearchForm = ({label=undefined, initValue=undefined, setRtnData=()=>{}} :thisProps) => { - - /* const { getDemandCompList, Sdm } = useDmStore((state) => state); - - const { - demandCompList - } = Sdm; */ +/** + * 수요처 검색 컴포넌트 + * @param label 라벨 + * @param initValue 초기값 + * @param rtnValue 검색 결과 반환 함수 + * @param dataList 검색할 때 넘기는 데이터 리스트 + * @param rtnData 검색 결과 반환 함수 + */ +export const SdmSearchForm = ({label=undefined, initValue=undefined, rtnValue=()=>{}, dataList=[], rtnData=()=>{}, rtnText=()=>{} } :thisProps) => { const [isOpen, setIsOpen] = useState(false); const [textValue, setTextValue] = useState(initValue || ''); @@ -20,16 +26,20 @@ export const SdmSearchForm = ({label=undefined, initValue=undefined, setRtnData= const handleInputChange = useCallback((e: React.ChangeEvent) => { const value = e.target.value; setTextValue(value); + rtnValue(value, 10); + rtnText(value); if (value) { setIsOpen(true); } else { setIsOpen(false); } - }, []); + }, [rtnValue, rtnText]); - /* const handleSuggestionClick = useCallback((dmdCoNm: string) => { - setTextValue(dmdCoNm); - }, []); */ + const handleSuggestionClick = useCallback((item: DemandCompType) => { + setTextValue(item.dmdCoNm); + rtnData(item); + setIsOpen(false); + }, [rtnData]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -43,7 +53,7 @@ export const SdmSearchForm = ({label=undefined, initValue=undefined, setRtnData= document.removeEventListener('mousedown', handleClickOutside); }; }, []); - + return (
- {/* {demandCompList && demandCompList.length > 0 ? demandCompList.map((item : DemandCompType, index : number) => ( + {dataList && dataList.length > 0 ? dataList.map((item : DemandCompType, index : number) => (
handleSuggestionClick(item.dmdCoNm)} + onClick={() => handleSuggestionClick(item)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { - handleSuggestionClick(item.dmdCoNm); + handleSuggestionClick(item); } }} > {item.dmdInfoPk.substring(0,10)} {item.dmdCoNm}
- )) : ( */} + )) : (
검색 결과가 없습니다.
- {/* )) : ( */} + )}
)} From 40f26c77673f68933c33da2124c885affb6e0dd6 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Wed, 4 Jun 2025 11:05:02 +0900 Subject: [PATCH 06/44] =?UTF-8?q?-=EC=88=98=EC=9A=94=EC=B2=98=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=9E=85=EB=A0=A5=ED=8F=BC=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=9E=91=EC=97=85?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91(=ED=8C=A8=EC=8A=A4=EC=9B=8C?= =?UTF-8?q?=EB=93=9C=EB=A7=8C=20=EB=82=A8=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/sdmInsertForm/form.tsx | 43 ++++++++++++++++++- .../src/ui/sdmInsertForm/index.tsx | 20 ++++++++- .../src/ui/sdmInsertForm/sdmNameBox.tsx | 12 +++++- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index 6d3219468..d59108c5b 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -21,6 +21,7 @@ interface FormSchema { telno: string; bznmTotNo?: string; rgtr?: string; + dmdAplyPswd?: string; } interface SdmRegFormProps { @@ -40,7 +41,8 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const [sigunValue, setSigunValue] = useState(''); const [filteredSigunData, setFilteredSigunData] = useState([{ text: "전체", val: '0' }]); const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); - + const [orgBznmNo, setOrgBznmNo] = useState(''); + const initializeSigunData = useCallback((sidoCd: string) => { if (sidoCd === '0') { setSigunValue('0'); @@ -92,6 +94,8 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun form.setValue('dmdInfoPk', rtnCompData?.dmdInfoPk.substring(0,10) || ''); form.setValue('bznmTotNo', rtnCompData?.dmdInfoPk.substring(10,14)==='0000' ? '' : rtnCompData?.dmdInfoPk.substring(10,14)); + // 수요처 불러온 후 사업자번호를 바꿀경우를 대비함 + setOrgBznmNo(rtnCompData?.dmdInfoPk); const sidoCd = rtnCompData?.ctpvCd.trim() || ''; setSidoValue(sidoCd); form.setValue('ctpvCd', sidoCd); @@ -120,6 +124,29 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun form.setValue('dmdCoNm', text); }, [form]); + const bznNoChange = useCallback((e: React.ChangeEvent, noType: string) => { + const value = e.target.value; + console.log('form value : '+form.getValues('dmdInfoPk')); + console.log('org value : '+orgBznmNo); + + if(noType === 'dmdInfoPk') { + if(orgBznmNo.substring(0,10) === value) { + setIsDuplicate(true); + } else { + setIsDuplicate(false); + } + form.setValue('dmdInfoPk', value); + } else { + if(orgBznmNo.substring(10,14) === value) { + setIsDuplicate(true); + } else { + setIsDuplicate(false); + } + form.setValue('bznmTotNo', value); + } + + }, [form, orgBznmNo, setIsDuplicate]); + const formFields: Array<{ name: keyof FormSchema; label: string; @@ -177,6 +204,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun type="text" className="!text-xl text-gray-600" {...field} + onChange={(e) => bznNoChange(e, 'dmdInfoPk')} /> ) }, @@ -195,6 +223,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun className="!mt-5 !text-xl text-gray-600" disabled={isBznmTotNoDisabled} {...field} + onChange={(e) => bznNoChange(e, 'bznmTotNo')} /> ) @@ -222,6 +251,18 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun {...field} /> ) + }, + { + name: "dmdAplyPswd", + label: "패스워드", + component: (field) => ( + + ) } ]; diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 4a972180a..c79712c58 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -30,11 +30,12 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => dmdInfoPk: z.string() .min(1, "사업자등록번호를 입력해주세요") .regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다") - .refine(() => !isDuplicate, "이미 등록된 사업자등록번호입니다"), + .refine(() => isBznmTotNoDisabled ? !isDuplicate : true, "이미 등록된 사업자등록번호입니다"), bznmTotNo: isBznmTotNoDisabled ? z.string().optional() : z.string() - .length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다"), + .length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다") + .refine(() => !isDuplicate, "이미 등록된 사업자등록번호입니다"), daddr: z.string() .min(1, "주소를 입력해주세요") .min(6, "주소는 6자 이상 입력해주세요") @@ -44,6 +45,10 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => .max(12, "전화번호는 12자 이내로 입력해주세요") .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), rgtr: z.string().optional(), + dmdAplyPswd : z.string() + .min(1, "패스워드를 입력해주세요") + .min(8, "패스워드는 8자 이상 입력해주세요") + .max(16, "패스워드는 16자 이내로 입력해주세요"), }); type FormSchema = ReturnType; @@ -58,8 +63,19 @@ const defaultValues: FormData = { daddr: '', telno: '', rgtr: "3fa85f64-5717-4562-b3fc-2c963f66afa6", + dmdAplyPswd : '', }; +/** + * 수요처 등록 폼 + * @param selectSidoData 시/도 데이터 + * @param selectSigunData 시/군/구 데이터 + * @param dataList 수요처 목록 + * @param rtnValue 검색 결과(텍스트) 콜백 + * @param rtnInputData 수요처 리스트 콜백 + * @returns + */ + export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData}: thisProps) { /* const { insertSdmData } = useDmStore((state) => state); const router = useRouter(); */ diff --git a/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx index 18d048692..83571b6ff 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx @@ -80,7 +80,17 @@ export const SdmSearchForm = ({label=undefined, initValue=undefined, rtnValue=() } }} > - {item.dmdInfoPk.substring(0,10)} {item.dmdCoNm} + {item.dmdInfoPk.substring(10,14) !== '0000' ? ( + <> + 사업자번호 : {item.dmdInfoPk.substring(0,10)}  + 종사업자번호 : {item.dmdInfoPk.substring(10,14)}  + + ) : ( + <> + 사업자번호 : {item.dmdInfoPk.substring(0,10)}  + + )} + {item.dmdCoNm} )) : (
From 2c773de04f5a4f6a964e3961fe59db5bca7cd110 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Wed, 4 Jun 2025 11:08:00 +0900 Subject: [PATCH 07/44] =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=84=EC=9A=94=ED=95=9C=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=9C=BC=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/sdmInsertForm/index.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index c79712c58..723d87cf6 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -76,6 +76,32 @@ const defaultValues: FormData = { * @returns */ +/** + * 페이지에서 작업 + const {Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm} = useDmStore((state) => state); + const { selectSidoData, selectSigunData } = Common; + const { demandCompList } = Sdm; + + // 공통데이터 (시도, 시군구 데이터 호출) + useEffect(() => { + initializeCommonData(); + }, [initializeCommonData]); + + // 컴포넌트에서 텍스트 검색 호출 + const callback = useCallback((searchText: string, pageCnt: number) => { + searchText = searchText.trim(); + if(searchText.length > 0) { + getDemandCompList(searchText, pageCnt); + } + }, [getDemandCompList]); + + // 컴포넌트에서 수요처 등록 호출 + const handleInsertSdmData = useCallback((data: DemandCompType) => { + insertSdmData(data); + + }, [insertSdmData]); + */ + export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData}: thisProps) { /* const { insertSdmData } = useDmStore((state) => state); const router = useRouter(); */ From b7f643ab0c0ffd7298706bce904bc57c2f63aafa Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Wed, 4 Jun 2025 11:20:51 +0900 Subject: [PATCH 08/44] =?UTF-8?q?=EC=88=98=EC=9A=94=EC=B2=98=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20export=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/business_components/src/ui/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common/business_components/src/ui/index.ts b/packages/common/business_components/src/ui/index.ts index 0986f7953..a1fa95f39 100644 --- a/packages/common/business_components/src/ui/index.ts +++ b/packages/common/business_components/src/ui/index.ts @@ -19,4 +19,4 @@ export * from "./tab"; export * from "./table"; export * from "./table_div"; export * from "./title"; -export * from "./sdmInsertForm/sdmNameBox"; \ No newline at end of file +export * from "./sdmInsertForm"; \ No newline at end of file From 8a2439b4ba53ab5e954980d633b74f15965f1d96 Mon Sep 17 00:00:00 2001 From: leedj312 Date: Wed, 4 Jun 2025 20:06:47 +0900 Subject: [PATCH 09/44] =?UTF-8?q?=EC=88=98=EC=9A=94=EC=B2=98=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EB=B0=94=EB=94=94=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/sdmInsertForm/index.tsx | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 723d87cf6..4b4a94648 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -2,7 +2,6 @@ /* import { useDmStore } from "@/app/store"; */ import { SdmRegForm } from "./form"; -import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; import { Button } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; /* import { useRouter } from "next/navigation"; */ @@ -13,12 +12,28 @@ import { toast, Toaster } from "sonner"; import * as z from "zod"; import type { SelectDataType } from "../search_input_select"; + +export type DemandCompType = { + dmdInfoPk : string | ''; + bzmnNo : string | ''; + dmdCoNm : string | ''; + daddr : string | ''; + telno : string | ''; + rgtr : string | ''; + useUsgCd : string | ''; + ctpvCd : string | ''; + sggCd : string | ''; + dmdAplyPswd: string | ''; +} + + type thisProps = { selectSidoData: SelectDataType[]; selectSigunData: SelectDataType[]; dataList: DemandCompType[]; rtnValue: (searchText: string, pageCnt: number) => void; rtnInputData: (data: DemandCompType) => void; + bodyClassNm?: string; } const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => z.object({ @@ -102,7 +117,7 @@ const defaultValues: FormData = { }, [insertSdmData]); */ -export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData}: thisProps) { +export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm}: thisProps) { /* const { insertSdmData } = useDmStore((state) => state); const router = useRouter(); */ @@ -177,7 +192,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList
-
+
Date: Thu, 5 Jun 2025 09:44:28 +0900 Subject: [PATCH 10/44] =?UTF-8?q?console.log=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/business_components/src/ui/sdmInsertForm/form.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index d59108c5b..c5c0ee0db 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -126,8 +126,6 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const bznNoChange = useCallback((e: React.ChangeEvent, noType: string) => { const value = e.target.value; - console.log('form value : '+form.getValues('dmdInfoPk')); - console.log('org value : '+orgBznmNo); if(noType === 'dmdInfoPk') { if(orgBznmNo.substring(0,10) === value) { From 59cc416eaaa833851ce583d1645dbe371a9037a2 Mon Sep 17 00:00:00 2001 From: yschoi Date: Thu, 5 Jun 2025 13:14:36 +0900 Subject: [PATCH 11/44] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/page.tsx | 14 ++--- .../business_components/src/ui/map/Gis.js | 2 +- .../src/ui/map/GisContent.js | 4 +- .../map/{MapToggleBtn.js => GisMapToggle.js} | 0 .../src/ui/map/Google3DMap.js | 60 ------------------- 5 files changed, 8 insertions(+), 72 deletions(-) rename packages/common/business_components/src/ui/map/{MapToggleBtn.js => GisMapToggle.js} (100%) delete mode 100644 packages/common/business_components/src/ui/map/Google3DMap.js diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx index b547fb7e4..da20fdaa7 100644 --- a/apps/sn/app/map/page.tsx +++ b/apps/sn/app/map/page.tsx @@ -1,18 +1,14 @@ -'use client' +"use client"; -import Gis from '@common/business_components/ui/map/Gis'; -import { SearchDiv, SelectBox, UiTable, MainDiv, Title, ContentsDiv, SearchFormLeft, SearchFormRight } from '@common/business_components'; -import ListDiv from '@common/business_components/ui/list_div'; -import { Button, Input } from '@common/components'; +import Gis from "@common/business_components/ui/map/Gis"; import { useEffect, useState } from "react"; import { mapService } from "@service"; const Map = () => { - const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 - const [selectedFeature,setSelectedFeature] = useState(null); // 선택된 피처 상태 + const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 - const fetchData = async () => { + const fetchData = async () => { try { const response = await mapService.getLyrStngs(); setTreeData(response.data); // API 응답 저장 @@ -29,7 +25,7 @@ const Map = () => { return (
-
+
); }; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index fd86a25b0..c501302f6 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -224,7 +224,7 @@ function Gis({ treeData = [], isSearch = false }) {
{/* - */} + */}
diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js index ad8a53605..6f86c4435 100644 --- a/packages/common/business_components/src/ui/map/GisContent.js +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -5,7 +5,7 @@ import GisLonLat from './GisLonLat'; import GisSelect from './GisSelect'; import GisToolBar from './GisToolBar'; import './lib/css/gis.edit.css'; -import MapToggleBtn from './MapToggleBtn'; +import GisMapToggle from './GisMapToggle'; import { Search } from 'lucide-react'; export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) { @@ -51,7 +51,7 @@ export default function GisContent({ edit = false, onFeatureSelect, isSearch, se return ( <> {/**/} - + diff --git a/packages/common/business_components/src/ui/map/MapToggleBtn.js b/packages/common/business_components/src/ui/map/GisMapToggle.js similarity index 100% rename from packages/common/business_components/src/ui/map/MapToggleBtn.js rename to packages/common/business_components/src/ui/map/GisMapToggle.js diff --git a/packages/common/business_components/src/ui/map/Google3DMap.js b/packages/common/business_components/src/ui/map/Google3DMap.js deleted file mode 100644 index 2ae6547dd..000000000 --- a/packages/common/business_components/src/ui/map/Google3DMap.js +++ /dev/null @@ -1,60 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import OLCesium from 'olcs/OLCesium'; -import olView from 'ol/View.js'; -import olSourceImageWMS from 'ol/source/ImageWMS.js'; -import olSourceOSM from 'ol/source/OSM.js'; -import olLayerImage from 'ol/layer/Image.js'; -import olLayerTile from 'ol/layer/Tile.js'; -import olMap from 'ol/Map.js'; -import { GLOBAL } from '../../js/consts/global'; - -const Google3DMap = () => { - const mapRef = useRef(null); - const [ol3d, setOl3d] = useState(null); - - useEffect(() => { - if (!mapRef.current) return; - - const customProjSource = new olSourceImageWMS({ - attributions: '© National parks / geo.admin.ch', - crossOrigin: 'anonymous', - params: { LAYERS: 'ch.bafu.schutzgebiete-paerke_nationaler_bedeutung' }, - projection: 'EPSG:21781', - url: 'https://wms.geo.admin.ch/', - }); - - const { Cesium } = window; - Cesium.Ion.defaultAccessToken = GLOBAL.CESIUM_ION_TOKEN; - - const ol2d = new olMap({ - layers: [new olLayerTile({ source: new olSourceOSM() }), new olLayerImage({ source: customProjSource })], - target: mapRef.current, - view: new olView({ - center: [860434.6266531206, 6029479.0044273855], - zoom: 6, - }), - }); - - const ol3dInstance = new OLCesium({ - map: ol2d, - time: () => Cesium.JulianDate.now(), - }); - - const scene = ol3dInstance.getCesiumScene(); - Cesium.createWorldTerrainAsync().then(tp => { - scene.terrainProvider = tp; - }); - - ol3dInstance.setEnabled(true); - setOl3d(ol3dInstance); - - return () => { - ol3dInstance.setEnabled(false); - ol2d.setTarget(null); - }; - }, []); - - return
; -}; - -export default Google3DMap; From a7216dfcc631f08b9fc6ad991e88a4f6b6a35ba9 Mon Sep 17 00:00:00 2001 From: swcho Date: Thu, 5 Jun 2025 17:13:25 +0900 Subject: [PATCH 12/44] =?UTF-8?q?feat:=20-=20=EC=B9=A8=EC=9E=85=EC=88=98?= =?UTF-8?q?=20=EB=B6=84=EC=84=9D=20=ED=99=94=EB=A9=B4=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=EA=B2=B0=EA=B3=BC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84,=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C,=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=A4=91=20-=20=EC=A4=91=EC=A0=90=EC=9A=B0?= =?UTF-8?q?=EC=88=98=ED=86=A0=EC=8B=A4=20=EC=83=81=EC=84=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20-=20verti?= =?UTF-8?q?calTable.tsx=EC=97=90=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20contentLayout.tsx=EC=97=90=20className=20prop?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20fileDownload.ts=EC=97=90=20BASE=5FU?= =?UTF-8?q?RL=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/components/lib/api/index.ts | 2 +- .../app/components/lib/utils/fileDownload.ts | 2 + apps/sn/app/components/ui/contentLayout.tsx | 3 +- apps/sn/app/components/ui/verticalTable.tsx | 79 ++++++- apps/sn/app/ii/BaseLayout.tsx | 189 ++++++++++------ apps/sn/app/ii/irsetting/page.tsx | 2 +- apps/sn/app/ii/result/page.tsx | 210 +++++++++++++----- apps/sn/app/map/components/common/index.ts | 10 +- apps/sn/app/map/components/common/table.ts | 59 ++++- .../app/map/components/popupComponent/Ssc.tsx | 173 ++++++++++----- apps/sn/app/map/featureClickEventPopup.tsx | 91 ++++++-- 11 files changed, 606 insertions(+), 214 deletions(-) diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 84f38002f..d7db93f77 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -43,7 +43,7 @@ export const putData = async (url: string, data: object): Promise => { return resData; }; -export const getData = async (url: string): Promise => { +export const getData = async (url: string, data?: object): Promise => { const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, { method: "GET", }); diff --git a/apps/sn/app/components/lib/utils/fileDownload.ts b/apps/sn/app/components/lib/utils/fileDownload.ts index 681203196..ffe47eced 100644 --- a/apps/sn/app/components/lib/utils/fileDownload.ts +++ b/apps/sn/app/components/lib/utils/fileDownload.ts @@ -1,3 +1,5 @@ +const BASE_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || ''; + export const handleDownload = async (api:string, fileNm:string) => { try { const response = await fetch(api, { diff --git a/apps/sn/app/components/ui/contentLayout.tsx b/apps/sn/app/components/ui/contentLayout.tsx index 75d61235f..de4406efd 100644 --- a/apps/sn/app/components/ui/contentLayout.tsx +++ b/apps/sn/app/components/ui/contentLayout.tsx @@ -5,8 +5,9 @@ type LayoutNode = { children : ReactNode title : string isLoading?: boolean + className?: string } -const ContentLayout = ({children, title, isLoading} : LayoutNode) => { +const ContentLayout = ({children, title, isLoading, className} : LayoutNode) => { return ( diff --git a/apps/sn/app/components/ui/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx index ae3068a67..e5e506796 100644 --- a/apps/sn/app/components/ui/verticalTable.tsx +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; type UiTableProps = { headName: React.ReactNode | undefined; + tableName1?: React.ReactNode | undefined; tableData: { [key: string]: string | number | boolean | object }[]; headlist: UiHeadProps[]; pageSize: number; @@ -14,6 +15,18 @@ type UiTableProps = { className?: string; resetPageNo?: boolean; isEditable?: boolean; + tableName2?: React.ReactNode | undefined; + tableData2?: { [key: string]: string | number | boolean | object }[]; + headlist2?: UiHeadProps[]; + pageSize2?: number; + total2?: number; + children2?: Record<string, React.ReactNode> | undefined; + cellClick2?: ((index: number | undefined, headId: string) => void) | undefined; + onPageChange2?: (page: number) => void; + className2?: string; + resetPageNo2?: boolean; + isEditable2?: boolean; + showSecondTable?: boolean; }; type UiHeadProps = { @@ -25,17 +38,30 @@ type UiHeadProps = { }; export function VerticalTable({ + headName, + tableName1, tableData, headlist, pageSize, total, children, - headName, cellClick, onPageChange, className = "", resetPageNo = false, isEditable = false, + tableName2, + tableData2, + headlist2, + pageSize2, + total2, + children2, + cellClick2, + onPageChange2, + className2 = "", + resetPageNo2 = false, + isEditable2 = false, + showSecondTable = false, }: UiTableProps) { const [cur, setCur] = useState<number>(1); const [formData, setFormData] = useState<{ [key: string]: string | number | boolean }>({}); @@ -72,6 +98,9 @@ export function VerticalTable({ </div> ) : null} <div className={`card !m-[1.2rem] !p-0r ${className}`}> + {tableName1 && ( // tableName prop이 있으면 표시 + <div className="text-xl font-semibold my-2 mx-3">{tableName1}</div> + )} <Table variant="secondary"> <TableBody className="text-center"> {headlist?.map((head) => ( @@ -115,6 +144,54 @@ export function VerticalTable({ ))} </TableBody> </Table> + {tableName2 && ( // tableName prop이 있으면 표시 + <div className="text-xl font-semibold mt-8 my-2 mx-3">{tableName2}</div> + )} + {showSecondTable && ( + <Table variant="secondary"> + <TableBody className="text-center"> + {headlist2?.map((head) => ( + <TableRow key={head.id}> + {/* 컬럼 제목 */} + <TableCell className="font-bold bg-blue-100 text-xs w-fit whitespace-nowrap"> + {head.title} + </TableCell> + {/* 데이터 값 또는 입력 필드 */} + <TableCell className="text-xs"> + {children && children[head.id] ? children[head.id] : isEditable2 ? ( + head.type === "select" ? ( + <select + id={head.id} + value={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" + > + {head.options?.map((option) => ( + <option key={option} value={option}> + {option} + </option> + ))} + </select> + ) : head.type === "input" ? ( + <input + id={head.id} + type="text" + value={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" + /> + ) : ( + <span>{formData[head.id] ?? "-"}</span> + ) + ) : ( + <span>{formData[head.id] ?? "-"}</span> + )} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + )} {total > 0 && ( <UiPageNations total={total} diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx index 3974b2407..308f5e350 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -1,13 +1,13 @@ 'use client' -import { getData } from '@/components/lib/api' +import { getData, deleteData } from '@/components/lib/api' import ContentLayout from '@/components/ui/contentLayout' import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm' -import { Form, SearchFormRight, SelectBox } from '@common/business_components/ui' -import { SidoSigunguSelectBox } from '../components/ui/SidoSigunguSelectBox' -import { SelectBox2 } from '@common/business_components/ui/select/index-2' +import { DatePicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui' import ListDiv from '@common/business_components/ui/list_div' +import { SelectBox2 } from '@common/business_components/ui/select/index-2' import { Button, Label } from '@common/components/ui' import { useEffect, useState, type ReactNode } from 'react' +import { handleDownload } from '@/components/lib/utils/fileDownload' type TitleName = { mainTitle: string @@ -17,7 +17,8 @@ type TitleName = { type SelectBox = { labelNm?: Label searchChk?: boolean - title?: string // SearchDiv의 제목 (선택 사항) + title?: string; + onSearchClick?: () => Promise<void>; } type Label = { first: string @@ -29,7 +30,7 @@ type Label = { interface ApiSelectOption { text: string; val: string; -} +} type BaseLayout = { children?: ReactNode[] titleName: TitleName @@ -38,6 +39,7 @@ type BaseLayout = { graphBtn?: boolean firstDivBtn?: boolean searchBox?: SelectBox + showDatePicker?: boolean; enoCd?: string | number setEnoCd?: (enoCd: string) => void ctpvCd?: string | number @@ -48,6 +50,8 @@ type BaseLayout = { onSwageTreatmentPlantChange?: (value: string | number) => void; selectedSwageProcessingZone?: string | number; onSwageProcessingZoneChange?: (value: string | number) => void; + aplcnYm?: Date + setAplcnYm?: (aplcnYm: Date | undefined) => void; } // TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ? /** @@ -59,23 +63,26 @@ type BaseLayout = { * @param graphBtn : 하단에 그래프 버튼 유무 처리 * @returns */ -const BaseLayout = ({ - children, - titleName, - form, - onSubmitEvent, - graphBtn = false, +const BaseLayout = ({ + children, + titleName, + form, + onSubmitEvent, + graphBtn = false, searchBox, + showDatePicker = false, enoCd, setEnoCd, ctpvCd, setCtpvCd, - sigungu, + sigungu, setSigungu, selectedSwageTreatmentPlant, onSwageTreatmentPlantChange, selectedSwageProcessingZone, onSwageProcessingZoneChange, + aplcnYm, + setAplcnYm }: BaseLayout) => { const defaultOption: ApiSelectOption = { val: '0', text: '선택' }; @@ -83,6 +90,9 @@ const BaseLayout = ({ const [processingZoneOptions, setProcessingZoneOptions] = useState<ApiSelectOption[]>([]); const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState<ApiSelectOption[]>([]); const [isLoadingAreaData, setIsLoadingAreaData] = useState<boolean>(false); + const [atflId, setAtflId] = useState('') + const [fileNm, setFileNm] = useState('') + const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('') useEffect(() => { const fetchData = async () => { @@ -159,66 +169,101 @@ const BaseLayout = ({ fetchData(); }, [sigungu]); // sigungu 상태가 변경될 때마다 실행 - return ( - <ContentLayout title={titleName.mainTitle}> - <ListDiv> - {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} - {searchBox?.title && ( - <div className="card-header"> - <h3 className="text-2xl font-medium">{searchBox.title}</h3> - <div> - {graphBtn ? <Button className="!mr-[1.2rem]">다운로드</Button> : null} - {/* 그래프 있을 시 */} - {graphBtn ? <Button className="!mr-[1.2rem]">삭제</Button> : null} - </div> - </div> - )} - <div className="flex items-center w-full"> - <GovSidoSigunguSearchForm - isShowPart={true} - setRtnEnvPartValue={(val) => setEnoCd?.(val)} - setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)} - setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달 - > - <SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} /> - <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> - </GovSidoSigunguSearchForm> - {searchBox?.searchChk ? <SearchFormRight><Button>조회</Button></SearchFormRight> : null} - - </div> - </ListDiv> - <ListDiv> - <div className="card-header"> - <h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3> - <div> - {graphBtn ? <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'>다운로드</Button> : null} - {/* 그래프 있을 시 */} - {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'>삭제</Button> : null} - </div> - </div> - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmitEvent)}> - <div className="card !m-[1.2rem]"> - {/* 폼 정보 입력 칸 */} - {children?.[0]} - </div> - </form> - </Form> - </ListDiv> - { - children?.[1] != null ? <ListDiv> - <div className="card-header flex items-center justify-between"> - <h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3> - <div> - {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'>다운로드</Button> : null} - {/* 그래프 있을 시 */} - {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'>삭제</Button> : null} - </div> - </div> - <div className="card !m-[1.2rem]">{children?.[1]}</div> - </ListDiv> : null + // 파일 다운로드 + const isChckFile = () => { + if (fileNm === '' || fileNm === undefined) { + alert('파일을 선택해주세요'); + return false + } + handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) + return true + } + // 파일 삭제 + const isDeleteFile = async () => { + if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) { + alert('삭제할 파일을 선택해주세요'); + return false + } + const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) + if (data) { + const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`); + if (delData.status == 200) { + alert('삭제되었습니다.') + searchBox?.onSearchClick?.(); + } else { + alert('삭제에 실패했습니다.') } - </ContentLayout > + } + } + + return ( + <Form {...form} className="min-h-screen flex flex-col"> + <ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden"> + <ListDiv> + {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} + {searchBox?.title && ( + <div className="card-header"> + <h3 className="text-2xl font-medium">{searchBox.title}</h3> + </div> + )} + <div className="flex items-center w-full"> + {showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링 + <> + <Label className="ml-4 w-20 text-xl text-gray-600 whitespace-nowrap">분석기간</Label> + <DatePicker + mode={"single"} + field={{ + value: aplcnYm ?? new Date(), + onChange: setAplcnYm ?? ((date: Date | undefined) => console.log("DatePicker onChange:", date)) + }} + selectClass="ml-5 date-picker-trigger" /> + </> + )} + <GovSidoSigunguSearchForm + isShowPart={true} + setRtnEnvPartValue={(val) => setEnoCd?.(val)} + setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)} + setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달 + > + <SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} /> + <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> + </GovSidoSigunguSearchForm> + {searchBox?.searchChk ? <SearchFormRight><Button onClick={searchBox.onSearchClick}>조회</Button></SearchFormRight> : null} + </div> + </ListDiv> + <ListDiv> + <div className="card-header"> + <h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3> + <div> + {graphBtn ? <Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" onClick={isChckFile}>파일 다운로드</Button> : null} + {/* 그래프 있을 시 */} + {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]" onClick={isDeleteFile}>삭제</Button> : null} + </div> + </div> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmitEvent)} className="flex-grow flex flex-col"> + <div className="card !m-[1.2rem] flex-grow flex flex-col"> + {/* 폼 정보 입력 칸 */} + {children?.[0]} + </div> + </form> + </Form> + </ListDiv> + { + children?.[1] != null ? <ListDiv> + <div className="card-header flex items-center justify-between"> + <h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3> + <div> + {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]">파일 다운로드</Button> : null} + {/* 그래프 있을 시 */} + {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]">삭제</Button> : null} + </div> + </div> + <div className="card !m-[1.2rem]">{children?.[1]}</div> + </ListDiv> : null + } + </ContentLayout > + </Form> ) } diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index be24229a0..2b9085ccd 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -196,7 +196,7 @@ const IrSetting = () => { swageTmplntCd: select4.toString(), // 하수처리장코드 swtrmtZoneCd: select5.toString(), // 하수처리구역코드 aplcnYm: new Date(selectedDate).toISOString().substring(0, 7).replace("-", ""), // 적용연월 - dtrwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) + drtwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) anlsInfo: input2, // 분석정보 atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) atflId: file1?.atflId, // 첨부파일ID (오수관망도) diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index 6e1d1b067..fe1ca129c 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -1,89 +1,181 @@ 'use client' -import React, { useState } from 'react' -import { FormField, SelectBox, UiTable } from '@common/business_components/ui' -import { Button, DatePicker, Input } from '@common/components/ui' -import { z } from 'zod' +import { getData } from '@/components/lib/api' +import { UiTable } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' +import { useState } from 'react' import { useForm } from 'react-hook-form' -import FormSection from '@/components/ui/formSection' +import { z } from 'zod' import BaseLayout from '../BaseLayout' -import { title } from 'process' + + +interface ApiSelectOption { + text: string; + val: string; +} + const formSchema = z.object({ - managenm: z.string(), - region: z.string(), - suyeonga: z.number(), - inputdate: z.coerce.date(), - rtkSource: z.string(), - suyeongaPlace: z.string() + enoCd: z.string().default('0'), // 환경청코드 + ctpvCd: z.string().default('0'), // 시도코드 + sggCd: z.object({ // 시군구 + val: z.string().default('0'), + text: z.string().default('전체'), + }).default({ val: '0', text: '전체' }), + swageTmplntCd: z.string().default('0'), // 하수처리장 + swtrmtZoneCd: z.string().default('0'), // 하수처리구역 + aplcnYm: z.date().default(new Date()), // 분석일자 (DatePicker와 연동) }) + +type ResultFormValues = z.infer<typeof formSchema> + // TODO: 분석구분에 따라서 테이블 컬럼 변경필요 const tableColumn = [ - { id: 'no', title: '번호' }, - { id: 'hasu', title: '하수처리장' }, - { id: 'guyeok', title: '처리구역' }, - { id: 'sangsu', title: '상수 사용량' }, - { id: 'rate', title: '오수 전환율' }, - { id: 'water', title: '순오수량' }, - { id: 'measureWater', title: '측정하수량' }, - { id: 'ii', title: '침입수량' }, - { id: 'date', title: '분석일자' }, - { id: 'usr', title: '분석자' } + // { id: 'no', title: '번호' }, + { id: 'inftwtAnlsSn', title: '분석번호' }, + { id: 'swageTmplntNm', title: '하수처리장' }, + { id: 'swtrmtZoneNm', title: '처리구역' }, + { id: 'aplcnYm', title: '적용연월' }, + { id: 'drtwSwtcRt', title: '오수 전환율' }, + { id: 'anlsInfo', title: '침입수 분석정보' }, + { + id: 'regDt', + title: '분석일자', + }, + { + id: 'rgtrNm', + title: '분석자', + } ] const tableColumn2 = [ { id: 'no', title: '번호' }, + { id: 'analysis no', title: '분석번호' }, { id: 'hasu', title: '하수처리장' }, { id: 'guyeok', title: '처리구역' }, { id: 'sangsu', title: '상수 사용량' }, { id: 'rate', title: '오수 전환율' }, - { id: 'rtx', title: 'RTX계수출처' }, + { id: 'rtx', title: 'RTX 계수 출처' }, { id: 'rdii', title: 'RDII 기여면적' }, - { id: 'water', title: '순오수량' }, - { id: 'measureWater', title: '강우유출량' }, - { id: 'ii', title: 'RDII(m2/d)' }, + { id: 'rdii analysis info', title: 'RDII 분석정보' }, { id: 'date', title: '분석일자' }, { id: 'usr', title: '분석자' } ] -const tableInfoData = [{ 'no': '1', 'hasu': '양평하수처리장', 'guyeok': '양평', 'sangsu': '1000', 'rate': '0.5', 'water': '500', 'measureWater': '1000', 'ii': '500', 'date': '2023-10-01', 'usr': '홍길동' }] + const Result = () => { - const form = useForm<z.infer<typeof formSchema>>({ + const form = useForm<ResultFormValues>({ resolver: zodResolver(formSchema), defaultValues: { - managenm: "", - region: "", - suyeonga: 0, - inputdate: new Date(), - rtkSource: "", - suyeongaPlace: "" + enoCd: '0', + ctpvCd: '0', + sggCd: { val: '0', text: '전체' }, + swageTmplntCd: '0', + swtrmtZoneCd: '0', + aplcnYm: new Date(), } }) + + + const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 + const [totalData, setTotalData] = useState(0) //총 데이터 수 + const [rdiiResultData, setRdiiResultData] = useState<any[]>([]); // RDII 결과 저장 상태 (필요시) + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + + const handleSearchButtonClick = async () => { + setIsLoadingResult(true); + const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기 + + const inftwtAnlsSearchDto = { + "aplcnYm": formValues.aplcnYm.toISOString().substring(0, 7).replace("-", ""), + "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, + "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, + "sggCd": formValues.sggCd.val === '0' ? null : formValues.sggCd.val, + // "swageTmplntCd": formValues.swageTmplntCd === '0' ? null : formValues.swageTmplntCd, + "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, + }; + + const queryParams = new URLSearchParams({ + page: '0', + size: '10', + sort: 'inftwtAnlsSn' + }); + Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 + queryParams.append(key, String(value)); + } + }); + const queryString = queryParams.toString(); + + console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); // 디버깅용 로그 + try { + const data = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); + setTableData(data.content || []); + setTotalData(tableData.length); + console.log("response:", data); + console.log("response data:", data.content || []); + } catch (error) { + console.error("Error fetching analysis result data:", error); + alert("분석결과 조회 중 오류가 발생했습니다."); + setTableData([]); + setTotalData(0); + } finally { + setIsLoadingResult(false); + } + }; + return ( - <BaseLayout searchBox={{labelNm: {first: "분석기간",second: "분석구분",third: "하수처리장",fourth: "하수처리구역",},searchChk: true,}} titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle:'RDII 산정결과 현황'}} form={form} onSubmitEvent={() => { }} graphBtn={true}> - {[<div key='table1'> + <BaseLayout + searchBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} + titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle: 'RDII 산정결과 현황' }} + form={form} // form 인스턴스 전달 + onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit + graphBtn={true} + showDatePicker={true} + enoCd={form.watch("enoCd")} + setEnoCd={(val) => form.setValue("enoCd", val)} + ctpvCd={form.watch("ctpvCd")} + setCtpvCd={(val) => form.setValue("ctpvCd", val)} + sigungu={form.watch("sggCd")} + setSigungu={(val) => form.setValue("sggCd", val)} + selectedSwageTreatmentPlant={form.watch("swageTmplntCd")} + onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} + selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} + onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} + aplcnYm={form.watch("aplcnYm")} + setAplcnYm={(val) => { + if (val instanceof Date) { + form.setValue("aplcnYm", val) + } else if (val) { + form.setValue("aplcnYm", new Date(val)) + } + }} + // DatePicker는 BaseLayout 내부에서 form.watch("aplcnYm")과 form.setValue("aplcnYm", ...)을 사용하도록 수정 필요 + > + {[<div key='table1' className="h-[55vh] flex flex-col p-4 border rounded-lg bg-white shadow-md"> <div className="mb-[1.2rem] text-xl font-medium">○ 침입수 산정결과 현황</div> - <UiTable - headName='' - headlist={tableColumn} - tableData={tableInfoData} - total={0} - pageSize={10} - children={undefined} - cellClick={(e) => { }} - onPageChange={(e) => { }} - /> + <div className="flex-grow overflow-auto"> + <UiTable + headName='' + headlist={tableColumn} + tableData={tableData} + total={totalData} + pageSize={10} + children={undefined} + cellClick={(e) => { }} + onPageChange={(e) => { }} + /> + </div> </div>, - <div key='table2'> - <UiTable - headName='' - headlist={tableColumn2} - tableData={[]} - total={0} - pageSize={10} - children={undefined} - cellClick={(e) => { }} - onPageChange={(e) => { }} - /> - </div> - ]} + // <div key='table2'> + // <UiTable + // headName='' + // headlist={tableColumn2} + // tableData={rdiiResultData} + // total={rdiiResultData.length} + // pageSize={10} + // children={undefined} + // cellClick={(e) => { }} + // onPageChange={(e) => { }} + // /> + // </div> + ]} </BaseLayout> ) } diff --git a/apps/sn/app/map/components/common/index.ts b/apps/sn/app/map/components/common/index.ts index c3e1a4a37..b72ec26d5 100644 --- a/apps/sn/app/map/components/common/index.ts +++ b/apps/sn/app/map/components/common/index.ts @@ -5,7 +5,11 @@ import Ssc from "../popupComponent/Ssc"; import StormWaterPump from "../popupComponent/stormWaterPump"; import SwtrmtFclt from "../popupComponent/SwtrmtFclt"; -type LayerComponentProps = { featureData: any; chkMainSsc?: boolean }; +type LayerComponentProps = { + featureData: any; + chkMainSsc?: boolean; + additionalData?: any; +}; type LayerContentMap = { [key: string]: { component?: React.FC<LayerComponentProps>; @@ -16,9 +20,7 @@ type LayerContentMap = { // 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업 const layerContentMap: LayerContentMap = { // TODO: gis데이터 추가되면 추가 작업 필요 - "하수처리시설": { - component: SwtrmtFclt, - }, + "하수처리시설": { component: SwtrmtFclt, }, "하수처리구역": { }, "우수토실":{component: Ssc,}, "중점우수토실":{component: Ssc, }, // 중점우수토실 여부 설정 diff --git a/apps/sn/app/map/components/common/table.ts b/apps/sn/app/map/components/common/table.ts index 148f5ef0f..6c641f9be 100644 --- a/apps/sn/app/map/components/common/table.ts +++ b/apps/sn/app/map/components/common/table.ts @@ -48,7 +48,7 @@ export const SwtrmtPrcsZoneCol = [ // { id: 'sggCd', title: '시군구코드' }, { id: 'sggNm', title: '시군구명' }, { id: 'swtrmtFcltNm', title: '하수처리시설명' }, -] +]; // 하수맨홀 export const MsSwageMhlCol = [ @@ -70,4 +70,59 @@ export const MsSwageMhlCol = [ // { id: 'mqltCd', title: '재질코드' }, { id: 'altdVl', title: '표고값' }, { id: 'dpthlVl', title: '심도값' }, - { id: 'instMngNo', title:'기관관리번호'}] + { id: 'instMngNo', title:'기관관리번호'} +]; + +// 중점우수토실 측정지점 +export const untrtSwgRainCol1 = [ + { id: 'manage_no', title: '관리번호' }, + { id: 'swr_ppl_eqp_nm', title: '처리구역' }, + { id: 'swr_ppl_trt_nm', title: '처리분구' }, + { id: 'addr', title: '인근주소' }, + { id: 'chmbr_xcnts', title: '토실 위도' }, + { id: 'chmbr_ydnts', title: '토실 경도' } +]; + +// 중점우수토실 측정지점 시설현황 +export const untrtSwgRainCol2 = [ + { id: 'flwmtr_mesure_tp_etc', title: '구분' }, + { id: 'discharge_pipe_spec', title: '방류관로 규격' }, + { id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' }, + { id: 'swr_trt_popl', title: '하수처리 인구(인)' }, + { id: 'screen_instl_at', title: '스크린설치 여부' }, + { id: 'weir_hg', title: '위어 높이(m)' }, + { id: 'weir_lt', title: '위어 길이(m)' } +]; + +// 중점우수토실 강우정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol3 = [ + { id: 'rain_begin_dt', title: '강우시작일시' }, + { id: 'rain_end_dt', title: '강우종료일시' }, + { id: 'precdng_dry', title: '선행건기(h)' }, + { id: 'rain_info', title: '강우정보' }, + { id: 'rain_fall', title: '누적강우량(mm)' }, + { id: 'cntnc_time', title: '지속시간(h)' }, + { id: 'rain_in', title: '강우강도(mm/h)' } +]; + +// 중점우수토실 측정정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol4 = [ + { id: 'mesure_begin_dt', title: '측정시작일시' }, + { id: 'mesure_end_dt', title: '측정종료일시' }, + { id: 'mesure_intrvl', title: '측정간격(분)' }, + { id: 'mesure_mhrls_instl_lc', title: '측정기기 설치위치' }, + { id: 'flwmtr_combined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼 + { id: 'qltwtr_begin_dt', title: '채수시작일시' }, + { id: 'qltwtr_end_dt', title: '채수종료일시' }, + { id: 'qltwtr_co', title: '채수횟수' } +]; + +// 중점우수토실 수량/수질측정 정보 +export const untrtSwgRainCol5 = [ + { id: 'odr', title: '회차' }, + { id: 'mesureDt', title: '측정일자' }, + { id: 'mesureQy', title: '수량(m)' }, + { id: 'mesureBod', title: 'BOD(mg/L)' }, + { id: 'mesureToc', title: 'TOC(mg/L)' }, + { id: 'mesureLoad', title: '오염부하량(kg)' } +]; \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/map/components/popupComponent/Ssc.tsx index 06b975739..aa20261ab 100644 --- a/apps/sn/app/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/map/components/popupComponent/Ssc.tsx @@ -1,18 +1,16 @@ // 우수토실 -import Modal from '@/components/ui/modal'; -import { VerticalTable } from '@/components/ui/verticalTable'; -import { UiTable } from '@common/business_components/ui'; +import { UiModal, UiTable } from '@common/business_components/ui'; import { Button, TableHead, TableRow } from '@common/components/ui'; -import React from 'react' -const inputColumn = [ - { id: "swtrmt_zone_cd", title: "처리구역", type: "input" }, - { id: "mainSscYn", title: "중점우수토실 대상여부", type: "select", options: ['Y', 'N'] }, -]; -const headlist = [ - { id: 'startrain', title: '강우시작일시' }, { id: 'endrain', title: '강우종료일시' }, { id: '선행건기', title: '선행건기(h)' }, { id: '강우정보', title: '강우정보' }, { id: '누적강우량', title: '누적강우량(mm)' }, { id: '지속시간', title: '지속시간(h)' }, { id: '강우강도', title: '강우강도(mm/h)' }, -] -const headlist3 = [{ id: 'nm', title: '회차' }, { id: 'date', title: '측정일자' }, { id: 'measure', title: '수량(m)' }, { id: 'bod', title: 'BOD(mg/L)' }, { id: 'toc', title: 'TOC(mg/L)' }, { id: '오염부하량', title: '오염부하량(kg)' }] -const Ssc = ({ featureData, chkMainSsc }: { featureData: any, chkMainSsc?: boolean}) => { +import React from 'react'; +import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; + +const Ssc = ({ featureData, chkMainSsc, additionalData }: { + featureData: any, + chkMainSsc?: boolean, + additionalData?: any +}) => { + console.log('Additional data received:', additionalData); // 추가 데이터 확인용 + const [isModalOpen, setIsModalOpen] = React.useState(false); const openModal = () => { setIsModalOpen(true); @@ -20,45 +18,120 @@ const Ssc = ({ featureData, chkMainSsc }: { featureData: any, chkMainSsc?: boole const closeModal = () => { setIsModalOpen(false); }; + + // 데이터 전처리: 합쳐진 필드 추가 + const preprocessedData = featureData ? [{ + ...featureData, + flwmtr_combined: `${featureData.flwmtr_mesure_tp_etc || '-'} / ${featureData.flwmtr_knd || '-'}` + }] : []; + + const renderModalBody = () => ( + <div className='grid grid-cols-1 gap-4 p-4'> + {/* 강우정보 테이블 */} + <UiTable + headName='강우정보' + tableData={preprocessedData} + headlist={untrtSwgRainCol3} + pageSize={10} + total={preprocessedData.length} + children={undefined} + cellClick={undefined} + onPageChange={function (page: number): void { + console.log('Rain data page change:', page); + }} + /> + + {/* 측정정보 테이블 */} + <UiTable + headName='측정정보' + tableData={preprocessedData} + headlist={untrtSwgRainCol4} + pageSize={10} + total={preprocessedData.length} + cellClick={undefined} + onPageChange={function (page: number): void { + console.log('Measure data page change:', page); + }} + > + {/* 커스텀 헤더 */} + <TableRow> + <TableHead colSpan={5} className="text-center bg-blue-100 font-bold">수량</TableHead> + <TableHead colSpan={3} className="text-center bg-blue-100 font-bold">수질</TableHead> + </TableRow> + <TableRow> + <TableHead className="text-center">측정시작일시</TableHead> + <TableHead className="text-center">측정종료일시</TableHead> + <TableHead className="text-center">측정간격(분)</TableHead> + <TableHead className="text-center">측정기기설치위치</TableHead> + <TableHead className="text-center">유량계 종류 및 모델명</TableHead> + <TableHead className="text-center">채수시작일시</TableHead> + <TableHead className="text-center">채수종료일시</TableHead> + <TableHead className="text-center">채수횟수</TableHead> + </TableRow> + </UiTable> + + {/* 수량/수질측정 정보 테이블 */} + <UiTable + headName='수량/수질측정 정보' + tableData={additionalData} + headlist={untrtSwgRainCol5} + pageSize={10} + total={additionalData.length} + children={undefined} + cellClick={undefined} + onPageChange={function (page: number): void { + console.log('Quality data page change:', page); + }} + /> + </div> + ); + 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.'); - }} />{!chkMainSsc ? null : - <div> - <div className="card-header"> - <h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3> + <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.'); + }} + /> */} + + {!chkMainSsc ? null : + <div> + <div className="card-header"> + <h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3> + </div> + <Button + className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600' + onClick={openModal} + > + 상세보기 + </Button> + + <UiModal + open={isModalOpen} + onOpenChange={setIsModalOpen} + bodyClass={` + h-[100vh] + w-[60vw] + max-w-none + max-h-none + overflow-visible + z-[101] + `} + body={renderModalBody()} + noHeaderYn='Y' + noFooterYn='N' + // footerContent={renderFooterContent()} + /> </div> - <Button className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600' onClick={openModal}>상세보기</Button> - <Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}> - <div className='grid grid-col-3'><UiTable headName='강우정보' tableData={[]} headlist={headlist} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }}></UiTable> - <UiTable headName='측정정보' tableData={[]} headlist={[]} pageSize={0} total={0} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }}> {/* 커스텀 헤더 */} - <TableRow> - <TableHead colSpan={5} className="text-center bg-blue-100 font-bold"> - 수량 - </TableHead> - <TableHead colSpan={3} className="text-center bg-blue-100 font-bold"> - 수질 - </TableHead> - </TableRow> - <TableRow> - <TableHead className="text-center">측정시작일시</TableHead> - <TableHead className="text-center">측정종료일시</TableHead> - <TableHead className="text-center">측정간격(분)</TableHead> - <TableHead className="text-center">측정기기설치위치</TableHead> - <TableHead className="text-center">유량계 종류 및 모델명</TableHead> - <TableHead className="text-center">채수시작일시</TableHead> - <TableHead className="text-center">채수종료일시</TableHead> - <TableHead className="text-center">채수횟수</TableHead> - </TableRow></UiTable> - <UiTable headName='수량/수질측정 정보' tableData={[]} headlist={headlist3} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }}></UiTable></div> - </Modal> - </div>} + } </div> ) } diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/map/featureClickEventPopup.tsx index 2f70adbc3..9b431f23c 100644 --- a/apps/sn/app/map/featureClickEventPopup.tsx +++ b/apps/sn/app/map/featureClickEventPopup.tsx @@ -1,12 +1,12 @@ 'use client' -import { VerticalTable } from "@/components/ui/verticalTable"; -import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui"; -import { useEffect, useRef, useState } from "react"; -import { SwtrmtFcltCol, SwtrmtPrcsZoneCol } from "./components/common/table"; -import { useGisContext } from "@common/business_components/ui/map/Gis"; import { getData, putData } from "@/components/lib/api"; -import { getLayerContentComponent } from "./components/common"; 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 } from "react"; +import { getLayerContentComponent } from "./components/common"; +import { SwtrmtFcltCol, SwtrmtPrcsZoneCol, untrtSwgRainCol1, untrtSwgRainCol2 } from "./components/common/table"; // TODO: const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; @@ -15,13 +15,16 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, ? featureData.getId() : featureData.fid; const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태 - const [tableData, setTableData] = useState<any>(null); // 피처 데이터 상태 + const [tableData, setTableData] = useState<any>(null); // 피처 데이터 + const [mntrngsData, setMntrngsData] = useState(false); // 수량/수질 측정 정보 const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 const [buttonChk, setButtonChk] = useState(true) const [fetchData, setfetchData] = useState<any>(); // api 호출여부 const contentRef = useRef<HTMLDivElement>(null); const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태 const [chkMainSsc, setChkMainSsc] = useState(false); // 중점우수토실 여부 + // 피쳐데이터의 현재 레이어 이름 가져오는 함수 const getSelectedLayers = () => { let result = null; @@ -38,19 +41,21 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, if (featureLayer.get('lyrNm') === null) { if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설'; else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역'; + else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실'; } else result = featureLayer.get('lyrNm'); - + return result; // return featureLayer.get('lyrNm'); // 레이어 이름 출력 }; + useEffect(() => { const fetchData = async () => { if (featureId) { const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 setLayerNm(selectedLayerName); // 상태 업데이트 const layerId = featureId.split('.')[0] - const data = await getData(`v1/fclts/${featureId}?lyrTypeNm=${layerId}`); + let data = await getData(`v1/fclts/${featureId}?lyrTypeNm=${layerId}`); setfetchData(data) switch (selectedLayerName) { case '하수처리시설': @@ -62,8 +67,20 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, setButtonChk(false) break; case '중점우수토실': + setHeadlist(untrtSwgRainCol1) + setHeadlist2(untrtSwgRainCol2) setButtonChk(false) setChkMainSsc(true) + + // 데이터 전처리: 합쳐진 필드 추가 + data = data ? { + ...data, + discharge_pipe_spec: `직경: ${data.pipe_diameter || '-'} / 높이: ${data.pipe_hg || '-'} / 길이: ${data.pipe_lt || '-'}` + } : {}; + + // 수량/수질측정 정보 데이터 호출 + const additionalResponse = await getData(`v1/onsemiro/swgRain/${data.adst_dstr_cd}/${data.manage_no}/mntrngs`); + setMntrngsData(additionalResponse.content); break; case '우수토실': setButtonChk(true) @@ -88,25 +105,29 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, } fetchData(); }, [featureId]); + // 저장 시 모달 띄우기 const handleSaveClick = () => { setIsModalOpen(true); // 모달 열기 }; + const handleModalSave = () => { setIsModalOpen(false); // 모달 닫기 btnClickEvent(); // 저장 이벤트 실행 }; + const handleModalClose = () => { setIsModalOpen(false); // 모달 닫기 }; - const btnClickEvent = async() => { + + const btnClickEvent = async () => { const bodyData: Record<string, any> = {} if (buttonChk && contentRef.current) { const inputElements = contentRef.current.querySelectorAll('input'); const selectElements = contentRef.current.querySelectorAll('select'); - // 카멜케이스 변환 함수 - const toCamelCase = (str: string) => - str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); + // 카멜케이스 변환 함수 + const toCamelCase = (str: string) => + str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); // select 태그의 값 가져오기 selectElements.forEach((select) => { const camelCaseId = toCamelCase(select.id); @@ -121,19 +142,19 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, else if (layerNm === '우수토실') bodyData['lyrTypeNm'] = 'tb_ms_stofcbr' 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' + else if (layerNm === '하수맨홀') bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl' bodyData['fid'] = featureId const data = await putData(`v1/fclts/${featureId}`, bodyData); - if(data.status==200){ - alert('저장되었습니다') - setTableData(data) - setfetchData(data) + if (data.status == 200) { + alert('저장되었습니다') + setTableData(data) + setfetchData(data) } } } const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null; return ( - <div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[200]"> + <div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[50]"> <div className="card-header bg-primary flex items-center justify-between p-4"> <div className="flex mx-3 flex-col"> <h3 className="text-2xl font-medium text-white text-label">{layerNm} 정보</h3> @@ -148,16 +169,40 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, </button> </div> <div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}> - <VerticalTable headName='LX 연계정보' tableData={[tableData]} headlist={headlist} pageSize={20} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - }}></VerticalTable> + <VerticalTable + headName='LX 연계정보' + tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined} + tableData={[tableData]} + headlist={headlist} + pageSize={20} + total={0} + children={undefined} + cellClick={undefined} + onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }} + // 두 번째 테이블 조건부 props + showSecondTable={layerNm === '중점우수토실' ? true : false} + tableName2={layerNm === '중점우수토실' ? "시설현황" : undefined} + tableData2={layerNm === '중점우수토실' && tableData ? [tableData] : []} + headlist2={layerNm === '중점우수토실' ? headlist2 : undefined} + pageSize2={20} + total2={0} + isEditable2={buttonChk} + /> <div className="w-full mx-5 flex items-center justify-end space-x-2"> {buttonChk ? <Button onClick={handleSaveClick} className="px-4 mr-5 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"> 저장 </Button> : null} </div> <Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal> - {ContentComponent && <ContentComponent featureData={fetchData} chkMainSsc={chkMainSsc} />} + {ContentComponent && + <ContentComponent + featureData={fetchData} + chkMainSsc={chkMainSsc} + additionalData={mntrngsData} + /> + } </div> </div> ) From 94359782917b3b6274e16ff4565342892afed471 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Thu, 5 Jun 2025 17:24:07 +0900 Subject: [PATCH 13/44] =?UTF-8?q?feat:=20=EC=A4=91=EC=A0=90=EC=9A=B0?= =?UTF-8?q?=EC=88=98=ED=86=A0=EC=8B=A4=20'=EA=B0=95=EC=9A=B0=EC=A0=95?= =?UTF-8?q?=EB=B3=B4',=20'=EC=B8=A1=EC=A0=95=EC=A0=95=EB=B3=B4'=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/components/popupComponent/Ssc.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/map/components/popupComponent/Ssc.tsx index aa20261ab..2ca7c174d 100644 --- a/apps/sn/app/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/map/components/popupComponent/Ssc.tsx @@ -33,7 +33,7 @@ const Ssc = ({ featureData, chkMainSsc, additionalData }: { tableData={preprocessedData} headlist={untrtSwgRainCol3} pageSize={10} - total={preprocessedData.length} + total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { @@ -47,7 +47,7 @@ const Ssc = ({ featureData, chkMainSsc, additionalData }: { tableData={preprocessedData} headlist={untrtSwgRainCol4} pageSize={10} - total={preprocessedData.length} + total={0} cellClick={undefined} onPageChange={function (page: number): void { console.log('Measure data page change:', page); From 6ca818293a086857755908e7b2ce60da587788d6 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 5 Jun 2025 18:03:12 +0900 Subject: [PATCH 14/44] =?UTF-8?q?page=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=AA=A8=EB=93=88=20=EC=A0=84=EB=8B=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=A4=91=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/page.tsx | 3 ++- packages/common/business_components/src/ui/map/Gis.js | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx index da20fdaa7..4a0e7c3f1 100644 --- a/apps/sn/app/map/page.tsx +++ b/apps/sn/app/map/page.tsx @@ -3,6 +3,7 @@ import Gis from "@common/business_components/ui/map/Gis"; import { useEffect, useState } from "react"; import { mapService } from "@service"; +import SearchModal from '@/map/SearchModal'; const Map = () => { const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 @@ -24,7 +25,7 @@ const Map = () => { return ( <div className="bg-container"> - <Gis treeData={treeData} isSearch={true} /> + <Gis treeData={treeData} isSearch={true} searchModal={SearchModal}/> </div> ); }; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index c501302f6..bde5858a9 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -11,12 +11,11 @@ import GisApp from './lib/gis.core'; import GisContent from './GisContent'; import FeatureClickEventPopup from '@/map/featureClickEventPopup'; import GisTree from './GisTree'; -import SearchModal from '@/map/SearchModal'; export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); export const useGisContext = () => useContext(GisContext); -function Gis({ treeData = [], isSearch = false }) { +function Gis({ treeData = [], isSearch = false, searchModal : SearchModal }) { const mapRef = useRef(); const [mapObject, setMapObject] = useState({}); const [tabVisible, setTabVisible] = useState(true); @@ -24,7 +23,7 @@ function Gis({ treeData = [], isSearch = false }) { const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터 const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); - const mapInstanceRef = useRef(null); // 맵 생성 여부를 추적할 ref + const mapInstanceRef = useRef(null); const checkedLayerIds = useRef([]); let gisMap = null; const closePopup = () => { From 213ac7b8cf77281b555aaf6a35ce635b9dcdfa69 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 5 Jun 2025 20:15:59 +0900 Subject: [PATCH 15/44] =?UTF-8?q?gis=20=EA=B3=B5=ED=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/page.tsx | 114 ++++++++ .../business_components/src/ui/map/Gis.js | 246 ++++++++++++++++++ .../src/ui/map/GisContent.js | 79 ++++++ 3 files changed, 439 insertions(+) create mode 100644 apps/sn/app/map/page.tsx create mode 100644 packages/common/business_components/src/ui/map/Gis.js create mode 100644 packages/common/business_components/src/ui/map/GisContent.js diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx new file mode 100644 index 000000000..7185c9660 --- /dev/null +++ b/apps/sn/app/map/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import Gis, { useGisContext } from "@common/business_components/ui/map/Gis"; +import { mapService } from "@service"; +import { useCallback, useEffect, useRef, useState } from "react"; +import GisApp from "@common/business_components/ui/map/lib/gis.core"; +import SearchModal from "./SearchModal"; +import FeatureClickEventPopup from "./featureClickEventPopup"; + +const Map = () => { + const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 + const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 + const gisMaps = useRef(null); + const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 + const fetchData = async () => { + try { + const response = await mapService.getLyrStngs(); + setTreeData(response.data); // API 응답 저장 + } catch (error) { + console.error("데이터 불러오기 실패:", error); + } + }; + + // 컴포넌트 마운트 시 1회 실행 + useEffect(() => { + fetchData(); + }, []); + + // SearchModal, GisContent 등에서 호출 + const onFeatureSelect = (data: any) => { + let map = gisMaps.current; + if (!map) { + console.warn("지도가 초기화되지 않았습니다."); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select as any; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature = null; + if ( + data && + typeof data.getGeometry === "function" && + typeof data.setStyle === "function" + ) { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + map + .getLayers() + .getArray() + .forEach((layer: any) => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); + + map.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn("OpenLayers Feature를 찾을 수 없습니다:", data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }; + return ( + <div className="bg-container"> + <Gis + treeData={treeData} + isSearch={true} + gisMaps={gisMaps} + isMiniMap={true} + searchView={SearchModal} + detailView={FeatureClickEventPopup} + onFeatureSelect={onFeatureSelect} + setIsOpen={setIsOpen} + setFeatureData={setFeatureData} + featureData={featureData} + setIsSearchModalOpen={setIsSearchModalOpen} + isOpen={isOpen} + isSearchModalOpen={isSearchModalOpen} + /> + </div> + ); +}; + +export default Map; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js new file mode 100644 index 000000000..4fd9a8eb4 --- /dev/null +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -0,0 +1,246 @@ +import './lib/css/gis.common.css'; +import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import OlMap from 'ol/Map'; +import View from 'ol/View'; +import { defaults as defaultControls, OverviewMap } from 'ol/control'; +import { defaults } from 'ol/interaction'; +import * as olProj from 'ol/proj'; +import proj4 from 'proj4'; +import { register } from 'ol/proj/proj4'; +import GisApp from './lib/gis.core'; +import GisContent from './GisContent'; +import GisTree from './GisTree'; +import GisLonLat from './GisLonLat'; +import GisSelect from './GisSelect'; +import GisToolBar from './GisToolBar'; +import GisMapToggle from './GisMapToggle'; +import { Search } from 'lucide-react'; + +export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null }); +export const useGisContext = () => useContext(GisContext); + +function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView : SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) { + const mapRef = useRef(); + const [mapObject, setMapObject] = useState({}); + const [tabVisible, setTabVisible] = useState(true); + // const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 + // const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터 + // const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 + const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); + const mapInstanceRef = useRef(null); + const checkedLayerIds = useRef([]); + let gisMap = null; + const closePopup = () => { + setIsOpen(false); // 팝업 닫기 + setFeatureData(null) + }; + + // SearchModal, GisContent 등에서 호출 + const handleFeatureSelect = useCallback((data) => { + gisMap = mapInstanceRef.current; + + if (!gisMap) { + console.warn('지도가 초기화되지 않았습니다.'); + return; + } + + // 항상 같은 select 객체 사용 + const select = GisApp.Module.select; + if (!select) return; + + // 1. 기존 선택 초기화 + select.resetSelectedFeature(); + + // 2. data가 OpenLayers Feature 인스턴스인지 확인 + let foundFeature = null; + if (data && typeof data.getGeometry === 'function' && typeof data.setStyle === 'function') { + // 이미 Feature 인스턴스라면 그대로 사용 + foundFeature = data; + } else { + // 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용 + const featureId = data.fid || data.ftrIdn || data.id; + gisMap.getLayers().getArray().forEach(layer => { + const source = layer.getSource?.(); + if (source?.getFeatureById) { + const feature = source.getFeatureById(String(featureId).trim()); + if (feature) foundFeature = feature; + } + }); + } + + // 3. 피처 선택 + if (foundFeature) { + let geometry = foundFeature.getGeometry(); + if (geometry) { + let extent = geometry.getExtent(); + + gisMap.getView().fit(extent, { + duration: 300, + padding: [50, 50, 50, 50], + maxZoom: 16, + constrainResolution: true, + nearest: true, + }); + } + select.selectFeature(foundFeature, true); + } else { + console.warn('OpenLayers Feature를 찾을 수 없습니다:', data); + } + + setFeatureData(data); + setIsOpen(true); + setIsSearchModalOpen(false); + }, [mapInstanceRef]); + + useEffect(() => { + if (!mapRef.current) return; + if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 + + // EPSG:5179 등록 + proj4.defs([ + [ + 'EPSG:5179', + '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + ], + ]); + register(proj4); + + //맵 생성 + const map = new OlMap({ + layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], + renderer: 'webgl', + target: mapRef.current, + interactions: defaults({ doubleClickZoom: false }), + controls: defaultControls({ + zoom: false, + rotate: false, + attributionOptions: { + collapsible: false, + }, + }).extend([]), + view: new View({ + projection: GisApp.TO_PROJ, + zoom: gisCore.defaults.zoom, + center: olProj.transform( + [126.6757, 37.5585], + GisApp.FROM_PROJ, + GisApp.TO_PROJ + ), + minZoom: gisCore.defaults.minZoom, + maxZoom: gisCore.defaults.maxZoom, + }), + }); + + gisMaps.current = map + + //미니맵 + if(isMiniMap){ + const overviewMap = new OverviewMap({ + layers: [GisApp.Module.core.baseMiniMapLayers['base']], + view: new View({ + projection: GisApp.TO_PROJ, + minZoom: 9, + }), + collapsed: false, + collapseLabel: '\u00BB', + label: '\u00AB', + }); + map.addControl(overviewMap); + } + + gisCore.__proto__.map = map; + gisCore.scaleControl(); + gisCore.changeBaseLayer('base'); + setMapObject({ map: map }); + mapInstanceRef.current = map; // 생성 완료 플래그 설정 + }, []); + + useEffect(() => { + if (!treeData || treeData.length === 0) return; + + gisCore.deleteAllOverlay(); + gisCore.deleteAllLayer(); + + const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn'); + GisApp.layerCode = layerCode; + + //setTreeData(data); + const layerItems = getLayerItems(layerCode); + //setEditLayerItems(prev => [...layerItems]) + }, [treeData]); + + const getLayerItems = (layerCode) => { + const layerItems = Object.entries(layerCode) + .filter(([key, obj], idx) => obj?.updtPsblYn === 'Y') + .map(([key, obj]) => ({ + ...obj, + name: obj.lyrNm, + label: obj.lyrNm, + value: obj.lyrSn + '', + })) + .sort((a, b) => { + if (a.sortOrdr > b.sortOrdr) return 1; + if (a.sortOrdr < b.sortOrdr) return -1; + return 0; + }); + return layerItems; + }; + + if (mapObject.map) { + try { + gisMap = mapObject.map; + } catch (e) { + console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력 + } + } + + return ( + <> + <GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}> + <div className="h-[calc(100vh-10rem)] flex relative"> + <div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} /> + <GisTree visible={tabVisible} setVisible={setTabVisible} /> + + {/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */} + + {/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */} + {isOpen && featureData && !isSearchModalOpen && ( + <DetailView + featureData={featureData} + onClose={closePopup} // 닫기 콜백 전달 + /> + )} + <SearchView + open={isSearchModalOpen} + onOpenChange={setIsSearchModalOpen} + onFeatureSelect={onFeatureSelect} + /> + <GisLonLat/> + <GisToolBar/> + <GisMapToggle/> + <GisSelect onFeatureSelect={onFeatureSelect} /> + </div> + + {isSearch && ( + <div className="absolute right-8 top-[305px] z-20"> + <button + type="button" + onClick={() => setIsSearchModalOpen(true)} + className="overflow-hidden w-[40px] h-[40px] bg-white + border border-[#9aa7b8] + rounded + shadow-[0_0_5px_#999] + flex items-center justify-center + p-0 + " + > + <Search size={24} className="text-gray-700" /> + </button> + </div> + )} + </GisContext.Provider> + </> + ); +} + +export default React.memo(Gis); \ No newline at end of file diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js new file mode 100644 index 000000000..136f394ad --- /dev/null +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -0,0 +1,79 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import GisApp from './lib/gis.core'; +import { useGisContext } from './Gis'; +import GisLonLat from './GisLonLat'; +import GisSelect from './GisSelect'; +import GisToolBar from './GisToolBar'; +import './lib/css/gis.edit.css'; +import GisMapToggle from './GisMapToggle'; +import { Search } from 'lucide-react'; + +export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) { + const { gisMap, gisCore } = useGisContext(); + const [visibleComponents, setVisibleComponents] = useState({}); + const addressResultLayer = useRef(null); + const [playShow, setPlayShow] = useState({ show: false }); + + //셀렉트 버튼 이벤트 + // const clickFeature = useCallback(feature => { + // if (feature) { + // const layer = feature.getLayer(); + // setVisibleComponents(prevState => ({ + // ...prevState, + // })); + // } + // }, []); + + const selectRslt = useCallback(feature => { + GisApp.Module.layer.highLightFeature( + [feature], + { + id: GisApp.hlLayerId, + zIndex: 10, + opacity: 1, + selectable: true, + }, + { width: 30 } + ); + clickFeature(feature); + }, []); + + useEffect(() => { + if (gisMap) { + setVisibleComponents(prevState => ({ + ...prevState, + })); + } + }, [gisMap]); + + if (!gisMap) return null; + + return ( + <> + {/*<GisTree />*/} + {/* <GisMapToggle /> + <GisLonLat /> + <GisToolBar /> */} + {/* <GisSelect onFeatureSelect={onFeatureSelect} /> */} + + {/* 검색 모달 */} + {isSearch && ( + <div className="absolute right-8 top-[254px] z-20"> + <button + type="button" + onClick={() => setIsSearchModalOpen(true)} + className="overflow-hidden w-[40px] h-[40px] bg-white + border border-[#9aa7b8] + rounded + shadow-[0_0_5px_#999] + flex items-center justify-center + p-0 + " + > + <Search size={24} className="text-gray-700" /> + </button> + </div> + )} + </> + ); +} \ No newline at end of file From 643575c1255a60dfb797b9180948b55348db59d6 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 5 Jun 2025 20:17:52 +0900 Subject: [PATCH 16/44] =?UTF-8?q?=EB=AA=B0=EB=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/layout.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 apps/sn/app/layout.js diff --git a/apps/sn/app/layout.js b/apps/sn/app/layout.js new file mode 100644 index 000000000..8525f5f8c --- /dev/null +++ b/apps/sn/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ children }) { + return ( + <html lang="en"> + <body>{children}</body> + </html> + ) +} From e4bf427fca08a182ab0c21751aec3a91f597a4da Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Mon, 9 Jun 2025 14:17:33 +0900 Subject: [PATCH 17/44] =?UTF-8?q?feat:=20-=20=EC=B9=A8=EC=9E=85=EC=88=98?= =?UTF-8?q?=20=EB=B6=84=EC=84=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9C=A0=EB=9F=89=EC=9E=90=EB=A3=8C=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20-?= =?UTF-8?q?=20BaseLayout.tsx=EC=9D=98=20searchBox=20props=EC=9D=98=20selec?= =?UTF-8?q?tBox=EB=A1=9C=20=EB=B3=80=EC=88=98=EB=AA=85=20=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20Excel=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20props=20=EC=B6=94=EA=B0=80=20-=20=EC=B9=A8?= =?UTF-8?q?=EC=9E=85=EC=88=98=EB=B6=84=EC=84=9D=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C,=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/BaseLayout.tsx | 48 +++++----- apps/sn/app/ii/irsetting/page.tsx | 35 +++++--- apps/sn/app/ii/rdii/page.tsx | 2 +- apps/sn/app/ii/result/page.tsx | 144 ++++++++++++++++++++++++------ 4 files changed, 164 insertions(+), 65 deletions(-) diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx index 308f5e350..18d9f9bf0 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -1,5 +1,5 @@ 'use client' -import { getData, deleteData } from '@/components/lib/api' +import { getData } from '@/components/lib/api' import ContentLayout from '@/components/ui/contentLayout' import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm' import { DatePicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui' @@ -7,7 +7,6 @@ import ListDiv from '@common/business_components/ui/list_div' import { SelectBox2 } from '@common/business_components/ui/select/index-2' import { Button, Label } from '@common/components/ui' import { useEffect, useState, type ReactNode } from 'react' -import { handleDownload } from '@/components/lib/utils/fileDownload' type TitleName = { mainTitle: string @@ -38,7 +37,7 @@ type BaseLayout = { onSubmitEvent: Function graphBtn?: boolean firstDivBtn?: boolean - searchBox?: SelectBox + selectBox?: SelectBox showDatePicker?: boolean; enoCd?: string | number setEnoCd?: (enoCd: string) => void @@ -52,6 +51,10 @@ type BaseLayout = { onSwageProcessingZoneChange?: (value: string | number) => void; aplcnYm?: Date setAplcnYm?: (aplcnYm: Date | undefined) => void; + // Excel 다운로드 관련 props 추가 + onExcelDownload?: () => void; + onDeleteFile?: () => void; + selectedRowData?: any; } // TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ? /** @@ -69,7 +72,7 @@ const BaseLayout = ({ form, onSubmitEvent, graphBtn = false, - searchBox, + selectBox, showDatePicker = false, enoCd, setEnoCd, @@ -82,7 +85,11 @@ const BaseLayout = ({ selectedSwageProcessingZone, onSwageProcessingZoneChange, aplcnYm, - setAplcnYm + setAplcnYm, + // Excel 다운로드 관련 props 추가 + onExcelDownload, + onDeleteFile, + selectedRowData }: BaseLayout) => { const defaultOption: ApiSelectOption = { val: '0', text: '선택' }; @@ -171,28 +178,19 @@ const BaseLayout = ({ // 파일 다운로드 const isChckFile = () => { - if (fileNm === '' || fileNm === undefined) { - alert('파일을 선택해주세요'); - return false + // Excel 다운로드 함수가 있으면 Excel 다운로드 실행 + if (onExcelDownload) { + onExcelDownload(); + return; } - handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) return true } // 파일 삭제 const isDeleteFile = async () => { - if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) { - alert('삭제할 파일을 선택해주세요'); - return false - } - const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) - if (data) { - const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`); - if (delData.status == 200) { - alert('삭제되었습니다.') - searchBox?.onSearchClick?.(); - } else { - alert('삭제에 실패했습니다.') - } + // Excel 다운로드 함수가 있으면 Excel 다운로드 실행 + if (onDeleteFile) { + onDeleteFile(); + return; } } @@ -201,9 +199,9 @@ const BaseLayout = ({ <ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden"> <ListDiv> {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} - {searchBox?.title && ( + {selectBox?.title && ( <div className="card-header"> - <h3 className="text-2xl font-medium">{searchBox.title}</h3> + <h3 className="text-2xl font-medium">{selectBox.title}</h3> </div> )} <div className="flex items-center w-full"> @@ -228,7 +226,7 @@ const BaseLayout = ({ <SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} /> <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> </GovSidoSigunguSearchForm> - {searchBox?.searchChk ? <SearchFormRight><Button onClick={searchBox.onSearchClick}>조회</Button></SearchFormRight> : null} + {selectBox?.searchChk ? <SearchFormRight><Button onClick={() => selectBox.onSearchClick?.()}>조회</Button></SearchFormRight> : null} </div> </ListDiv> <ListDiv> diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index 2b9085ccd..8cb36c2bf 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -141,18 +141,27 @@ const IrSetting = () => { console.log("유량자료 정보:", file4); // 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인) - if (!file1?.file || !file2?.file || !file3?.file || !file4?.file) { + // 유량자료는 선택값 + if (!file1?.file || !file2?.file || !file3?.file) { alert('모든 분석용 파일을 선택해주세요.'); return; } - const filePickers = [ - // filePicked 유틸리티가 File 객체를 직접 받는지, ArrayBuffer 등을 받는지 확인 필요 - // 여기서는 File 객체를 받고, 내부적으로 ArrayBuffer로 변환하여 parser에 전달한다고 가정 - { file: file2, parser: excelToMatchingResultData }, - { file: file3, parser: excelToTimePatternData }, - { file: file4, parser: excelToFlowRateData } - ]; + type MatchingResult = { anlsSn: number; mhlMngNo: string; rcptrNo: string; }; + type TimePattern = { hrId: number; rt?: number | null | undefined; }; + type FlowRate = { pplMngNo: string; mtrngDt: string; oilq?: number | null | undefined; }; + + const filePickers: { + file: UploadedFileInfo; + parser: (excelData: any) => MatchingResult[] | TimePattern[] | FlowRate[]; + }[] = [ + { file: file2, parser: excelToMatchingResultData }, + { file: file3, parser: excelToTimePatternData } + ]; + + if (file4?.file) { + filePickers.push({ file: file4, parser: excelToFlowRateData }); + } try { const parsedResults = await Promise.all( @@ -186,7 +195,9 @@ const IrSetting = () => { // 각 파싱 결과 접근 const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData; const timePatternData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData; - const flowRateData = parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData; + const flowRateData = file4?.file + ? parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData + : null; // 새로운 JSON 데이터 생성 const combinedAnalysisData = { @@ -203,10 +214,10 @@ const IrSetting = () => { atflInpNm: file1?.file?.name, // 오수관망도 파일명 atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명 atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명 - atflOilqNm: file4?.file?.name, // 유량자료 파일명 + atflOilqNm: file4?.file?.name || null, // 유량자료 파일명 (없을 수 있음) tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 tbSnHrPttn: timePatternData, // 오수량 시간패턴 - tbSnOilqData: flowRateData, // 유량자료 + tbSnOilqData: flowRateData, // 유량자료 (없을 수 있음) }; console.log("통합 분석 데이터 (JSON):", combinedAnalysisData); @@ -228,7 +239,7 @@ const IrSetting = () => { form={form} onSubmitEvent={() => { }} graphBtn={false} - searchBox={{ title: '분석 대상지역 선택' }} + selectBox={{ title: '분석 대상지역 선택' }} enoCd={select1} setEnoCd={setSelect1} ctpvCd={select2} diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index 5f87c52b0..bbfe2ce08 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -313,7 +313,7 @@ export default function RTKForm() { return ( // <BaseLayout searchBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} > <BaseLayout - searchBox={{ title: '분석 대상지역 선택' }} + selectBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} // react-hook-form 인스턴스 전달 onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index fe1ca129c..1c9863b32 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -1,13 +1,12 @@ 'use client' -import { getData } from '@/components/lib/api' -import { UiTable } from '@common/business_components/ui' +import { getData, deleteData } from '@/components/lib/api' +import { UiTable, downloadExcel } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import BaseLayout from '../BaseLayout' - interface ApiSelectOption { text: string; val: string; @@ -27,7 +26,6 @@ const formSchema = z.object({ type ResultFormValues = z.infer<typeof formSchema> -// TODO: 분석구분에 따라서 테이블 컬럼 변경필요 const tableColumn = [ // { id: 'no', title: '번호' }, { id: 'inftwtAnlsSn', title: '분석번호' }, @@ -36,14 +34,8 @@ const tableColumn = [ { id: 'aplcnYm', title: '적용연월' }, { id: 'drtwSwtcRt', title: '오수 전환율' }, { id: 'anlsInfo', title: '침입수 분석정보' }, - { - id: 'regDt', - title: '분석일자', - }, - { - id: 'rgtrNm', - title: '분석자', - } + { id: 'regDt', title: '분석일자' }, + { id: 'rgtrNm', title: '분석자' } ] const tableColumn2 = [ { id: 'no', title: '번호' }, @@ -75,21 +67,36 @@ const Result = () => { const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 const [totalData, setTotalData] = useState(0) //총 데이터 수 - const [rdiiResultData, setRdiiResultData] = useState<any[]>([]); // RDII 결과 저장 상태 (필요시) const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + const [selectedRowData, setSelectedRowData] = useState<any>(null); + const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined); - const handleSearchButtonClick = async () => { + + const handleSearchButtonClick = async (customFormValues?: Partial<ResultFormValues>) => { setIsLoadingResult(true); - const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기 + const formValues = customFormValues || form.getValues(); // react-hook-form에서 현재 값 가져오기 + console.log("formValues", formValues); - const inftwtAnlsSearchDto = { - "aplcnYm": formValues.aplcnYm.toISOString().substring(0, 7).replace("-", ""), - "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, - "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, - "sggCd": formValues.sggCd.val === '0' ? null : formValues.sggCd.val, - // "swageTmplntCd": formValues.swageTmplntCd === '0' ? null : formValues.swageTmplntCd, - "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, - }; + let inftwtAnlsSearchDto; + + if (customFormValues) { + inftwtAnlsSearchDto = { + "aplcnYm": formValues.aplcnYm, + "enoCd": formValues.enoCd, + "ctpvCd": formValues.ctpvCd, + "sggCd": formValues.sggCd, + "swtrmtZoneCd": formValues.swtrmtZoneCd, + }; + } else { + inftwtAnlsSearchDto = { + "aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null, + "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, + "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, + "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, + "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, + }; + } + console.log("Search DTO:", inftwtAnlsSearchDto); const queryParams = new URLSearchParams({ page: '0', @@ -103,7 +110,7 @@ const Result = () => { }); const queryString = queryParams.toString(); - console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); // 디버깅용 로그 + console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); try { const data = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); setTableData(data.content || []); @@ -120,9 +127,89 @@ const Result = () => { } }; + // 테이블 셀 클릭 핸들러 추가 + const handleCellClick = (index: number | undefined, headId: string) => { + if (index !== undefined && tableData[index]) { + setSelectedRowData(tableData[index]); + setSelectedRowIndex(index); + console.log("Selected row data:", tableData[index]); + } + }; + + // Excel 다운로드 핸들러 추가 + const handleExcelDownload = async () => { + console.log("Selected row data for download:", selectedRowData); + if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + alert('다운로드할 데이터를 선택해주세요.'); + return; + } + + try { + const data = await getData(`v1/inftwtAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}`); + + // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 + const excelData = data.content; // 배열로 감싸서 전달 + + // 파일명 생성 (분석번호 기반) + const formValues = form.getValues(); + const fileName = `침입수분석결과_${selectedRowData.inftwtAnlsSn}_${selectedRowData.aplcnYm}`; + + downloadExcel({ + data: excelData as JSON[], + fileName: fileName, + sheetNameArray: ["침입수 분석결과"], + headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정 + }); + } catch (error) { + console.error("Excel 다운로드 중 오류 발생:", error); + alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; + + // 파일 삭제 핸들러 추가 + const handleFileDelete = async () => { + console.log("Selected row data for deletion:", selectedRowData); + if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + alert('삭제할 파일을 선택해주세요.'); + return; + } + // 삭제 확인 대화상자 + const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?'); + if (!confirmDelete) { + return; + } + + try { + // 삭제 API 호출 + const response1 = await deleteData(`v1/inftwtAnlsRslt/${selectedRowData.inftwtAnlsSn}`); + const response2 = await deleteData(`v1/inftwtAnls/${selectedRowData.inftwtAnlsSn}`); + console.log("File delete response1:", response1); + console.log("File delete response2:", response2); + + // 삭제 후 테이블 새로고침을 위해 재검색 + const customFormValues: Partial<ResultFormValues> = { + aplcnYm: selectedRowData.aplcnYm || '0', + enoCd: selectedRowData.enoCd || '0', + ctpvCd: selectedRowData.ctpvCd || '0', + sggCd: selectedRowData.sggCd || '0', + swtrmtZoneCd: selectedRowData.swtrmtZoneCd || '0', + }; + await handleSearchButtonClick(customFormValues); + + // 선택된 행 초기화 + setSelectedRowData(null); + setSelectedRowIndex(undefined); + + alert('삭제가 완료되었습니다.'); + } catch (error) { + console.error("분석결과 파일 삭제 중 오류 발생:", error); + alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; + return ( <BaseLayout - searchBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} + selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle: 'RDII 산정결과 현황' }} form={form} // form 인스턴스 전달 onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit @@ -146,7 +233,10 @@ const Result = () => { form.setValue("aplcnYm", new Date(val)) } }} - // DatePicker는 BaseLayout 내부에서 form.watch("aplcnYm")과 form.setValue("aplcnYm", ...)을 사용하도록 수정 필요 + // Excel 다운로드 핸들러 추가 + onExcelDownload={handleExcelDownload} + onDeleteFile={handleFileDelete} + selectedRowData={selectedRowData} > {[<div key='table1' className="h-[55vh] flex flex-col p-4 border rounded-lg bg-white shadow-md"> <div className="mb-[1.2rem] text-xl font-medium">○ 침입수 산정결과 현황</div> @@ -158,7 +248,7 @@ const Result = () => { total={totalData} pageSize={10} children={undefined} - cellClick={(e) => { }} + cellClick={handleCellClick} onPageChange={(e) => { }} /> </div> From f9f913560c3a8a694d74214e7819104c0ec14efa Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Mon, 9 Jun 2025 15:33:46 +0900 Subject: [PATCH 18/44] =?UTF-8?q?feat:=20RDII=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/rdii_result/page.tsx | 245 ++++++++++++++++++++++++++++ apps/sn/app/ii/result/page.tsx | 34 +--- apps/sn/app/page.tsx | 3 +- 3 files changed, 249 insertions(+), 33 deletions(-) create mode 100644 apps/sn/app/ii/rdii_result/page.tsx diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/ii/rdii_result/page.tsx new file mode 100644 index 000000000..8d98d6a33 --- /dev/null +++ b/apps/sn/app/ii/rdii_result/page.tsx @@ -0,0 +1,245 @@ +'use client' +import { getData, deleteData } from '@/components/lib/api' +import { UiTable, downloadExcel } from '@common/business_components/ui' +import { zodResolver } from '@hookform/resolvers/zod' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import BaseLayout from '../BaseLayout' + + +const formSchema = z.object({ + enoCd: z.string().default('0'), // 환경청코드 + ctpvCd: z.string().default('0'), // 시도코드 + sggCd: z.object({ // 시군구 + val: z.string().default('0'), + text: z.string().default('전체'), + }).default({ val: '0', text: '전체' }), + swageTmplntCd: z.string().default('0'), // 하수처리장 + swtrmtZoneCd: z.string().default('0'), // 하수처리구역 + aplcnYm: z.date().default(new Date()), // 분석일자 (DatePicker와 연동) +}) + +type ResultFormValues = z.infer<typeof formSchema> + +const tableColumn = [ + { id: 'no', title: '번호' }, + { id: 'analysis no', title: '분석번호' }, + { id: 'hasu', title: '하수처리장' }, + { id: 'guyeok', title: '처리구역' }, + { id: 'sangsu', title: '상수 사용량' }, + { id: 'rate', title: '오수 전환율' }, + { id: 'rtx', title: 'RTX 계수 출처' }, + { id: 'rdii', title: 'RDII 기여면적' }, + { id: 'rdii analysis info', title: 'RDII 분석정보' }, + { id: 'date', title: '분석일자' }, + { id: 'usr', title: '분석자' } +] + +const Result = () => { + const form = useForm<ResultFormValues>({ + resolver: zodResolver(formSchema), + defaultValues: { + enoCd: '0', + ctpvCd: '0', + sggCd: { val: '0', text: '전체' }, + swageTmplntCd: '0', + swtrmtZoneCd: '0', + aplcnYm: new Date(), + } + }) + + + const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 + const [totalData, setTotalData] = useState(0) //총 데이터 수 + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + const [selectedRowData, setSelectedRowData] = useState<any>(null); + const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined); + + + const handleSearchButtonClick = async (customFormValues?: Partial<ResultFormValues>) => { + setIsLoadingResult(true); + const formValues = customFormValues || form.getValues(); // react-hook-form에서 현재 값 가져오기 + console.log("formValues", formValues); + + let inftwtAnlsSearchDto; + + if (customFormValues) { + inftwtAnlsSearchDto = { + "aplcnYm": formValues.aplcnYm, + "enoCd": formValues.enoCd, + "ctpvCd": formValues.ctpvCd, + "sggCd": formValues.sggCd, + "swtrmtZoneCd": formValues.swtrmtZoneCd, + }; + } else { + inftwtAnlsSearchDto = { + "aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null, + "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, + "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, + "sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val, + "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, + }; + } + console.log("Search DTO:", inftwtAnlsSearchDto); + + const queryParams = new URLSearchParams({ + page: '0', + size: '10', + sort: 'inftwtAnlsSn' + }); + Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 + queryParams.append(key, String(value)); + } + }); + const queryString = queryParams.toString(); + + console.log("API: ", `v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); + try { + const data = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`); + setTableData(data.content || []); + setTotalData(tableData.length); + console.log("response:", data); + console.log("response data:", data.content || []); + } catch (error) { + console.error("Error fetching analysis result data:", error); + alert("분석결과 조회 중 오류가 발생했습니다."); + setTableData([]); + setTotalData(0); + } finally { + setIsLoadingResult(false); + } + }; + + // 테이블 셀 클릭 핸들러 추가 + const handleCellClick = (index: number | undefined, headId: string) => { + if (index !== undefined && tableData[index]) { + setSelectedRowData(tableData[index]); + setSelectedRowIndex(index); + console.log("Selected row data:", tableData[index]); + } + }; + + // Excel 다운로드 핸들러 추가 + const handleExcelDownload = async () => { + console.log("Selected row data for download:", selectedRowData); + if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + alert('다운로드할 데이터를 선택해주세요.'); + return; + } + + try { + const data = await getData(`v1/rdiiAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}`); + + // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 + const excelData = data.content; // 배열로 감싸서 전달 + + // 파일명 생성 (분석번호 기반) + const fileName = `RDII분석결과_${selectedRowData.inftwtAnlsSn}_${selectedRowData.aplcnYm}`; + + downloadExcel({ + data: excelData as JSON[], + fileName: fileName, + sheetNameArray: ["RDII 분석결과"], + headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정 + }); + } catch (error) { + console.error("Excel 다운로드 중 오류 발생:", error); + alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; + + // 파일 삭제 핸들러 추가 + const handleFileDelete = async () => { + console.log("Selected row data for deletion:", selectedRowData); + if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + alert('삭제할 파일을 선택해주세요.'); + return; + } + // 삭제 확인 대화상자 + const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?'); + if (!confirmDelete) { + return; + } + + try { + // 삭제 API 호출 + const response1 = await deleteData(`v1/rdiiAnlsRslt/${selectedRowData.inftwtAnlsSn}`); + const response2 = await deleteData(`v1/rdiiAnls/${selectedRowData.inftwtAnlsSn}`); + console.log("File delete response1:", response1); + console.log("File delete response2:", response2); + + // 삭제 후 테이블 새로고침을 위해 재검색 + const customFormValues: Partial<ResultFormValues> = { + aplcnYm: selectedRowData.aplcnYm || '0', + enoCd: selectedRowData.enoCd || '0', + ctpvCd: selectedRowData.ctpvCd || '0', + sggCd: selectedRowData.sggCd || '0', + swtrmtZoneCd: selectedRowData.swtrmtZoneCd || '0', + }; + await handleSearchButtonClick(customFormValues); + + // 선택된 행 초기화 + setSelectedRowData(null); + setSelectedRowIndex(undefined); + + alert('삭제가 완료되었습니다.'); + } catch (error) { + console.error("분석결과 파일 삭제 중 오류 발생:", error); + alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); + } + }; + + return ( + <BaseLayout + selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} + titleName={{ mainTitle: '분석결과', firstCardTitle: 'RDII 산정결과 현황' }} + form={form} // form 인스턴스 전달 + onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit + graphBtn={true} + showDatePicker={true} + enoCd={form.watch("enoCd")} + setEnoCd={(val) => form.setValue("enoCd", val)} + ctpvCd={form.watch("ctpvCd")} + setCtpvCd={(val) => form.setValue("ctpvCd", val)} + sigungu={form.watch("sggCd")} + setSigungu={(val) => form.setValue("sggCd", val)} + selectedSwageTreatmentPlant={form.watch("swageTmplntCd")} + onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} + selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} + onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} + aplcnYm={form.watch("aplcnYm")} + setAplcnYm={(val) => { + if (val instanceof Date) { + form.setValue("aplcnYm", val) + } else if (val) { + form.setValue("aplcnYm", new Date(val)) + } + }} + // Excel 다운로드 핸들러 추가 + onExcelDownload={handleExcelDownload} + onDeleteFile={handleFileDelete} + selectedRowData={selectedRowData} + > + {[<div key='table1' className="h-[55vh] flex flex-col p-4 border rounded-lg bg-white shadow-md"> + <div className="mb-[1.2rem] text-xl font-medium">○ RDII 분석결과 현황</div> + <div className="flex-grow overflow-auto"> + <UiTable + headName='' + headlist={tableColumn} + tableData={tableData} + total={totalData} + pageSize={10} + children={undefined} + cellClick={handleCellClick} + onPageChange={(e) => { }} + /> + </div> + </div> + ]} + </BaseLayout> + ) +} + +export default Result \ No newline at end of file diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index 1c9863b32..552504e0b 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -7,10 +7,6 @@ import { useForm } from 'react-hook-form' import { z } from 'zod' import BaseLayout from '../BaseLayout' -interface ApiSelectOption { - text: string; - val: string; -} const formSchema = z.object({ enoCd: z.string().default('0'), // 환경청코드 @@ -37,19 +33,6 @@ const tableColumn = [ { id: 'regDt', title: '분석일자' }, { id: 'rgtrNm', title: '분석자' } ] -const tableColumn2 = [ - { id: 'no', title: '번호' }, - { id: 'analysis no', title: '분석번호' }, - { id: 'hasu', title: '하수처리장' }, - { id: 'guyeok', title: '처리구역' }, - { id: 'sangsu', title: '상수 사용량' }, - { id: 'rate', title: '오수 전환율' }, - { id: 'rtx', title: 'RTX 계수 출처' }, - { id: 'rdii', title: 'RDII 기여면적' }, - { id: 'rdii analysis info', title: 'RDII 분석정보' }, - { id: 'date', title: '분석일자' }, - { id: 'usr', title: '분석자' } -] const Result = () => { const form = useForm<ResultFormValues>({ @@ -151,7 +134,6 @@ const Result = () => { const excelData = data.content; // 배열로 감싸서 전달 // 파일명 생성 (분석번호 기반) - const formValues = form.getValues(); const fileName = `침입수분석결과_${selectedRowData.inftwtAnlsSn}_${selectedRowData.aplcnYm}`; downloadExcel({ @@ -210,7 +192,7 @@ const Result = () => { return ( <BaseLayout selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} - titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle: 'RDII 산정결과 현황' }} + titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력' }} form={form} // form 인스턴스 전달 onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit graphBtn={true} @@ -252,19 +234,7 @@ const Result = () => { onPageChange={(e) => { }} /> </div> - </div>, - // <div key='table2'> - // <UiTable - // headName='' - // headlist={tableColumn2} - // tableData={rdiiResultData} - // total={rdiiResultData.length} - // pageSize={10} - // children={undefined} - // cellClick={(e) => { }} - // onPageChange={(e) => { }} - // /> - // </div> + </div> ]} </BaseLayout> ) diff --git a/apps/sn/app/page.tsx b/apps/sn/app/page.tsx index fbf788346..36295f554 100644 --- a/apps/sn/app/page.tsx +++ b/apps/sn/app/page.tsx @@ -20,7 +20,8 @@ export default function Home() { <div className="flex flex-row gap-4 p-4"> <Link href={"/ii/irsetting"}><Button>침입수 분석</Button></Link> <Link href={"/ii/rdii"}><Button>RDII 분석</Button></Link> - <Link href={"/ii/result"}><Button>분석결과</Button></Link> + <Link href={"/ii/result"}><Button>침입수 분석 결과</Button></Link> + <Link href={"/ii/rdii_result"}><Button>RDII 분석 결과</Button></Link> </div> </div> </div> From 922cdbf2043699a00b21d9ce19f803cc18689dd4 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Mon, 9 Jun 2025 17:06:11 +0900 Subject: [PATCH 19/44] =?UTF-8?q?feat:=20RDII=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=9D=B4=EB=B8=94=20Column=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/rdii_result/page.tsx | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/ii/rdii_result/page.tsx index 8d98d6a33..ae220aff8 100644 --- a/apps/sn/app/ii/rdii_result/page.tsx +++ b/apps/sn/app/ii/rdii_result/page.tsx @@ -23,17 +23,16 @@ const formSchema = z.object({ type ResultFormValues = z.infer<typeof formSchema> const tableColumn = [ - { id: 'no', title: '번호' }, - { id: 'analysis no', title: '분석번호' }, - { id: 'hasu', title: '하수처리장' }, - { id: 'guyeok', title: '처리구역' }, - { id: 'sangsu', title: '상수 사용량' }, - { id: 'rate', title: '오수 전환율' }, - { id: 'rtx', title: 'RTX 계수 출처' }, - { id: 'rdii', title: 'RDII 기여면적' }, - { id: 'rdii analysis info', title: 'RDII 분석정보' }, - { id: 'date', title: '분석일자' }, - { id: 'usr', title: '분석자' } + { id: 'rainInfwtAnlsSn', title: '분석번호' }, + { id: 'swageTmplntNm', title: '하수처리장' }, + { id: 'swtrmtZoneNm', title: '처리구역' }, + { id: 'aplcnYm', title: '상수사용연월' }, + { id: 'drtwSwtcRt', title: '오수 전환율' }, + { id: 'srcNm', title: 'RTX 계수 출처' }, + { id: 'cntbArea', title: 'RDII 기여면적' }, + { id: 'anlsInfo', title: 'RDII 분석정보' }, + { id: 'regDt', title: '분석일자' }, + { id: 'rgtrNm', title: '분석자' } ] const Result = () => { @@ -86,7 +85,7 @@ const Result = () => { const queryParams = new URLSearchParams({ page: '0', size: '10', - sort: 'inftwtAnlsSn' + sort: 'rainInfwtAnlsSn' }); Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 From 3c912e3826c752b87a90fa3056c6c0a2bc66554d Mon Sep 17 00:00:00 2001 From: YangJinSoo94 <jschrome782@gmail.com> Date: Mon, 9 Jun 2025 17:20:45 +0900 Subject: [PATCH 20/44] =?UTF-8?q?-=20=EC=B2=98=EB=A6=AC=EA=B5=AC=EC=97=AD?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=99=84=EB=A3=8C=20-=20SearchBaseForm=20?= =?UTF-8?q?=EC=9D=98=20selectPartData(=EC=9C=A0=EC=97=AD=EB=B3=84,?= =?UTF-8?q?=ED=96=89=EC=A0=95=EB=B3=84,=ED=99=98=EA=B2=BD=EC=B2=AD?= =?UTF-8?q?=EB=B3=84)=20=EA=B0=80=20=EC=B2=98=EB=A6=AC=EA=B5=AC=EC=97=AD?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EC=97=90=EC=84=9C=20=ED=96=89=EC=A0=95?= =?UTF-8?q?=EB=B3=84=EB=A7=8C=20=EB=B3=B4=EC=97=AC=EC=95=BC=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EB=95=8C=EB=AC=B8=EC=97=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/search_base_form/index.tsx | 20 +++++++++++-- .../src/ui/search_form_left/index.tsx | 28 +++++++++++++++++-- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/common/business_components/src/ui/search_base_form/index.tsx b/packages/common/business_components/src/ui/search_base_form/index.tsx index b8d8355ae..2649c67db 100644 --- a/packages/common/business_components/src/ui/search_base_form/index.tsx +++ b/packages/common/business_components/src/ui/search_base_form/index.tsx @@ -20,6 +20,7 @@ type SearchBaseType = { initClick:() => void; btnClass:string; ref:React.RefObject<ReturnType>; + isAdmin:boolean; } export type ReturnType = { partValue: string; @@ -29,9 +30,10 @@ export type ReturnType = { unitWaterShedValue: string; middleRegionValue: string; envPartValue: string; + sidoText: string; + sigunText: string; } - function SearchBaseForm ({ leftChildren, rightChildren, @@ -46,11 +48,14 @@ function SearchBaseForm ({ queryClick, excelClick, initClick, - ref + ref, + isAdmin=false }: SearchBaseType) { const [partValue, setPartValue] = useState<string>("A"); const [sidoValue, setSidoValue] = useState<string>("0"); const [sigunValue, setSigunValue] = useState<string>("0"); + const [sidoText, setSidoText] = useState<string>(""); + const [sigunText, setSigunText] = useState<string>(""); const [regionValue, setRegionValue] = useState<string>("0"); const [untitWaterShedValue, setUntitWaterShedValue] = useState<string>("0"); const [middleRegionValue, setMiddleRegionValue] = useState<string>("0"); @@ -66,6 +71,8 @@ function SearchBaseForm ({ unitWaterShedValue: untitWaterShedValue, middleRegionValue: middleRegionValue, envPartValue: envPartValue, + sidoText: sidoText, + sigunText: sigunText, }); } const initVal = useCallback(() => { @@ -86,6 +93,8 @@ function SearchBaseForm ({ unitWaterShedValue: untitWaterShedValue, middleRegionValue: middleRegionValue, envPartValue: envPartValue, + sidoText: sidoText, + sigunText: sigunText, }) }; useEffect(() => { @@ -98,8 +107,10 @@ function SearchBaseForm ({ customRef.unitWaterShedValue = untitWaterShedValue; customRef.middleRegionValue = middleRegionValue; customRef.envPartValue = envPartValue; + customRef.sidoText = sidoText; + customRef.sigunText = sigunText; } - }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]); + }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, sidoText, sigunText, ref]); return ( <div className="flex card card-border !mt-6"> <SearchFormLeft isShowPart={true} @@ -118,7 +129,10 @@ function SearchBaseForm ({ setRtnEnvPartValue={setEnvPartValue} setRtnEnvPartSidoValue={setSidoValue} setRtnEnvPartSigunValue={setSigunValue} + setRtnSidoText={setSidoText} + setRtnSigunText={setSigunText} resetTrigger={resetTrigger} + isAdmin={isAdmin} > {leftChildren} </SearchFormLeft> diff --git a/packages/common/business_components/src/ui/search_form_left/index.tsx b/packages/common/business_components/src/ui/search_form_left/index.tsx index 02112f084..e7858df2a 100644 --- a/packages/common/business_components/src/ui/search_form_left/index.tsx +++ b/packages/common/business_components/src/ui/search_form_left/index.tsx @@ -40,7 +40,10 @@ type SearchFormLeftProps = { setRtnEnvPartValue?: (value: string) => void; setRtnEnvPartSidoValue?: (value: string) => void; setRtnEnvPartSigunValue?: (value: string) => void; + setRtnSidoText?: (value: string) => void; + setRtnSigunText?: (value: string) => void; resetTrigger?: boolean; + isAdmin?: boolean; } export type EnvPartSelectDataType = SelectDataType & { @@ -53,12 +56,14 @@ const selectPartData:SelectDataType[] = [ { text: "환경청별", val: 'G' }, ]; -function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) { +function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) { /******************선택된 데이터******************/ const [partValue, setPartValue] = useState<string>('A'); //행정별 시도 시군구 const [sidoValue, setSidoValue] = useState<string>('0'); const [sigunValue, setSigunValue] = useState<string>('0'); + const [sidoText, setSidoText] = useState<string>(''); + const [sigunText, setSigunText] = useState<string>(''); //유역별 const [regionValue, setRegionValue] = useState<string>('0'); const [unitWaterShedValue, setUnitWaterShedValue] = useState<string>('0'); @@ -152,6 +157,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel } const onSelectValue = (val: string, type: string) => { + let selectedSido; + let selectedSigun; + let selectedEnvPartSido; + let selectedEnvPartSigun; + switch(type) { case 'part': setPartValue(val); @@ -180,9 +190,14 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel break; case 'sido': setSidoValue(val); + selectedSido = selectSidoData.find(item => item.val === val); + setSidoText(selectedSido?.text || ''); if (setRtnSidoValue) { setRtnSidoValue(val); } + if (setRtnSidoText) { + setRtnSidoText(selectedSido?.text || ''); + } initializeSigunData(val); onSelectValue(selectSigunData[0]?.val || '0', 'sigun'); break; @@ -191,6 +206,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel setRtnSigunValue(val); } setSigunValue(val); + selectedSigun = filteredSigunData.find(item => item.val === val); + if (setRtnSigunText) { + setRtnSigunText(selectedSigun?.text || ''); + } + setSigunText(selectedSigun?.text || ''); break; // 유역 세팅 @@ -228,6 +248,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel break; case 'envPartSido' : setSidoValue(val); + selectedEnvPartSido = filteredEnvPartSidoData.find(item => item.val === val); + setSidoText(selectedEnvPartSido?.text || ''); if (setRtnEnvPartSidoValue) { setRtnEnvPartSidoValue(val); } @@ -236,6 +258,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel break; case 'envPartSigun' : setSigunValue(val); + selectedEnvPartSigun = filteredEnvPartSigunData.find(item => item.val === val); + setSigunText(selectedEnvPartSigun?.text || ''); if (setRtnEnvPartSigunValue) { setRtnEnvPartSigunValue(val); } @@ -285,7 +309,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel return ( isShowPart ? ( <div className="grid grid-cols-12 gap-5 w-11/12 m-4"> - <SelectBox className="col-span-3" label={'구분'} selectArray={selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/> + <SelectBox className="col-span-3" label={'구분'} selectArray={isAdmin ? selectPartData.slice(0, 1) : selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/> {partValue === 'R' ? ( <> <SelectBox className="col-span-3" label={'유역'} selectArray={selectRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/> From de1fe7fe4203e70e150e6a5c2e9bcf941e2ec0b4 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Mon, 9 Jun 2025 17:52:32 +0900 Subject: [PATCH 21/44] =?UTF-8?q?feat:=20RDII=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C,=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/rdii_result/page.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/ii/rdii_result/page.tsx index ae220aff8..ada7f4c56 100644 --- a/apps/sn/app/ii/rdii_result/page.tsx +++ b/apps/sn/app/ii/rdii_result/page.tsx @@ -123,19 +123,19 @@ const Result = () => { // Excel 다운로드 핸들러 추가 const handleExcelDownload = async () => { console.log("Selected row data for download:", selectedRowData); - if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { alert('다운로드할 데이터를 선택해주세요.'); return; } try { - const data = await getData(`v1/rdiiAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}`); + const data = await getData(`v1/rdiiAnlsRslt/ppl/${selectedRowData.rainInfwtAnlsSn}`); // 선택된 행 데이터를 Excel 다운로드 형식으로 변환 const excelData = data.content; // 배열로 감싸서 전달 // 파일명 생성 (분석번호 기반) - const fileName = `RDII분석결과_${selectedRowData.inftwtAnlsSn}_${selectedRowData.aplcnYm}`; + const fileName = `RDII분석결과_${selectedRowData.rainInfwtAnlsSn}_${selectedRowData.aplcnYm}`; downloadExcel({ data: excelData as JSON[], @@ -152,7 +152,7 @@ const Result = () => { // 파일 삭제 핸들러 추가 const handleFileDelete = async () => { console.log("Selected row data for deletion:", selectedRowData); - if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { alert('삭제할 파일을 선택해주세요.'); return; } @@ -164,8 +164,8 @@ const Result = () => { try { // 삭제 API 호출 - const response1 = await deleteData(`v1/rdiiAnlsRslt/${selectedRowData.inftwtAnlsSn}`); - const response2 = await deleteData(`v1/rdiiAnls/${selectedRowData.inftwtAnlsSn}`); + const response1 = await deleteData(`v1/rdiiAnlsRslt/${selectedRowData.rainInfwtAnlsSn}`); + const response2 = await deleteData(`v1/rdiiAnls/${selectedRowData.rainInfwtAnlsSn}`); console.log("File delete response1:", response1); console.log("File delete response2:", response2); From 0d4d529af9ea900f3b8f48d0c4d52f361e280936 Mon Sep 17 00:00:00 2001 From: jyha00 <jyha@mind-one.co.kr> Date: Mon, 9 Jun 2025 19:15:13 +0900 Subject: [PATCH 22/44] =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=95=84=EC=9D=B4=EB=94=94=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/table/HeadMakeColSpan.tsx | 4 ++-- packages/common/business_components/src/ui/table/index.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx index ebd611c1d..c0ec33233 100644 --- a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx +++ b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx @@ -44,11 +44,11 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { <TableRow key={rowIndex+ " headerRow"} className="bg-gray-50"> {upHeadList .filter((item) => item.visible != false) - .map((head: TableUpperProps) => { + .map((head: TableUpperProps, idx) => { if (head.row === rowIndex) { return ( <TableHead - key={head.id} + key={head.id?head.id:rowIndex+"_"+idx} colSpan={head.colSpan || 1} rowSpan={head.rowSpan || 1} style={{ whiteSpace: 'pre-line' }} diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index 791eddffe..e8c5bac8c 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -27,7 +27,6 @@ type UiHeadProps = { export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false, boldArray = [] }: UiTableProps) { const [cur, setCur] = useState<number>(1); const [idx, setIdx] = useState<number>(-1); - useEffect(() => { if (resetPageNo) { setCur(1); From 91fe1597c4d8f4b22fe755658ee11c71740d3b9d Mon Sep 17 00:00:00 2001 From: jyha00 <jyha@mind-one.co.kr> Date: Tue, 10 Jun 2025 09:17:16 +0900 Subject: [PATCH 23/44] =?UTF-8?q?chartConfig=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/chart/index.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/common/business_components/src/ui/chart/index.tsx b/packages/common/business_components/src/ui/chart/index.tsx index b30eef3c3..0d17a99b1 100644 --- a/packages/common/business_components/src/ui/chart/index.tsx +++ b/packages/common/business_components/src/ui/chart/index.tsx @@ -34,7 +34,7 @@ const CHART_FORM = { type MainChartProps = { chartForm: string; - chartConfig: any; + chartConfig: ChartConfig; chartData: any; chartTitle: string; chartClassName?: string; @@ -47,23 +47,19 @@ type MainChartProps = { function MainChart(props: MainChartProps) { if( props.chartForm == CHART_FORM.BAR ){ return ( - <BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}> - </BarChart> + <BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/> ); } else if( props.chartForm == CHART_FORM.LINE ){ return ( - <LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}> - </LineChart> + <LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/> ); } else if( props.chartForm == CHART_FORM.DN ){ return ( - <DoughnutChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}> - </DoughnutChart> + <DoughnutChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}/> ); } else if (props.chartForm == CHART_FORM.LINEBAR){ return ( - <LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}> - </LineBarchart> + <LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}/> ); } } From 85e836210080cf459b1fcac2fa9ce10782395d8e Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 10 Jun 2025 11:06:03 +0900 Subject: [PATCH 24/44] =?UTF-8?q?feat:=20-=20I/I=20=EB=B6=84=EC=84=9D=20Lo?= =?UTF-8?q?adingBar=20=EC=A0=81=EC=9A=A9=20-=20rdii=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=98=A4=ED=83=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=EA=B0=92=20=EC=9E=85=EB=A0=A5=20(=EC=B6=94?= =?UTF-8?q?=ED=9B=84=20=EC=82=AD=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/BaseLayout.tsx | 4 +++- apps/sn/app/ii/irsetting/page.tsx | 11 +++++++-- apps/sn/app/ii/rdii/page.tsx | 35 ++++++++++++++++------------- apps/sn/app/ii/rdii_result/page.tsx | 13 ++++++++--- apps/sn/app/ii/result/page.tsx | 15 ++++++++++--- 5 files changed, 54 insertions(+), 24 deletions(-) diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx index 18d9f9bf0..b08fdcbe4 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -51,6 +51,7 @@ type BaseLayout = { onSwageProcessingZoneChange?: (value: string | number) => void; aplcnYm?: Date setAplcnYm?: (aplcnYm: Date | undefined) => void; + isLoading?: boolean; // Excel 다운로드 관련 props 추가 onExcelDownload?: () => void; onDeleteFile?: () => void; @@ -86,6 +87,7 @@ const BaseLayout = ({ onSwageProcessingZoneChange, aplcnYm, setAplcnYm, + isLoading, // Excel 다운로드 관련 props 추가 onExcelDownload, onDeleteFile, @@ -196,7 +198,7 @@ const BaseLayout = ({ return ( <Form {...form} className="min-h-screen flex flex-col"> - <ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden"> + <ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden" isLoading={isLoading}> <ListDiv> {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} {selectBox?.title && ( diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index 8cb36c2bf..162cb9e3f 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -67,6 +67,7 @@ const IrSetting = () => { const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장코드 const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역코드 const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 // 폼 초기화 함수 const resetAllForms = () => { @@ -117,6 +118,7 @@ const IrSetting = () => { const registFile = async (analysisData: any) => { + setIsLoadingResult(true); // 로딩 상태 시작 console.log("Submitting combined analysis data to backend:", analysisData); // 추후 API 엔드포인트에 맞게 URL 수정 필요 const data = await postData('v1/inftwtAnls', analysisData); @@ -125,10 +127,12 @@ const IrSetting = () => { alert("분석 데이터가 성공적으로 제출되었습니다."); resetAllForms(); // 성공 시 폼 초기화 함수 호출 } else alert("분석 데이터 제출 중 오류가 발생했습니다."); + setIsLoadingResult(false); // 로딩 상태 시작 }; // 침입수 분석 실행 버튼 클릭 핸들러 const handleAnalysisClick = async () => { + setIsLoadingResult(true); // 로딩 상태 시작 console.log(uploadedFilesData); const file1 = uploadedFilesData[0]; // 오수관망도 const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과 @@ -224,8 +228,9 @@ const IrSetting = () => { // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. await registFile(combinedAnalysisData); - + setIsLoadingResult(false); } catch (error) { + setIsLoadingResult(false); console.error("파일 파싱 중 오류 발생:", error); alert("파일 처리 중 오류가 발생했습니다."); return; @@ -249,7 +254,9 @@ const IrSetting = () => { selectedSwageTreatmentPlant={select4} onSwageTreatmentPlantChange={setSelect4} selectedSwageProcessingZone={select5} - onSwageProcessingZoneChange={setSelect5}> + onSwageProcessingZoneChange={setSelect5} + isLoading={isLoadingResult} + > <div className="grid gap-4"> <FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'> <FileUploadForm diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index bbfe2ce08..18cf11b28 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -4,7 +4,7 @@ import { postData } from '@/components/lib/api' import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" import BaseLayout from "@/ii/BaseLayout" -import { filePicked, DatePicker } from "@common/business_components/ui" +import { DatePicker, filePicked } from "@common/business_components/ui" import { Button, Input } from "@common/components/ui" import { zodResolver } from "@hookform/resolvers/zod" import { Label } from '@radix-ui/react-label' @@ -47,31 +47,31 @@ const formSchema = z.object({ }) // zod 스키마 타입 추론 type FormSchemaType = z.infer<typeof formSchema>; -// 기본 폼 값 정의 +// 기본 폼 값 정의 (테스트 편의를 위해 초기값 설정, 추후 삭제 필요) const defaultFormValues: FormSchemaType = { enoCd: "", ctpvCd: "", sggCd: "", swageTmplntCd: "", swtrmtZoneCd: "", - rtkSource: "", - rdiiArea: "", // 문자열로 기본값 설정 + rtkSource: "출처", + rdiiArea: "123", // 문자열로 기본값 설정 unitFactors: [ - { response: "Short-Term", R: "", T: "", K: "" }, // 문자열로 기본값 설정 - { response: "Medium-Term", R: "", T: "", K: "" }, - { response: "Long-Term", R: "", T: "", K: "" }, + { response: "Short-Term", R: "0.040", T: "0.5", K: "1" }, // 문자열로 기본값 설정 + { response: "Medium-Term", R: "0.005", T: "1", K: "2" }, + { response: "Long-Term", R: "0.005", T: "2", K: "3" }, ], initialDepths: [ - { response: "Short-Term", Dmax: "", Drec: "", Do: "" }, // 문자열로 기본값 설정 - { response: "Medium-Term", Dmax: "", Drec: "", Do: "" }, - { response: "Long-Term", Dmax: "", Drec: "", Do: "" }, + { response: "Short-Term", Dmax: "0", Drec: "0", Do: "0" }, // 문자열로 기본값 설정 + { response: "Medium-Term", Dmax: "0", Drec: "0", Do: "0" }, + { response: "Long-Term", Dmax: "0", Drec: "0", Do: "0" }, ], // osumap: undefined, // matchingResultFile: undefined, aplcnYm: "", - rate: "", // 문자열로 기본값 설정 + rate: "0.8", // 문자열로 기본값 설정 // rainFile: undefined, - analysisInfo: "", + analysisInfo: "test 분석정보", date: new Date(), }; @@ -92,6 +92,7 @@ interface ApiSelectOption { export default function RTKForm() { const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태 + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 // FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열 // 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨 @@ -107,6 +108,7 @@ export default function RTKForm() { const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장 (swageTmplntCd) const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역 (swtrmtZoneCd) + // form 인스턴스 생성 const form = useForm<FormSchemaType>({ resolver: zodResolver(formSchema), @@ -160,6 +162,7 @@ export default function RTKForm() { // 침입수 분석 실행 버튼 클릭 핸들러 const handleAnalysisClick = async () => { + setIsLoadingResult(true); // 로딩 상태 시작 const formData = form.getValues(); console.log(uploadedFilesData); const file1 = uploadedFilesData[0]; // 오수관망도 @@ -256,7 +259,7 @@ export default function RTKForm() { longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? formData.initialDepths[2]?.Drec : "0") || null, // 장기간회복비율 longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? formData.initialDepths[2]?.Do : "0") || null, // 장기간시작깊이 - dtrwSwtcRt: parseFloat(formData.rate !== undefined ? formData.rate : "0") || null, // 오수전환율 + drtwSwtcRt: parseFloat(formData.rate !== undefined ? formData.rate : "0") || null, // 오수전환율 anlsInfo: formData.analysisInfo, // 분석정보 tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 @@ -267,8 +270,9 @@ export default function RTKForm() { // 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다. await registFile(combinedAnalysisData); - + setIsLoadingResult(false); } catch (error) { + setIsLoadingResult(false); console.error("파일 파싱 중 오류 발생:", error); alert("파일 처리 중 오류가 발생했습니다."); return; @@ -329,6 +333,7 @@ export default function RTKForm() { onSwageTreatmentPlantChange={setSelect4} selectedSwageProcessingZone={select5} onSwageProcessingZoneChange={setSelect5} + isLoading={isLoadingResult} > {[<div className="space-y-[1.2rem] overflow-x-auto" key="form-section"> <div className="grid grid-cols-2 gap-4"> @@ -414,7 +419,7 @@ export default function RTKForm() { }} // className을 직접 사용하거나, DatePicker 내부에서 selectClass를 처리하도록 수정 필요 // 여기서는 DatePicker의 selectClass prop을 사용하고, date-picker.css의 스타일을 적용 - selectClass="ml-5 date-picker-trigger" + selectClass="ml-5 date-picker-trigger" /> </FormSection> <FormSection name='rate' form={form} titleNm='○ 분석 정보'> diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/ii/rdii_result/page.tsx index ada7f4c56..dba3a1232 100644 --- a/apps/sn/app/ii/rdii_result/page.tsx +++ b/apps/sn/app/ii/rdii_result/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { getData, deleteData } from '@/components/lib/api' +import { deleteData, getData } from '@/components/lib/api' import { UiTable, downloadExcel } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' @@ -101,13 +101,13 @@ const Result = () => { setTotalData(tableData.length); console.log("response:", data); console.log("response data:", data.content || []); + setIsLoadingResult(false); } catch (error) { + setIsLoadingResult(false); console.error("Error fetching analysis result data:", error); alert("분석결과 조회 중 오류가 발생했습니다."); setTableData([]); setTotalData(0); - } finally { - setIsLoadingResult(false); } }; @@ -122,8 +122,10 @@ const Result = () => { // Excel 다운로드 핸들러 추가 const handleExcelDownload = async () => { + setIsLoadingResult(true); console.log("Selected row data for download:", selectedRowData); if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { + setIsLoadingResult(false); alert('다운로드할 데이터를 선택해주세요.'); return; } @@ -143,7 +145,9 @@ const Result = () => { sheetNameArray: ["RDII 분석결과"], headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정 }); + setIsLoadingResult(false); } catch (error) { + setIsLoadingResult(false); console.error("Excel 다운로드 중 오류 발생:", error); alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); } @@ -151,6 +155,7 @@ const Result = () => { // 파일 삭제 핸들러 추가 const handleFileDelete = async () => { + setIsLoadingResult(true); console.log("Selected row data for deletion:", selectedRowData); if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) { alert('삭제할 파일을 선택해주세요.'); @@ -188,6 +193,7 @@ const Result = () => { console.error("분석결과 파일 삭제 중 오류 발생:", error); alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); } + setIsLoadingResult(false); }; return ( @@ -216,6 +222,7 @@ const Result = () => { form.setValue("aplcnYm", new Date(val)) } }} + isLoading={isLoadingResult} // Excel 다운로드 핸들러 추가 onExcelDownload={handleExcelDownload} onDeleteFile={handleFileDelete} diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index 552504e0b..99f0f902a 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -1,5 +1,5 @@ 'use client' -import { getData, deleteData } from '@/components/lib/api' +import { deleteData, getData } from '@/components/lib/api' import { UiTable, downloadExcel } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' @@ -100,13 +100,13 @@ const Result = () => { setTotalData(tableData.length); console.log("response:", data); console.log("response data:", data.content || []); + setIsLoadingResult(false); } catch (error) { + setIsLoadingResult(false); console.error("Error fetching analysis result data:", error); alert("분석결과 조회 중 오류가 발생했습니다."); setTableData([]); setTotalData(0); - } finally { - setIsLoadingResult(false); } }; @@ -121,8 +121,10 @@ const Result = () => { // Excel 다운로드 핸들러 추가 const handleExcelDownload = async () => { + setIsLoadingResult(true); console.log("Selected row data for download:", selectedRowData); if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + setIsLoadingResult(false); alert('다운로드할 데이터를 선택해주세요.'); return; } @@ -142,7 +144,9 @@ const Result = () => { sheetNameArray: ["침입수 분석결과"], headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정 }); + setIsLoadingResult(false); } catch (error) { + setIsLoadingResult(false); console.error("Excel 다운로드 중 오류 발생:", error); alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요."); } @@ -150,8 +154,10 @@ const Result = () => { // 파일 삭제 핸들러 추가 const handleFileDelete = async () => { + setIsLoadingResult(true); console.log("Selected row data for deletion:", selectedRowData); if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) { + setIsLoadingResult(false); alert('삭제할 파일을 선택해주세요.'); return; } @@ -181,9 +187,11 @@ const Result = () => { // 선택된 행 초기화 setSelectedRowData(null); setSelectedRowIndex(undefined); + setIsLoadingResult(false); alert('삭제가 완료되었습니다.'); } catch (error) { + setIsLoadingResult(false); console.error("분석결과 파일 삭제 중 오류 발생:", error); alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요."); } @@ -215,6 +223,7 @@ const Result = () => { form.setValue("aplcnYm", new Date(val)) } }} + isLoading={isLoadingResult} // 로딩 상태 전달 // Excel 다운로드 핸들러 추가 onExcelDownload={handleExcelDownload} onDeleteFile={handleFileDelete} From 28830ac66dadc78a68b01e90e1b74870ec241570 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 10 Jun 2025 11:19:20 +0900 Subject: [PATCH 25/44] =?UTF-8?q?feat:=20RDII=20=EB=B6=84=EC=84=9D=20RTK?= =?UTF-8?q?=20=EA=B3=84=EC=88=98=20=EC=B6=9C=EC=B2=98,=20RDII=20=EA=B8=B0?= =?UTF-8?q?=EC=97=AC=EB=A9=B4=EC=A0=81(ha)=20Input=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=81=EB=8B=A8=20hover=20border=20?= =?UTF-8?q?=EC=9E=98=EB=A6=AC=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/rdii/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index 18cf11b28..2a9a49dbc 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -335,7 +335,7 @@ export default function RTKForm() { onSwageProcessingZoneChange={setSelect5} isLoading={isLoadingResult} > - {[<div className="space-y-[1.2rem] overflow-x-auto" key="form-section"> + {[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section"> <div className="grid grid-cols-2 gap-4"> <FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처"> <Input {...form.register("rtkSource")} className="h-12 text-center text-xl w-[225px]" /> From 761c3f8c4c8fe29c0362566d00301d6afd488250 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Tue, 10 Jun 2025 14:10:17 +0900 Subject: [PATCH 26/44] =?UTF-8?q?feat:=20-=20=EC=B9=A8=EC=9E=85=EC=88=98?= =?UTF-8?q?=20=EB=B6=84=EC=84=9D,=20RDII=20=EB=B6=84=EC=84=9D=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F?= =?UTF-8?q?=20=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?-=20RDII=20=EB=B6=84=EC=84=9D=20null=20=EA=B0=92=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/BaseLayout.tsx | 16 ++++++---- apps/sn/app/ii/irsetting/page.tsx | 32 +++++++++++++++----- apps/sn/app/ii/rdii/page.tsx | 50 ++++++++++++++++--------------- 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx index b08fdcbe4..544802744 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -36,6 +36,7 @@ type BaseLayout = { form: any onSubmitEvent: Function graphBtn?: boolean + analysisBtn?: string firstDivBtn?: boolean selectBox?: SelectBox showDatePicker?: boolean; @@ -55,6 +56,7 @@ type BaseLayout = { // Excel 다운로드 관련 props 추가 onExcelDownload?: () => void; onDeleteFile?: () => void; + onAnalysis?: () => void; selectedRowData?: any; } // TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ? @@ -73,6 +75,7 @@ const BaseLayout = ({ form, onSubmitEvent, graphBtn = false, + analysisBtn, selectBox, showDatePicker = false, enoCd, @@ -91,6 +94,7 @@ const BaseLayout = ({ // Excel 다운로드 관련 props 추가 onExcelDownload, onDeleteFile, + onAnalysis, selectedRowData }: BaseLayout) => { @@ -228,16 +232,18 @@ const BaseLayout = ({ <SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} /> <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> </GovSidoSigunguSearchForm> - {selectBox?.searchChk ? <SearchFormRight><Button onClick={() => selectBox.onSearchClick?.()}>조회</Button></SearchFormRight> : null} + {selectBox?.searchChk ? <SearchFormRight><Button size="sm" onClick={() => selectBox.onSearchClick?.()}>조회</Button></SearchFormRight> : null} </div> </ListDiv> <ListDiv> <div className="card-header"> <h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3> <div> - {graphBtn ? <Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" onClick={isChckFile}>파일 다운로드</Button> : null} + {graphBtn ? <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> : null} {/* 그래프 있을 시 */} - {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]" onClick={isDeleteFile}>삭제</Button> : null} + {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> : null} + {/* 분석 버튼 */} + {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={onAnalysis}>{analysisBtn}</Button> : null} </div> </div> <Form {...form}> @@ -254,9 +260,9 @@ const BaseLayout = ({ <div className="card-header flex items-center justify-between"> <h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3> <div> - {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]">파일 다운로드</Button> : null} + {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">파일 다운로드</Button> : null} {/* 그래프 있을 시 */} - {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]">삭제</Button> : null} + {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">삭제</Button> : null} </div> </div> <div className="card !m-[1.2rem]">{children?.[1]}</div> diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index 162cb9e3f..2944af9c0 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -3,7 +3,8 @@ import { postData } from '@/components/lib/api' import FileUploadForm from '@/components/ui/fileUploadForm' import FormSection from '@/components/ui/formSection' import { DatePicker, filePicked } from '@common/business_components/ui' -import { Button, Input } from '@common/components/ui' +import ListDiv from '@common/business_components/ui/list_div' +import { Input } from '@common/components/ui' import { zodResolver } from '@hookform/resolvers/zod' import { useState } from 'react' import { useForm } from 'react-hook-form' @@ -147,6 +148,7 @@ const IrSetting = () => { // 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인) // 유량자료는 선택값 if (!file1?.file || !file2?.file || !file3?.file) { + setIsLoadingResult(false); alert('모든 분석용 파일을 선택해주세요.'); return; } @@ -240,10 +242,11 @@ const IrSetting = () => { return ( <> <BaseLayout - titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력', secondCardTitle: '실측자료 등록' }} + titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력' }} form={form} onSubmitEvent={() => { }} graphBtn={false} + analysisBtn={'침입수분석 실행'} selectBox={{ title: '분석 대상지역 선택' }} enoCd={select1} setEnoCd={setSelect1} @@ -256,8 +259,9 @@ const IrSetting = () => { selectedSwageProcessingZone={select5} onSwageProcessingZoneChange={setSelect5} isLoading={isLoadingResult} + onAnalysis={handleAnalysisClick} > - <div className="grid gap-4"> + {[<div className="grid gap-4"> <FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'> <FileUploadForm key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key @@ -338,8 +342,22 @@ const IrSetting = () => { onChange={(e) => setInput2(e.target.value)} className="ml-5 h-[3.2rem] text-center text-xl w-[150px]" /> </FormSection> - </div> - <div className="flex items-center gap-4"> + <ListDiv> + <div className="card !m-[1.2rem]"> + <FormSection name='region' form={form} titleNm='▷ 유량자료 등록'> + <FileUploadForm + key={`file-upload-3-${formResetKey}`} // 폼 초기화를 위한 key + onFileSelect={(file, cd, id) => handleFileSelection(3, file, cd, id)} + showButton={false} + showPreview={true} + accept={".xls, .xlsx, .xlsm, .xltx"} + fileIdx={3} + /> + </FormSection> + </div> + </ListDiv> + </div>]} + {/* <div className="flex items-center gap-4"> <FormSection name='region' form={form} titleNm='▷ 유량자료 등록'> <FileUploadForm @@ -351,9 +369,9 @@ const IrSetting = () => { fileIdx={3} /> </FormSection> <div className="flex justify-center mt-1"> - <Button type="submit" onClick={handleAnalysisClick}>침입수분석 실행</Button> + <Button size="sm" type="submit" onClick={handleAnalysisClick}>침입수분석 실행</Button> </div> - </div> + </div> */} </BaseLayout> </> ) diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index 2a9a49dbc..c80c0991e 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -5,7 +5,7 @@ import FileUploadForm from "@/components/ui/fileUploadForm" import FormSection from "@/components/ui/formSection" import BaseLayout from "@/ii/BaseLayout" import { DatePicker, filePicked } from "@common/business_components/ui" -import { Button, Input } from "@common/components/ui" +import { Input } from "@common/components/ui" import { zodResolver } from "@hookform/resolvers/zod" import { Label } from '@radix-ui/react-label' import { useState } from "react" @@ -175,6 +175,7 @@ export default function RTKForm() { // 모든 필수 파일이 선택되었는지 확인 if (!file1?.file || !file2?.file || !file3?.file) { + setIsLoadingResult(false); alert('모든 분석용 파일을 선택해주세요.'); return; } @@ -235,31 +236,31 @@ export default function RTKForm() { cntbArea: parseFloat(formData.rdiiArea) || null, // RDII 기여면적(ha) srcNm: formData.rtkSource, // 출처이름 (RTK계수 출처) - shortPrdR: parseFloat(formData.unitFactors[0]?.R !== undefined ? formData.unitFactors[0]?.R : "0") || null, // 단기간R - shortPrdT: parseFloat(formData.unitFactors[0]?.T !== undefined ? formData.unitFactors[0]?.T : "0") || null, // 단기간T - shortPrdK: parseFloat(formData.unitFactors[0]?.K !== undefined ? formData.unitFactors[0]?.K : "0") || null, // 단기간K + shortPrdR: parseFloat(formData.unitFactors[0]?.R || "0"), + shortPrdT: parseFloat(formData.unitFactors[0]?.T || "0"), + shortPrdK: parseFloat(formData.unitFactors[0]?.K || "0"), - mdPrdR: parseFloat(formData.unitFactors[1]?.R !== undefined ? formData.unitFactors[1]?.R : "0") || null, // 중간기간R - mdPrdT: parseFloat(formData.unitFactors[1]?.T !== undefined ? formData.unitFactors[1]?.T : "0") || null, // 중간기간T - mdPrdK: parseFloat(formData.unitFactors[1]?.K !== undefined ? formData.unitFactors[1]?.K : "0") || null, // 중간기간K + mdPrdR: parseFloat(formData.unitFactors[1]?.R || "0"), + mdPrdT: parseFloat(formData.unitFactors[1]?.T || "0"), + mdPrdK: parseFloat(formData.unitFactors[1]?.K || "0"), - longPrdR: parseFloat(formData.unitFactors[2]?.R !== undefined ? formData.unitFactors[2]?.R : "0") || null, // 장기간R - longPrdT: parseFloat(formData.unitFactors[2]?.T !== undefined ? formData.unitFactors[2]?.T : "0") || null, // 장기간T - longPrdK: parseFloat(formData.unitFactors[2]?.K !== undefined ? formData.unitFactors[2]?.K : "0") || null, // 장기간K + longPrdR: parseFloat(formData.unitFactors[2]?.R || "0"), + longPrdT: parseFloat(formData.unitFactors[2]?.T || "0"), + longPrdK: parseFloat(formData.unitFactors[2]?.K || "0"), - shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax !== undefined ? formData.initialDepths[0]?.Dmax : "0") || null, // 단기간최대깊이 - shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec !== undefined ? formData.initialDepths[0]?.Drec : "0") || null, // 단기간회복비율 - shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do !== undefined ? formData.initialDepths[0]?.Do : "0") || null, // 단기간시작깊이 + shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax || "0"), + shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec || "0"), + shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do || "0"), - mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax !== undefined ? formData.initialDepths[1]?.Dmax : "0") || null, // 중간기간최대깊이 - mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec !== undefined ? formData.initialDepths[1]?.Drec : "0") || null, // 중간기간회복비율 - mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do !== undefined ? formData.initialDepths[1]?.Do : "0") || null, // 중간기간시작깊이 + mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax || "0"), + mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec || "0"), + mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do || "0"), - longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax !== undefined ? formData.initialDepths[2]?.Dmax : "0") || null, // 장기간최대깊이 - longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? formData.initialDepths[2]?.Drec : "0") || null, // 장기간회복비율 - longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? formData.initialDepths[2]?.Do : "0") || null, // 장기간시작깊이 + longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax || "0"), + longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec || "0"), + longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do || "0"), - drtwSwtcRt: parseFloat(formData.rate !== undefined ? formData.rate : "0") || null, // 오수전환율 + drtwSwtcRt: parseFloat(formData.rate || "0"), // 오수전환율 anlsInfo: formData.analysisInfo, // 분석정보 tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과 @@ -322,7 +323,7 @@ export default function RTKForm() { form={form} // react-hook-form 인스턴스 전달 onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit graphBtn={false} - // select 상태 및 핸들러 전달 + analysisBtn={'RDII분석 실행'} enoCd={select1} setEnoCd={setSelect1} ctpvCd={select2} @@ -334,6 +335,7 @@ export default function RTKForm() { selectedSwageProcessingZone={select5} onSwageProcessingZoneChange={setSelect5} isLoading={isLoadingResult} + onAnalysis={handleAnalysisClick} > {[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section"> <div className="grid grid-cols-2 gap-4"> @@ -426,9 +428,9 @@ export default function RTKForm() { <Input {...form.register("analysisInfo")} className="h-12 text-center text-xl w-[225px] ml-5" /> </FormSection> </div> - <div className="flex justify-end mt-1"> - <Button type="submit" onClick={handleAnalysisClick}>RDII분석 실행</Button> - </div> + {/* <div className="flex justify-end mt-1"> + <Button size="sm" type="submit" onClick={handleAnalysisClick}>RDII분석 실행</Button> + </div> */} </div> ]} </BaseLayout > From b00e5c48435a3507ad24cf5c8fab12f1d86e5ced Mon Sep 17 00:00:00 2001 From: YangJinSoo94 <jschrome782@gmail.com> Date: Tue, 10 Jun 2025 15:50:25 +0900 Subject: [PATCH 27/44] =?UTF-8?q?=EB=8F=84=EB=84=9B=EB=B0=94=20=EC=83=89?= =?UTF-8?q?=EC=83=81=EC=9E=85=ED=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/chart/DoughnutChart.tsx | 125 +++++++++++++++--- .../src/ui/chart/LineBarChart.tsx | 3 - 2 files changed, 109 insertions(+), 19 deletions(-) diff --git a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx index f88b35476..d666f1d14 100644 --- a/packages/common/business_components/src/ui/chart/DoughnutChart.tsx +++ b/packages/common/business_components/src/ui/chart/DoughnutChart.tsx @@ -1,27 +1,116 @@ "use client" import { Pie, PieChart } from "recharts"; - +import type { CSSProperties } from "react"; import { Card, CardContent } from "./lib/card"; -import { - ChartContainer, - ChartTooltip, - ChartTooltipContent, - type ChartConfig -} from "./lib/chart"; +import { ChartContainer, ChartTooltip } from "./lib/chart"; -// eslint-disable-next-line react-refresh/only-export-components -export default (props: { chartConfig: ChartConfig; chartData: never[] | undefined; chartTitle:string}) =>{ +type ChartDataItem = { + label: string; + val: number; +}; + +type ChartConfigItem = { + label: string; + fill: string; +}; + +type ChartConfig = { + [key: string]: ChartConfigItem; +}; + +type TooltipPayloadItem = { + name: string; + value: number; + payload: { + label: string; + val: number; + fill: string; + }; +}; + +type LegendEntry = { + value: string; + fill: string; + type?: string; + payload: { + label: string; + val: number; + fill: string; + }; +}; + +type LegendProps = { + payload?: LegendEntry[]; +}; + +/** 차트 기본 스타일 */ +const customStyles = { + legendContainer: { + display: "flex", + justifyContent: "center", + gap: "20px", + fontSize: "9px", + fontWeight: "bold", + marginTop: "10px", + } as CSSProperties, +}; + +// 커스텀 툴팁 +const CustomTooltip = ({ active, payload, label }: { + active?: boolean; + payload?: TooltipPayloadItem[]; + label?: string +}) => { + if (active && payload && payload.length) { + return ( + <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="text-[12px] font-bold m-3">{label}</p> + {payload.map((item, index) => ( + <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> + <div style={{ width: "10px", height: "10px", backgroundColor: item.payload.fill, borderRadius: "50%" }}></div> + <span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span> + </div> + ))} + </div> + ); + } + return null; +}; + +// 커스텀 범례 +const CustomLegend = (props: LegendProps) => { + const { payload } = props; + if (!payload) return null; + + return ( + <div style={customStyles.legendContainer}> + {payload.map((entry: LegendEntry, index: number) => ( + <div key={`legend-${index}`} className="flex items-center gap-2"> + <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.fill }}></div> + <span className="text-[12px]">{entry.value}</span> + </div> + ))} + </div> + ); +}; + +export default function DoughnutChart(props: { + chartConfig: ChartConfig; + chartData: ChartDataItem[] | undefined; + chartTitle: string +}) { return ( <Card className="flex flex-col"> <CardContent className="flex-1 pb-0"> - {props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? ( - <div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}> - {props.chartTitle} - </div>): null} + {props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? ( + <div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}> + {props.chartTitle} + </div> + ) : null} <div className="h-[calc(100%-42px)] justify-center"> <ChartContainer config={props.chartConfig} @@ -30,18 +119,22 @@ export default (props: { chartConfig: ChartConfig; chartData: never[] | undefine <PieChart> <ChartTooltip cursor={false} - content={<ChartTooltipContent hideLabel />} + content={<CustomTooltip />} /> <Pie - data={props.chartData} + data={props.chartData?.map(item => ({ + ...item, + fill: props.chartConfig[item.label]?.fill || 'blue' + }))} dataKey="val" nameKey="label" innerRadius={60} /> + <CustomLegend /> </PieChart> </ChartContainer> </div> </CardContent> </Card> - ) + ); } diff --git a/packages/common/business_components/src/ui/chart/LineBarChart.tsx b/packages/common/business_components/src/ui/chart/LineBarChart.tsx index 2e83458d8..8c7202104 100644 --- a/packages/common/business_components/src/ui/chart/LineBarChart.tsx +++ b/packages/common/business_components/src/ui/chart/LineBarChart.tsx @@ -92,9 +92,6 @@ export default function LineBarchart({ return <div>데이터가 없습니다.</div>; } - const barConfig = chartConfig?.barVal; - const lineConfig = chartConfig?.lineVal; - // 데이터의 최대값 계산 const getMaxValue = (data: any[], keyPrefix: string) => { return Math.max(...data.map(item => From 068c6a08cd2669fa34daac1ce835c4a484f96b4b Mon Sep 17 00:00:00 2001 From: YangJinSoo94 <jschrome782@gmail.com> Date: Tue, 10 Jun 2025 17:34:31 +0900 Subject: [PATCH 28/44] =?UTF-8?q?=EB=8D=94=EB=B8=94=EB=B0=94=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/chart/DoubleBar.tsx | 137 ++++++++++++++++++ .../src/ui/chart/index.tsx | 9 +- 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 packages/common/business_components/src/ui/chart/DoubleBar.tsx diff --git a/packages/common/business_components/src/ui/chart/DoubleBar.tsx b/packages/common/business_components/src/ui/chart/DoubleBar.tsx new file mode 100644 index 000000000..5b136690a --- /dev/null +++ b/packages/common/business_components/src/ui/chart/DoubleBar.tsx @@ -0,0 +1,137 @@ +"use client" +import { Bar, BarChart, CartesianGrid, Legend, XAxis, YAxis } from "recharts"; +import { + ChartContainer, + ChartTooltip, + type ChartConfig +} from "./lib/chart"; +import type { CSSProperties } from "react"; + +const customStyles = { + legendContainer: { + display: "flex", + justifyContent: "center", + gap: "20px", + fontSize: "9px", + fontWeight : "bold", + marginTop: "10px", + } as CSSProperties, +}; + +type BarChartProps = { + chartConfig: ChartConfig | undefined; + chartData: any[] | undefined; + chartTitle: string; + chartClassName?: string; + graphSize?: number; + yFieldText?: string; + showLegend?: boolean; +} + +// 커스텀 툴팁 +const CustomTooltip = ({ + active, + payload, + label +}: { + active?: boolean; + payload?: Array<{ + name: string; + value: number; + color: string; + }>; + label?: string; +}) => { + if (active && payload && payload.length) { + return ( + <div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]"> + <p className="text-[12px] font-bold m-3">{label}</p> + {payload.map((item, index) => ( + <div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}> + {/* 색상 박스 추가 (범례 색상과 동일) */} + <div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div> + <span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span> + </div> + ))} + </div> + ); + } + return null; +}; + +// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가) +const CustomLegend = (props: any) => { + const { payload } = props; + return ( + <div style={customStyles.legendContainer}> + {payload.map((entry: unknown, index: number) => ( + <div key={`legend-${index}`} className="flex items-center gap-2"> + <div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: (entry as {color: string}).color }}></div> + <span className="text-[12px]">{(entry as {value: string}).value}</span> + </div> + ))} + </div> + ); +}; + +export default function DoubleBarChart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) { + const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#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()); + }; + console.log(chartData); + const barMaxValue = getMaxValue(chartData, 'barVal'); + return ( + <> + {chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? ( + <div className="!mt-3 text-[2.5rem] font-bold"> + {chartTitle} + </div>): null} + <div className="flex justify-center"> + <ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}> + <BarChart accessibilityLayer data={chartData}> + <CartesianGrid vertical={false} /> + <XAxis + dataKey="label" + tickLine={true} + tickMargin={10} + axisLine={true} + fontSize={13} + fontWeight={500} + /> + <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} + /> + <ChartTooltip cursor={false} content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />}/> + {chartData && chartData.length > 0 && ( + Object.keys(chartData[0]) + .filter(key => key !== "label") + .map((key, idx) => ( + <Bar + dataKey={key} + radius={5} + key={key} + stackId="a" + barSize={graphSize} + fill={colors[idx]} + /> + )) + )} + {showLegend && <Legend content={CustomLegend}/>} + </BarChart> + </ChartContainer> + </div> + </> + ) +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/chart/index.tsx b/packages/common/business_components/src/ui/chart/index.tsx index 0d17a99b1..4c58549cc 100644 --- a/packages/common/business_components/src/ui/chart/index.tsx +++ b/packages/common/business_components/src/ui/chart/index.tsx @@ -5,6 +5,7 @@ import DoughnutChart from './DoughnutChart'; import { type ChartConfig } from './lib/chart'; import LineChart from './LineChart'; import LineBarchart from './LineBarChart'; +import DoubleBarChart from './DoubleBar'; /**차트 FORM */ const CHART_FORM = { @@ -15,7 +16,9 @@ const CHART_FORM = { /**도넛 차트 형태*/ DN :'daughnut', /**LINE,BAR차트 */ - LINEBAR : 'linebar' + LINEBAR : 'linebar', + /**더블바 차트 형태*/ + DOUBLEBAR : 'doublebar' }; /** @@ -61,6 +64,10 @@ function MainChart(props: MainChartProps) { return ( <LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}/> ); + } else if (props.chartForm == CHART_FORM.DOUBLEBAR){ + return ( + <DoubleBarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/> + ); } } From c2fae1bf4bbe18791d3777e8d6e11c2bc70acef6 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 <jschrome782@gmail.com> Date: Tue, 10 Jun 2025 18:59:56 +0900 Subject: [PATCH 29/44] =?UTF-8?q?=EB=8D=94=EB=B8=94=EB=B0=94=20=ED=83=80?= =?UTF-8?q?=EC=9D=B4=ED=8B=80=ED=81=AC=EA=B8=B0=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/business_components/src/ui/chart/DoubleBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/common/business_components/src/ui/chart/DoubleBar.tsx b/packages/common/business_components/src/ui/chart/DoubleBar.tsx index 5b136690a..9627485aa 100644 --- a/packages/common/business_components/src/ui/chart/DoubleBar.tsx +++ b/packages/common/business_components/src/ui/chart/DoubleBar.tsx @@ -88,8 +88,8 @@ export default function DoubleBarChart({chartConfig=undefined, chartData=[], cha return ( <> {chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? ( - <div className="!mt-3 text-[2.5rem] font-bold"> - {chartTitle} + <div className="text-left text-[13px] font-bold mt-[13px] mb-[10px]"> + {chartTitle} </div>): null} <div className="flex justify-center"> <ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}> From a27a14e319aa555dcf1ed123376b3380c6c1db0d Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Wed, 11 Jun 2025 09:39:05 +0900 Subject: [PATCH 30/44] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=EC=9E=84?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/featureClickEventPopup.tsx | 2 +- .../common/business_components/src/ui/map/lib/gis.select.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/map/featureClickEventPopup.tsx index 9b431f23c..24174585c 100644 --- a/apps/sn/app/map/featureClickEventPopup.tsx +++ b/apps/sn/app/map/featureClickEventPopup.tsx @@ -158,7 +158,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, <div className="card-header bg-primary flex items-center justify-between p-4"> <div className="flex mx-3 flex-col"> <h3 className="text-2xl font-medium text-white text-label">{layerNm} 정보</h3> - <div className="font-medium text-white text-label">(00구 00리 하수관로)</div> + {/* <div className="font-medium text-white text-label">(00구 00리 하수관로)</div> */} </div> <button onClick={onClose} diff --git a/packages/common/business_components/src/ui/map/lib/gis.select.js b/packages/common/business_components/src/ui/map/lib/gis.select.js index 7f52ab2a3..7586ef3fb 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.select.js +++ b/packages/common/business_components/src/ui/map/lib/gis.select.js @@ -1,5 +1,4 @@ import GisApp from './gis.core'; -import _ from 'lodash'; import Style from 'ol/style/Style.js'; import { click, mouseOnly, never } from 'ol/events/condition'; import DragBox from 'ol/interaction/DragBox.js'; @@ -11,7 +10,6 @@ import VectorSource from 'ol/source/Vector.js'; import CircleStyle from 'ol/style/Circle.js'; import Fill from 'ol/style/Fill.js'; import Stroke from 'ol/style/Stroke.js'; -//import DOMPurify from 'dompurify'; const GisOlSelect = function (map, options) { GisApp.select = {}; From 97eabbcffcb74ac0397ea7ffe6db9d166cc70517 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Wed, 11 Jun 2025 10:19:00 +0900 Subject: [PATCH 31/44] =?UTF-8?q?fix=20:=20EditableFormTable=20import=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/rdii/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index c80c0991e..2f61b57b9 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -12,7 +12,7 @@ import { useState } from "react" import { useFieldArray, useForm } from "react-hook-form" import { z } from "zod" import { excelToMatchingResultData, excelToRainData } from '../../components/lib/utils/excelToJson' -import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/editableFormTable' +import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable' // zod 스키마 정의 From a4f9842ed0a7c5943636bdeb67373f81c4eac026 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Wed, 11 Jun 2025 10:27:40 +0900 Subject: [PATCH 32/44] =?UTF-8?q?feat:=20I/I=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EB=82=B4=EB=A6=BC=EC=B0=A8=EC=88=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/ii/rdii_result/page.tsx | 2 +- apps/sn/app/ii/result/page.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/ii/rdii_result/page.tsx index dba3a1232..8c799dc31 100644 --- a/apps/sn/app/ii/rdii_result/page.tsx +++ b/apps/sn/app/ii/rdii_result/page.tsx @@ -85,7 +85,7 @@ const Result = () => { const queryParams = new URLSearchParams({ page: '0', size: '10', - sort: 'rainInfwtAnlsSn' + sort: 'rainInfwtAnlsSn,desc' }); Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index 99f0f902a..e41915593 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -84,7 +84,7 @@ const Result = () => { const queryParams = new URLSearchParams({ page: '0', size: '10', - sort: 'inftwtAnlsSn' + sort: 'inftwtAnlsSn,desc' }); Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 From 9ecc43f6f3d62ff9c95d4b4300453a20115a2a95 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Wed, 11 Jun 2025 10:58:42 +0900 Subject: [PATCH 33/44] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/layout.js | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 apps/sn/app/layout.js diff --git a/apps/sn/app/layout.js b/apps/sn/app/layout.js deleted file mode 100644 index 8525f5f8c..000000000 --- a/apps/sn/app/layout.js +++ /dev/null @@ -1,12 +0,0 @@ -export const metadata = { - title: 'Next.js', - description: 'Generated by Next.js', -} - -export default function RootLayout({ children }) { - return ( - <html lang="en"> - <body>{children}</body> - </html> - ) -} From f12c0d5ee2e99f6fcee9c332f2c87a0c570a655b Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Wed, 11 Jun 2025 17:40:43 +0900 Subject: [PATCH 34/44] =?UTF-8?q?feat:=20I/I=20=EB=B6=84=EC=84=9D=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EC=B2=A8=EB=B6=80=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC(.inp)=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/api/attachment/attachment-service.tsx | 1 + .../components/lib/utils/fileDeleteUtil.tsx | 109 ++++++++++++++++++ apps/sn/app/ii/rdii_result/page.tsx | 7 ++ apps/sn/app/ii/result/page.tsx | 7 ++ 4 files changed, 124 insertions(+) create mode 100644 apps/sn/app/components/lib/utils/fileDeleteUtil.tsx diff --git a/apps/sn/app/components/lib/api/attachment/attachment-service.tsx b/apps/sn/app/components/lib/api/attachment/attachment-service.tsx index 36390c0fb..ee3fb6cd9 100644 --- a/apps/sn/app/components/lib/api/attachment/attachment-service.tsx +++ b/apps/sn/app/components/lib/api/attachment/attachment-service.tsx @@ -16,6 +16,7 @@ export const ENDPOINT = { ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", DOWNLOAD: "/attach-service/api/v1/atflcd/download", STREAM_VIDEO: "/attach-service/api/v1/stream/video", + DELETE_FILE_BY_ATFLCD: "/attach-service/api/v1/atfl/attachments/atflCd", }, }; diff --git a/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx new file mode 100644 index 000000000..bf8a70130 --- /dev/null +++ b/apps/sn/app/components/lib/utils/fileDeleteUtil.tsx @@ -0,0 +1,109 @@ +import { getCookie } from "@utils"; +import { ENDPOINT } from "../api/attachment/attachment-service"; + +const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; +const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization"; + +type DeleteOptions = { + handleDeleteStart?: () => void; + handleDeleteSuccess?: (response: Response) => void; + handleDeleteError?: (error: Error) => void; +}; + +export const handleDelete = async ( + atflCd: string, + options?: DeleteOptions +): Promise<void> => { + if (!atflCd) { + const error = new Error("첨부파일 코드가 필요합니다."); + options?.handleDeleteError?.(error); + throw error; + } + + try { + options?.handleDeleteStart?.(); + + const response = await fetch( + `${ENDPOINT.ATTACH_SERVICE.DELETE_FILE_BY_ATFLCD}/${atflCd}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + [CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "", + [AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "", + }, + } + ); + + if (!response.ok) { + throw new Error(`파일 삭제 실패: ${response.status} ${response.statusText}`); + } + + options?.handleDeleteSuccess?.(response); + console.log("파일 삭제 완료:", atflCd); + + } catch (error) { + const deleteError = error instanceof Error + ? error + : new Error("파일 삭제 중 알 수 없는 오류가 발생했습니다."); + + console.error("파일 삭제 오류:", deleteError); + options?.handleDeleteError?.(deleteError); + throw deleteError; + } +}; + +// 여러 파일을 삭제하는 함수 +// export const handleMultipleDelete = async ( +// atflCds: string[], +// options?: DeleteOptions & { +// handleProgress?: (completed: number, total: number) => void; +// } +// ): Promise<void> => { +// if (!atflCds || atflCds.length === 0) { +// const error = new Error("삭제할 파일이 없습니다."); +// options?.handleDeleteError?.(error); +// throw error; +// } + +// try { +// options?.handleDeleteStart?.(); + +// const deletePromises = atflCds.map(async (atflCd, index) => { +// try { +// await handleDelete(atflCd); +// options?.handleProgress?.(index + 1, atflCds.length); +// return { success: true, atflCd }; +// } catch (error) { +// return { success: false, atflCd, error }; +// } +// }); + +// const results = await Promise.allSettled(deletePromises); +// const failedDeletes = results +// .filter((result): result is PromiseFulfilledResult<{ success: false; atflCd: string; error: any }> => +// result.status === 'fulfilled' && !result.value.success +// ) +// .map(result => result.value); + +// if (failedDeletes.length > 0) { +// const error = new Error( +// `일부 파일 삭제 실패: ${failedDeletes.map(f => f.atflCd).join(', ')}` +// ); +// options?.handleDeleteError?.(error); +// throw error; +// } + +// options?.handleDeleteSuccess?.(new Response()); +// console.log("모든 파일 삭제 완료:", atflCds); + +// } catch (error) { +// const deleteError = error instanceof Error +// ? error +// : new Error("파일 삭제 중 알 수 없는 오류가 발생했습니다."); + +// console.error("파일 삭제 오류:", deleteError); +// options?.handleDeleteError?.(deleteError); +// throw deleteError; +// } +// }; \ No newline at end of file diff --git a/apps/sn/app/ii/rdii_result/page.tsx b/apps/sn/app/ii/rdii_result/page.tsx index 8c799dc31..130c25419 100644 --- a/apps/sn/app/ii/rdii_result/page.tsx +++ b/apps/sn/app/ii/rdii_result/page.tsx @@ -6,6 +6,7 @@ import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import BaseLayout from '../BaseLayout' +import { handleDelete } from '@/components/lib/utils/fileDeleteUtil' const formSchema = z.object({ @@ -171,6 +172,12 @@ const Result = () => { // 삭제 API 호출 const response1 = await deleteData(`v1/rdiiAnlsRslt/${selectedRowData.rainInfwtAnlsSn}`); const response2 = await deleteData(`v1/rdiiAnls/${selectedRowData.rainInfwtAnlsSn}`); + // 첨부파일(inp파일) 삭제 API 호출 + handleDelete(selectedRowData.atflCd, { + handleDeleteStart: () => console.log("파일 삭제 시작"), + handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response), + handleDeleteError: (error) => console.error("파일 삭제 실패:", error), + }); console.log("File delete response1:", response1); console.log("File delete response2:", response2); diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index e41915593..e2015c8d9 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -6,6 +6,7 @@ import { useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import BaseLayout from '../BaseLayout' +import { handleDelete } from '@/components/lib/utils/fileDeleteUtil' const formSchema = z.object({ @@ -171,6 +172,12 @@ const Result = () => { // 삭제 API 호출 const response1 = await deleteData(`v1/inftwtAnlsRslt/${selectedRowData.inftwtAnlsSn}`); const response2 = await deleteData(`v1/inftwtAnls/${selectedRowData.inftwtAnlsSn}`); + // 첨부파일(inp파일) 삭제 API 호출 + handleDelete(selectedRowData.atflCd, { + handleDeleteStart: () => console.log("파일 삭제 시작"), + handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response), + handleDeleteError: (error) => console.error("파일 삭제 실패:", error), + }); console.log("File delete response1:", response1); console.log("File delete response2:", response2); From 7b8f76c78d5c765a2cf11eea947aab3f4e827fee Mon Sep 17 00:00:00 2001 From: YangJinSoo94 <jschrome782@gmail.com> Date: Thu, 12 Jun 2025 09:35:27 +0900 Subject: [PATCH 35/44] =?UTF-8?q?-=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=ED=99=94=20=EC=A7=84=ED=96=89=20-=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=B4=EB=82=98=20=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=20=EC=88=98=EC=9A=94=EC=B2=98=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EC=9D=B4=ED=9B=84=20=EC=88=98=EC=9A=94=EC=B2=98=20=EC=84=B8?= =?UTF-8?q?=ED=8C=85=EC=99=84=EB=A3=8C(=EB=A1=9C=EA=B7=B8=EC=9D=B8=20>=20?= =?UTF-8?q?=EC=88=98=EC=9A=94=EC=B2=98=20=EB=93=B1=EB=A1=9D=20=ED=98=B9?= =?UTF-8?q?=EC=9D=80=20=EB=B9=88=EC=B9=B8)=20-=20=EC=88=98=EC=9A=94?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=93=B1=EB=A1=9D=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business_components/src/ui/form_ui/index.tsx | 2 +- .../src/ui/sdmInsertForm/form.tsx | 13 ++++++------- .../src/ui/sdmInsertForm/index.tsx | 2 -- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/common/business_components/src/ui/form_ui/index.tsx b/packages/common/business_components/src/ui/form_ui/index.tsx index de4b37f61..8f0ce136a 100644 --- a/packages/common/business_components/src/ui/form_ui/index.tsx +++ b/packages/common/business_components/src/ui/form_ui/index.tsx @@ -39,7 +39,7 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>( checked={isChecked} onCheckedChange={handleCheckboxChange} /> - <label className="w-20 text-xl text-gray-600" htmlFor="check">해당없음</label> + <label className="w-28 text-xl text-gray-600" htmlFor="check">해당없음</label> </div> </div> {children} diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index c5c0ee0db..f5ff20e1d 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -124,7 +124,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun form.setValue('dmdCoNm', text); }, [form]); - const bznNoChange = useCallback((e: React.ChangeEvent<HTMLInputElement>, noType: string) => { + const bznNoChange = useCallback((e: React.ChangeEvent<HTMLInputElement>, noType: string, field?: ControllerRenderProps<FormSchema, keyof FormSchema>) => { const value = e.target.value; if(noType === 'dmdInfoPk') { @@ -133,17 +133,16 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun } else { setIsDuplicate(false); } - form.setValue('dmdInfoPk', value); + field?.onChange(value); } else { if(orgBznmNo.substring(10,14) === value) { setIsDuplicate(true); } else { setIsDuplicate(false); } - form.setValue('bznmTotNo', value); + field?.onChange(value); } - - }, [form, orgBznmNo, setIsDuplicate]); + }, [orgBznmNo, setIsDuplicate]); const formFields: Array<{ name: keyof FormSchema; @@ -202,7 +201,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun type="text" className="!text-xl text-gray-600" {...field} - onChange={(e) => bznNoChange(e, 'dmdInfoPk')} + onChange={(e) => bznNoChange(e, 'dmdInfoPk', field)} /> ) }, @@ -221,7 +220,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun className="!mt-5 !text-xl text-gray-600" disabled={isBznmTotNoDisabled} {...field} - onChange={(e) => bznNoChange(e, 'bznmTotNo')} + onChange={(e) => bznNoChange(e, 'bznmTotNo', field)} /> </FormUi> ) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 4b4a94648..767f3dd56 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -176,8 +176,6 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList telno: data.telno, }); - //router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`); - toast.success('수요처가 성공적으로 등록되었습니다.'); } catch (error) { console.error('Error submitting form:', error); toast.error('수요처 등록에 실패했습니다.'); From 2fc50d614e8da5f0912360126e0a1c4c4770152f Mon Sep 17 00:00:00 2001 From: YangJinSoo94 <jschrome782@gmail.com> Date: Thu, 12 Jun 2025 11:07:42 +0900 Subject: [PATCH 36/44] =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/sdmInsertForm/index.tsx | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 767f3dd56..f66aadadf 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -14,16 +14,41 @@ import type { SelectDataType } from "../search_input_select"; export type DemandCompType = { - dmdInfoPk : string | ''; - bzmnNo : string | ''; - dmdCoNm : string | ''; - daddr : string | ''; - telno : string | ''; - rgtr : string | ''; - useUsgCd : string | ''; - ctpvCd : string | ''; - sggCd : string | ''; - dmdAplyPswd: string | ''; + dmdInfoPk: string; + bzmnNo: string; + dmdCoNm: string; + daddr: string; + telno: string; + rgtr: string; + useUsgCd: string; + ctpvCd: string; + sggCd: string; + dmdAplyPswd: string; + dmdInfoFk: string; + wntUntprc: number | ""; + dmndTmng: "" | Date; + dmndQty: number | ""; + bznmTotNo: string; + grssClfg: number | ""; + tn: number | ""; + combResdch: number | ""; + tph: number | ""; + useYn: string; + delYn: string; + regDt: string; + updtDt: string; + updtr: string; + trbt: number | ""; + ph: number | ""; + blgcOxygDmndQnt: number | ""; + clIonQnt: number | ""; + dmndQnt: number | ""; + dmndUntprc: number | ""; + dmndAmt: number | ""; + smll: number | ""; + cdtnv: number | ""; + crmty: number | ""; + rmrk: string; } @@ -60,16 +85,13 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => .max(12, "전화번호는 12자 이내로 입력해주세요") .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), rgtr: z.string().optional(), - dmdAplyPswd : z.string() - .min(1, "패스워드를 입력해주세요") - .min(8, "패스워드는 8자 이상 입력해주세요") - .max(16, "패스워드는 16자 이내로 입력해주세요"), + dmdAplyPswd: z.string().optional() + .refine((val) => !val || (val.length >= 8 && val.length <= 16), "패스워드는 8자 이상 16자 이내로 입력해주세요"), }); -type FormSchema = ReturnType<typeof createFormSchema>; -export type FormData = z.infer<FormSchema>; +type FormSchema = z.infer<ReturnType<typeof createFormSchema>>; -const defaultValues: FormData = { +const defaultValues: FormSchema = { ctpvCd: '', sggCd: '', dmdCoNm: '', @@ -78,7 +100,7 @@ const defaultValues: FormData = { daddr: '', telno: '', rgtr: "3fa85f64-5717-4562-b3fc-2c963f66afa6", - dmdAplyPswd : '', + dmdAplyPswd: '', }; /** @@ -125,7 +147,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList const [isDuplicate, setIsDuplicate] = useState(false); const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate); - const form = useForm<z.infer<FormSchema>>({ + const form = useForm<FormSchema>({ resolver: zodResolver(formSchema), defaultValues, mode: "onChange", @@ -146,7 +168,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList rtnValue(searchText, pageCnt); }, [rtnValue]); - const onSubmit: SubmitHandler<z.infer<FormSchema>> = async (data) => { + const onSubmit: SubmitHandler<FormSchema> = async (data) => { try { // form 에러 체크 const formErrors = form.formState.errors; @@ -160,10 +182,10 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList const formData = { ...data, dmdInfoPk, + bzmnNo: data.dmdInfoPk, }; console.log(formData); - /* await insertSdmData(formData); */ rtnInputData(formData as DemandCompType); const queryParams = new URLSearchParams({ From 245ad6ebbedd48cbd3ceea797da9d10f1c71e8c4 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 12 Jun 2025 11:17:55 +0900 Subject: [PATCH 37/44] =?UTF-8?q?config=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/page.tsx | 2 +- apps/sn/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/map/page.tsx b/apps/sn/app/map/page.tsx index 7185c9660..e1a90d5f1 100644 --- a/apps/sn/app/map/page.tsx +++ b/apps/sn/app/map/page.tsx @@ -10,7 +10,7 @@ import FeatureClickEventPopup from "./featureClickEventPopup"; const Map = () => { const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정 const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태 - const gisMaps = useRef(null); + const gisMaps = useRef<any | null>(null); const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태 const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터 diff --git a/apps/sn/tsconfig.json b/apps/sn/tsconfig.json index 5ed74c59e..a65fab364 100644 --- a/apps/sn/tsconfig.json +++ b/apps/sn/tsconfig.json @@ -33,7 +33,8 @@ "@common/network": ["../../../packages/common/network/"], "@store": ["./store"], "@service": ["./service"], - "@utils": ["./components/lib/utils"] + "@utils": ["./components/lib/utils"], + "@common/config": ["../../../packages/common/config/dist/"] } }, "include": [ From aea80936e4a2bde6abb0a3fe7f3d607d667d4e67 Mon Sep 17 00:00:00 2001 From: YangJinSoo94 <jschrome782@gmail.com> Date: Thu, 12 Jun 2025 13:40:47 +0900 Subject: [PATCH 38/44] =?UTF-8?q?=EB=B9=8C=EB=93=9C=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(=ED=94=84=EB=A1=9C=ED=8D=BC=ED=8B=B0=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/sdmInsertForm/index.tsx | 51 +------------------ 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index f66aadadf..451997cd2 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -11,46 +11,7 @@ import { toast, Toaster } from "sonner"; /* import { Toaster, toast } from "react-hot-toast"; */ import * as z from "zod"; import type { SelectDataType } from "../search_input_select"; - - -export type DemandCompType = { - dmdInfoPk: string; - bzmnNo: string; - dmdCoNm: string; - daddr: string; - telno: string; - rgtr: string; - useUsgCd: string; - ctpvCd: string; - sggCd: string; - dmdAplyPswd: string; - dmdInfoFk: string; - wntUntprc: number | ""; - dmndTmng: "" | Date; - dmndQty: number | ""; - bznmTotNo: string; - grssClfg: number | ""; - tn: number | ""; - combResdch: number | ""; - tph: number | ""; - useYn: string; - delYn: string; - regDt: string; - updtDt: string; - updtr: string; - trbt: number | ""; - ph: number | ""; - blgcOxygDmndQnt: number | ""; - clIonQnt: number | ""; - dmndQnt: number | ""; - dmndUntprc: number | ""; - dmndAmt: number | ""; - smll: number | ""; - cdtnv: number | ""; - crmty: number | ""; - rmrk: string; -} - +import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; type thisProps = { selectSidoData: SelectDataType[]; @@ -188,16 +149,6 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList console.log(formData); rtnInputData(formData as DemandCompType); - const queryParams = new URLSearchParams({ - dmdCoNm: data.dmdCoNm, - ctpvCd: data.ctpvCd, - sggCd: data.sggCd, - dmdInfoPk, - bznmTotNo: data.bznmTotNo || '', - daddr: data.daddr, - telno: data.telno, - }); - } catch (error) { console.error('Error submitting form:', error); toast.error('수요처 등록에 실패했습니다.'); From 55f6440ac6d12dee2ffda3400e3a358004530cc3 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 12 Jun 2025 16:23:48 +0900 Subject: [PATCH 39/44] =?UTF-8?q?=EA=B5=AD=ED=86=A0=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=ED=94=8C=EB=A0=9B=ED=8F=BC=20=EB=B0=B0=EA=B2=BD=20=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/map/lib/gis.core.js | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/packages/common/business_components/src/ui/map/lib/gis.core.js b/packages/common/business_components/src/ui/map/lib/gis.core.js index caa068f76..db58c898f 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.core.js +++ b/packages/common/business_components/src/ui/map/lib/gis.core.js @@ -13,10 +13,12 @@ import { Fill, Stroke, Style } from 'ol/style'; import CircleStyle from 'ol/style/Circle'; import VectorLayer from 'ol/layer/Vector'; import { Point } from 'ol/geom'; -import { boundingExtent } from 'ol/extent'; +import { getTopLeft } from 'ol/extent'; import TileWMTS from 'ol/source/WMTS'; -import WMTS from 'ol/tilegrid/WMTS'; -import { get as getProjection } from 'ol/proj'; +import WMTSTileGrid from 'ol/tilegrid/WMTS'; +// import { get as getProjection } from 'ol/proj'; +import proj4 from 'proj4'; +import { register } from 'ol/proj/proj4'; import olSourceImageWMS from 'ol/source/ImageWMS'; import { GeoJSON } from 'ol/format'; import GisOlEditLayer from './gis.edit'; @@ -43,8 +45,8 @@ GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&k GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`; // TODO: NGII 정보 -GisApp.NGII_API_KEY = '305CC242F2920F46BB93BCC13A52DD8A'; // NGII에서 발급받은 API 키 -GisApp.NGII_WMTS_URL = `http://map.ngii.go.kr/openapi/wmts_ngiiMap.js?apikey=${GisApp.NGII_API_KEY}`; +GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키 +GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`; GisApp.EMAP_URL = '/emap/'; GisApp.EMAP_SATELLITE_URL = '/emap/satellite/'; @@ -92,9 +94,9 @@ GisApp.core.prototype = { target: '', centerX: 37.2806094, centerY: 127.0032502, - zoom: 11, - minZoom: 5, - maxZoom: 22, + zoom: 12, + minZoom: 6, + maxZoom: 20, }, options: { @@ -121,14 +123,13 @@ GisApp.core.prototype = { GisApp.Module.layer = new GisOlLayer(this.defaults); GisApp.Module.edit = new GisOlEditLayer(this.defaults); // TODO: vworld layer 정보 (getNGIIbaseMap)활성화 하면 ngii base map으로 변경됨 - this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base'); - this.baseLayers['base'] = this.getBaseMap('base'); - this.baseLayers['satellite'] = this.getBaseMap('satellite'); - this.baseLayers['3d'] = this.getBaseMap('3d'); - // this.baseLayers['3d'] = this.getNGIIBaseMap('3d'); - // this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base'); - // this.baseLayers['base'] = this.getNGIIBaseMap('base'); - // this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite'); + // this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base'); + // this.baseLayers['base'] = this.getBaseMap('base'); + // this.baseLayers['satellite'] = this.getBaseMap('satellite'); + // this.baseLayers['3d'] = this.getBaseMap('3d'); + this.baseLayers['base'] = this.getNGIIBaseMap('base','korean_map'); + this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite','satellite_map'); + this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base','korean_map'); }, layerType: { point: 'POINT', @@ -142,44 +143,43 @@ GisApp.core.prototype = { this.setZoom(15); }, // NGII Base Map - getNGIIBaseMap: function (_id) { - const layers = { - base: 'korean_map', // 일반지도 - satellite: 'satellite_map', // 위성지도 - night: 'night_map', // 야간지도 - }; - // 선택된 레이어 - const layerName = layers[_id] || layers.base; - // EPSG:5179 좌표계 설정 - const projection = getProjection('EPSG:5179'); - const extent = [705680.0000, 1349270.0000,1388291.0000, 2581448.0000]; // NGII 좌표계 범위 - const resolutions = [ - 2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255, - ]; - const tileGrid = new WMTS({ - extent: extent, - resolutions: resolutions, - matrixIds: resolutions.map((_, index) => index.toString()), // WMTS matrix IDs - }); + getNGIIBaseMap: function (_id, _layer) { + // EPSG:5179 정의 및 등록이 필요함 (아래에 설명 추가) + proj4.defs([['EPSG:5179','+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',],]); + register(proj4); + const projection = olProj.get('EPSG:5179'); + projection.setExtent([-200000.0, -28024123.62, 31824123.62, 4000000.0]); + + // const resolutions = [2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255]; + // const matrixIds = ["L05", "L06", "L07", "L08", "L09", "L10", "L11", "L12", "L13", "L14", "L15", "L16", "L17", "L18"]; + const resolutions = [1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255]; + const matrixIds = ["L06", "L07", "L08", "L09", "L10", "L11", "L12", "L13", "L14", "L15", "L16", "L17", "L18"]; - // WMTS 소스 생성 const wmtsSource = new TileWMTS({ url: GisApp.NGII_WMTS_URL, - layer: layerName, + layer: _layer, matrixSet: 'EPSG:5179', format: 'image/png', projection: projection, - tileGrid: tileGrid, - style: 'default', - crossOrigin: 'anonymous', + style: 'korean', + tileGrid: new WMTSTileGrid({ + origin: getTopLeft(projection.getExtent()), + resolutions: resolutions, + matrixIds: matrixIds, + tileSize: [256, 256] + }), + requestEncoding: 'KVP', + crossOrigin: 'anonymous' }); - // TileLayer 생성 - return new TileLayer({ - source: wmtsSource, + let baseMap = new TileLayer({ + id: _id, visible: true, - zIndex: 0, + type: 'base', + source: wmtsSource, + zIndex: 0 }); + return baseMap; }, getBaseMap: function (_id) { let url = GisApp.VWORLD_URL; From 9d31cc3cdb0b2a4b77c1842a1467ce74ddab8e58 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 12 Jun 2025 18:13:03 +0900 Subject: [PATCH 40/44] =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/service/map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sn/app/service/map.ts b/apps/sn/app/service/map.ts index 062cfeedb..83c6a937e 100644 --- a/apps/sn/app/service/map.ts +++ b/apps/sn/app/service/map.ts @@ -32,6 +32,6 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 getLyrStngs: () => { - return axios.get(`${API_URL}/lyrStngs`) + return axios.get(`${API_URL}/lyrStngs/WR_NETWORK`) } } From 59ec32fdf85244519bc8b50bc393d841ab5d6019 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 12 Jun 2025 19:43:15 +0900 Subject: [PATCH 41/44] =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/service/map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sn/app/service/map.ts b/apps/sn/app/service/map.ts index 83c6a937e..2182e88c8 100644 --- a/apps/sn/app/service/map.ts +++ b/apps/sn/app/service/map.ts @@ -32,6 +32,6 @@ export type LyrStngProps = { export const mapService = { //레이어 설정 조회 getLyrStngs: () => { - return axios.get(`${API_URL}/lyrStngs/WR_NETWORK`) + return axios.get(`${API_URL}/lyrStngs/SN_NETWORK`) } } From c80c9203d87967b71bc2868d7ee3dcd71a140f8f Mon Sep 17 00:00:00 2001 From: leedj312 <dlehd467@gmail.com> Date: Thu, 12 Jun 2025 20:15:36 +0900 Subject: [PATCH 42/44] =?UTF-8?q?=EC=88=98=EC=9A=94=EC=B2=98=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/sdmInsertForm/form.tsx | 119 ++++++++++++++++-- .../src/ui/sdmInsertForm/index.tsx | 34 ++--- .../src/ui/sdmInsertForm/sdmNameBox.tsx | 8 +- 3 files changed, 132 insertions(+), 29 deletions(-) diff --git a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx index f5ff20e1d..6ccaac946 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/form.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -2,10 +2,43 @@ import { SelectBox } from "../select"; import { Input } from "@common/components"; import { FormUi } from "../form_ui"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { ControllerRenderProps, UseFormReturn } from "react-hook-form"; import { SdmSearchForm } from "./sdmNameBox"; -import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; +import { set } from "date-fns"; + +// 수요처 정보 +export type DemandCompType = { + index : number | ''; + dmdInfoPk : string | ''; + dmdInfoFk : string | ''; + bzmnNo : string | ''; + dmdCoNm : string | ''; + daddr : string | ''; + telno : string | ''; + rgtr : string | ''; + useUsgCd : string | ''; + ctpvCd : string | ''; + sggCd : string | ''; + dmdAplyPswd : string | ''; + wntUntprc : number | ''; + dmndTmng : Date | ''; + dmndQty : number | ''; + grssClfg : number | ''; + tn : number | ''; + combResdch : number | ''; + tph : number | ''; + trbt : number | ''; + ph : number | ''; + blgcOxygDmndQnt : number | ''; + clIonQnt : number | ''; + smll : number | ''; + cdtnv : number | ''; + crmty : number | ''; + rmrk : string | ''; + reqDt : string | ''; +} + type SelectDataType = { text: string; @@ -32,16 +65,19 @@ interface SdmRegFormProps { dataList: DemandCompType[]; rtnValue: (searchText: string, pageCnt: number) => void; setIsDuplicate: (value: boolean) => void; + updateList?:DemandCompType } type FieldComponent = (field: ControllerRenderProps<FormSchema, keyof FormSchema>) => JSX.Element; -export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigunData, dataList, rtnValue, setIsDuplicate }: SdmRegFormProps) { +export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigunData, dataList, rtnValue, setIsDuplicate ,updateList}: SdmRegFormProps) { const [sidoValue, setSidoValue] = useState<string>(''); const [sigunValue, setSigunValue] = useState<string>(''); const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([{ text: "전체", val: '0' }]); const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); const [orgBznmNo, setOrgBznmNo] = useState<string>(''); + const [updateCoNm, setUpdateNm] = useState<string >('') + const initializeSigunData = useCallback((sidoCd: string) => { if (sidoCd === '0') { @@ -62,6 +98,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun type: string, field: ControllerRenderProps<FormSchema, keyof FormSchema> | undefined ) => { + if(!updateList?.dmdInfoPk) switch (type) { case "ctpvCd": initializeSigunData(val); @@ -77,7 +114,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun form.setValue('bznmTotNo', ''); break; } - }, [initializeSigunData, form]); + }, [initializeSigunData, form,updateList]); const handleCheckboxChange = useCallback((checked: boolean) => { setIsBznmTotNoDisabled(checked); @@ -90,6 +127,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const setRtnData = useCallback((item: DemandCompType) => { const rtnCompData = item; + form.setValue('dmdCoNm', rtnCompData?.dmdCoNm || ''); form.setValue('dmdInfoPk', rtnCompData?.dmdInfoPk.substring(0,10) || ''); form.setValue('bznmTotNo', rtnCompData?.dmdInfoPk.substring(10,14)==='0000' ? '' : rtnCompData?.dmdInfoPk.substring(10,14)); @@ -126,7 +164,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun const bznNoChange = useCallback((e: React.ChangeEvent<HTMLInputElement>, noType: string, field?: ControllerRenderProps<FormSchema, keyof FormSchema>) => { const value = e.target.value; - + if(noType === 'dmdInfoPk') { if(orgBznmNo.substring(0,10) === value) { setIsDuplicate(true); @@ -144,6 +182,27 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun } }, [orgBznmNo, setIsDuplicate]); + + useEffect(() => { + if(updateList?.dmdInfoPk){ + form.reset(updateList) + if(updateList?.dmdInfoPk.slice(10,14) !== '0000') { + handleCheckboxChange( false); + form.setValue('bznmTotNo', updateList?.dmdInfoPk.slice(10,14)); + } else { + handleCheckboxChange( true); + } + form.setValue('dmdInfoPk', updateList?.dmdInfoPk.slice(0,10)); + setUpdateNm(updateList?.dmdCoNm) + // setSigunValue(updateList?.sggCd) + }else{ + handleCheckboxChange( true); + } + }, [updateList,form]); + + useEffect(() => { + },[form.formState.errors]) + const formFields: Array<{ name: keyof FormSchema; label: string; @@ -153,28 +212,51 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun name: "ctpvCd", label: "시도", component: (field) => ( - <SelectBox + updateList?.dmdInfoPk ? ( + <Input + placeholder="시도" + type="text" + className="!text-xl text-gray-600" + onChange={(e) =>{field.onChange(e.target.value)} } + value={ selectSidoData.find(item => item.val === field.value?.trim() )?.text??''} + readOnly={!!updateList?.dmdInfoPk} + /> + ) : ( + <SelectBox selectClass={undefined} label="" selectArray={selectSidoData} - onSelectValue={(val) => onSelectValue(val.toString(), 'ctpvCd', field)} + onSelectValue={(val) => onSelectValue(val.toString(), 'ctpvCd', field??'')} className="" value={sidoValue} /> + ) ) }, { name: "sggCd", label: "시군구", component: (field) => ( + updateList?.dmdInfoPk ? ( + <Input + placeholder="시도" + type="text" + className="!text-xl text-gray-600" + onChange={(e) =>{field.onChange(e.target.value)} } + value={selectSigunData.find(item => item.val === field.value?.trim() )?.text??''} + readOnly={!!updateList?.dmdInfoPk} + /> + ) : ( <SelectBox - selectClass={undefined} + selectClass={"!text-xl text-gray-600"} label="" selectArray={filteredSigunData} onSelectValue={(val) => onSelectValue(val.toString(), 'sggCd', field)} className="" value={sigunValue} /> + + ) ) }, { @@ -184,7 +266,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun <SdmSearchForm rtnData={setRtnData} label="수요처명" - initValue={''} + initValue={updateCoNm} dataList={dataList} rtnValue={setRtnValue} rtnText={setRtnText} @@ -200,8 +282,9 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun placeholder="사업자등록번호를 입력하세요" type="text" className="!text-xl text-gray-600" - {...field} onChange={(e) => bznNoChange(e, 'dmdInfoPk', field)} + value={field.value} + readOnly={!!updateList?.dmdInfoPk} /> ) }, @@ -219,8 +302,10 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun type="text" className="!mt-5 !text-xl text-gray-600" disabled={isBznmTotNoDisabled} - {...field} onChange={(e) => bznNoChange(e, 'bznmTotNo', field)} + // {...field} + value={ field.value} + readOnly={!!updateList?.dmdInfoPk} /> </FormUi> ) @@ -249,6 +334,7 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun /> ) }, + { name: "dmdAplyPswd", label: "패스워드", @@ -261,11 +347,18 @@ export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigun /> ) } - ]; + ] return ( <> - {formFields.map(({ name, label, component }) => ( + {formFields.filter((item)=> + {if(updateList?.dmdInfoPk){ + return item.name !== 'dmdAplyPswd' + }else{ + return item + } + }) + .map(({ name, label, component }) => ( <FormField key={String(name)} control={form.control} diff --git a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx index 451997cd2..4e6ad06d6 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/index.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -1,17 +1,13 @@ "use client"; - -/* import { useDmStore } from "@/app/store"; */ -import { SdmRegForm } from "./form"; +import { SdmRegForm, type DemandCompType } from "./form"; import { Button } from "@common/components/ui"; import { zodResolver } from "@hookform/resolvers/zod"; -/* import { useRouter } from "next/navigation"; */ import { useCallback, useEffect, useState } from "react"; import { FormProvider, useForm, type SubmitHandler } from "react-hook-form"; import { toast, Toaster } from "sonner"; -/* import { Toaster, toast } from "react-hot-toast"; */ import * as z from "zod"; import type { SelectDataType } from "../search_input_select"; -import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; + type thisProps = { selectSidoData: SelectDataType[]; @@ -20,9 +16,10 @@ type thisProps = { rtnValue: (searchText: string, pageCnt: number) => void; rtnInputData: (data: DemandCompType) => void; bodyClassNm?: string; + updateList?:DemandCompType } -const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => z.object({ +const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,updateList?:DemandCompType) => z.object({ ctpvCd: z.string().min(1, "시/도를 선택해주세요"), sggCd: z.string().min(1, "시/군/구를 선택해주세요"), dmdCoNm: z.string() @@ -43,11 +40,14 @@ const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean) => .max(40, "주소는 40자 이내로 입력해주세요"), telno: z.string() .min(8, "전화번호는 8자 이상 입력해주세요") - .max(12, "전화번호는 12자 이내로 입력해주세요") + .max(11, "전화번호는 11자 이내로 입력해주세요") .regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"), rgtr: z.string().optional(), - dmdAplyPswd: z.string().optional() + dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우 + ? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음 + : z.string().optional() .refine((val) => !val || (val.length >= 8 && val.length <= 16), "패스워드는 8자 이상 16자 이내로 입력해주세요"), + }); type FormSchema = z.infer<ReturnType<typeof createFormSchema>>; @@ -100,13 +100,13 @@ const defaultValues: FormSchema = { }, [insertSdmData]); */ -export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm}: thisProps) { +export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm, updateList}: thisProps) { /* const { insertSdmData } = useDmStore((state) => state); const router = useRouter(); */ - const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true); + const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false); const [isDuplicate, setIsDuplicate] = useState(false); - const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate); + const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate, updateList ); const form = useForm<FormSchema>({ resolver: zodResolver(formSchema), @@ -119,7 +119,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList // 체크박스 상태가 변경될 때마다 bznmTotNo 필드의 에러를 초기화 useEffect(() => { - if (isBznmTotNoDisabled) { + if (isBznmTotNoDisabled===true) { form.clearErrors('bznmTotNo'); form.setValue('bznmTotNo', ''); } @@ -145,13 +145,16 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList dmdInfoPk, bzmnNo: data.dmdInfoPk, }; + + console.log(' formData ', formData) - console.log(formData); + /* await insertSdmData(formData); */ rtnInputData(formData as DemandCompType); + + //router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`); } catch (error) { console.error('Error submitting form:', error); - toast.error('수요처 등록에 실패했습니다.'); } }; @@ -174,6 +177,7 @@ export default function SdmInsertForm({selectSidoData, selectSigunData, dataList dataList={dataList} rtnValue={setRtnValue} setIsDuplicate={setIsDuplicate} + updateList={updateList} /> <div className="flex justify-center pt-4"> <Button diff --git a/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx index 83571b6ff..3c8fd7192 100644 --- a/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx +++ b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import type { DemandCompType } from "../../../../../../apps/dm/app/store/sdmMng"; +import type { DemandCompType } from './form'; export type thisProps = { label: string | undefined; @@ -53,6 +53,12 @@ export const SdmSearchForm = ({label=undefined, initValue=undefined, rtnValue=() document.removeEventListener('mousedown', handleClickOutside); }; }, []); + + useEffect(() => { + if(initValue&& initValue?.length>0){ + setTextValue(initValue) + } + },[initValue]) return ( <div ref={wrapperRef}> From 41602b2c2cdca7397ee581ab482b44676711b261 Mon Sep 17 00:00:00 2001 From: yschoi <yschoi> Date: Thu, 12 Jun 2025 21:41:09 +0900 Subject: [PATCH 43/44] =?UTF-8?q?lodash=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=EB=9F=AC=EB=A6=AC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/ui/map/lib/gis.select.js | 2 +- .../business_components/src/ui/map/lib/gis.style.js | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/common/business_components/src/ui/map/lib/gis.select.js b/packages/common/business_components/src/ui/map/lib/gis.select.js index 7586ef3fb..21ab39609 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.select.js +++ b/packages/common/business_components/src/ui/map/lib/gis.select.js @@ -32,7 +32,7 @@ GisOlSelect.prototype = { selectFeatureFlag: false, initialize: function (map, options) { this.map = map; - _.extend(this.defaults, options); + Object.assign(this.defaults, options); this.setInteraction(); this.pointerMoveEventHandler(); }, diff --git a/packages/common/business_components/src/ui/map/lib/gis.style.js b/packages/common/business_components/src/ui/map/lib/gis.style.js index f791592cf..713347f54 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.style.js +++ b/packages/common/business_components/src/ui/map/lib/gis.style.js @@ -8,8 +8,6 @@ import Text from 'ol/style/Text.js'; import Point from 'ol/geom/Point.js'; import RegularShape from 'ol/style/RegularShape.js'; import Overlay from 'ol/Overlay'; -import _ from 'lodash'; -//import DOMPurify from 'dompurify'; import { createPortal } from 'react-dom'; import ReactDOM from 'react-dom/client'; import React from 'react'; @@ -96,7 +94,7 @@ GisOlStyle.prototype = { scale = Math.max(minScale, Math.min(maxScale, scale)); image.setScale(scale); style.setImage(image); - _.extend(style, options); + Object.assign(style, options); return style; }, line: function (layer, options, feature, resolution) { @@ -125,7 +123,7 @@ GisOlStyle.prototype = { color, }); style.setFill(fill); - _.extend(style, options); + Object.assign(style, options); return style; }, lineLabelPoligon: function (layer, options, feature, resolution) { @@ -152,7 +150,7 @@ GisOlStyle.prototype = { } style.setFill(fill); style.setStroke(stroke); - _.extend(style, options); + Object.assign(style, options); return style; }, imagePoligon: function (layer, options, feature, resolution) { @@ -169,8 +167,7 @@ GisOlStyle.prototype = { image.setScale(1); style.setGeometry(retGeom); style.setImage(image); - - _.extend(style, options); + Object.assign(style, options); return style; }, setOverlay: function (layer, feature, overlayHtml) { @@ -267,7 +264,7 @@ GisOlStyle.prototype = { style.setStroke(null); style.setFill(null); style.setText(text); - _.extend(style, options); + Object.assign(style, options); return style; }, From af967ab7f4f4893c79688f3dc87ff20d08ab877b Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Fri, 13 Jun 2025 09:19:20 +0900 Subject: [PATCH 44/44] =?UTF-8?q?fix:=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EC=8B=9C=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20=ED=95=9C?= =?UTF-8?q?=EA=B8=80=20=EA=B9=A8=EC=A7=90=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/components/lib/utils/fileDownload.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sn/app/components/lib/utils/fileDownload.ts b/apps/sn/app/components/lib/utils/fileDownload.ts index ffe47eced..31fcfac76 100644 --- a/apps/sn/app/components/lib/utils/fileDownload.ts +++ b/apps/sn/app/components/lib/utils/fileDownload.ts @@ -15,7 +15,7 @@ export const handleDownload = async (api:string, fileNm:string) => { // 다운로드를 트리거할 가짜 <a> 태그 생성 const link = document.createElement('a'); link.href = url; - link.download = fileNm; // 원하는 파일명 지정 + link.download = fileNm.normalize('NFC'); // 원하는 파일명 지정 document.body.appendChild(link); link.click();