feat: 콘텐츠 수정 작업

This commit is contained in:
2025-04-18 14:40:39 +09:00
parent 9665189c95
commit 6c014fe0ce
19 changed files with 304 additions and 23 deletions
@@ -0,0 +1,10 @@
import { ModifyPage as VModifyPage } from "@/pages/contents";
export default async function ModifyPage({
params,
}: {
params: Promise<{ sequential: string }>;
}) {
const { sequential } = await params;
return <VModifyPage sequential={sequential} />;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -2,6 +2,7 @@ export interface ICode {
lwpoCd: string;
upCd: string;
lwpoCdNm: string;
unit: string;
}
export interface CodeService {
@@ -4,8 +4,10 @@ import type {
ContentsService,
ContentsParams,
ContentsList,
Contents,
ContentsFormData,
ContentsVersionParams,
ContentsVersionList,
ContentsDetail,
} from "../model/contents-interface";
export class ContentsApiService implements ContentsService {
@@ -17,9 +19,22 @@ export class ContentsApiService implements ContentsService {
return response.data;
}
public async getContentsInfo(contsSn: number): Promise<Contents> {
const response = await client.get<Contents>(
`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
public async getContentsVersionList(
params: ContentsVersionParams,
): Promise<ContentsVersionList> {
const response = await client.get<ContentsVersionList>(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}`,
{ params },
);
return response.data;
}
public async getLatestContentsInfo(
contsSn: number,
contsVer: number,
): Promise<ContentsDetail> {
const response = await client.get<ContentsDetail>(
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
);
return response.data;
}
@@ -34,10 +49,11 @@ export class ContentsApiService implements ContentsService {
public async updateContents(
contsSn: number,
contsVer: number,
data: ContentsFormData,
): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.CONTENTS}/${contsSn}`,
`${ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS}/${contsSn}/${contsVer}`,
data,
);
return response.data;
+1
View File
@@ -1,5 +1,6 @@
export { ContentsApi } from "./api/contents-service";
export {
CONTENTS_APPROVAL_STATUS,
contentsFormSchema,
type Contents,
type ContentsParams,
@@ -1,12 +1,38 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export const CONTENTS_APPROVAL_STATUS = {
DRAFT: "01",
REQUESTED: "02",
APPROVED: "99",
} as const;
export type ContentsApprovalStatus =
(typeof CONTENTS_APPROVAL_STATUS)[keyof typeof CONTENTS_APPROVAL_STATUS];
export interface ContentsParams extends PageableParams {
contsTtl?: string;
aprvContsVer?: number;
useYn?: string;
}
export interface ContentsVersionParams extends PageableParams {
contsSn?: number;
contsCn?: string;
aprvCd?: string;
cclCd?: string;
koglCd?: string;
tag?: string;
useYn?: string;
}
export interface ContentsVersion {
contsSn: number;
contsVer: number;
contsCn: string;
useYn: string;
}
export const contentsSchema = z.object({
contsSn: z.number(),
contsTtl: z.string(),
@@ -22,26 +48,47 @@ export const contentsSchema = z.object({
workContsVer: z.number(),
});
export const contentsFormSchema = z.object({
contsSn: z.number().nullish(),
export const contentsDetailSchema = z.object({
contsSn: z.number(),
contsVer: z.number(),
contsTtl: z.string(),
contsCn: z.string(),
contsRmrk: z.string().nullish(),
aprvCd: z.string().nullish(),
aprvCd: z.string(),
cclCd: z.string().nullish(),
koglCd: z.string().nullish(),
tag: z.string().nullish(),
aprvDt: z.string(),
autzr: z.string(),
useYn: z.string(),
});
export const contentsFormSchema = contentsDetailSchema.omit({
contsVer: true,
aprvDt: true,
autzr: true,
});
export type Contents = z.infer<typeof contentsSchema>;
export type ContentsDetail = z.infer<typeof contentsDetailSchema>;
export type ContentsFormData = z.infer<typeof contentsFormSchema>;
export type ContentsList = PageableList<Contents>;
export type ContentsVersionList = PageableList<ContentsVersion>;
export interface ContentsService {
getContentsList(params: ContentsParams): Promise<ContentsList>;
getContentsInfo(contsSn: number): Promise<Contents>;
getContentsVersionList(
params: ContentsVersionParams,
): Promise<ContentsVersionList>;
getLatestContentsInfo(
contsSn: number,
contsVer: number,
): Promise<ContentsDetail>;
createContents(data: ContentsFormData): Promise<void>;
updateContents(contsSn: number, data: ContentsFormData): Promise<void>;
updateContents(
contsSn: number,
contsVer: number,
data: ContentsFormData,
): Promise<void>;
deleteContents(contsSn: number): Promise<void>;
}
+1
View File
@@ -1,2 +1,3 @@
export { ContentsPage } from "./ui/contents-page";
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
+76 -13
View File
@@ -14,6 +14,10 @@ import {
import { FormWrapper } from "@/widgets/form-wrapper";
import { FormItem, FormControl, FormField } from "@/shared/ui/form";
import type { ContentsFormData } from "@/entities/content";
import { useQuery } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { CodeApi } from "@/entities/code/api/code-service";
import Image from "next/image";
const Editor = dynamic(
() => import("@/entities/editor").then((module) => module.Editor),
@@ -23,11 +27,27 @@ const Editor = dynamic(
interface ContentsFormProps {
form: UseFormReturn<ContentsFormData>;
contsSn?: number;
disabledFields?: Partial<Record<keyof ContentsFormData, boolean>>;
handleSave: (data: ContentsFormData) => Promise<void>;
handleDelete?: () => Promise<void>;
}
function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) {
function ContentsForm({
form,
contsSn,
disabledFields,
...props
}: ContentsFormProps) {
const { data: cclCode } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "CCLCD"],
queryFn: () => CodeApi.getCode("CCLCD"),
});
const { data: koglCode } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "KOGLCD"],
queryFn: () => CodeApi.getCode("KOGLCD"),
});
return (
<FormWrapper form={form} isModifyMode={!!contsSn} {...props}>
<div className="card">
@@ -51,6 +71,7 @@ function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) {
value={field.value ?? ""}
className="!text-[1.3rem]"
placeholder="콘텐츠 제목을 입력하십시오."
disabled={disabledFields?.contsTtl}
/>
</FormControl>
</FormItem>
@@ -93,13 +114,33 @@ function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) {
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>
<RadioGroupItem value="" id="ccl-none" />
<Label
htmlFor="ccl-none"
className="whitespace-nowrap"
>
해당없음
</Label>
</div>
{cclCode?.map((code) => (
<div
className="flex items-center space-x-2"
key={code.lwpoCd}
>
<RadioGroupItem
value={code.lwpoCd}
id={`ccl-${code.lwpoCd}`}
/>
<Label htmlFor={`ccl-${code.lwpoCd}`}>
<Image
src={`/images/${code.unit}`}
alt="CCL"
width={77}
height={27}
/>
</Label>
</div>
))}
</RadioGroup>
</FormControl>
</FormItem>
@@ -123,13 +164,35 @@ function ContentsForm({ form, contsSn, ...props }: ContentsFormProps) {
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>
<RadioGroupItem value="" id="koglNone" />
<Label
htmlFor="koglNone"
className="whitespace-nowrap"
>
해당없음
</Label>
</div>
{koglCode?.map((code) => (
<div
className="flex items-center space-x-2"
key={code.lwpoCd}
>
<RadioGroupItem
value={code.lwpoCd}
id={`kogl-${code.lwpoCd}`}
/>
<Label htmlFor={`kogl-${code.lwpoCd}`}>
<div className="relative h-[2.7rem] w-[11rem]">
<Image
src={`/images/${code.unit}`}
alt="공공누리"
fill
className="object-contain"
/>
</div>
</Label>
</div>
))}
</RadioGroup>
</FormControl>
</FormItem>
@@ -0,0 +1,127 @@
"use client";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore } from "@/shared/lib/use-alert-store";
import {
CONTENTS_APPROVAL_STATUS,
ContentsApi,
contentsFormSchema,
type ContentsFormData,
} from "@/entities/content";
import { ContentsForm } from "./contents-form";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@common/components/ui";
function ModifyPage({ sequential }: { sequential: string }) {
const { setMessage: alert } = useAlertStore((state) => state);
const [contentsVersion, setContentsVersion] = useState(0);
const [disabledFields, setDisabledFields] = useState<
Partial<Record<keyof ContentsFormData, boolean>>
>({});
const { data: contentsVersionList } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, sequential],
queryFn: () =>
ContentsApi.getContentsVersionList({
contsSn: Number(sequential),
page: 0,
size: 100,
}),
});
const { data: contentsDetail } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.CONTENTS_DETAILS, contentsVersion],
queryFn: () =>
ContentsApi.getLatestContentsInfo(Number(sequential), contentsVersion),
enabled: !!contentsVersion,
});
const { mutateAsync: createContents } = useMutation({
mutationFn: (data: ContentsFormData) => ContentsApi.createContents(data),
onSuccess: () => {
alert("수정되었습니다.");
},
});
const { mutateAsync: updateContents } = useMutation({
mutationFn: (data: ContentsFormData) =>
ContentsApi.updateContents(
Number(sequential),
contentsDetail?.contsVer ?? 0,
data,
),
onSuccess: () => {
alert("수정되었습니다.");
},
});
const form = useForm<ContentsFormData>({
resolver: zodResolver(contentsFormSchema),
values: contentsDetail,
});
const isApprovedContents =
contentsDetail?.aprvCd === CONTENTS_APPROVAL_STATUS.APPROVED;
useEffect(() => {
if (!contentsVersionList?.content?.length) return;
const latestContent = contentsVersionList?.content[0];
if (!latestContent) return;
if (latestContent?.contsVer > 1) {
setDisabledFields({
contsTtl: true,
});
}
setContentsVersion(latestContent?.contsVer ?? 0);
}, [contentsVersionList?.content]);
return (
<div className="card card-border !mb-0 !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">템플릿 수정</h3>
<div>
<Select
value={contentsVersion.toString()}
onValueChange={(value) => {
setContentsVersion(Number(value));
}}
>
<SelectTrigger className="w-[28rem]">
<SelectValue placeholder="선택하세요" />
</SelectTrigger>
<SelectContent>
{contentsVersionList?.content?.map((item) => (
<SelectItem
key={item.contsVer}
value={item.contsVer.toString()}
>
{item.contsCn}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<ContentsForm
form={form}
contsSn={Number(sequential)}
disabledFields={disabledFields}
handleSave={isApprovedContents ? createContents : updateContents}
/>
</div>
);
}
export { ModifyPage };
@@ -28,6 +28,10 @@ export const createColumns = (
{
accessorKey: "atflFlieSz",
header: "파일크기",
cell: ({ row }) => {
const size = row.getValue("atflFlieSz");
return size ? formatFileSize(Number(size)) : "0 Bytes";
},
},
{
@@ -60,3 +64,14 @@ export const createColumns = (
),
},
];
const formatFileSize = (bytes: number): string => {
console.log("bytes: ", bytes);
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};