feat: 조직도 폼 작업

This commit is contained in:
2025-04-21 19:36:21 +09:00
parent e719834c80
commit c6c5eff11e
5 changed files with 284 additions and 13 deletions
@@ -15,13 +15,15 @@ export class OrganizationApiService implements OrganizationService {
return response.data;
}
public async createOrganization(
organization: OrganizationFormData,
): Promise<void> {
await client.post<void>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`,
organization,
public async getOrganizationInfo(ognzSn: number): Promise<Organization> {
const response = await client.get<Organization>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
);
return response.data;
}
public async createOrganization(form: OrganizationFormData): Promise<void> {
await client.post<void>(`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}`, form);
}
public async deleteOrganization(ognzSn: number): Promise<void> {
@@ -39,6 +41,16 @@ export class OrganizationApiService implements OrganizationService {
);
return response.data;
}
public async updateOrganization(
ognzSn: number,
form: OrganizationFormData,
): Promise<void> {
await client.put<void>(
`${ENDPOINT.CMS_SERVICE.ORGANIZATIONS}/${ognzSn}`,
form,
);
}
}
export const OrganizationApi = new OrganizationApiService();
@@ -21,7 +21,9 @@ export type OrganizationTree = Organization & {
export interface OrganizationService {
getOrganizationsTreeList(): Promise<OrganizationTree[]>;
createOrganization(organization: OrganizationFormData): Promise<void>;
getOrganizationInfo(ognzSn: number): Promise<Organization>;
createOrganization(form: OrganizationFormData): Promise<void>;
deleteOrganization(ognzSn: number): Promise<void>;
updateOrganizationName(ognzSn: number, name: string): Promise<Organization>;
updateOrganization(ognzSn: number, form: OrganizationFormData): Promise<void>;
}
@@ -0,0 +1,200 @@
"use client";
import type { UseFormReturn } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
Input,
Label,
RadioGroup,
RadioGroupItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
} from "@common/components";
import type { OrganizationFormData } from "@/entities/organization";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
type OrganizationFormProps = {
form: UseFormReturn<OrganizationFormData>;
ognzSn?: number;
handleCreate: (data: OrganizationFormData) => Promise<void>;
handleUpdate: ({
ognzSn,
data,
}: {
ognzSn: number;
data: OrganizationFormData;
}) => Promise<void>;
handleDelete?: (ognzSn: number) => Promise<void>;
};
function OrganizationForm({
form,
ognzSn,
handleCreate,
handleUpdate,
handleDelete,
}: OrganizationFormProps) {
const isModifyMode = !!ognzSn;
const { confirm } = useConfirm();
const onSubmit = async (data: OrganizationFormData) => {
if (isModifyMode) {
await onUpdate(data);
} else {
await onCreate(data);
}
};
const onCreate = async (data: OrganizationFormData) => {
if (await confirm("저장하시겠습니까?")) {
handleCreate(data);
}
};
const onUpdate = async (data: OrganizationFormData) => {
if (await confirm("수정하시겠습니까?")) {
handleUpdate({ ognzSn: ognzSn!, data });
}
};
const onDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
handleDelete?.(ognzSn!);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="15%" />
<col width="85%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>조직명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="ognzNm"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="조직명을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>조직설명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="ognzExpln"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="조직설명을 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>정렬순서</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="sortSeq"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="number"
value={field.value ?? 0}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="!text-[1.3rem]"
placeholder="정렬순서를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="useYn"
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="useY" />
<Label htmlFor="useY">사용</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="useN" />
<Label htmlFor="useN">사용안함</Label>
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button size="lg" type="submit">
저장
</Button>
<Button
size="lg"
color="red"
type="button"
onClick={onDelete}
disabled={!isModifyMode}
>
삭제
</Button>
</div>
</form>
</Form>
);
}
export { OrganizationForm };
@@ -17,6 +17,7 @@ import {
} from "@/entities/organization";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import { OrganizationForm } from "./organization-form";
function OrganizationPage() {
const queryClient = useQueryClient();
@@ -34,9 +35,19 @@ function OrganizationPage() {
select: (data) => generateOrganizationTreeData(data),
});
const { data: organizationInfo } = useQuery({
queryKey: [
ENDPOINT.CMS_SERVICE.ORGANIZATIONS,
selectedOrganization?.ognzSn,
],
queryFn: () =>
OrganizationApi.getOrganizationInfo(selectedOrganization!.ognzSn!),
enabled: !!selectedOrganization?.ognzSn,
});
const { mutateAsync: createOrganization } = useMutation({
mutationFn: (organization: OrganizationFormData) =>
OrganizationApi.createOrganization(organization),
mutationFn: (form: OrganizationFormData) =>
OrganizationApi.createOrganization(form),
onSuccess: () => {
alert("저장되었습니다.");
queryClient.invalidateQueries({
@@ -49,6 +60,7 @@ function OrganizationPage() {
mutationFn: (ognzSn: number) => OrganizationApi.deleteOrganization(ognzSn),
onSuccess: () => {
alert("삭제되었습니다.");
setSelectedOrganization(null);
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
});
@@ -66,6 +78,32 @@ function OrganizationPage() {
},
});
const { mutateAsync: updateOrganization } = useMutation({
mutationFn: ({
ognzSn,
data,
}: {
ognzSn: number;
data: OrganizationFormData;
}) => OrganizationApi.updateOrganization(ognzSn, data),
onSuccess: () => {
alert("수정되었습니다.");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.ORGANIZATIONS],
});
},
});
const form = useForm<OrganizationFormData>({
resolver: zodResolver(organizationFormSchema),
defaultValues: {
ognzNm: "",
sortSeq: 1,
useYn: "Y",
},
values: organizationInfo,
});
const additionalNodeForm = useForm<OrganizationFormData>({
resolver: zodResolver(organizationFormSchema),
defaultValues: {
@@ -159,10 +197,19 @@ function OrganizationPage() {
updateNodeName={async (id, name) => {
await updateOrganizationName({ ognzSn: id as number, name });
}}
classes={{ wrapper: "max-h-[calc(100vh-29rem)]" }}
/>
</div>
</div>
<div className="card card-border flex-1"></div>
<div className="card card-border flex-1">
<OrganizationForm
form={form}
ognzSn={selectedOrganization?.ognzSn ?? 0}
handleCreate={createOrganization}
handleUpdate={updateOrganization}
handleDelete={deleteOrganization}
/>
</div>
</div>
);
}
+13 -3
View File
@@ -11,6 +11,7 @@ import {
type DropOptions,
} from "@minoru/react-dnd-treeview";
import { Button } from "@common/components/ui";
import { cn } from "@common/components/lib";
import { CustomNode } from "./custom-node";
interface TreeData extends Record<string, unknown> {
@@ -19,9 +20,12 @@ interface TreeData extends Record<string, unknown> {
type EditableNodeProps = { depth: number };
type TreeProps<T extends TreeData> = {
type DndTreeProps<T extends TreeData> = {
tree: NodeModel<T>[];
selectedNodeId: number;
classes?: {
wrapper: string;
};
renderEditableNode: (props: EditableNodeProps) => ReactElement;
handleSelectNode: (node: NodeModel<T>) => void;
handleDropNode: (newTree: NodeModel<T>[], options: DropOptions<T>) => void;
@@ -31,11 +35,12 @@ type TreeProps<T extends TreeData> = {
function DndTree<T extends TreeData>({
tree,
selectedNodeId,
classes,
renderEditableNode,
handleSelectNode,
handleDropNode,
updateNodeName,
}: TreeProps<T>) {
}: DndTreeProps<T>) {
const treeMethods = useRef<TreeMethods>(null);
const isAdditionalParent = (node: NodeModel<T>): boolean => {
@@ -46,7 +51,12 @@ function DndTree<T extends TreeData>({
return (
<>
<div className="max-h-[calc(100vh-33.5rem)] overflow-auto">
<div
className={cn(
"max-h-[calc(100vh-33.5rem)] overflow-auto",
classes?.wrapper,
)}
>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
tree={tree}