From b96207cced396af35ee526e0883c1f2a0b2b16ab Mon Sep 17 00:00:00 2001 From: YangJinSoo94 Date: Tue, 3 Jun 2025 17:01:43 +0900 Subject: [PATCH] =?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}
- )) : ( */} + )) : (
검색 결과가 없습니다.
- {/* )) : ( */} + )}
)}