feat: 템플릿 관리 페이지 작업

This commit is contained in:
2025-04-10 18:05:57 +09:00
parent 1f99d450e9
commit 93e066ebe5
21 changed files with 299 additions and 30 deletions
@@ -0,0 +1,10 @@
import { ModifyPage as VModifyPage } from "@/pages/template";
export default async function ModifyPage({
params,
}: {
params: Promise<{ sequential: string }>;
}) {
const { sequential } = await params;
return <VModifyPage sequential={sequential} />;
}
@@ -0,0 +1,19 @@
import { client } from "@/shared/api";
import type {
EditorService,
FileInfo,
UploadFile,
} from "../model/editor-interface";
import { ENDPOINT } from "@/shared/config";
export class EditorApiService implements EditorService {
async uploadImage(data: UploadFile): Promise<FileInfo> {
const response = await client.post<FileInfo>(
ENDPOINT.ATTACH_SERVICE.EDITOR_UPLOAD,
data,
);
return response.data;
}
}
export const EditorApi = new EditorApiService();
+1
View File
@@ -0,0 +1 @@
export { Editor } from "./ui/editor";
@@ -0,0 +1,37 @@
import { SERVER_API_URL } from "@/shared/config";
export const editorBaseConfig = {
apiKey: process.env.NEXT_PUBLIC_TINY_API_KEY,
init: {
language: "ko_KR",
height: 500,
plugins: [
"advlist",
"autolink",
"lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"codesample",
"fullscreen",
"insertdatetime",
"media",
"table",
"code",
"help",
"wordcount",
"save",
],
toolbar: [
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table",
"link image",
],
images_upload_base_path: SERVER_API_URL,
file_picker_types: "image",
},
};
@@ -0,0 +1,20 @@
export interface UploadFile {
fieldName: string;
fileType: string;
fileBase64: string;
originalName: string;
size: number;
}
export interface FileInfo {
originalFileName: string;
message: string;
fileType: string;
size: number;
uploaded: number;
url: string;
}
export interface EditorService {
uploadImage(data: UploadFile): Promise<FileInfo>;
}
+70
View File
@@ -0,0 +1,70 @@
import { Editor as TinyEditor, type IAllProps } from "@tinymce/tinymce-react";
import { editorBaseConfig as baseConfig } from "../model/editor-base-config";
import { EditorApi } from "../api/editor-service";
const Editor = ({ ...props }: IAllProps) => {
return (
<TinyEditor
{...baseConfig}
init={{
...baseConfig.init,
images_upload_handler: (blobInfo: {
blob: () => Blob;
filename: () => string;
}) =>
new Promise(async (resolve) => {
const blob = blobInfo.blob();
const fileInfo = {
fieldName: `${blob.type.split("/")[1]}`,
fileType: blob.type,
fileBase64: "",
originalName: blobInfo.filename(),
size: blob.size,
};
const reader = new FileReader();
reader.onload = async () => {
const base64 = reader.result as string;
fileInfo.fileBase64 = base64.split(",")[1] || "";
const data = await EditorApi.uploadImage(fileInfo);
resolve(data.url);
};
reader.readAsDataURL(blob);
}),
file_picker_callback() {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
// input.addEventListener("change", (e) => {
// const file = e.target.files[0];
// const reader = new FileReader();
// reader.addEventListener("load", () => {
// /*
// Note: Now we need to register the blob in TinyMCEs image blob
// registry. In the next release this part hopefully won't be
// necessary, as we are looking to handle it internally.
// */
// const id = "blobid" + new Date().getTime();
// const blobCache = tinymce.activeEditor.editorUpload.blobCache;
// const base64 = reader.result.split(",")[1];
// const blobInfo = blobCache.create(id, file, base64);
// blobCache.add(blobInfo);
// /* call the callback and populate the Title field with the file name */
// cb(blobInfo.blobUri(), { title: file.name });
// });
// reader.readAsDataURL(file);
// });
input.click();
},
}}
{...props}
/>
);
};
export { Editor };
+1 -1
View File
@@ -30,7 +30,7 @@ import { handleUpload } from "../lib/utils";
type BannerFormProps = {
form: UseFormReturn<BannerFormData>;
bnrSn?: string;
bnrSn?: number;
handleSave: (data: BannerFormData) => Promise<void>;
handleDelete?: () => Promise<void>;
};
+1 -1
View File
@@ -108,7 +108,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
</div>
<BannerForm
form={form}
bnrSn={sequential}
bnrSn={Number(sequential)}
handleSave={updateBanner}
handleDelete={deleteBanner}
/>
+4 -1
View File
@@ -24,11 +24,12 @@ import { AttachmentApi } from "@/entities/attachment";
import { ENDPOINT } from "@/shared/config";
import { useTus } from "@/shared/lib/use-tus";
import { FormControl, FormField, FormItem } from "@/shared/ui/form";
import { useAttachmentStore } from "@/shared/lib/use-attachment-store";
import type { ImageFormData } from "../model/image-interface";
type ImageFormProps = {
form: UseFormReturn<ImageFormData>;
imgSn?: string;
imgSn?: number;
handleSave: (data: ImageFormData) => Promise<void>;
handleDelete?: () => Promise<void>;
};
@@ -43,6 +44,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
progress,
setFile,
} = useTus();
const { setAttachmentFileCode } = useAttachmentStore((state) => state);
const [imageUrl, setImageUrl] = useState("");
const { data: attachmentData } = useQuery({
@@ -201,6 +203,7 @@ function ImageForm({ form, imgSn, ...props }: ImageFormProps) {
onUpload={(e) => {
const files = e.target.files;
if (!files) return;
setAttachmentFileCode("");
setFile(files[0] ?? null);
}}
customPreview={
+1 -1
View File
@@ -104,7 +104,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
</div>
<ImageForm
form={form}
imgSn={sequential}
imgSn={Number(sequential)}
handleSave={updateImage}
handleDelete={deleteImage}
/>
@@ -5,6 +5,7 @@ import type {
TemplateList,
TemplateParams,
TemplateFormData,
Template,
} from "../model/template-interface";
export class TemplateApiService implements TemplateService {
@@ -16,6 +17,13 @@ export class TemplateApiService implements TemplateService {
return response.data;
}
public async getTemplateInfo(tmpltSn: string): Promise<Template> {
const response = await client.get<Template>(
`${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`,
);
return response.data;
}
public async createTemplate(data: TemplateFormData): Promise<void> {
const response = await client.post<void>(
`${ENDPOINT.CMS_SERVICE.TEMPLATE}`,
@@ -23,6 +31,17 @@ export class TemplateApiService implements TemplateService {
);
return response.data;
}
public async updateTemplate(
tmpltSn: string,
data: TemplateFormData,
): Promise<void> {
const response = await client.put<void>(
`${ENDPOINT.CMS_SERVICE.TEMPLATE}/${tmpltSn}`,
data,
);
return response.data;
}
}
export const TemplateApi = new TemplateApiService();
+1
View File
@@ -1,2 +1,3 @@
export { CreatePage } from "./ui/create-page";
export { ModifyPage } from "./ui/modify-page";
export { TemplatePage } from "./ui/template-page";
@@ -11,10 +11,14 @@ export interface TemplateParams extends PageableParams {
export interface Template {
tmpltSn: number;
tmpltNm: string;
tmpltCn: string;
tmpltSeCd: string;
tmpltSeNm: string;
useYn: string;
delYn: string;
regDt: string;
mdfcnDt: string;
rgTrNm: string;
mdfr: string;
mdfrNm: string;
}
@@ -23,12 +27,14 @@ export const templateFormSchema = z.object({
tmpltNm: z.string(),
tmpltCn: z.string(),
tmpltSeCd: z.string(),
useYn: z.string(),
useYn: z.string().nullish(),
});
export type TemplateList = PageableList<Template>;
export type TemplateFormData = z.infer<typeof templateFormSchema>;
export interface TemplateService {
getTemplateList(params: TemplateParams): Promise<TemplateList>;
getTemplateInfo(tmpltSn: string): Promise<Template>;
createTemplate(data: TemplateFormData): Promise<void>;
updateTemplate(tmpltSn: string, data: TemplateFormData): Promise<void>;
}
@@ -0,0 +1,50 @@
"use client";
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 {
templateFormSchema,
type TemplateFormData,
} from "../model/template-interface";
import { TemplateApi } from "../api/template-service";
import { TemplateForm } from "./template-form";
function ModifyPage({ sequential }: { sequential: string }) {
const { setMessage: alert } = useAlertStore((state) => state);
const { data: templateDetail } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.TEMPLATE, sequential],
queryFn: () => TemplateApi.getTemplateInfo(sequential),
});
const { mutateAsync: updateTemplate } = useMutation({
mutationFn: (data: TemplateFormData) =>
TemplateApi.updateTemplate(sequential, data),
onSuccess: () => {
alert("수정되었습니다.");
},
});
const form = useForm<TemplateFormData>({
resolver: zodResolver(templateFormSchema),
values: templateDetail,
});
return (
<div className="card card-border !mb-0 !mt-6">
<div className="card-header">
<h3 className="text-2xl font-medium text-label">템플릿 수정</h3>
</div>
<TemplateForm
form={form}
tmpltSn={Number(sequential)}
handleSave={updateTemplate}
/>
</div>
);
}
export { ModifyPage };
@@ -17,7 +17,7 @@ import { FormItem, FormControl, FormField } from "@/shared/ui/form";
import type { TemplateFormData } from "../model/template-interface";
const Editor = dynamic(
() => import("@/shared/ui/editor").then((module) => module.Editor),
() => import("@/entities/editor").then((module) => module.Editor),
{ ssr: false },
);
@@ -34,8 +34,8 @@ function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
<div className="card">
<Table variant="secondary">
<colgroup>
<col width="20%" />
<col width="80%" />
<col width="10%" />
<col width="90%" />
</colgroup>
<TableBody>
<TableRow>
@@ -85,10 +85,13 @@ function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
<FormField
control={form.control}
name="tmpltCn"
render={() => (
render={({ field }) => (
<FormItem>
<FormControl>
<Editor />
<Editor
value={field.value ?? ""}
onEditorChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
@@ -107,6 +110,7 @@ function TemplateForm({ form, tmpltSn, ...props }: TemplateFormProps) {
<RadioGroup
{...field}
name="useYn"
value={field.value ?? ""}
onValueChange={field.onChange}
>
<div className="flex items-center space-x-2">
+1
View File
@@ -5,6 +5,7 @@ export const BASE_URL = `${PROXY_HOST}/server`;
export const ENDPOINT = {
ATTACH_SERVICE: {
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor",
IMAGES: "/attach-service/api/v1/images",
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
},
@@ -0,0 +1,14 @@
import { useStore } from "zustand";
import { useContext } from "react";
import type { AttachmentStore } from "../store/attachment-store";
import { StoreContext } from "../providers/store-provider";
export const useAttachmentStore = <T>(
selector: (store: AttachmentStore) => T,
): T => {
const store = useContext(StoreContext);
if (!store) {
throw new Error("useAttachmentStore must be used within StoreProvider");
}
return useStore(store.attachmentStore, selector);
};
@@ -1,10 +1,16 @@
"use client";
import { createContext, useRef } from "react";
import { createAlertStore, createMenuStore, createUserStore } from "../store";
import {
createAlertStore,
createAttachmentStore,
createMenuStore,
createUserStore,
} from "../store";
type StoreContextType = {
alertStore: ReturnType<typeof createAlertStore>;
attachmentStore: ReturnType<typeof createAttachmentStore>;
menuStore: ReturnType<typeof createMenuStore>;
userStore: ReturnType<typeof createUserStore>;
};
@@ -18,6 +24,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
if (storeRef.current === null) {
storeRef.current = {
alertStore: createAlertStore(),
attachmentStore: createAttachmentStore(),
menuStore: createMenuStore(),
userStore: createUserStore(),
};
@@ -0,0 +1,25 @@
import { createStore } from "zustand/vanilla";
export type AttachmentState = {
attachmentFileCode: string;
};
export type AttachmentActions = {
setAttachmentFileCode: (attachmentFileCode: string) => void;
};
export type AttachmentStore = AttachmentState & AttachmentActions;
export const defaultInitState: AttachmentState = {
attachmentFileCode: "",
};
export const createAttachmentStore = (
initState: AttachmentState = defaultInitState,
) => {
return createStore<AttachmentStore>()((set) => ({
...initState,
setAttachmentFileCode: (attachmentFileCode: string) =>
set({ attachmentFileCode }),
}));
};
+1
View File
@@ -1,3 +1,4 @@
export { createAlertStore } from "./alert-store";
export { createAttachmentStore } from "./attachment-store";
export { createMenuStore } from "./menu-store";
export { createUserStore, type IUserInfo } from "./user-store";
-19
View File
@@ -1,19 +0,0 @@
import { Editor as TinyEditor } from "@tinymce/tinymce-react";
const Editor = () => {
return (
<TinyEditor
apiKey={process.env.NEXT_PUBLIC_TINY_API_KEY}
init={{
language: "ko_KR",
height: 500,
menubar: false,
plugins: "lists advlist table",
toolbar:
"undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist blockquote | table",
}}
/>
);
};
export { Editor };