feat: 조직도 소속원 저장 및 선택 삭제 기능 작업

This commit is contained in:
2025-04-24 11:10:17 +09:00
parent aa8edc71c7
commit f6bcf8ebfe
4 changed files with 91 additions and 42 deletions
@@ -5,8 +5,10 @@ import type {
UserOrganizationParams, UserOrganizationParams,
UserOrganizationList, UserOrganizationList,
UserOrganizationService, UserOrganizationService,
UserOrganizationPayloadData,
SearchUserParams, SearchUserParams,
SearchUserList, SearchUserList,
User,
} from "../model/user-organization-interface"; } from "../model/user-organization-interface";
export class UserOrganizationApiService implements UserOrganizationService { export class UserOrganizationApiService implements UserOrganizationService {
@@ -34,6 +36,16 @@ export class UserOrganizationApiService implements UserOrganizationService {
); );
return response.data; return response.data;
} }
public async saveUserOrganization(
data: UserOrganizationPayloadData[],
): Promise<User[]> {
const response = await client.post<User[]>(
`${ENDPOINT.CMS_SERVICE.USER_ORGANIZATION}`,
data,
);
return response.data;
}
} }
export const UserOrganizationApi = new UserOrganizationApiService(); export const UserOrganizationApi = new UserOrganizationApiService();
@@ -81,4 +81,5 @@ export interface UserOrganizationService {
): Promise<UserOrganizationList>; ): Promise<UserOrganizationList>;
getUserOrganizationTreeList(): Promise<TreeUserOrganization[]>; getUserOrganizationTreeList(): Promise<TreeUserOrganization[]>;
searchUser(params: SearchUserParams): Promise<SearchUserList>; searchUser(params: SearchUserParams): Promise<SearchUserList>;
saveUserOrganization(data: UserOrganizationPayloadData[]): Promise<User[]>;
} }
@@ -54,7 +54,9 @@ function AdditionalUserDialog({
const selectedRows = table.getSelectedRowModel().rows; const selectedRows = table.getSelectedRowModel().rows;
const userData = selectedRows.map((row) => row.original); const userData = selectedRows.map((row) => row.original);
if (!userData) return; if (!userData) return;
onAddUsers(userData); onAddUsers(userData);
table.setRowSelection({});
}; };
const handleCancel = () => { const handleCancel = () => {
@@ -1,8 +1,14 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import React, { useEffect, useState, useCallback, memo } from "react"; import React, { useEffect, useState, useCallback, memo } from "react";
import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form"; import { useFieldArray, useForm, type UseFormReturn } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery, keepPreviousData } from "@tanstack/react-query"; import {
useQuery,
useMutation,
useQueryClient,
keepPreviousData,
} from "@tanstack/react-query";
import { import {
Button, Button,
Checkbox, Checkbox,
@@ -24,11 +30,13 @@ import {
UserOrganizationApi, UserOrganizationApi,
userOrganizationFormSchema, userOrganizationFormSchema,
type UserOrganizationFormData, type UserOrganizationFormData,
type UserOrganizationPayloadData,
type User, type User,
type SearchUser, type SearchUser,
} from "@/entities/user-organization"; } from "@/entities/user-organization";
import { CodeApi, type ICode } from "@/entities/code"; import { CodeApi, type ICode } from "@/entities/code";
import { ENDPOINT } from "@/shared/config"; import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { AdditionalUserDialog } from "./additional-user-dialog"; import { AdditionalUserDialog } from "./additional-user-dialog";
type UserOrganizationSectionProps = { type UserOrganizationSectionProps = {
@@ -36,6 +44,10 @@ type UserOrganizationSectionProps = {
}; };
function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) { function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
const queryClient = useQueryClient();
const { confirm } = useConfirm();
const { setMessage: alert } = useAlertStore((state) => state);
const [userList, setUserList] = useState<User[]>([]); const [userList, setUserList] = useState<User[]>([]);
const [selectedUsers, setSelectedUsers] = useState<User[]>([]); const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
@@ -59,17 +71,29 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
queryFn: () => CodeApi.getCode("JDGDCLCD"), queryFn: () => CodeApi.getCode("JDGDCLCD"),
}); });
const { mutate: saveUserOrganization } = useMutation({
mutationFn: (data: UserOrganizationPayloadData[]) =>
UserOrganizationApi.saveUserOrganization(data),
onSuccess: () => {
alert("저장되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.USER_ORGANIZATION, ognzSn],
});
},
});
const form = useForm<UserOrganizationFormData>({ const form = useForm<UserOrganizationFormData>({
resolver: zodResolver(userOrganizationFormSchema), resolver: zodResolver(userOrganizationFormSchema),
values: { values: {
userOrganization: userList.map((user) => ({ userOrganization:
mode: user.mode, userOrganizationList?.content.map((user) => ({
ognzSn: user.ognzSn, mode: user.mode,
userUnqId: user.userUnqId, ognzSn: user.ognzSn,
jbpsSeCd: user.jbpsSeCd, userUnqId: user.userUnqId,
jbgdSeCd: user.jbgdSeCd, jbpsSeCd: user.jbpsSeCd,
sortSeq: user.sortSeq, jbgdSeCd: user.jbgdSeCd,
})), sortSeq: user.sortSeq,
})) ?? [],
}, },
}); });
@@ -79,30 +103,25 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
}); });
const handleAddUsers = (users: SearchUser[]) => { const handleAddUsers = (users: SearchUser[]) => {
const newForm = users.map((user) => ({ const generateUserData = (user: SearchUser) => ({
mode: "C", mode: "C",
ognzSn, ognzSn,
userUnqId: user.userUnqId, userUnqId: user.userUnqId,
jbpsSeCd: "", jbpsSeCd: "",
jbgdSeCd: "", jbgdSeCd: "",
sortSeq: 0, sortSeq: 0,
})); });
append(newForm);
const newFormData = users.map(generateUserData);
append(newFormData);
const newUserList = users.map((user) => ({ const newUserList = users.map((user) => ({
mode: "C", ...generateUserData(user),
ognzSn,
userUnqId: user.userUnqId,
userUnqNm: user.userNm, userUnqNm: user.userNm,
jbpsSeCd: "",
jbgdSeCd: "",
sortSeq: 0,
})); }));
const existingUserIds = new Set(newUserList.map((user) => user.userUnqId));
const filteredUserList = userList.filter( const filteredUserList = userList.filter(
(existingUser) => (user) => !existingUserIds.has(user.userUnqId),
!newUserList.some(
(newUser) => newUser.userUnqId === existingUser.userUnqId,
),
); );
setUserList([...newUserList, ...filteredUserList]); setUserList([...newUserList, ...filteredUserList]);
}; };
@@ -130,16 +149,29 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
}; };
const handleDeleteUsers = () => { const handleDeleteUsers = () => {
if (selectedUsers.length === 0) return;
const selectedUserIds = selectedUsers.map((user) => user.userUnqId); const selectedUserIds = selectedUsers.map((user) => user.userUnqId);
const updatedFormData = form.getValues("userOrganization").map((item) => {
if (selectedUserIds.includes(item.userUnqId)) {
return { ...item, mode: "D" };
}
return item;
});
form.setValue("userOrganization", updatedFormData);
setUserList((prevUsers) => setUserList((prevUsers) =>
prevUsers.filter((user) => !selectedUserIds.includes(user.userUnqId)), prevUsers.filter((user) => !selectedUserIds.includes(user.userUnqId)),
); );
setSelectedUsers([]); setSelectedUsers([]);
}; };
const handleSaveUsers = async () => {
if (await confirm("저장하시겠습니까?")) {
const filteredData = form
.watch("userOrganization")
.filter((item) => item.mode !== "R");
saveUserOrganization(filteredData);
}
};
const isUserSelected = useCallback( const isUserSelected = useCallback(
(userUnqId: string) => (userUnqId: string) =>
selectedUsers.some((item) => item.userUnqId === userUnqId), selectedUsers.some((item) => item.userUnqId === userUnqId),
@@ -166,7 +198,9 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
> >
선택삭제 선택삭제
</Button> </Button>
<Button size="sm">저장</Button> <Button size="sm" onClick={handleSaveUsers}>
저장
</Button>
</div> </div>
</div> </div>
<div className="card"> <div className="card">
@@ -196,12 +230,12 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
onCheckedChange={() => handleToggleSelectUser(user)} onCheckedChange={() => handleToggleSelectUser(user)}
/> />
</TableCell> </TableCell>
<UserTableRow <UserCells
form={form} form={form}
user={user} user={user}
index={index} index={index}
jobPositionCodes={jobPositionCodes} jobPositionCodeData={jobPositionCodes}
jobGradeCodes={jobGradeCodes} jobGradeCodeData={jobGradeCodes}
/> />
</TableRow> </TableRow>
))} ))}
@@ -212,28 +246,28 @@ function UserOrganizationSection({ ognzSn }: UserOrganizationSectionProps) {
); );
} }
type UserTableRowProps = { type UserCellsProps = {
form: UseFormReturn<UserOrganizationFormData>; form: UseFormReturn<UserOrganizationFormData>;
user: User; user: User;
index: number; index: number;
jobPositionCodes?: ICode[]; jobPositionCodeData?: ICode[];
jobGradeCodes?: ICode[]; jobGradeCodeData?: ICode[];
}; };
const UserTableRow = memo( const UserCells = memo(
({ ({
form, form,
user, user,
index, index,
jobPositionCodes, jobPositionCodeData,
jobGradeCodes, jobGradeCodeData,
}: UserTableRowProps) => ( }: UserCellsProps) => (
<> <>
<TableCell align="center">{index + 1}</TableCell> <TableCell align="center">{index + 1}</TableCell>
<TableCell align="center">{user.userUnqNm}</TableCell> <TableCell align="center">{user.userUnqNm}</TableCell>
<TableCell align="center"> <TableCell align="center">
<CodeSelect <CodeSelect
codeData={jobPositionCodes} codeData={jobPositionCodeData}
defaultValue={user.jbpsSeCd} defaultValue={user.jbpsSeCd}
onValueChange={(value) => { onValueChange={(value) => {
const mode = form.watch(`userOrganization.${index}.mode`); const mode = form.watch(`userOrganization.${index}.mode`);
@@ -247,7 +281,7 @@ const UserTableRow = memo(
</TableCell> </TableCell>
<TableCell align="center"> <TableCell align="center">
<CodeSelect <CodeSelect
codeData={jobGradeCodes} codeData={jobGradeCodeData}
defaultValue={user.jbgdSeCd} defaultValue={user.jbgdSeCd}
onValueChange={(value) => { onValueChange={(value) => {
const mode = form.watch(`userOrganization.${index}.mode`); const mode = form.watch(`userOrganization.${index}.mode`);
@@ -267,7 +301,7 @@ const UserTableRow = memo(
), ),
); );
UserTableRow.displayName = "UserTableRow"; UserCells.displayName = "UserCells";
type CodeSelectProps = React.ComponentPropsWithoutRef<typeof Select> & { type CodeSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
codeData?: ICode[]; codeData?: ICode[];
@@ -276,14 +310,14 @@ type CodeSelectProps = React.ComponentPropsWithoutRef<typeof Select> & {
const CodeSelect = memo(({ codeData, ...props }: CodeSelectProps) => { const CodeSelect = memo(({ codeData, ...props }: CodeSelectProps) => {
if (!codeData) return null; if (!codeData) return null;
const defaultValueText = const placeholderText =
codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ?? codeData.find((item) => item.lwpoCd === props.defaultValue)?.lwpoCdNm ??
"선택"; "선택";
return ( return (
<Select {...props}> <Select {...props}>
<SelectTrigger className="w-[12rem]"> <SelectTrigger className="w-[10rem]">
<SelectValue placeholder={defaultValueText} /> <SelectValue placeholder={placeholderText} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{codeData.map((item) => ( {codeData.map((item) => (