refactor: form wrapper 선언 및 적용

This commit is contained in:
2025-04-08 13:59:04 +09:00
parent d6b48ea554
commit 77539bb785
13 changed files with 1146 additions and 1259 deletions
+1
View File
@@ -0,0 +1 @@
export { CodeSelect } from "../ui/code-select";
@@ -25,8 +25,8 @@ import {
DialogFooter,
} from "@common/components/ui";
import { cn } from "@common/components/lib";
import { CodeSelect } from "@/entities/code";
import { SiteApi, type Site } from "@/entities/site";
import { CodeSelect } from "@/entities/code/@x/posting";
import { SiteApi, type Site } from "@/entities/site/@x/posting";
import type { PageableList } from "@/shared/model";
import { ENDPOINT } from "@/shared/config";
import { PostingApi } from "../api/posting-service";
+2
View File
@@ -0,0 +1,2 @@
export { SiteApi } from "../api/site-service";
export type { Site } from "../model/site-interface";
@@ -35,17 +35,17 @@ export interface SiteDetail extends SiteFormData {
export const siteFormSchema = z.object({
siteId: z.string().max(2).nonempty(),
siteNm: z.string().max(100).nonempty(),
siteExpln: z.string().max(1000).nullable(),
siteExpln: z.string().max(1000).nullish(),
siteKndCd: z.string().max(10).nonempty(),
siteAddr: z.string().max(2000).nullable(),
faxNo: z.string().max(100).nullable(),
telNo: z.string().max(1000).nullable(),
lwndCn: z.string().max(2000).nullable(),
siteAddr: z.string().max(2000).nullish(),
faxNo: z.string().max(100).nullish(),
telNo: z.string().max(1000).nullish(),
lwndCn: z.string().max(2000).nullish(),
bscSiteYn: z.string(),
bkmkIcon: z.string().max(100).nullable(),
bkmkIcon: z.string().max(100).nullish(),
useYn: z.string(),
siteSkn: z.string().max(10).nullable(),
dmnAddr: z.string().nullable(),
siteSkn: z.string().max(10).nullish(),
dmnAddr: z.string().nullish(),
});
export type SiteFormData = z.infer<typeof siteFormSchema>;
+7 -55
View File
@@ -2,13 +2,10 @@
import Image from "next/image";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import type { UseFormReturn } from "react-hook-form";
import type { HttpResponse, Upload } from "tus-js-client";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
FileUploader,
Input,
Label,
@@ -25,9 +22,10 @@ import {
import { CodeSelect } from "@/entities/code";
import { AttachmentApi } from "@/entities/attachment";
import { ENDPOINT } from "@/shared/config";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
import type { BannerFormData } from "../model/banner-interface";
import { handleUpload } from "../lib/utils";
import { FormWrapper } from "@/widgets/form-wrapper";
type BannerFormProps = {
form: UseFormReturn<BannerFormData>;
@@ -36,17 +34,9 @@ type BannerFormProps = {
handleDelete?: () => Promise<void>;
};
function BannerForm({
form,
bnrSn,
handleSave,
handleDelete,
}: BannerFormProps) {
const isModifyMode = !!bnrSn;
function BannerForm({ form, bnrSn, ...props }: BannerFormProps) {
const atflCd = form.watch("atflCd");
const router = useRouter();
const { confirm } = useConfirm();
const [imageUrl, setImageUrl] = useState("");
const [upload, setUpload] = useState<Upload | null>(null);
const [progress, setProgress] = useState(0);
@@ -58,22 +48,6 @@ function BannerForm({
enabled: !!atflCd,
});
const onSubmit = async (data: BannerFormData) => {
const confirmMessage = isModifyMode
? "수정하시겠습니까?"
: "저장하시겠습니까?";
if (await confirm(confirmMessage)) {
await handleSave(data);
}
};
const onDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
await handleDelete?.();
}
};
const handleProgress = (percentage: string) => {
setProgress(Number(percentage));
};
@@ -96,9 +70,7 @@ function BannerForm({
}, [atflId]);
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormWrapper form={form} isModifyMode={!!bnrSn} {...props}>
<div className="card">
<Table variant="secondary">
<colgroup>
@@ -197,8 +169,8 @@ function BannerForm({
<div className="mb-2 text-lg font-medium text-label">
<ul className="ml-10 list-disc">
<li>
이미지 파일(jpg, jpeg, png, gif)만
업로드 가능합니다.
이미지 파일(jpg, jpeg, png, gif)만 업로드
가능합니다.
</li>
</ul>
</div>
@@ -314,27 +286,7 @@ function BannerForm({
</TableBody>
</Table>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
</>
</FormWrapper>
);
}
+5 -48
View File
@@ -2,13 +2,10 @@
import Image from "next/image";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import type { UseFormReturn } from "react-hook-form";
import type { HttpResponse, Upload } from "tus-js-client";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import {
Button,
FileUploader,
Input,
Label,
@@ -21,10 +18,11 @@ import {
TableHead,
TableRow,
} from "@common/components";
import { FormWrapper } from "@/widgets/form-wrapper";
import { CodeSelect } from "@/entities/code";
import { AttachmentApi } from "@/entities/attachment";
import { ENDPOINT } from "@/shared/config";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
import type { ImageFormData } from "../model/image-interface";
import { handleUpload } from "../lib/utils";
@@ -35,13 +33,10 @@ type ImageFormProps = {
handleDelete?: () => Promise<void>;
};
function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
const isModifyMode = !!imgSn;
function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
const atflYn = form.watch("atflYn");
const atflCd = form.watch("atflCd");
const router = useRouter();
const { confirm } = useConfirm();
const [imageUrl, setImageUrl] = useState("");
const [upload, setUpload] = useState<Upload | null>(null);
const [progress, setProgress] = useState(0);
@@ -53,22 +48,6 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
enabled: !!atflCd,
});
const onSubmit = async (data: ImageFormData) => {
const confirmMessage = isModifyMode
? "수정하시겠습니까?"
: "저장하시겠습니까?";
if (await confirm(confirmMessage)) {
await handleSave(data);
}
};
const onDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
await handleDelete?.();
}
};
const handleProgress = (percentage: string) => {
setProgress(Number(percentage));
};
@@ -91,9 +70,7 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
}, [atflId]);
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormWrapper form={form} isModifyMode={!!imgSn} {...props}>
<div className="card">
<Table variant="secondary">
<colgroup>
@@ -319,27 +296,7 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
</TableBody>
</Table>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
</>
</FormWrapper>
);
}
+1 -22
View File
@@ -1,9 +1,7 @@
import { useState } from "react";
import type { UseFormReturn } from "react-hook-form";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { Button } from "@common/components";
import { Form } from "@/shared/ui/form";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import type { MenuFormData } from "../model/menu-interface";
import { BaseMenuForm } from "./base-menu-form";
import { TypeMenuForm } from "./type-menu-form";
@@ -33,13 +31,6 @@ function MenuForm({
const isModifyMode = !!menuCd;
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false);
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
const onSubmit = async (data: MenuFormData) => {
if (isModifyMode) {
@@ -50,37 +41,27 @@ function MenuForm({
};
const onCreate = async (data: MenuFormData) => {
await handleConfirm(async () => {
if (await confirm("저장하시겠습니까?")) {
setOnConfirm(false);
handleCreate(data);
}
});
};
const onUpdate = async (data: MenuFormData) => {
await handleConfirm(async () => {
if (await confirm("수정하시겠습니까?")) {
setOnConfirm(false);
handleUpdate({ menuCd: menuCd!, data });
}
});
};
const onDelete = () => {
handleConfirm(async () => {
const onDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
setOnConfirm(false);
handleDelete?.(menuCd!);
}
});
};
const menuType = form.watch("menuLnkgTypeCd");
const isSubmitDisabled = !form.watch("siteId");
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card card-border">
@@ -107,8 +88,6 @@ function MenuForm({
</div>
</form>
</Form>
<ConfirmDialog open={onConfirm} />
</>
);
}
+2 -2
View File
@@ -29,9 +29,9 @@ function CreatePage() {
faxNo: "",
telNo: "",
lwndCn: "",
bscSiteYn: "",
bscSiteYn: "Y",
bkmkIcon: "",
useYn: "",
useYn: "Y",
siteSkn: "",
},
});
@@ -32,7 +32,6 @@ import {
type Domain,
type DomainFormData,
} from "../model/domain-interface";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAlertStore } from "@/shared/lib/use-alert-store";
type DomainManagementDialogProps = {
@@ -79,7 +78,6 @@ function DomainManagementDialog({
name: "domains",
});
const [onConfirm, setOnConfirm] = useState(false);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const initialForm = () => {
@@ -88,14 +86,11 @@ function DomainManagementDialog({
};
const onSubmit = async (data: DomainFormData) => {
setOnConfirm(true);
if (await confirm("저장하시겠습니까?")) {
setOnConfirm(false);
await mutateAsync(data.domains);
handleSaveDomain?.(data.domains);
alert("저장되었습니다.");
}
setOnConfirm(false);
};
const handleAddDomain = () => {
@@ -153,7 +148,6 @@ function DomainManagementDialog({
fields.length > 0 && selectedRows.length === fields.length;
return (
<>
<Dialog {...props}>
<DialogTrigger asChild>
<Button>도메인 관리</Button>
@@ -305,8 +299,6 @@ function DomainManagementDialog({
</Form>
</DialogContent>
</Dialog>
<ConfirmDialog open={onConfirm} />
</>
);
}
@@ -28,7 +28,6 @@ import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { ENDPOINT } from "@/shared/config";
import { IpApi } from "../api/ip-service";
import { ipFormSchema, type IP, type IPFormData } from "../model/ip-interface";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { useAlertStore } from "@/shared/lib/use-alert-store";
type IPManagementDialogProps = {
@@ -75,7 +74,6 @@ function IPManagementDialog({
name: "ips",
});
const [onConfirm, setOnConfirm] = useState(false);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
const initialForm = () => {
@@ -84,13 +82,10 @@ function IPManagementDialog({
};
const onSubmit = async (data: IPFormData) => {
setOnConfirm(true);
if (await confirm("저장하시겠습니까?")) {
setOnConfirm(false);
await mutateAsync(data.ips);
alert("저장되었습니다.");
}
setOnConfirm(false);
};
const handleAddIP = () => {
@@ -140,7 +135,6 @@ function IPManagementDialog({
fields.length > 0 && selectedRows.length === fields.length;
return (
<>
<Dialog {...props}>
<DialogTrigger asChild>
<Button disabled={triggerDisabled}>IP 관리</Button>
@@ -271,8 +265,6 @@ function IPManagementDialog({
</Form>
</DialogContent>
</Dialog>
<ConfirmDialog open={onConfirm} />
</>
);
}
+5 -62
View File
@@ -1,9 +1,6 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { type UseFormReturn } from "react-hook-form";
import {
Button,
Input,
Label,
RadioGroup,
@@ -14,10 +11,10 @@ import {
TableHead,
TableRow,
} from "@common/components";
import { FormWrapper } from "@/widgets/form-wrapper";
import { CodeSelect } from "@/entities/code";
import type { SiteFormData } from "@/entities/site";
import { FormItem, FormControl, FormField, Form } from "@/shared/ui/form";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
import type { Domain } from "../model/domain-interface";
import { IPManagementDialog } from "./ip-management-dialog";
import { DomainManagementDialog } from "./domain-management-dialog";
@@ -29,21 +26,12 @@ type SiteFormProps = {
handleDelete?: () => Promise<void>;
};
function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
function SiteForm({ form, siteId, ...props }: SiteFormProps) {
const isModifyMode = !!siteId;
const router = useRouter();
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false);
const [isIpManagementOpen, setIsIpManagementOpen] = useState(false);
const [isDomainManagementOpen, setIsDomainManagementOpen] = useState(false);
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
const handleSaveDomain = (rows: Domain[]) => {
form.setValue(
"dmnAddr",
@@ -51,32 +39,8 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
);
};
const onSubmit = async (data: SiteFormData) => {
const confirmMessage = isModifyMode
? "수정하시겠습니까?"
: "저장하시겠습니까?";
await handleConfirm(async () => {
if (await confirm(confirmMessage)) {
setOnConfirm(false);
handleSave(data);
}
});
};
const onDelete = async () => {
await handleConfirm(async () => {
if (await confirm("삭제하시겠습니까?")) {
setOnConfirm(false);
handleDelete?.();
}
});
};
return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormWrapper form={form} isModifyMode={isModifyMode} {...props}>
<div className="card">
<Table variant="secondary">
<colgroup>
@@ -382,28 +346,7 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
</TableBody>
</Table>
</div>
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
<ConfirmDialog open={onConfirm} />
</>
</FormWrapper>
);
}
@@ -0,0 +1 @@
export { FormWrapper } from "./ui/form-wrapper";
@@ -0,0 +1,68 @@
import { useRouter } from "next/navigation";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { type FieldValues, type UseFormReturn } from "react-hook-form";
import { Button } from "@common/components/ui";
import { Form } from "@/shared/ui/form";
type FormWrapperProps<T extends FieldValues> = {
form: UseFormReturn<T>;
children: React.ReactNode;
isModifyMode?: boolean;
handleSave: (data: T) => Promise<void>;
handleDelete?: () => Promise<void>;
};
function FormWrapper<T extends FieldValues>({
form,
children,
isModifyMode,
handleSave,
handleDelete,
}: FormWrapperProps<T>) {
const router = useRouter();
const { confirm } = useConfirm();
const onSubmit = async (data: T) => {
const confirmMessage = isModifyMode
? "수정하시겠습니까?"
: "저장하시겠습니까?";
if (await confirm(confirmMessage)) {
await handleSave(data);
}
};
const onDelete = async () => {
if (await confirm("삭제하시겠습니까?")) {
await handleDelete?.();
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{children}
<div className="flex justify-center gap-4 pb-12 pt-10">
<Button
size="lg"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
);
}
export { FormWrapper };