Merge branch 'iipbs_cs' of https://www.onasys.co.kr:3000/ONAANDALL/frontend
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { ModifyPage } from "@/pages/users/ui/modify-page";
|
||||
|
||||
export default async function UserModifyPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ userUnqId: string }>;
|
||||
}) {
|
||||
const { userUnqId } = await params;
|
||||
return <ModifyPage userUnqId={userUnqId} />;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { UserDetail as VUserDetail } from "@/pages/users";
|
||||
|
||||
export default async function UserDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ userUnqId: string }>;
|
||||
}) {
|
||||
const { userUnqId } = await params;
|
||||
return <VUserDetail userUnqId={userUnqId} />;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { CreatePage } from "@/pages/users";
|
||||
export default CreatePage;
|
||||
@@ -18,7 +18,6 @@ type CodeSelectProps = {
|
||||
|
||||
function CodeSelect({
|
||||
upCd,
|
||||
defaultValue,
|
||||
placeholder = "선택",
|
||||
className,
|
||||
...props
|
||||
@@ -29,13 +28,10 @@ function CodeSelect({
|
||||
enabled: !!upCd,
|
||||
});
|
||||
|
||||
const defaultValueText =
|
||||
code?.find((item) => item.lwpoCd === defaultValue)?.lwpoCdNm ?? placeholder;
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className={cn("w-[12rem]", className)}>
|
||||
<SelectValue placeholder={defaultValueText} />
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{code?.map((item) => (
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
export { UserApi } from "./api/user-service";
|
||||
export {
|
||||
USER_STATE_CODE,
|
||||
userSaveSchema,
|
||||
userPasswordUpdateSchema,
|
||||
userUpdateSchema,
|
||||
type UserSearchParams,
|
||||
type UserListResponse,
|
||||
type UserSaveData,
|
||||
type UserDetailResponse,
|
||||
type UserPasswordUpdateData,
|
||||
type UserUpdateData,
|
||||
} from "./model/user-interface";
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export const USER_STATE_CODE = {
|
||||
대기: "00",
|
||||
정상: "01",
|
||||
정지: "07",
|
||||
탈퇴: "08",
|
||||
삭제: "09",
|
||||
} as const;
|
||||
|
||||
type UserStateCode = (typeof USER_STATE_CODE)[keyof typeof USER_STATE_CODE];
|
||||
|
||||
// 사용자 검색 파라미터
|
||||
export interface UserSearchParams extends PageableParams {
|
||||
keywordType?: string;
|
||||
@@ -19,7 +29,7 @@ export interface UserListResponse {
|
||||
email: string;
|
||||
roleId: string;
|
||||
roleName: string;
|
||||
userStateCode: string;
|
||||
userStateCode: UserStateCode;
|
||||
userStateCodeName: string;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
@@ -49,7 +59,7 @@ export interface UserDetailResponse {
|
||||
email: string;
|
||||
encryptedPassword: string;
|
||||
refreshToken: string;
|
||||
userStateCode: string;
|
||||
userStateCode: UserStateCode;
|
||||
lastLoginDate: string;
|
||||
loginFailCount: number;
|
||||
pswdHnt: string;
|
||||
@@ -75,12 +85,14 @@ export interface UserDetailResponse {
|
||||
instSeCd: string;
|
||||
aprvYmd: string;
|
||||
enoCd: string;
|
||||
dtlInstCd: string;
|
||||
laeSeNm: string;
|
||||
ogdpSeNm: string;
|
||||
instSeNm: string;
|
||||
userStateNm: string;
|
||||
pswdHntNm: string;
|
||||
ecoNm: string;
|
||||
dtlInstNm: string;
|
||||
}
|
||||
|
||||
// 사용자 상태 통계 응답 인터페이스
|
||||
@@ -97,7 +109,10 @@ export const userUpdateSchema = z.object({
|
||||
email: z.string().email().optional(),
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
)
|
||||
.optional(),
|
||||
roleId: z.string().optional(),
|
||||
userStateCode: z.string(),
|
||||
@@ -114,52 +129,61 @@ export const userUpdateSchema = z.object({
|
||||
autzrId: z.string().uuid().optional(),
|
||||
expryYmd: z.string().max(8),
|
||||
instSeCd: z.string().optional(),
|
||||
dtlInstCd: z.string().optional(),
|
||||
aprvYmd: z.string().max(8).optional(),
|
||||
enoCd: z.string().optional(),
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 변경 스키마
|
||||
export const userPasswordUpdateSchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.optional()
|
||||
.describe("기존 패스워드"),
|
||||
provider: z.string().optional().describe("공급자(기본값:password)"),
|
||||
token: z.string().optional(),
|
||||
newPassword: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
|
||||
.describe("신규 패스워드"),
|
||||
newPasswordConfirm: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
|
||||
.describe("신규 패스워드 확인"),
|
||||
});
|
||||
export const userPasswordUpdateSchema = z
|
||||
.object({
|
||||
password: z.string().optional(),
|
||||
provider: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
newPassword: z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
newPasswordConfirm: z.string(),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.newPasswordConfirm, {
|
||||
message: "비밀번호가 일치하지 않습니다.",
|
||||
path: ["newPasswordConfirm"],
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 찾기 변경 스키마
|
||||
export const userFindPasswordUpdateSchema = z.object({
|
||||
tokenValue: z.string(),
|
||||
password: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 확인 스키마
|
||||
export const userPasswordMatchSchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
});
|
||||
|
||||
// 사용자 회원정보 변경 스키마
|
||||
export const userUpdateInfoSchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.optional()
|
||||
.describe("기존 패스워드"),
|
||||
provider: z.string().optional().describe("공급자(기본값:password)"),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
)
|
||||
.optional(),
|
||||
provider: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
userName: z.string(),
|
||||
email: z.string().email().optional(),
|
||||
@@ -167,25 +191,28 @@ export const userUpdateInfoSchema = z.object({
|
||||
|
||||
// 사용자 저장 스키마
|
||||
export const userSaveSchema = z.object({
|
||||
userId: z.string().describe("사용자 ID"),
|
||||
userName: z.string().describe("사용자명"),
|
||||
email: z.string().email().optional().describe("이메일"),
|
||||
userId: z.string(),
|
||||
userName: z.string(),
|
||||
email: z.string().email().optional(),
|
||||
password: z
|
||||
.string()
|
||||
.regex(/(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20}/)
|
||||
.describe("패스워드"),
|
||||
pswdHnt: z.string().max(10).describe("비밀번호 힌트"),
|
||||
pswdAns: z.string().max(100).optional().describe("비밀번호 답변"),
|
||||
mphn: z.string().max(11).optional().describe("휴대폰 번호"),
|
||||
telno: z.string().max(11).optional().describe("전화번호"),
|
||||
zip: z.string().max(5).optional().describe("우편번호"),
|
||||
addr: z.string().max(200).optional().describe("주소"),
|
||||
daddr: z.string().max(200).optional().describe("상세 주소"),
|
||||
laeSeCd: z.string().max(10).describe("법적 구분 코드"),
|
||||
ogdpSeCd: z.string().max(10).describe("조직 구분 코드"),
|
||||
expryYmd: z.string().max(8).describe("만료 일자(YYYYMMDD)"),
|
||||
instSeCd: z.string().optional().describe("기관구분코드"),
|
||||
enoCd: z.string().optional().describe("환경청코드"),
|
||||
.regex(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]).{8,20}$/,
|
||||
"비밀번호는 8~20자, 영문/숫자/특수기호를 모두 포함해야 합니다.",
|
||||
),
|
||||
pswdHnt: z.string().max(10),
|
||||
pswdAns: z.string().max(100).optional(),
|
||||
mphn: z.string().max(11).optional(),
|
||||
telno: z.string().max(11).optional(),
|
||||
zip: z.string().max(5).optional(),
|
||||
addr: z.string().max(200).optional(),
|
||||
daddr: z.string().max(200).optional(),
|
||||
laeSeCd: z.string().max(10),
|
||||
ogdpSeCd: z.string().max(10),
|
||||
expryYmd: z.string().max(8),
|
||||
instSeCd: z.string().optional(),
|
||||
dtlInstCd: z.string().optional(),
|
||||
enoCd: z.string().optional(),
|
||||
});
|
||||
|
||||
// 사용자 비밀번호 찾기 저장 스키마
|
||||
@@ -201,9 +228,8 @@ export const userVerifySchema = z.object({
|
||||
password: z
|
||||
.string()
|
||||
.regex(/((?=.*[0-9])(?=.*[a-zA-Z])(?=.*\W)(?=\S+$).{8,20})|()/)
|
||||
.optional()
|
||||
.describe("기존 패스워드"),
|
||||
provider: z.string().optional().describe("공급자(기본값:password)"),
|
||||
.optional(),
|
||||
provider: z.string().optional(),
|
||||
token: z.string().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { client } from "@/shared/api";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import type { PageableList, PageableParams } from "@/shared/model";
|
||||
|
||||
export interface ZipSearchParams extends PageableParams {
|
||||
zoneNo?: string; // 구역번호
|
||||
ctpvNm?: string; // 시도명
|
||||
sggNm?: string; // 시군구명
|
||||
emdNm?: string; // 읍면동명
|
||||
roadNm?: string; // 도로명
|
||||
stdgNm?: string; // 법정동명
|
||||
dongNm?: string; // 행정동명
|
||||
lotnoMno?: number | null; // 지번본번
|
||||
lotnoSno?: number | null; // 지번부번
|
||||
}
|
||||
|
||||
export interface Zip {
|
||||
zipId: number;
|
||||
zoneNo: string;
|
||||
ctpvNm: string;
|
||||
sggNm: string;
|
||||
emdNm: string;
|
||||
roadNm: string;
|
||||
lotnoMno: number;
|
||||
lotnoSno: number;
|
||||
lotno: string;
|
||||
zipAddr: string;
|
||||
}
|
||||
|
||||
type ZipList = PageableList<Zip>;
|
||||
|
||||
export class ZipApiService {
|
||||
public async searchZip(params: ZipSearchParams): Promise<ZipList> {
|
||||
const response = await client.get<ZipList>(`${ENDPOINT.CMS_SERVICE.ZIPS}`, {
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const ZipApi = new ZipApiService();
|
||||
@@ -1 +1,3 @@
|
||||
export { UsersPage } from "./ui/users-page";
|
||||
export { CreatePage } from "./ui/create-page";
|
||||
export { UserDetail } from "./ui/user-detail";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { UserListResponse } from "@/entities/user";
|
||||
import dayjs from "dayjs";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import type { UserListResponse } from "@/entities/user";
|
||||
|
||||
export const columns: ColumnDef<UserListResponse>[] = [
|
||||
{
|
||||
@@ -50,5 +51,10 @@ export const columns: ColumnDef<UserListResponse>[] = [
|
||||
{
|
||||
accessorKey: "lastLoginDate",
|
||||
header: "최종접속일",
|
||||
cell: ({ row }) => {
|
||||
return row.original.lastLoginDate
|
||||
? dayjs(row.original.lastLoginDate).format("YYYY-MM-DD")
|
||||
: "";
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
DataTable,
|
||||
} from "@common/components";
|
||||
import {
|
||||
LwpoCodeApi,
|
||||
type LwpoCode,
|
||||
type LwpoCodeParams,
|
||||
} from "@/entities/code";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
interface AgencySearchDialogProps {
|
||||
onSelectAgency: (data: { lwpoCd: string; lwpoCdNm: string }) => void;
|
||||
}
|
||||
|
||||
function AgencySearchDialog({ onSelectAgency }: AgencySearchDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedAgency, setSelectedAgency] = useState<LwpoCode | null>(null);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [searchQuery, setSearchQuery] = useState<LwpoCodeParams>({
|
||||
lwpoCdNm: "",
|
||||
});
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE, pagination],
|
||||
queryFn: () =>
|
||||
LwpoCodeApi.getLwpoCodeList({
|
||||
...searchQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: data?.content ?? [],
|
||||
columns: [
|
||||
{
|
||||
accessorKey: "lwpoCd",
|
||||
header: "기관코드",
|
||||
},
|
||||
{
|
||||
accessorKey: "lwpoCdNm",
|
||||
header: "기관명",
|
||||
},
|
||||
],
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
rowCount: data?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getRowId: (row) => row.lwpoCd.toString(),
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
});
|
||||
|
||||
const handleSearch = async () => {
|
||||
await refetch();
|
||||
queryClient.removeQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.LWPO_CODE],
|
||||
});
|
||||
setPagination({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
table.setRowSelection({});
|
||||
setSelectedAgency(null);
|
||||
};
|
||||
|
||||
const handleRowClick = (row: LwpoCode) => {
|
||||
table.setRowSelection({
|
||||
[row.lwpoCd.toString()]: true,
|
||||
});
|
||||
setSelectedAgency(row);
|
||||
};
|
||||
|
||||
const handleSelectAgency = () => {
|
||||
if (!selectedAgency) return;
|
||||
onSelectAgency({
|
||||
lwpoCd: selectedAgency.lwpoCd,
|
||||
lwpoCdNm: selectedAgency.lwpoCdNm,
|
||||
});
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="px-6">기관 검색</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>기관 검색</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSearch();
|
||||
}}
|
||||
>
|
||||
<div className="card card-border !mx-0 flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="mr-2 whitespace-nowrap">기관명</Label>
|
||||
<Input
|
||||
value={searchQuery.lwpoCdNm ?? ""}
|
||||
onChange={(e) =>
|
||||
setSearchQuery({
|
||||
...searchQuery,
|
||||
lwpoCdNm: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="기관명을 입력하세요"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
<Button type="submit">검색</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination
|
||||
/>
|
||||
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={handleSelectAgency}
|
||||
disabled={!selectedAgency}
|
||||
>
|
||||
선택
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AgencySearchDialog };
|
||||
@@ -0,0 +1,55 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutationWrapper, useAlertStore } from "@/shared/lib";
|
||||
import { UserApi, userSaveSchema, type UserSaveData } from "@/entities/user";
|
||||
import { UsersForm } from "./users-form";
|
||||
|
||||
function CreatePage() {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const form = useForm<UserSaveData>({
|
||||
resolver: zodResolver(userSaveSchema),
|
||||
defaultValues: {
|
||||
userId: "",
|
||||
userName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
pswdHnt: "",
|
||||
pswdAns: "",
|
||||
mphn: "",
|
||||
telno: "",
|
||||
zip: "",
|
||||
addr: "",
|
||||
daddr: "",
|
||||
laeSeCd: "",
|
||||
ogdpSeCd: "",
|
||||
expryYmd: "",
|
||||
instSeCd: "",
|
||||
enoCd: "",
|
||||
},
|
||||
});
|
||||
|
||||
// 사용자 등록 mutation
|
||||
const { mutateAsync: saveUser } = useMutationWrapper({
|
||||
mutationFn: (data: UserSaveData) => UserApi.saveUser(data),
|
||||
onSuccess: () => {
|
||||
alert("회원이 등록되었습니다.");
|
||||
router.push("/users");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">회원 등록</h3>
|
||||
</div>
|
||||
<UsersForm form={form} handleSave={saveUser} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CreatePage };
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
UserApi,
|
||||
userUpdateSchema,
|
||||
type UserUpdateData,
|
||||
} from "@/entities/user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useMutationWrapper, useAlertStore } from "@/shared/lib";
|
||||
import { UsersForm } from "./users-form";
|
||||
|
||||
interface ModifyPageProps {
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
export function ModifyPage({ userUnqId }: ModifyPageProps) {
|
||||
const router = useRouter();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data: userData } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
|
||||
queryFn: () => UserApi.findByUserUnqId(userUnqId),
|
||||
enabled: !!userUnqId,
|
||||
});
|
||||
|
||||
const form = useForm<UserUpdateData>({
|
||||
resolver: zodResolver(userUpdateSchema),
|
||||
defaultValues: {
|
||||
userId: userData?.userId ?? "",
|
||||
userName: userData?.userName ?? "",
|
||||
email: userData?.email ?? "",
|
||||
userStateCode: userData?.userStateCode ?? "",
|
||||
pswdHnt: userData?.pswdHnt ?? "",
|
||||
pswdAns: userData?.pswdAns ?? "",
|
||||
mphn: userData?.mphn ?? "",
|
||||
telno: userData?.telno ?? "",
|
||||
zip: userData?.zip ?? "",
|
||||
addr: userData?.addr ?? "",
|
||||
daddr: userData?.daddr ?? "",
|
||||
laeSeCd: userData?.laeSeCd ?? "",
|
||||
ogdpSeCd: userData?.ogdpSeCd ?? "",
|
||||
aprvYn: userData?.aprvYn ?? "",
|
||||
expryYmd: userData?.expryYmd ?? "",
|
||||
instSeCd: userData?.instSeCd ?? "",
|
||||
dtlInstCd: userData?.dtlInstCd ?? "",
|
||||
aprvYmd: userData?.aprvYmd ?? "",
|
||||
enoCd: userData?.enoCd ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: updateUser } = useMutationWrapper({
|
||||
mutationFn: (data: UserUpdateData) => UserApi.updateUser(userUnqId, data),
|
||||
onSuccess: () => {
|
||||
alert("회원 정보가 수정되었습니다.", router.back);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">회원 정보 수정</h3>
|
||||
</div>
|
||||
<UsersForm
|
||||
userId={userData?.userId}
|
||||
form={form}
|
||||
handleSave={updateUser}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
Input,
|
||||
DialogTrigger,
|
||||
} from "@common/components";
|
||||
import {
|
||||
UserApi,
|
||||
userPasswordUpdateSchema,
|
||||
type UserPasswordUpdateData,
|
||||
} from "@/entities/user";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
|
||||
interface PasswordResetDialogProps {
|
||||
userUnqId: string;
|
||||
}
|
||||
|
||||
export function PasswordResetDialog({ userUnqId }: PasswordResetDialogProps) {
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm<UserPasswordUpdateData>({
|
||||
resolver: zodResolver(userPasswordUpdateSchema),
|
||||
defaultValues: {
|
||||
newPassword: "",
|
||||
newPasswordConfirm: "",
|
||||
provider: "password",
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: onPasswordReset } = useMutationWrapper({
|
||||
mutationFn: (data: UserPasswordUpdateData) =>
|
||||
UserApi.initializePassword(userUnqId, data),
|
||||
onSuccess: () => {
|
||||
alert("비밀번호가 초기화되었습니다.");
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button color="gray">
|
||||
<i className="diveicon di-arrow-reset" /> 비밀번호 초기화
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>비밀번호 초기화</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => onPasswordReset(data))}
|
||||
className="space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>새 비밀번호</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="newPasswordConfirm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>새 비밀번호 확인</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="password"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" className="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">확인</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@common/components";
|
||||
import { USER_STATE_CODE, UserApi } from "@/entities/user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useAlertStore, useMutationWrapper } from "@/shared/lib";
|
||||
import { PasswordResetDialog } from "./password-reset-dialog";
|
||||
|
||||
function UserDetail({ userUnqId }: { userUnqId: string }) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const queryClient = useQueryClient();
|
||||
const alert = useAlertStore((state) => state.setMessage);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
|
||||
queryFn: () => UserApi.findByUserUnqId(userUnqId),
|
||||
enabled: !!userUnqId,
|
||||
});
|
||||
|
||||
const { mutate: onLockUser } = useMutationWrapper({
|
||||
mutationFn: () => UserApi.lockUser(userUnqId),
|
||||
onSuccess: () => {
|
||||
alert("계정 잠금이 활성화 되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: onUnlockUser } = useMutationWrapper({
|
||||
mutationFn: () => UserApi.unlockUser(userUnqId),
|
||||
onSuccess: () => {
|
||||
alert("계정 잠금이 해제되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: onApplyUser } = useMutationWrapper({
|
||||
mutationFn: () => UserApi.applyUser(userUnqId),
|
||||
onSuccess: () => {
|
||||
alert("사용자 승인이 완료되었습니다.");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [ENDPOINT.USER_SERVICE.USERS, userUnqId],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card card-border !mt-6">
|
||||
<div className="card-header">
|
||||
<h3 className="text-2xl font-medium text-label">회원 정보</h3>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="mb-4 flex justify-end gap-2">
|
||||
{data?.userStateCode === USER_STATE_CODE.정지 ? (
|
||||
<Button color="orange" onClick={() => onUnlockUser()}>
|
||||
<i className="diveicon di-lock-open" />
|
||||
잠금해제
|
||||
</Button>
|
||||
) : (
|
||||
<Button color="orange" onClick={() => onLockUser()}>
|
||||
<i className="diveicon di-lock" />
|
||||
계정잠금
|
||||
</Button>
|
||||
)}
|
||||
<PasswordResetDialog userUnqId={userUnqId} />
|
||||
{data?.userStateCode === USER_STATE_CODE.대기 && (
|
||||
<Button className="emerald" onClick={() => onApplyUser()}>
|
||||
<i className="diveicon di-check-circle-line" /> 가입승인
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Table variant="secondary">
|
||||
<colgroup>
|
||||
<col width="20%" />
|
||||
<col width="80%" />
|
||||
</colgroup>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableHead>아이디</TableHead>
|
||||
<TableCell className="!px-4">{data?.userId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableCell className="!px-4">{data?.userName}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>연락처</TableHead>
|
||||
<TableCell className="!px-4">휴대폰: {data?.mphn}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="!px-4">전화번호: {data?.telno}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>이메일</TableHead>
|
||||
<TableCell className="!px-4">{data?.email}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead rowSpan={2}>주소</TableHead>
|
||||
<TableCell className="!px-4">
|
||||
{data?.zip && `(${data?.zip})`} {data?.addr}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className="!px-4">{data?.daddr}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>기관구분</TableHead>
|
||||
<TableCell className="!px-4">{data?.ogdpSeNm}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>세부기관</TableHead>
|
||||
<TableCell className="!px-4">{data?.dtlInstNm}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>소속</TableHead>
|
||||
<TableCell className="!px-4">{data?.laeSeNm}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>환경청</TableHead>
|
||||
<TableCell className="!px-4">{data?.ecoNm}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>가입일</TableHead>
|
||||
<TableCell className="!px-4">
|
||||
{data?.regDt ? dayjs(data?.regDt).format("YYYY-MM-DD") : ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>최근로그인</TableHead>
|
||||
<TableCell className="!px-4">
|
||||
{data?.lastLoginDate
|
||||
? dayjs(data?.lastLoginDate).format("YYYY-MM-DD HH:mm:ss")
|
||||
: ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableHead>계정상태</TableHead>
|
||||
<TableCell className="!px-4">{data?.userStateNm}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex justify-center gap-4 pb-12 pt-10">
|
||||
<Button size="lg" className="white" onClick={router.back}>
|
||||
목록
|
||||
</Button>
|
||||
<Button size="lg" onClick={() => router.push(`${pathname}/modify`)}>
|
||||
수정
|
||||
</Button>
|
||||
<Button size="lg" color="red">
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { UserDetail };
|
||||
@@ -0,0 +1,669 @@
|
||||
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.unit?.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>아이디</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>비밀번호</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>비밀번호 힌트</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>이름</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>기관구분</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>소속</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 };
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
import { CodeApi } from "@/entities/code";
|
||||
import { UserApi, type UserSearchParams } from "@/entities/user";
|
||||
import {
|
||||
UserApi,
|
||||
type UserListResponse,
|
||||
type UserSearchParams,
|
||||
} from "@/entities/user";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { columns } from "../model/table-columns";
|
||||
|
||||
@@ -92,8 +96,8 @@ function UsersPage() {
|
||||
refetch();
|
||||
};
|
||||
|
||||
const handleRowClick = (row: unknown) => {
|
||||
console.log(row);
|
||||
const handleRowClick = (row: UserListResponse) => {
|
||||
router.push(`/users/${row.userUnqId}`);
|
||||
};
|
||||
|
||||
const handlePresetDateRange = (value: string) => {
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import {
|
||||
keepPreviousData,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
type PaginationState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
DataTable,
|
||||
} from "@common/components";
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { ZipApi, type Zip, type ZipSearchParams } from "../api/zip-service";
|
||||
|
||||
interface ZipSearchDialogProps {
|
||||
onSelectAddress: (data: { zip: string; addr: string }) => void;
|
||||
}
|
||||
|
||||
function ZipSearchDialog({ onSelectAddress }: ZipSearchDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedAddress, setSelectedAddress] = useState<Zip | null>(null);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [searchQuery, setSearchQuery] = useState<ZipSearchParams>({
|
||||
roadNm: "",
|
||||
lotnoMno: null,
|
||||
lotnoSno: null,
|
||||
});
|
||||
const hasFetchedOnce = useRef(false);
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ZIPS, pagination],
|
||||
queryFn: () =>
|
||||
ZipApi.searchZip({
|
||||
...searchQuery,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: data?.content ?? [],
|
||||
columns: [
|
||||
{
|
||||
accessorKey: "zoneNo",
|
||||
header: "우편번호",
|
||||
},
|
||||
{
|
||||
accessorKey: "zipAddr",
|
||||
header: "주소",
|
||||
},
|
||||
],
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
rowCount: data?.totalElements ?? 0,
|
||||
manualPagination: true,
|
||||
enableMultiRowSelection: false,
|
||||
getRowId: (row) => row.zipId.toString(),
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
});
|
||||
|
||||
const handleSearch = async () => {
|
||||
await refetch();
|
||||
queryClient.removeQueries({
|
||||
queryKey: [ENDPOINT.CMS_SERVICE.ZIPS],
|
||||
});
|
||||
setPagination({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
table.setRowSelection({});
|
||||
setSelectedAddress(null);
|
||||
hasFetchedOnce.current = true;
|
||||
};
|
||||
|
||||
const handleRowClick = (row: Zip) => {
|
||||
table.setRowSelection({
|
||||
[row.zipId.toString()]: true,
|
||||
});
|
||||
setSelectedAddress(row);
|
||||
};
|
||||
|
||||
const handleSelectAddress = () => {
|
||||
if (!selectedAddress) return;
|
||||
onSelectAddress({
|
||||
zip: selectedAddress.zoneNo,
|
||||
addr: selectedAddress.zipAddr,
|
||||
});
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (hasFetchedOnce.current) refetch();
|
||||
}, [pagination, refetch]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="px-6">우편번호 검색</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[60rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>주소 검색</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSearch();
|
||||
}}
|
||||
className="mb-4 space-y-2"
|
||||
>
|
||||
<div className="card card-border !m-0 flex flex-col gap-4 p-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex flex-col">
|
||||
<Label className="mb-1 text-base">도로명</Label>
|
||||
<Input
|
||||
value={searchQuery.roadNm || ""}
|
||||
onChange={(e) =>
|
||||
setSearchQuery({
|
||||
...searchQuery,
|
||||
roadNm: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="예: 테헤란로"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Label className="mb-1 text-base">지번본번</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={searchQuery.lotnoMno ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: null;
|
||||
setSearchQuery({
|
||||
...searchQuery,
|
||||
lotnoMno: value,
|
||||
});
|
||||
}}
|
||||
placeholder="예: 123"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<Label className="mb-1 text-base">지번부번</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={searchQuery.lotnoSno ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: null;
|
||||
setSearchQuery({
|
||||
...searchQuery,
|
||||
lotnoSno: value,
|
||||
});
|
||||
}}
|
||||
placeholder="예: 45"
|
||||
className="!text-[1.3rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">검색</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{data?.content.length && (
|
||||
<DataTable
|
||||
table={table}
|
||||
variant="secondary"
|
||||
onRowClick={handleRowClick}
|
||||
viewPagination
|
||||
/>
|
||||
)}
|
||||
|
||||
<DialogFooter className="!justify-center">
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={handleSelectAddress}
|
||||
disabled={!selectedAddress}
|
||||
>
|
||||
선택
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button size="lg" color="white">
|
||||
취소
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ZipSearchDialog };
|
||||
@@ -44,6 +44,7 @@ export const ENDPOINT = {
|
||||
SURVEY: "/cms-service/api/v1/srvy",
|
||||
SURVEY_QUESTION: "/cms-service/api/v1/srvy-qitem",
|
||||
SURVEY_ARTICLE: "/cms-service/api/v1/srvy-artcl",
|
||||
ZIPS: "/cms-service/api/v1/zips",
|
||||
},
|
||||
BOARD_SERVICE: {
|
||||
BBS: "/board-service/api/v1/bbs",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UseFormReturn, FieldPath } from "react-hook-form";
|
||||
import type { UseFormReturn, Path } from "react-hook-form";
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import {
|
||||
Input,
|
||||
@@ -35,7 +35,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
form,
|
||||
handleSave,
|
||||
}: PostFormProps<T>) {
|
||||
const atflCd = form.getValues("atflCd" as FieldPath<T>);
|
||||
const atflCd = form.getValues("atflCd" as Path<T>);
|
||||
|
||||
const router = useRouter();
|
||||
const { confirm } = useConfirm();
|
||||
@@ -81,7 +81,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"pstTtl" as FieldPath<T>}
|
||||
name={"pstTtl" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -102,7 +102,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"wrtrNm" as FieldPath<T>}
|
||||
name={"wrtrNm" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -124,7 +124,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"pstPswd" as FieldPath<T>}
|
||||
name={"pstPswd" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormControl>
|
||||
@@ -147,7 +147,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"pstCn" as FieldPath<T>}
|
||||
name={"pstCn" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -170,7 +170,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<TableCell className="border">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"rfrncUrl" as FieldPath<T>}
|
||||
name={"rfrncUrl" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -194,7 +194,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<div className="flex items-center gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"popupYn" as FieldPath<T>}
|
||||
name={"popupYn" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -218,7 +218,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"popupYnEtc" as FieldPath<T>}
|
||||
name={"popupYnEtc" as Path<T>}
|
||||
render={({ field }) => {
|
||||
const fieldValues = form.getValues().popupYnEtc?.[0];
|
||||
return (
|
||||
@@ -227,9 +227,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<PostingPeriodSelector
|
||||
defaultValues={{ ...fieldValues }}
|
||||
disabled={
|
||||
form.getValues(
|
||||
"popupYn" as FieldPath<T>,
|
||||
) === "N"
|
||||
form.getValues("popupYn" as Path<T>) === "N"
|
||||
}
|
||||
handlePeriodValueChange={(
|
||||
periodCode,
|
||||
@@ -267,7 +265,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<div className="flex items-center gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"upndFixYn" as FieldPath<T>}
|
||||
name={"upndFixYn" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -291,7 +289,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"upndFixYnEtc" as FieldPath<T>}
|
||||
name={"upndFixYnEtc" as Path<T>}
|
||||
render={({ field }) => {
|
||||
const fieldValues =
|
||||
form.getValues().upndFixYnEtc?.[0];
|
||||
@@ -301,9 +299,8 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<PostingPeriodSelector
|
||||
defaultValues={{ ...fieldValues }}
|
||||
disabled={
|
||||
form.getValues(
|
||||
"upndFixYn" as FieldPath<T>,
|
||||
) === "N"
|
||||
form.getValues("upndFixYn" as Path<T>) ===
|
||||
"N"
|
||||
}
|
||||
handlePeriodValueChange={(
|
||||
periodCode,
|
||||
@@ -341,7 +338,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<div className="flex items-center gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"prdPstgYn" as FieldPath<T>}
|
||||
name={"prdPstgYn" as Path<T>}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -365,7 +362,7 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={"prdPstgYnEtc" as FieldPath<T>}
|
||||
name={"prdPstgYnEtc" as Path<T>}
|
||||
render={({ field }) => {
|
||||
const fieldValues =
|
||||
form.getValues().prdPstgYnEtc?.[0];
|
||||
@@ -375,9 +372,8 @@ function PostForm<T extends PostFormData | PostUpdateFormData>({
|
||||
<PostingPeriodSelector
|
||||
defaultValues={{ ...fieldValues }}
|
||||
disabled={
|
||||
form.getValues(
|
||||
"prdPstgYn" as FieldPath<T>,
|
||||
) === "N"
|
||||
form.getValues("prdPstgYn" as Path<T>) ===
|
||||
"N"
|
||||
}
|
||||
handlePeriodValueChange={(
|
||||
periodCode,
|
||||
|
||||
Reference in New Issue
Block a user