feat: 템플릿 관리 페이지 작업
This commit is contained in:
@@ -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();
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ function ModifyPage({ sequential }: { sequential: string }) {
|
||||
</div>
|
||||
<BannerForm
|
||||
form={form}
|
||||
bnrSn={sequential}
|
||||
bnrSn={Number(sequential)}
|
||||
handleSave={updateBanner}
|
||||
handleDelete={deleteBanner}
|
||||
/>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user