feat: 이미지 업데이트 및 삭제 작업

This commit is contained in:
2025-04-02 14:07:36 +09:00
parent 6297d528fb
commit 1d132a86de
16 changed files with 356 additions and 267 deletions
@@ -0,0 +1,10 @@
import { ModifyPage as VModifyPage } from "@/pages/image";
export default async function ModifyPage({
params,
}: {
params: Promise<{ serial: string }>;
}) {
const { serial } = await params;
return <VModifyPage serial={serial} />;
}
@@ -34,7 +34,7 @@ export class SiteApiService implements SiteService {
return response.data;
}
public async modifySite(siteId: string, data: SiteFormData): Promise<void> {
public async updateSite(siteId: string, data: SiteFormData): Promise<void> {
const validatedData = siteFormSchema.parse(data);
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`,
@@ -54,6 +54,6 @@ export type SiteList = PageableList<Site>;
export interface SiteService {
getSiteList(params: SiteParams): Promise<SiteList>;
siteDetail(siteId: string): Promise<SiteDetail>;
modifySite(siteId: string, data: SiteFormData): Promise<void>;
updateSite(siteId: string, data: SiteFormData): Promise<void>;
createSite(data: SiteFormData): Promise<void>;
}
+4 -10
View File
@@ -26,19 +26,13 @@ export class ImageApiService implements ImageService {
return response.data;
}
public async createImage(data: ImageFormData): Promise<Image> {
const response = await client.post<Image>(
ENDPOINT.CMS_SERVICE.IMAGES,
data,
);
public async createImage(data: ImageFormData): Promise<void> {
const response = await client.post<void>(ENDPOINT.CMS_SERVICE.IMAGES, data);
return response.data;
}
public async updateImage(
imgSn: string,
data: Partial<Image>,
): Promise<Image> {
const response = await client.put<Image>(
public async updateImage(imgSn: string, data: Partial<Image>): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.IMAGES}/${imgSn}`,
data,
);
+1
View File
@@ -1,2 +1,3 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { ImagePage } from "./ui/image-page";
@@ -46,7 +46,7 @@ export type ImageList = PageableList<Image>;
export interface ImageService {
getImageList: (params: ImageParams) => Promise<ImageList>;
getImageInfo: (imgSn: string) => Promise<Image>;
createImage: (data: ImageFormData) => Promise<Image>;
updateImage: (imgSn: string, data: ImageFormData) => Promise<Image>;
createImage: (data: ImageFormData) => Promise<void>;
updateImage: (imgSn: string, data: ImageFormData) => Promise<void>;
deleteImage: (imgSn: string) => Promise<void>;
}
+239 -213
View File
@@ -3,6 +3,8 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
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,
@@ -18,15 +20,15 @@ import {
TableRow,
} from "@common/components";
import { CodeSelect } from "@/entities/code";
import { ConfirmDialog } from "@/shared/ui/confirm-dialog";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import type { ImageFormData } from "../model/image-interface";
import { handleUpload } from "../lib/utils";
import type { HttpResponse, Upload } from "tus-js-client";
import type { ImageFormData } from "../model/image-interface";
type ImageFormProps = {
form: UseFormReturn<ImageFormData>;
imgSn?: string;
handleSave?: (data: ImageFormData) => Promise<void>;
handleSave: (data: ImageFormData) => Promise<void>;
handleDelete?: () => Promise<void>;
};
@@ -35,15 +37,35 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
const atflYn = form.watch("atflYn");
const router = useRouter();
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false);
const [upload, setUpload] = useState<Upload | null>(null);
const [progress, setProgress] = useState(0);
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await callback();
setOnConfirm(false);
};
const onSubmit = async (data: ImageFormData) => {
await handleSave?.(data);
const confirmMessage = isModifyMode
? "수정하시겠습니까?"
: "저장하시겠습니까?";
await handleConfirm(async () => {
if (await confirm(confirmMessage)) {
handleSave(data);
}
});
};
const onDelete = async () => {
await handleDelete?.();
await handleConfirm(async () => {
if (await confirm("삭제하시겠습니까?")) {
await handleDelete?.();
}
});
};
const handleProgress = (percentage: string) => {
@@ -58,165 +80,22 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<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="imgTtl"
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 ?? ""}
onChange={(e) => {
const value = e.target.value;
if (isNaN(Number(value))) return;
field.onChange(Number(value));
}}
className="!text-[1.3rem]"
placeholder="정렬순서를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>이미지유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="imgTypeCd"
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
{...field}
value={field.value ?? ""}
upCd="IMGTYPE"
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="atflYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="atflYn"
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="atflY" />
<Label htmlFor="atflY">이미지파일</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="atflN" />
<Label htmlFor="atflN">외부링크</Label>
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{atflYn === "Y" && (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
</colgroup>
<TableBody>
<TableRow>
<TableHead>이미지파일</TableHead>
<TableHead>이미지명</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="atflCd"
render={() => (
<FormItem className="flex flex-col">
<FormControl>
<FileUploader
id="atflCd"
className="!h-auto !max-w-none flex-col !items-start"
onUpload={(e) => {
const files = e.target.files;
if (!files) return;
handleUpload(files, {
handleUploadStart: setUpload,
handleProgress,
handleSuccess: setUpload,
handleResponse: handleUploadResponse,
});
}}
customPreview={
<Label htmlFor="atflCd">
<div className="relative h-[27rem] w-[48rem] rounded-sm border border-border">
<p className="absolute left-0 top-0 p-2 text-base leading-none">
{(upload?.file as File)?.name}
</p>
<div></div>
<Progress
value={progress}
className="absolute bottom-0 left-0"
/>
</div>
</Label>
}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{atflYn === "N" && (
<TableRow>
<TableHead>외부링크</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="lnkgUrlAddr"
name="imgTtl"
render={({ field }) => (
<FormItem>
<FormControl>
@@ -224,7 +103,7 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
{...field}
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="외부링크를 입력해주세요"
placeholder="이미지명을 입력해주세요"
/>
</FormControl>
</FormItem>
@@ -232,59 +111,206 @@ function ImageForm({ form, imgSn, handleSave, handleDelete }: ImageFormProps) {
/>
</TableCell>
</TableRow>
)}
<TableRow>
<TableHead>사용여부</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="useYn"
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"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
<TableRow>
<TableHead>정렬순서</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="sortSeq"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
type="number"
min={0}
value={field.value ?? ""}
onChange={(e) => {
const value = e.target.value;
if (isNaN(Number(value))) return;
field.onChange(Number(value));
}}
className="w-[12rem] !text-[1.3rem]"
placeholder="정렬순서를 입력해주세요"
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>이미지유형</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="imgTypeCd"
render={({ field }) => (
<FormItem>
<FormControl>
<CodeSelect
{...field}
value={field.value ?? ""}
upCd="IMGTYPE"
onValueChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
<TableRow>
<TableHead>첨부파일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="atflYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="atflYn"
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="Y" id="atflY" />
<Label htmlFor="atflY">이미지파일</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="N" id="atflN" />
<Label htmlFor="atflN">외부링크</Label>
</div>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
{atflYn === "Y" && (
<TableRow>
<TableHead>이미지파일</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="atflCd"
render={() => (
<FormItem className="flex flex-col">
<FormControl>
<FileUploader
id="atflCd"
className="!h-auto !max-w-none flex-col !items-start"
onUpload={(e) => {
const files = e.target.files;
if (!files) return;
handleUpload(files, {
handleUploadStart: setUpload,
handleProgress,
handleSuccess: setUpload,
handleResponse: handleUploadResponse,
});
}}
customPreview={
<Label htmlFor="atflCd">
<div className="relative h-[27rem] w-[48rem] rounded-sm border border-border">
<p className="absolute left-0 top-0 p-2 text-base leading-none">
{(upload?.file as File)?.name}
</p>
<div></div>
<Progress
value={progress}
className="absolute bottom-0 left-0"
/>
</div>
</Label>
}
/>
</FormControl>
</FormItem>
)}
/>
</TableCell>
</TableRow>
)}
{atflYn === "N" && (
<TableRow>
<TableHead>외부링크</TableHead>
<TableCell className="border">
<FormField
control={form.control}
name="lnkgUrlAddr"
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="useYn"
render={({ field }) => (
<FormItem>
<FormControl>
<RadioGroup
{...field}
name="useYn"
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"
color="white"
type="button"
onClick={() => router.back()}
>
목록
</Button>
)}
</div>
</form>
</Form>
<Button size="lg" type="submit">
저장
</Button>
{isModifyMode && (
<Button size="lg" color="red" type="button" onClick={onDelete}>
삭제
</Button>
)}
</div>
</form>
</Form>
<ConfirmDialog open={onConfirm} />
</>
);
}
@@ -0,0 +1,56 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { imageFormSchema, type ImageFormData } from "../model/image-interface";
import { ImageApi } from "../api/image-service";
import { ImageForm } from "./image-form";
import { useAlertStore } from "@/shared/lib/use-alert-store";
function ModifyPage({ serial }: { serial: string }) {
const router = useRouter();
const { setMessage: alert } = useAlertStore((state) => state);
const { data: imageDetail } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.IMAGES, serial],
queryFn: () => ImageApi.getImageInfo(serial),
});
const { mutateAsync: updateImage } = useMutation({
mutationFn: (data: ImageFormData) => ImageApi.updateImage(serial, data),
onSuccess: () => {
alert("수정되었습니다.");
},
});
const { mutateAsync: deleteImage } = useMutation({
mutationFn: () => ImageApi.deleteImage(serial),
onSuccess: () => {
alert("삭제되었습니다.", router.back);
},
});
const form = useForm<ImageFormData>({
resolver: zodResolver(imageFormSchema),
values: imageDetail,
});
return (
<div className="card card-border !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">이미지 수정</h3>
</div>
<ImageForm
form={form}
imgSn={serial}
handleSave={updateImage}
handleDelete={deleteImage}
/>
</div>
);
}
export { ModifyPage };
+3 -3
View File
@@ -24,7 +24,7 @@ export class MenuApiService implements MenuService {
return response.data;
}
public async modifyMenu(
public async updateMenu(
menuCd: number,
data: Omit<MenuFormData, "siteId">,
): Promise<void> {
@@ -49,7 +49,7 @@ export class MenuApiService implements MenuService {
return response.data;
}
public async modifyTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
public async updateTreeMenu(siteId: string, data: TreeNode[]): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${siteId}/tree`,
data,
@@ -57,7 +57,7 @@ export class MenuApiService implements MenuService {
return response.data;
}
public async modifyMenuName(menuCd: number, name: string): Promise<void> {
public async updateMenuName(menuCd: number, name: string): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.MENUS}/${menuCd}/${name}`,
);
@@ -75,12 +75,12 @@ export type Menu = Omit<MenuFormData, "iconNm"> & {
export interface MenuService {
getMenuInfo: (menuCd: number) => Promise<Menu>;
createMenu: (data: MenuFormData) => Promise<void>;
modifyMenu: (
updateMenu: (
menuCd: number,
data: Omit<MenuFormData, "siteId">,
) => Promise<void>;
deleteMenu: (menuCd: number) => Promise<void>;
getTreeMenuList: (siteId: string) => Promise<TreeMenu[]>;
modifyTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
modifyMenuName: (menuCd: number, name: string) => Promise<void>;
updateTreeMenu: (siteId: string, data: TreeNode[]) => Promise<void>;
updateMenuName: (menuCd: number, name: string) => Promise<void>;
}
+7 -7
View File
@@ -13,7 +13,7 @@ interface MenuFormProps {
form: UseFormReturn<MenuFormData>;
menuCd?: number;
handleCreate: (data: MenuFormData) => void;
handleModify: ({
handleUpdate: ({
menuCd,
data,
}: {
@@ -27,7 +27,7 @@ function MenuForm({
form,
menuCd,
handleCreate,
handleModify,
handleUpdate,
handleDelete,
}: MenuFormProps) {
const isModifyMode = !!menuCd;
@@ -35,15 +35,15 @@ function MenuForm({
const { confirm } = useConfirm();
const [onConfirm, setOnConfirm] = useState(false);
const handleConfirm = async (fn: () => Promise<void>) => {
const handleConfirm = async (callback: () => Promise<void>) => {
setOnConfirm(true);
await fn();
await callback();
setOnConfirm(false);
};
const onSubmit = async (data: MenuFormData) => {
if (isModifyMode) {
await onModify(data);
await onUpdate(data);
} else {
await onCreate(data);
}
@@ -57,10 +57,10 @@ function MenuForm({
});
};
const onModify = async (data: MenuFormData) => {
const onUpdate = async (data: MenuFormData) => {
await handleConfirm(async () => {
if (await confirm("수정하시겠습니까?")) {
handleModify({ menuCd: menuCd!, data });
handleUpdate({ menuCd: menuCd!, data });
}
});
};
+3 -3
View File
@@ -34,10 +34,10 @@ function MenuNode({
const queryClient = useQueryClient();
const { mutateAsync: modifyMenuName } = useMutation({
const { mutateAsync: updateMenuName } = useMutation({
mutationFn: (nodeName: string) => {
const encodedName = encodeURIComponent(nodeName);
return MenuApi.modifyMenuName(id as number, encodedName);
return MenuApi.updateMenuName(id as number, encodedName);
},
onSuccess: () => {
queryClient.invalidateQueries({
@@ -53,7 +53,7 @@ function MenuNode({
const handleSelect = () => onSelect(node);
const handleSave = async () => {
await modifyMenuName(nodeName);
await updateMenuName(nodeName);
setEditMode(false);
};
+3 -3
View File
@@ -80,14 +80,14 @@ function MenuPage() {
},
});
const { mutate: modifyMenu } = useMutation({
const { mutate: updateMenu } = useMutation({
mutationFn: ({
menuCd,
data,
}: {
menuCd: number;
data: Omit<MenuFormData, "siteId">;
}) => MenuApi.modifyMenu(menuCd, data),
}) => MenuApi.updateMenu(menuCd, data),
onSuccess: () => {
alert("수정되었습니다.");
queryClient.invalidateQueries({
@@ -250,7 +250,7 @@ function MenuPage() {
form={form}
menuCd={selectedMenu?.menuCd}
handleCreate={createMenu}
handleModify={modifyMenu}
handleUpdate={updateMenu}
handleDelete={deleteMenu}
/>
</div>
+3 -3
View File
@@ -41,9 +41,9 @@ function MenuTree({
const queryClient = useQueryClient();
const treeMethods = useRef<TreeMethods>(null);
const { mutate: modifyTreeMenu } = useMutation({
const { mutate: updateTreeMenu } = useMutation({
mutationFn: (newTree: TreeNode) =>
MenuApi.modifyTreeMenu(siteId, [newTree]),
MenuApi.updateTreeMenu(siteId, [newTree]),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS],
@@ -96,7 +96,7 @@ function MenuTree({
/>
);
}}
onDrop={handleDropNode(modifyTreeMenu)}
onDrop={handleDropNode(updateTreeMenu)}
classes={{
root: "p-4",
container: "flex flex-col",
+4 -6
View File
@@ -10,8 +10,6 @@ import { useAlertStore } from "@/shared/lib/use-alert-store";
import { SiteForm } from "./site-form";
function ModifyPage({ siteId }: { siteId: string }) {
const isModifyMode = !!siteId;
const router = useRouter();
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
@@ -30,11 +28,11 @@ function ModifyPage({ siteId }: { siteId: string }) {
bscSiteYn: data.basicSiteYn,
};
},
enabled: isModifyMode,
enabled: !!siteId,
});
const { mutateAsync: modifySite } = useMutation({
mutationFn: (data: SiteFormData) => SiteApi.modifySite(siteId, data),
const { mutateAsync: updateSite } = useMutation({
mutationFn: (data: SiteFormData) => SiteApi.updateSite(siteId, data),
onSuccess: () => {
alert("수정되었습니다.", () => {
queryClient.invalidateQueries({
@@ -69,7 +67,7 @@ function ModifyPage({ siteId }: { siteId: string }) {
<SiteForm
form={form}
siteId={siteId}
handleSave={modifySite}
handleSave={updateSite}
handleDelete={deleteSite}
/>
</div>
+16 -12
View File
@@ -38,6 +38,12 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
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",
@@ -50,21 +56,19 @@ function SiteForm({ form, siteId, handleSave, handleDelete }: SiteFormProps) {
? "수정하시겠습니까?"
: "저장하시겠습니까?";
setOnConfirm(true);
if (await confirm(confirmMessage)) {
setOnConfirm(false);
handleSave(data);
}
setOnConfirm(false);
await handleConfirm(async () => {
if (await confirm(confirmMessage)) {
handleSave(data);
}
});
};
const onDelete = async () => {
setOnConfirm(true);
if (await confirm("삭제하시겠습니까?")) {
setOnConfirm(false);
handleDelete?.();
}
setOnConfirm(false);
await handleConfirm(async () => {
if (await confirm("삭제하시겠습니까?")) {
handleDelete?.();
}
});
};
return (