refactor: 조직도 소속원 폼 리팩토링

This commit is contained in:
2025-04-23 04:18:25 +09:00
parent 14bb8e9e60
commit d1677af87b
3 changed files with 186 additions and 143 deletions
@@ -16,18 +16,18 @@ export interface SearchUserParams extends PageableParams {
}
export interface User {
mode: string;
ognzSn: number;
userUnqId: string;
userUnqNm: string;
userUnqNm?: string;
jbpsSeCd: string;
jbpsSeNm: string;
jbpsSeNm?: string;
jbgdSeCd: string;
jbgdSeNm: string;
jbgdSeNm?: string;
sortSeq: number;
mdfcnDt: string;
mdfr: string;
mdfrNm: string;
mode: string;
mdfcnDt?: string;
mdfr?: string;
mdfrNm?: string;
}
export interface SearchUser {
@@ -1,91 +0,0 @@
import dayjs from "dayjs";
import type { UseFormReturn } from "react-hook-form";
import type { ColumnDef } from "@tanstack/react-table";
import { Checkbox, Select } from "@common/components/ui";
import type {
User,
UserOrganizationFormData,
} from "@/entities/user-organization";
export const createColumns = (
form: UseFormReturn<UserOrganizationFormData>,
renderJobPositionSelect: (
props: React.ComponentPropsWithoutRef<typeof Select>,
) => React.ReactNode,
renderJobGradeSelect: (
props: React.ComponentPropsWithoutRef<typeof Select>,
) => React.ReactNode,
): ColumnDef<User>[] => {
return [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(checked) =>
table.toggleAllPageRowsSelected(!!checked)
}
className="h-4 w-4 rounded border-gray-300"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
className="h-4 w-4 rounded border-gray-300"
/>
),
},
{
id: "no",
header: "NO",
cell: ({ row }) => row.index + 1,
},
{
accessorKey: "userUnqNm",
header: "이름",
},
{
accessorKey: "jbpsSeCd",
header: "직위",
cell: ({ row }) =>
renderJobPositionSelect({
defaultValue: row.original.jbpsSeCd,
value: form.watch(`userOrganization.${row.index}.jbpsSeCd`),
onValueChange: (value) => {
const mode = form.watch(`userOrganization.${row.index}.mode`);
if (mode === "R")
form.setValue(`userOrganization.${row.index}.mode`, "U");
form.setValue(`userOrganization.${row.index}.jbpsSeCd`, value);
},
}),
},
{
accessorKey: "jbgdSeCd",
header: "직급",
cell: ({ row }) =>
renderJobGradeSelect({
defaultValue: row.original.jbgdSeCd,
value: form.watch(`userOrganization.${row.index}.jbgdSeCd`),
onValueChange: (value) => {
const mode = form.watch(`userOrganization.${row.index}.mode`);
if (mode === "R")
form.setValue(`userOrganization.${row.index}.mode`, "U");
form.setValue(`userOrganization.${row.index}.jbgdSeCd`, value);
},
}),
},
{
accessorKey: "mdfrNm",
header: "최종수정자",
},
{
accessorKey: "mdfcnDt",
header: "최종수정일",
cell: ({ row }) => {
if (!row.original.mdfcnDt) return "";
return dayjs(row.original.mdfcnDt).format("YYYY-MM-DD");
},
},
];
};
@@ -1,10 +1,18 @@
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import dayjs from "dayjs";
import React, { useEffect, useState, useCallback, memo } from "react";
import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery, keepPreviousData } from "@tanstack/react-query";
import { useReactTable, getCoreRowModel } from "@tanstack/react-table";
import { cn } from "@common/components/lib";
import { Button, DataTable } from "@common/components/ui";
import {
Button,
Checkbox,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components/ui";
import {
Select,
SelectContent,
@@ -21,20 +29,22 @@ import {
} from "@/entities/user-organization";
import { CodeApi, type ICode } from "@/entities/code";
import { ENDPOINT } from "@/shared/config";
import { createColumns } from "../model/user-organization-table-columns";
import { AdditionalUserDialog } from "./additional-user-dialog";
type UserOrganizationSectionProps = {
ognzSn?: number | null;
ognzSn: number;
};
function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
const [userList, setUserList] = useState<User[]>([]);
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const isAllSelected =
userList.length > 0 && selectedUsers.length === userList.length;
const { data: userOrganizationList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn],
queryFn: () =>
UserOrganizationApi.getUserOrganizationList({ ognzSn: ognzSn! }),
queryFn: () => UserOrganizationApi.getUserOrganizationList({ ognzSn }),
enabled: !!ognzSn,
placeholderData: keepPreviousData,
});
@@ -52,37 +62,26 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
const form = useForm<UserOrganizationFormData>({
resolver: zodResolver(userOrganizationFormSchema),
values: {
userOrganization: [
...userList.map((user) => ({
mode: user.mode,
ognzSn: user.ognzSn,
userUnqId: user.userUnqId,
jbpsSeCd: user.jbpsSeCd,
jbgdSeCd: user.jbgdSeCd,
sortSeq: user.sortSeq,
})),
],
userOrganization: userList.map((user) => ({
mode: user.mode,
ognzSn: user.ognzSn,
userUnqId: user.userUnqId,
jbpsSeCd: user.jbpsSeCd,
jbgdSeCd: user.jbgdSeCd,
sortSeq: user.sortSeq,
})),
},
});
const { append } = useFieldArray({
control: form.control,
name: "userOrganization",
});
const table = useReactTable({
data: userList,
columns: createColumns(
form,
(props) => <CodeSelect codeData={jobPositionCodes} {...props} />,
(props) => <CodeSelect codeData={jobGradeCodes} {...props} />,
),
getCoreRowModel: getCoreRowModel(),
const { append } = useFieldArray({
name: "userOrganization",
control: form.control,
});
const handleAddUsers = (users: SearchUser[]) => {
const newForm = users.map((user) => ({
mode: "C",
ognzSn: ognzSn!,
ognzSn,
userUnqId: user.userUnqId,
jbpsSeCd: "",
jbgdSeCd: "",
@@ -91,51 +90,184 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
append(newForm);
const newUserList = users.map((user) => ({
ognzSn: ognzSn!,
mode: "C",
ognzSn,
userUnqId: user.userUnqId,
userUnqNm: user.userNm,
jbpsSeCd: "",
jbpsSeNm: "",
jbgdSeCd: "",
jbgdSeNm: "",
sortSeq: 0,
mdfcnDt: "",
mdfr: "",
mdfrNm: "",
mode: "C",
}));
setUserList([...newUserList, ...userList]);
};
const handleToggleSelectUser = useCallback((user: User) => {
setSelectedUsers((prevSelected) => {
const isSelected = prevSelected.some(
(item) => item.userUnqId === user.userUnqId,
);
if (isSelected) {
return prevSelected.filter((item) => item.userUnqId !== user.userUnqId);
} else {
return [...prevSelected, user];
}
});
}, []);
const handleToggleSelectAll = () => {
if (isAllSelected) {
setSelectedUsers([]);
} else {
setSelectedUsers([...userList]);
}
};
const handleDeleteUsers = () => {
if (selectedUsers.length === 0) return;
const selectedUserIds = selectedUsers.map((user) => user.userUnqId);
setUserList((prevUsers) =>
prevUsers.filter((user) => !selectedUserIds.includes(user.userUnqId)),
);
setSelectedUsers([]);
};
const isUserSelected = useCallback(
(userUnqId: string) =>
selectedUsers.some((item) => item.userUnqId === userUnqId),
[selectedUsers],
);
useEffect(() => {
if (userOrganizationList?.content)
if (userOrganizationList?.content) {
setUserList(userOrganizationList.content);
}
}, [userOrganizationList]);
return (
<div className={cn("card card-border", !ognzSn && "hidden")}>
<div className="card card-border">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">소속원 목록</h3>
<div className="flex gap-2">
<AdditionalUserDialog onAddUsers={handleAddUsers} />
<Button size="sm" color="red">
<Button
size="sm"
color="red"
onClick={handleDeleteUsers}
disabled={selectedUsers.length === 0}
>
선택삭제
</Button>
<Button size="sm">저장</Button>
</div>
</div>
<div className="card">
<DataTable table={table} />
<Table>
<TableHeader>
<TableRow>
<TableHead>
<Checkbox
checked={isAllSelected}
onCheckedChange={handleToggleSelectAll}
/>
</TableHead>
<TableHead>NO</TableHead>
<TableHead>이름</TableHead>
<TableHead>직위</TableHead>
<TableHead>직급</TableHead>
<TableHead>최종수정자</TableHead>
<TableHead>최종수정일</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{userList.map((user, index) => (
<TableRow key={user.userUnqId}>
<TableCell align="center">
<Checkbox
checked={isUserSelected(user.userUnqId)}
onCheckedChange={() => handleToggleSelectUser(user)}
/>
</TableCell>
<UserTableRow
form={form}
user={user}
index={index}
jobPositionCodes={jobPositionCodes}
jobGradeCodes={jobGradeCodes}
/>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}
type UserTableRowProps = {
form: UseFormReturn<UserOrganizationFormData>;
user: User;
index: number;
jobPositionCodes?: ICode[];
jobGradeCodes?: ICode[];
};
const UserTableRow = memo(
({
form,
user,
index,
jobPositionCodes,
jobGradeCodes,
}: UserTableRowProps) => (
<>
<TableCell align="center">{index + 1}</TableCell>
<TableCell align="center">{user.userUnqNm}</TableCell>
<TableCell align="center">
<CodeSelect
codeData={jobPositionCodes}
defaultValue={user.jbpsSeCd}
onValueChange={(value) => {
const mode = form.watch(`userOrganization.${index}.mode`);
if (mode === "R") {
form.setValue(`userOrganization.${index}.mode`, "U");
}
form.setValue(`userOrganization.${index}.jbpsSeCd`, value);
}}
{...form.register(`userOrganization.${index}.jbpsSeCd`)}
/>
</TableCell>
<TableCell align="center">
<CodeSelect
codeData={jobGradeCodes}
defaultValue={user.jbgdSeCd}
onValueChange={(value) => {
const mode = form.watch(`userOrganization.${index}.mode`);
if (mode === "R") {
form.setValue(`userOrganization.${index}.mode`, "U");
}
form.setValue(`userOrganization.${index}.jbgdSeCd`, value);
}}
{...form.register(`userOrganization.${index}.jbgdSeCd`)}
/>
</TableCell>
<TableCell align="center">{user.mdfrNm}</TableCell>
<TableCell align="center">
{dayjs(user.mdfcnDt).format("YYYY-MM-DD")}
</TableCell>
</>
),
);
UserTableRow.displayName = "UserTableRow";
type CodeSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
codeData?: ICode[];
};
const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => {
const CodeSelect = memo(({ codeData, ...props }: CodeSelectProps) => {
if (!codeData) return null;
const defaultValueText =
@@ -156,6 +288,8 @@ const CodeSelect = ({ codeData, ...props }: CodeSelectProps) => {
</SelectContent>
</Select>
);
};
});
CodeSelect.displayName = "CodeSelect";
export { UserOrganizationSection };