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 6bc96c8a2..96214b6cf 100644 --- a/packages/common/business_components/src/ui/form_ui/index.tsx +++ b/packages/common/business_components/src/ui/form_ui/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { FormControl, FormItem, FormLabel, FormMessage } from "../form"; import { Button, Checkbox } from "@common/components/ui"; @@ -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..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 "./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..d59108c5b --- /dev/null +++ b/packages/common/business_components/src/ui/sdmInsertForm/form.tsx @@ -0,0 +1,287 @@ +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; + dmdAplyPswd?: 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 [orgBznmNo, setOrgBznmNo] = useState(''); + + 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)); + + // 수요처 불러온 후 사업자번호를 바꿀경우를 대비함 + setOrgBznmNo(rtnCompData?.dmdInfoPk); + 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 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; + 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) => ( + bznNoChange(e, 'dmdInfoPk')} + /> + ) + }, + { + name: "bznmTotNo", + label: "종사업자등록번호", + component: (field) => ( + + bznNoChange(e, 'bznmTotNo')} + /> + + ) + }, + { + name: "daddr", + label: "사업자주소", + component: (field) => ( + + ) + }, + { + name: "telno", + label: "전화번호", + component: (field) => ( + + ) + }, + { + name: "dmdAplyPswd", + 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..723d87cf6 --- /dev/null +++ b/packages/common/business_components/src/ui/sdmInsertForm/index.tsx @@ -0,0 +1,206 @@ +"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(() => isBznmTotNoDisabled ? !isDuplicate : true, "이미 등록된 사업자등록번호입니다"), + bznmTotNo: isBznmTotNoDisabled + ? z.string().optional() + : z.string() + .length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다") + .refine(() => !isDuplicate, "이미 등록된 사업자등록번호입니다"), + 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(), + dmdAplyPswd : z.string() + .min(1, "패스워드를 입력해주세요") + .min(8, "패스워드는 8자 이상 입력해주세요") + .max(16, "패스워드는 16자 이내로 입력해주세요"), +}); + +type FormSchema = ReturnType; +export type FormData = z.infer; + +const defaultValues: FormData = { + ctpvCd: '', + sggCd: '', + dmdCoNm: '', + dmdInfoPk: '', + bznmTotNo: '', + daddr: '', + telno: '', + rgtr: "3fa85f64-5717-4562-b3fc-2c963f66afa6", + dmdAplyPswd : '', +}; + +/** + * 수요처 등록 폼 + * @param selectSidoData 시/도 데이터 + * @param selectSigunData 시/군/구 데이터 + * @param dataList 수요처 목록 + * @param rtnValue 검색 결과(텍스트) 콜백 + * @param rtnInputData 수요처 리스트 콜백 + * @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(); */ + + 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/sdmInsertForm/sdmNameBox.tsx b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx new file mode 100644 index 000000000..83571b6ff --- /dev/null +++ b/packages/common/business_components/src/ui/sdmInsertForm/sdmNameBox.tsx @@ -0,0 +1,105 @@ +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; + dataList : DemandCompType[]; + rtnValue : (searchText: string, pageCnt: number) => void; + rtnData : (item: DemandCompType) => void; + rtnText : (text: string) => void; +} + +/** + * 수요처 검색 컴포넌트 + * @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 || ''); + const wrapperRef = useRef(null); + 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((item: DemandCompType) => { + setTextValue(item.dmdCoNm); + rtnData(item); + setIsOpen(false); + }, [rtnData]); + + 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 && ( +
+
+ {dataList && dataList.length > 0 ? dataList.map((item : DemandCompType, index : number) => ( +
handleSuggestionClick(item)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + handleSuggestionClick(item); + } + }} + > + {item.dmdInfoPk.substring(10,14) !== '0000' ? ( + <> + 사업자번호 : {item.dmdInfoPk.substring(0,10)}  + 종사업자번호 : {item.dmdInfoPk.substring(10,14)}  + + ) : ( + <> + 사업자번호 : {item.dmdInfoPk.substring(0,10)}  + + )} + {item.dmdCoNm} +
+ )) : ( +
+ 검색 결과가 없습니다. +
+ )} +
+
+ )} +
+ ) +} \ No newline at end of file