670 lines
23 KiB
TypeScript
670 lines
23 KiB
TypeScript
import { useState, useMemo, useCallback } from "react";
|
|
import {
|
|
useWatch,
|
|
type Path,
|
|
type PathValue,
|
|
type UseFormReturn,
|
|
} from "react-hook-form";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import {
|
|
Button,
|
|
Input,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableRow,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@common/components";
|
|
import { FormWrapper } from "@/widgets/form-wrapper";
|
|
import { CodeApi, CodeSelect } from "@/entities/code";
|
|
import {
|
|
UserApi,
|
|
type UserSaveData,
|
|
type UserUpdateData,
|
|
} from "@/entities/user";
|
|
import {
|
|
FormField,
|
|
FormItem,
|
|
FormControl,
|
|
FormMessage,
|
|
} from "@/shared/ui/form";
|
|
import { ENDPOINT } from "@/shared/config";
|
|
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
|
import { ZipSearchDialog } from "./zip-search-dialog";
|
|
import { AgencySearchDialog } from "./agency-search-dialog";
|
|
|
|
type UsersFormProps<T extends UserSaveData | UserUpdateData> = {
|
|
userId?: string;
|
|
form: UseFormReturn<T>;
|
|
handleSave: (data: T) => Promise<string>;
|
|
handleDelete?: () => Promise<void>;
|
|
};
|
|
|
|
function UsersForm<T extends UserSaveData | UserUpdateData>({
|
|
userId,
|
|
form,
|
|
...props
|
|
}: UsersFormProps<T>) {
|
|
const isModifyMode = !!userId;
|
|
|
|
const [watchOgdpSeCd, watchDtlInstCd] = useWatch({
|
|
control: form.control,
|
|
name: ["ogdpSeCd" as Path<T>, "dtlInstCd" as Path<T>],
|
|
});
|
|
const alert = useAlertStore((state) => state.setMessage);
|
|
|
|
const [isValidUserId, setIsValidUserId] = useState(false);
|
|
const [isValidEmail, setIsValidEmail] = useState(false);
|
|
const [passwordMatch, setPasswordMatch] = useState(true);
|
|
const [instSeCdNm, setInstSeCdNm] = useState("");
|
|
|
|
const { data: authSeCd } = useQuery({
|
|
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "AUTHSECD"],
|
|
queryFn: () => CodeApi.getCode("AUTHSECD"),
|
|
});
|
|
|
|
const { data: laeSeCd } = useQuery({
|
|
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "LAESECD"],
|
|
queryFn: () => CodeApi.getCode("LAESECD"),
|
|
});
|
|
|
|
const { mutateAsync: checkDuplicateUserId } = useMutationWrapper({
|
|
mutationFn: (userId: string) => UserApi.existsUserId({ userId }),
|
|
onSuccess: (duplicate) => {
|
|
if (!duplicate) {
|
|
alert("사용가능한 아이디입니다.");
|
|
setIsValidUserId(true);
|
|
return;
|
|
}
|
|
alert("중복된 아이디입니다.");
|
|
},
|
|
});
|
|
|
|
const { mutateAsync: checkDuplicateEmail } = useMutationWrapper({
|
|
mutationFn: (email: string) => UserApi.existsEmail({ email }),
|
|
onSuccess: (duplicate) => {
|
|
if (!duplicate) {
|
|
alert("사용가능한 이메일입니다.");
|
|
setIsValidEmail(true);
|
|
return;
|
|
}
|
|
alert("중복된 이메일입니다.");
|
|
},
|
|
});
|
|
|
|
const authSeCdSelectData = useMemo(() => {
|
|
if (watchOgdpSeCd === "1")
|
|
return (
|
|
authSeCd?.filter(
|
|
(code) =>
|
|
code.lwpoCd === "ROLE_USER1" ||
|
|
code.lwpoCd === "ROLE_USER2" ||
|
|
code.lwpoCd === "ROLE_USER3",
|
|
) ?? []
|
|
);
|
|
if (watchOgdpSeCd === "3" || watchOgdpSeCd === "4")
|
|
return (
|
|
authSeCd?.filter(
|
|
(code) =>
|
|
code.lwpoCd === "ROLE_USER4" || code.lwpoCd === "ROLE_USER5",
|
|
) ?? []
|
|
);
|
|
return [];
|
|
}, [authSeCd, watchOgdpSeCd]);
|
|
|
|
const laeSeCdSelectData = useMemo(() => {
|
|
if (watchDtlInstCd === "ROLE_USER1")
|
|
return laeSeCd?.filter((code) => code.lwpoCd === "G102") ?? [];
|
|
if (watchDtlInstCd === "ROLE_USER2")
|
|
return laeSeCd?.filter((code) => code.lwpoCd === "G101") ?? [];
|
|
if (watchDtlInstCd === "ROLE_USER3")
|
|
return laeSeCd?.filter((code) => code.lwpoCd.startsWith("G2")) ?? [];
|
|
if (watchDtlInstCd === "ROLE_USER4")
|
|
return (
|
|
laeSeCd?.filter(
|
|
(code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 4,
|
|
) ?? []
|
|
);
|
|
if (watchDtlInstCd === "ROLE_USER5")
|
|
return (
|
|
laeSeCd?.filter(
|
|
(code) => code.lwpoCd.startsWith("G3") && code.lwpoCd.length === 7,
|
|
) ?? []
|
|
);
|
|
return [];
|
|
}, [laeSeCd, watchDtlInstCd]);
|
|
|
|
const enoCdSelectData = useMemo(
|
|
() => laeSeCd?.filter((code) => code.lwpoCd?.startsWith("G2")) ?? [],
|
|
[laeSeCd],
|
|
);
|
|
|
|
const handlePasswordConfirmChange = useCallback((value: string) => {
|
|
const password = form.getValues("password" as Path<T>);
|
|
setPasswordMatch(!password || value === password);
|
|
}, []);
|
|
|
|
const handleAddressSelect = useCallback(
|
|
(data: { zip: string; addr: string }) => {
|
|
form.setValue("zip" as Path<T>, data.zip as PathValue<T, Path<T>>);
|
|
form.setValue("addr" as Path<T>, data.addr as PathValue<T, Path<T>>);
|
|
},
|
|
[form],
|
|
);
|
|
|
|
const handleAgencySelect = useCallback(
|
|
(data: { lwpoCd: string; lwpoCdNm: string }) => {
|
|
form.setValue(
|
|
"instSeCd" as Path<T>,
|
|
data.lwpoCd as PathValue<T, Path<T>>,
|
|
);
|
|
setInstSeCdNm(data.lwpoCdNm);
|
|
},
|
|
[form],
|
|
);
|
|
|
|
return (
|
|
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
|
|
<div className="card">
|
|
<Table variant="secondary">
|
|
<colgroup>
|
|
<col width="20%" />
|
|
<col width="80%" />
|
|
</colgroup>
|
|
<TableBody>
|
|
{/* 아이디 */}
|
|
<TableRow>
|
|
<TableHead data-required>아이디</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"userId" as Path<T>}
|
|
render={({ field }) => (
|
|
<div className="flex items-center gap-2">
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
onChange={(e) => {
|
|
field.onChange(e);
|
|
setIsValidUserId(false);
|
|
}}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="아이디를 입력하세요"
|
|
disabled={isModifyMode}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
{!isModifyMode && (
|
|
<Button
|
|
color="green"
|
|
className="px-6"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
checkDuplicateUserId(field.value as string);
|
|
}}
|
|
disabled={!field.value || isValidUserId}
|
|
>
|
|
{isValidUserId && <i className="diveicon di-check" />}
|
|
중복확인
|
|
</Button>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 비밀번호 */}
|
|
<TableRow>
|
|
<TableHead data-required>비밀번호</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"password" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
type="password"
|
|
value={field.value ?? ""}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="비밀번호를 입력하세요"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 비밀번호 확인 */}
|
|
<TableRow>
|
|
<TableHead>비밀번호 확인</TableHead>
|
|
<TableCell className="border">
|
|
<div className="flex flex-col">
|
|
<Input
|
|
type="password"
|
|
onChange={(e) =>
|
|
handlePasswordConfirmChange(e.target.value)
|
|
}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="비밀번호를 다시 입력하세요"
|
|
/>
|
|
{!passwordMatch && (
|
|
<p className="mt-2 text-base text-red-500">
|
|
비밀번호가 일치하지 않습니다.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 비밀번호 힌트 */}
|
|
<TableRow>
|
|
<TableHead data-required>비밀번호 힌트</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"pswdHnt" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<CodeSelect
|
|
{...field}
|
|
upCd="PSWDHNT"
|
|
className="w-[24rem]"
|
|
onValueChange={field.onChange}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 비밀번호 정답 */}
|
|
<TableRow>
|
|
<TableHead>비밀번호 정답</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"pswdAns" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="비밀번호 정답을 입력하세요"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 이름 */}
|
|
<TableRow>
|
|
<TableHead data-required>이름</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"userName" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="이름을 입력하세요"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 연락처 */}
|
|
<TableRow>
|
|
<TableHead rowSpan={2}>연락처</TableHead>
|
|
<TableCell>
|
|
<FormField
|
|
control={form.control}
|
|
name={"mphn" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="휴대폰 번호(-를 제외하고 입력)"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>
|
|
<FormField
|
|
control={form.control}
|
|
name={"telno" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="전화번호(-를 제외하고 입력)"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 이메일 */}
|
|
<TableRow>
|
|
<TableHead>이메일</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"email" as Path<T>}
|
|
render={({ field }) => (
|
|
<div className="flex items-center gap-2">
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
onChange={(e) => {
|
|
field.onChange(e);
|
|
setIsValidEmail(false);
|
|
}}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
placeholder="이메일을 입력하세요"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
<Button
|
|
color="green"
|
|
className="px-6"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
if (field.value) {
|
|
checkDuplicateEmail(field.value as string);
|
|
}
|
|
}}
|
|
disabled={!field.value || isValidEmail}
|
|
>
|
|
{isValidEmail && <i className="diveicon di-check" />}
|
|
중복확인
|
|
</Button>
|
|
</div>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 주소 */}
|
|
<TableRow>
|
|
<TableHead rowSpan={2}>주소</TableHead>
|
|
<TableCell className="border">
|
|
<div className="flex flex-col gap-2">
|
|
<div className="mb-2 flex items-center gap-2">
|
|
<FormField
|
|
control={form.control}
|
|
name={"zip" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[10rem] !text-[1.3rem]"
|
|
disabled
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<ZipSearchDialog onSelectAddress={handleAddressSelect} />
|
|
</div>
|
|
<FormField
|
|
control={form.control}
|
|
name={"addr" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[36rem] !text-[1.3rem]"
|
|
disabled
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow>
|
|
<TableCell>
|
|
<FormField
|
|
control={form.control}
|
|
name={"daddr" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[36rem] !text-[1.3rem]"
|
|
placeholder="상세 주소를 입력하세요"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 기관구분 */}
|
|
<TableRow>
|
|
<TableHead data-required>기관구분</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"ogdpSeCd" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<CodeSelect
|
|
{...field}
|
|
upCd="OGDPSECD"
|
|
value={field.value ?? ""}
|
|
onValueChange={(value) => {
|
|
field.onChange(value);
|
|
form.setValue(
|
|
"dtlInstCd" as Path<T>,
|
|
"" as PathValue<T, Path<T>>,
|
|
);
|
|
form.setValue(
|
|
"laeSeCd" as Path<T>,
|
|
"" as PathValue<T, Path<T>>,
|
|
);
|
|
form.setValue(
|
|
"enoCd" as Path<T>,
|
|
"" as PathValue<T, Path<T>>,
|
|
);
|
|
}}
|
|
className="w-[16rem]"
|
|
placeholder="기관 선택"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 세부기관 */}
|
|
<TableRow>
|
|
<TableHead>세부기관</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"dtlInstCd" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Select
|
|
value={field.value ?? ""}
|
|
onValueChange={(value) => {
|
|
field.onChange(value);
|
|
form.setValue(
|
|
"laeSeCd" as Path<T>,
|
|
"" as PathValue<T, Path<T>>,
|
|
);
|
|
form.setValue(
|
|
"enoCd" as Path<T>,
|
|
"" as PathValue<T, Path<T>>,
|
|
);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[16rem]">
|
|
<SelectValue placeholder="세부기관 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{authSeCdSelectData?.map((code) => (
|
|
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
|
|
{code.lwpoCdNm}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 소속 */}
|
|
<TableRow>
|
|
<TableHead data-required>소속</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"laeSeCd" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Select
|
|
value={field.value ?? ""}
|
|
onValueChange={(value) => {
|
|
field.onChange(value);
|
|
form.setValue(
|
|
"enoCd" as Path<T>,
|
|
"" as PathValue<T, Path<T>>,
|
|
);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[16rem]">
|
|
<SelectValue placeholder="소속 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{laeSeCdSelectData?.map((code) => (
|
|
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
|
|
{code.lwpoCdNm}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 환경청 */}
|
|
<TableRow>
|
|
<TableHead>환경청</TableHead>
|
|
<TableCell className="border">
|
|
<FormField
|
|
control={form.control}
|
|
name={"enoCd" as Path<T>}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Select
|
|
value={field.value ?? ""}
|
|
onValueChange={field.onChange}
|
|
disabled={
|
|
!(
|
|
watchDtlInstCd === "ROLE_USER4" ||
|
|
watchDtlInstCd === "ROLE_USER5"
|
|
)
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[16rem]">
|
|
<SelectValue placeholder="환경청 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{enoCdSelectData?.map((code) => (
|
|
<SelectItem key={code.lwpoCd} value={code.lwpoCd}>
|
|
{code.lwpoCdNm}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* 환경청 기관/업체명 */}
|
|
<TableRow>
|
|
<TableHead>기관/업체명</TableHead>
|
|
<TableCell className="border">
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
value={instSeCdNm}
|
|
className="w-[24rem] !text-[1.3rem]"
|
|
disabled
|
|
/>
|
|
<AgencySearchDialog onSelectAgency={handleAgencySelect} />
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</FormWrapper>
|
|
);
|
|
}
|
|
|
|
export { UsersForm };
|