diff --git a/apps/cs/.env.local b/apps/cs/.env.local deleted file mode 100644 index 0648024ff..000000000 --- a/apps/cs/.env.local +++ /dev/null @@ -1,2 +0,0 @@ -NEXT_PUBLIC_PORT=3000 -NEXT_PUBLIC_SERVER_API_URL=http://192.168.0.102:8000 diff --git a/apps/cs/.gitignore b/apps/cs/.gitignore index 5340dd9a1..e8d8437ee 100644 --- a/apps/cs/.gitignore +++ b/apps/cs/.gitignore @@ -36,3 +36,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.env* \ No newline at end of file diff --git a/apps/cs/app/(with-base-layout)/banner/page.tsx b/apps/cs/app/(with-base-layout)/banner/page.tsx new file mode 100644 index 000000000..23f63defe --- /dev/null +++ b/apps/cs/app/(with-base-layout)/banner/page.tsx @@ -0,0 +1,2 @@ +import { BannerPage } from "@/pages/banner"; +export default BannerPage; diff --git a/apps/cs/app/(with-base-layout)/banner/register/[sequential]/page.tsx b/apps/cs/app/(with-base-layout)/banner/register/[sequential]/page.tsx new file mode 100644 index 000000000..bc19150cb --- /dev/null +++ b/apps/cs/app/(with-base-layout)/banner/register/[sequential]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/banner"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ sequential: string }>; +}) { + const { sequential } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/banner/register/page.tsx b/apps/cs/app/(with-base-layout)/banner/register/page.tsx new file mode 100644 index 000000000..46eb12c1c --- /dev/null +++ b/apps/cs/app/(with-base-layout)/banner/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/banner"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/image/register/[sequential]/page.tsx b/apps/cs/app/(with-base-layout)/image/register/[sequential]/page.tsx new file mode 100644 index 000000000..800f0b382 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/image/register/[sequential]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/image"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ sequential: string }>; +}) { + const { sequential } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/image/register/[serial]/page.tsx b/apps/cs/app/(with-base-layout)/image/register/[serial]/page.tsx deleted file mode 100644 index 8df89b17a..000000000 --- a/apps/cs/app/(with-base-layout)/image/register/[serial]/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ModifyPage as VModifyPage } from "@/pages/image"; - -export default async function ModifyPage({ - params, -}: { - params: Promise<{ serial: string }>; -}) { - const { serial } = await params; - return ; -} diff --git a/apps/cs/app/(with-base-layout)/loading.tsx b/apps/cs/app/(with-base-layout)/loading.tsx new file mode 100644 index 000000000..3a3b15932 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/loading.tsx @@ -0,0 +1,2 @@ +import { SkeletonPage } from "@/shared/ui/skeleton-page"; +export default SkeletonPage; diff --git a/apps/cs/app/(with-base-layout)/template/page.tsx b/apps/cs/app/(with-base-layout)/template/page.tsx new file mode 100644 index 000000000..f5eab5953 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/template/page.tsx @@ -0,0 +1,2 @@ +import { TemplatePage } from "@/pages/template"; +export default TemplatePage; diff --git a/apps/cs/app/(with-base-layout)/template/register/[sequential]/page.tsx b/apps/cs/app/(with-base-layout)/template/register/[sequential]/page.tsx new file mode 100644 index 000000000..3878a4e41 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/template/register/[sequential]/page.tsx @@ -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 ; +} diff --git a/apps/cs/app/(with-base-layout)/template/register/page.tsx b/apps/cs/app/(with-base-layout)/template/register/page.tsx new file mode 100644 index 000000000..9be3f975a --- /dev/null +++ b/apps/cs/app/(with-base-layout)/template/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/template"; +export default CreatePage; diff --git a/apps/cs/app/(with-base-layout)/terms/page.tsx b/apps/cs/app/(with-base-layout)/terms/page.tsx new file mode 100644 index 000000000..47f4c8e02 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/terms/page.tsx @@ -0,0 +1,2 @@ +import { TermsPage } from "@/pages/terms"; +export default TermsPage; diff --git a/apps/cs/app/(with-base-layout)/terms/register/[sequential]/page.tsx b/apps/cs/app/(with-base-layout)/terms/register/[sequential]/page.tsx new file mode 100644 index 000000000..8ff8b2010 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/terms/register/[sequential]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage as VModifyPage } from "@/pages/terms"; + +export default async function ModifyPage({ + params, +}: { + params: Promise<{ sequential: string }>; +}) { + const { sequential } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/terms/register/page.tsx b/apps/cs/app/(with-base-layout)/terms/register/page.tsx new file mode 100644 index 000000000..997fc119a --- /dev/null +++ b/apps/cs/app/(with-base-layout)/terms/register/page.tsx @@ -0,0 +1,2 @@ +import { CreatePage } from "@/pages/terms"; +export default CreatePage; diff --git a/apps/cs/middleware.ts b/apps/cs/middleware.ts index 81039090b..493699bb2 100644 --- a/apps/cs/middleware.ts +++ b/apps/cs/middleware.ts @@ -1,4 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; +import { CLAIM_NAME, AUTH_USER_ID } from "./src/shared/config"; const serverApiUrl = process.env.NEXT_PUBLIC_SERVER_API_URL ?? "http://localhost:3000"; @@ -16,8 +17,17 @@ export function middleware(req: NextRequest) { } if (pathname.startsWith("/attach-service")) { + const requestHeaders = new Headers(req.headers); + const token = req.cookies.get(CLAIM_NAME)?.value; + const userId = req.cookies.get(AUTH_USER_ID)?.value; const newUrl = `${serverApiUrl}${pathname}`; - return NextResponse.rewrite(new URL(newUrl)); + + if (token) requestHeaders.set(CLAIM_NAME, token); + if (userId) requestHeaders.set(AUTH_USER_ID, userId); + + return NextResponse.rewrite(new URL(newUrl), { + headers: requestHeaders, + }); } return NextResponse.next(); diff --git a/apps/cs/package.json b/apps/cs/package.json index 38d378299..1a8f88c44 100644 --- a/apps/cs/package.json +++ b/apps/cs/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-slot": "^1.1.2", "@tanstack/react-query": "^5.67.1", "@tanstack/react-table": "^8.21.2", + "@tinymce/tinymce-react": "^6.1.0", "axios": "^1.8.1", "chart.js": "^4.4.8", "dayjs": "^1.11.13", diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index fb4b65f27..6fd488b90 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -6,6 +6,7 @@ import { AuthWrapper } from "@/entities/auth"; import { MenuWrapper } from "@/entities/menu"; import { StoreProvider } from "@/shared/providers"; import { AlertDialog } from "@/shared/ui/alert-dialog"; +import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { const queryClient = new QueryClient(); @@ -16,6 +17,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { {children} + diff --git a/apps/cs/src/entities/attachment/api/attachment-service.ts b/apps/cs/src/entities/attachment/api/attachment-service.ts new file mode 100644 index 000000000..c930b7012 --- /dev/null +++ b/apps/cs/src/entities/attachment/api/attachment-service.ts @@ -0,0 +1,43 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + Attachment, + AttachmentService, +} from "../model/attachment-interface"; + +export class AttachmentApiService implements AttachmentService { + public async getAttachments(atflCd: string): Promise { + const response = await client.get( + `${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`, + ); + return response.data; + } + + public async deleteAttachment(atflId: string): Promise { + await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`); + } + + public async deleteAttachmentByAtflCd(atflCd: string): Promise { + await client.delete( + `${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/atflCd/${atflCd}`, + ); + } + + public async getImageUrl(atflId: string): Promise { + if (!atflId) return ""; + const response = await fetch(`${ENDPOINT.ATTACH_SERVICE.IMAGES}/${atflId}`); + const blob = await response.blob(); + return URL.createObjectURL(blob); + } + + public async getImageUrlByAtflCd(atflCd: string): Promise { + if (!atflCd) return ""; + const response = await fetch( + `${ENDPOINT.ATTACH_SERVICE.IMAGES}/atflCd/${atflCd}`, + ); + const blob = await response.blob(); + return URL.createObjectURL(blob); + } +} + +export const AttachmentApi = new AttachmentApiService(); diff --git a/apps/cs/src/entities/attachment/index.ts b/apps/cs/src/entities/attachment/index.ts new file mode 100644 index 000000000..182ef9fd6 --- /dev/null +++ b/apps/cs/src/entities/attachment/index.ts @@ -0,0 +1 @@ +export { AttachmentApi } from "./api/attachment-service"; diff --git a/apps/cs/src/entities/attachment/model/attachment-interface.ts b/apps/cs/src/entities/attachment/model/attachment-interface.ts new file mode 100644 index 000000000..5986a96d1 --- /dev/null +++ b/apps/cs/src/entities/attachment/model/attachment-interface.ts @@ -0,0 +1,22 @@ +export interface Attachment { + atflCd: string; + atflSn: number; + atflId: string; + physFileNm: string; + orgnlFileNm: string; + atflFlieSz: number; + atflType: string; + dwnldNocs: number; + delYn: string; + lnkgDmnId: string; + lnkgDmnNm: string; + regDt: string; +} + +export interface AttachmentService { + getAttachments: (atflCd: string) => Promise; + deleteAttachment: (atflId: string) => Promise; + deleteAttachmentByAtflCd: (atflCd: string) => Promise; + getImageUrl: (atflId: string) => Promise; + getImageUrlByAtflCd: (atflCd: string) => Promise; +} diff --git a/apps/cs/src/entities/code/@x/posting.ts b/apps/cs/src/entities/code/@x/posting.ts new file mode 100644 index 000000000..0525684d9 --- /dev/null +++ b/apps/cs/src/entities/code/@x/posting.ts @@ -0,0 +1 @@ +export { CodeSelect } from "../ui/code-select"; diff --git a/apps/cs/src/entities/editor/api/editor-service.ts b/apps/cs/src/entities/editor/api/editor-service.ts new file mode 100644 index 000000000..3e2fc75a4 --- /dev/null +++ b/apps/cs/src/entities/editor/api/editor-service.ts @@ -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 { + const response = await client.post( + ENDPOINT.ATTACH_SERVICE.EDITOR_UPLOAD, + data, + ); + return response.data; + } +} + +export const EditorApi = new EditorApiService(); diff --git a/apps/cs/src/entities/editor/index.ts b/apps/cs/src/entities/editor/index.ts new file mode 100644 index 000000000..1ae82fb4f --- /dev/null +++ b/apps/cs/src/entities/editor/index.ts @@ -0,0 +1 @@ +export { Editor } from "./ui/editor"; diff --git a/apps/cs/src/entities/editor/lib/blob-to-base64.ts b/apps/cs/src/entities/editor/lib/blob-to-base64.ts new file mode 100644 index 000000000..2f0e68c0f --- /dev/null +++ b/apps/cs/src/entities/editor/lib/blob-to-base64.ts @@ -0,0 +1,10 @@ +export const blobToBase64 = (blob: Blob): Promise => { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + const base64 = reader.result as string; + resolve(base64.split(",")[1] || ""); + }; + reader.readAsDataURL(blob); + }); +}; diff --git a/apps/cs/src/entities/editor/lib/generate-file-info.ts b/apps/cs/src/entities/editor/lib/generate-file-info.ts new file mode 100644 index 000000000..c36ed3577 --- /dev/null +++ b/apps/cs/src/entities/editor/lib/generate-file-info.ts @@ -0,0 +1,16 @@ +import { blobToBase64 } from "./blob-to-base64"; + +export const generateFileInfo = async (blobInfo: { + blob: () => Blob; + filename: () => string; +}) => { + const blob = blobInfo.blob(); + const base64 = await blobToBase64(blob); + return { + fieldName: `${blob.type.split("/")[1]}`, + fileType: blob.type, + fileBase64: base64, + originalName: blobInfo.filename(), + size: blob.size, + }; +}; diff --git a/apps/cs/src/entities/editor/model/editor-base-config.ts b/apps/cs/src/entities/editor/model/editor-base-config.ts new file mode 100644 index 000000000..1b97ba9a0 --- /dev/null +++ b/apps/cs/src/entities/editor/model/editor-base-config.ts @@ -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", + }, +}; diff --git a/apps/cs/src/entities/editor/model/editor-interface.ts b/apps/cs/src/entities/editor/model/editor-interface.ts new file mode 100644 index 000000000..ed6b4a987 --- /dev/null +++ b/apps/cs/src/entities/editor/model/editor-interface.ts @@ -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; +} diff --git a/apps/cs/src/entities/editor/ui/editor.tsx b/apps/cs/src/entities/editor/ui/editor.tsx new file mode 100644 index 000000000..b5ef02153 --- /dev/null +++ b/apps/cs/src/entities/editor/ui/editor.tsx @@ -0,0 +1,60 @@ +import { useRef } from "react"; +import { + Editor as TinyEditor, + type Editor as TinyEditorInstance, + type IAllProps, +} from "@tinymce/tinymce-react"; +import { editorBaseConfig as baseConfig } from "../model/editor-base-config"; +import { generateFileInfo } from "../lib/generate-file-info"; +import { EditorApi } from "../api/editor-service"; + +const Editor = ({ ...props }: IAllProps) => { + const editorRef = useRef(null); + + const imagesUploadHandler = (blobInfo: { + blob: () => Blob; + filename: () => string; + }) => { + return new Promise(async (resolve) => { + const fileInfo = await generateFileInfo(blobInfo); + const data = await EditorApi.uploadImage(fileInfo); + resolve(data.url); + }); + }; + + const filePickerCallback = ( + callback: (base64: string, meta: { title: string }) => void, + ) => { + const input = document.createElement("input"); + input.setAttribute("type", "file"); + input.setAttribute("accept", "image/*"); + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = async () => { + const base64 = reader.result as string; + callback(base64, { title: file.name }); + }; + reader.readAsDataURL(file); + }; + input.click(); + }; + + return ( + { + editorRef.current = editor; + }} + init={{ + ...baseConfig.init, + images_upload_handler: imagesUploadHandler, + file_picker_callback: filePickerCallback, + }} + {...props} + /> + ); +}; + +export { Editor }; diff --git a/apps/cs/src/entities/menu-authority/api/menu-authority-service.ts b/apps/cs/src/entities/menu-authority/api/menu-authority-service.ts new file mode 100644 index 000000000..f9b01b4d8 --- /dev/null +++ b/apps/cs/src/entities/menu-authority/api/menu-authority-service.ts @@ -0,0 +1,32 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + MenuAuthorityList, + MenuAuthorityParams, + MenuAuthorityPayloadData, + MenuAuthorityService, +} from "../model/menu-authority-interface"; + +export class MenuAuthorityApiService implements MenuAuthorityService { + public async getMenuAuthorityList( + params: MenuAuthorityParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + { params }, + ); + return response.data; + } + + public async saveMenuAuthority( + data: MenuAuthorityPayloadData[], + ): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.MENU_AUTHRTS}`, + data, + ); + return response.data; + } +} + +export const MenuAuthorityApi = new MenuAuthorityApiService(); diff --git a/apps/cs/src/entities/menu-authority/index.ts b/apps/cs/src/entities/menu-authority/index.ts new file mode 100644 index 000000000..07433a098 --- /dev/null +++ b/apps/cs/src/entities/menu-authority/index.ts @@ -0,0 +1 @@ +export { ManagementAuthorityDialog } from "./ui/management-authority-dialog"; diff --git a/apps/cs/src/entities/menu-authority/model/menu-authority-interface.ts b/apps/cs/src/entities/menu-authority/model/menu-authority-interface.ts new file mode 100644 index 000000000..d6775838c --- /dev/null +++ b/apps/cs/src/entities/menu-authority/model/menu-authority-interface.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export interface MenuAuthorityParams extends PageableParams { + groupId?: string; + menuCd?: number; + useYn?: string; +} + +export const menuAuthoritySchema = z.object({ + groupId: z.string(), + groupNm: z.string(), + menuCd: z.number(), + mngAuthrtYn: z.string(), + inqAuthrtYn: z.string(), + regAuthrtYn: z.string(), + mdfcnAuthrtYn: z.string(), + delAuthrtYn: z.string(), + useYn: z.string(), + mode: z.string(), +}); + +export const menuAuthorityPayloadSchema = menuAuthoritySchema.omit({ + groupNm: true, +}); + +export const menuAuthorityFormSchema = z.object({ + menuAuthority: z.array(menuAuthorityPayloadSchema), +}); + +export type MenuAuthority = z.infer; +export type MenuAuthorityList = PageableList; +export type MenuAuthorityPayloadData = z.infer< + typeof menuAuthorityPayloadSchema +>; +export type MenuAuthorityFormData = z.infer; + +export interface MenuAuthorityService { + getMenuAuthorityList(params: MenuAuthorityParams): Promise; + saveMenuAuthority(data: MenuAuthorityPayloadData[]): Promise; +} diff --git a/apps/cs/src/entities/menu-authority/model/menu-authority-table-columns.tsx b/apps/cs/src/entities/menu-authority/model/menu-authority-table-columns.tsx new file mode 100644 index 000000000..90bff735d --- /dev/null +++ b/apps/cs/src/entities/menu-authority/model/menu-authority-table-columns.tsx @@ -0,0 +1,117 @@ +import type { ColumnDef } from "@tanstack/react-table"; +import type { UseFormReturn } from "react-hook-form"; +import { Checkbox } from "@common/components/ui"; +import type { + MenuAuthority, + MenuAuthorityFormData, + MenuAuthorityPayloadData, +} from "./menu-authority-interface"; + +export const columns = ( + form: UseFormReturn, +): ColumnDef[] => { + const getCurrentRow = (i: number) => form.watch("menuAuthority")[i]; + + const handleCheckedChange = + (i: number, key: keyof MenuAuthorityPayloadData) => (checked: boolean) => { + if (getCurrentRow(i)?.mode === "R") { + form.setValue(`menuAuthority.${i}.mode`, "U"); + } + form.setValue(`menuAuthority.${i}.${key}`, checked ? "Y" : "N"); + }; + + return [ + { + accessorKey: "groupNm", + header: "그룹명", + }, + { + accessorKey: "mngAuthrtYn", + header: "관리권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "regAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "mdfcnAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "delAuthrtYn", + )(checked as boolean); + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "mngAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "regAuthrtYn", + header: "등록권한", + cell: ({ row }) => { + return ( + { + if (checked) { + handleCheckedChange( + row.index, + "inqAuthrtYn", + )(checked as boolean); + } + handleCheckedChange(row.index, "regAuthrtYn")(checked as boolean); + }} + /> + ); + }, + }, + { + accessorKey: "mdfcnAuthrtYn", + header: "수정권한 ", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "delAuthrtYn", + header: "삭제권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + { + accessorKey: "inqAuthrtYn", + header: "조회권한", + cell: ({ row }) => { + return ( + + ); + }, + }, + ]; +}; diff --git a/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx b/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx new file mode 100644 index 000000000..8b940634a --- /dev/null +++ b/apps/cs/src/entities/menu-authority/ui/management-authority-dialog.tsx @@ -0,0 +1,101 @@ +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Button, + DataTable, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@common/components/ui"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; +import { + menuAuthorityFormSchema, + type MenuAuthorityFormData, + type MenuAuthorityPayloadData, +} from "../model/menu-authority-interface"; +import { columns } from "../model/menu-authority-table-columns"; +import { MenuAuthorityApi } from "../api/menu-authority-service"; + +type ManagementAuthorityDialogProps = { + menuCd?: number; +} & React.ComponentProps; + +function ManagementAuthorityDialog({ + menuCd, + ...props +}: ManagementAuthorityDialogProps) { + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + + const { data: menuAuthorityList } = useQuery({ + queryKey: [ENDPOINT.CMS_SERVICE.MENU_AUTHRTS, menuCd], + queryFn: () => MenuAuthorityApi.getMenuAuthorityList({ menuCd }), + enabled: !!menuCd, + }); + + const { mutateAsync: saveMenuAuthority } = useMutation({ + mutationFn: (data: MenuAuthorityPayloadData[]) => + MenuAuthorityApi.saveMenuAuthority(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(menuAuthorityFormSchema), + values: { + menuAuthority: menuAuthorityList?.content ?? [], + }, + }); + + const table = useReactTable({ + data: menuAuthorityList?.content ?? [], + columns: columns(form), + getCoreRowModel: getCoreRowModel(), + }); + + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("menuAuthority"); + const filteredData = data.filter((item) => item.mode !== "R"); + await saveMenuAuthority(filteredData); + } + }; + + return ( + + + + + + + 그룹별 메뉴 권한 + + + + + + + + + + + ); +} + +export { ManagementAuthorityDialog }; diff --git a/apps/cs/src/entities/menu/lib/utils.ts b/apps/cs/src/entities/menu/lib/utils.ts index 5606d7aa2..1cf4514d9 100644 --- a/apps/cs/src/entities/menu/lib/utils.ts +++ b/apps/cs/src/entities/menu/lib/utils.ts @@ -5,10 +5,16 @@ export const getCurrentMenu = ( pathname: string, ): MenuItem | null => { for (const menu of menus) { + if ( + menu.urlPath !== "" && + (menu.urlPath === pathname || pathname.startsWith(menu.urlPath)) + ) + return menu; + if (menu.children) { - return getCurrentMenu(menu.children, pathname); + const children = getCurrentMenu(menu.children, pathname); + if (children) return children; } - if (menu.urlPath === pathname) return menu; } return null; }; diff --git a/apps/cs/src/entities/posting/api/posting-service.ts b/apps/cs/src/entities/posting/api/posting-service.ts new file mode 100644 index 000000000..2fafd6557 --- /dev/null +++ b/apps/cs/src/entities/posting/api/posting-service.ts @@ -0,0 +1,31 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { PageableList } from "@/shared/model"; +import type { + PostService, + PostingParams, + Post, + PostPayloadData, +} from "../model/posting-interface"; + +export class PostingApiService implements PostService { + public async getPostingHistory( + params: PostingParams, + ): Promise> { + const response = await client.get>( + `${ENDPOINT.CMS_SERVICE.POSTINGS}`, + { params }, + ); + return response.data; + } + + public async savePosting(data: PostPayloadData[]): Promise { + const response = await client.post( + `${ENDPOINT.CMS_SERVICE.POSTINGS}`, + data, + ); + return response.data; + } +} + +export const PostingApi = new PostingApiService(); diff --git a/apps/cs/src/entities/posting/index.ts b/apps/cs/src/entities/posting/index.ts new file mode 100644 index 000000000..c6029d757 --- /dev/null +++ b/apps/cs/src/entities/posting/index.ts @@ -0,0 +1,7 @@ +export { PostingApi } from "./api/posting-service"; +export { + POSTING_TYPE, + type Post, + type PostPayloadData, +} from "./model/posting-interface"; +export { PostingDialog } from "./ui/posting-dialog"; diff --git a/apps/cs/src/entities/posting/model/posting-interface.ts b/apps/cs/src/entities/posting/model/posting-interface.ts new file mode 100644 index 000000000..b14b2ab66 --- /dev/null +++ b/apps/cs/src/entities/posting/model/posting-interface.ts @@ -0,0 +1,50 @@ +import type { PageableList, PageableParams } from "@/shared/model"; +import { z } from "zod"; + +export const POSTING_TYPE = { + POPUP: "POPUP", + BANNER: "BANNER", + IMAGE: "IMAGE", +} as const; + +export type PostingType = (typeof POSTING_TYPE)[keyof typeof POSTING_TYPE]; + +export interface PostingParams extends PageableParams { + pstgTrgtNo: number; +} + +export const postSchema = z.object({ + siteId: z.string(), + siteNm: z.string(), + pstgTypeCd: z.string(), + pstgTypeNm: z.string(), + pstgTrgtNo: z.number(), + pstgBgngDt: z.string().optional(), + pstgEndDt: z.string().optional(), + pstgPrdSeCd: z.string(), + pstgPrdSeNm: z.string(), + delYn: z.string(), +}); + +export const postPayloadSchema = postSchema + .extend({ + mode: z.string(), + }) + .omit({ + siteNm: true, + pstgTypeNm: true, + pstgPrdSeNm: true, + }); + +export const postingFormSchema = z.object({ + pstg: z.array(postPayloadSchema), +}); + +export type Post = z.infer; +export type PostPayloadData = z.infer; +export type PostingFormData = z.infer; + +export interface PostService { + getPostingHistory: (params: PostingParams) => Promise>; + savePosting: (data: PostPayloadData[]) => Promise; +} diff --git a/apps/cs/src/entities/posting/ui/posting-dialog.tsx b/apps/cs/src/entities/posting/ui/posting-dialog.tsx new file mode 100644 index 000000000..9e27a4485 --- /dev/null +++ b/apps/cs/src/entities/posting/ui/posting-dialog.tsx @@ -0,0 +1,323 @@ +import dayjs from "dayjs"; +import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useFieldArray, useForm } from "react-hook-form"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { + Dialog, + DialogTrigger, + Button, + DialogContent, + DialogHeader, + DialogTitle, + Badge, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Checkbox, + Select, + DateRangePicker, + DialogClose, + DialogFooter, +} from "@common/components/ui"; +import { cn } from "@common/components/lib"; +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"; +import { + postingFormSchema, + type PostingFormData, + type PostingType, + type PostPayloadData, +} from "../model/posting-interface"; +import { useAlertStore } from "@/shared/lib/use-alert-store"; + +type DateRange = { + from: string; + to?: string; +}; + +type PostingTarget = { + pstgTrgtNm: string; + pstgTrgtNo: number; +}; + +type PostingDialogProps = { + postingType: PostingType; + postingTargets: PostingTarget[]; +}; + +const POSTING_TYPE_LABEL = { + POPUP: "팝업", + BANNER: "배너", + IMAGE: "이미지", +} as const; + +function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + const [selectedRows, setSelectedRows] = useState([]); + const [rememberPeriodCode, setRememberPeriodCode] = useState< + Record + >({}); + const [rememberPeriodDate, setRememberPeriodDate] = useState< + Record + >({}); + + const { data: siteList } = useQuery>({ + queryKey: [ENDPOINT.CMS_SERVICE.SITES], + queryFn: () => SiteApi.getSiteList({}), + }); + + const { mutateAsync: savePosting } = useMutation({ + mutationFn: (data: PostPayloadData[]) => PostingApi.savePosting(data), + onSuccess: () => { + alert("저장되었습니다."); + }, + }); + + const form = useForm({ + resolver: zodResolver(postingFormSchema), + }); + const { fields, append, replace } = useFieldArray({ + control: form.control, + name: "pstg", + }); + + const isFormValid = + fields.length && fields.every((field) => field.pstgPrdSeCd !== ""); + + const isAllSelected = + siteList && selectedRows.length === siteList.content.length; + + const postingPeriod = (siteId: string) => { + const periodCode = Number(rememberPeriodCode[siteId]); + + if (periodCode === 99) return "상시"; + + return `${dayjs().format("YYYY-MM-DD")} ~ ${dayjs() + .add(periodCode, "day") + .format("YYYY-MM-DD")}`; + }; + + const handleOpenChange = (open: boolean) => { + if (open) return true; + replace([]); + setSelectedRows([]); + setRememberPeriodCode({}); + setRememberPeriodDate({}); + }; + + const handleAppendFields = (i: number) => { + const siteId = siteList?.content[i]?.siteId ?? ""; + const newFields = postingTargets.map((target) => ({ + siteId, + pstgTypeCd: postingType, + pstgTrgtNo: target.pstgTrgtNo, + pstgBgngDt: rememberPeriodDate[siteId]?.from ?? "", + pstgEndDt: rememberPeriodDate[siteId]?.to ?? "", + pstgPrdSeCd: rememberPeriodCode[siteId] ?? "", + delYn: "N", + mode: "C", + })); + append(newFields); + }; + + const handleUpdateFields = + (siteId: string) => (obj: Record) => { + const updatedFields = fields.map((field) => + "siteId" in field && field.siteId === siteId + ? { ...field, ...obj } + : field, + ); + replace(updatedFields); + }; + + const handleRemoveFields = (siteId: string) => { + const filteredFields = fields.filter( + (field) => "siteId" in field && field.siteId !== siteId, + ); + replace(filteredFields); + }; + + const handleSelectRow = (i: number) => { + const isSelected = selectedRows.includes(i); + setSelectedRows((prev) => + isSelected ? prev.filter((j) => j !== i) : [...prev, i], + ); + + if (isSelected) { + const siteId = siteList?.content[i]?.siteId; + if (!siteId) return; + handleRemoveFields(siteId); + return; + } + + handleAppendFields(i); + }; + + const handleSelectAll = () => { + if (selectedRows.length === (siteList?.content.length ?? 0)) { + replace([]); + setSelectedRows([]); + } else { + replace([]); + setSelectedRows(siteList?.content.map((_, i) => i) ?? []); + siteList?.content.forEach((_, i) => handleAppendFields(i)); + } + }; + + const handlePeriodCodeChange = (siteId: string) => (value: string) => { + const from = dayjs().format("YYYY-MM-DD 00:00:00"); + const to = dayjs().add(Number(value), "day").format("YYYY-MM-DD 23:59:59"); + + setRememberPeriodCode((prev) => ({ + ...prev, + [siteId]: value, + })); + setRememberPeriodDate((prev) => ({ + ...prev, + [siteId]: { from, to }, + })); + handleUpdateFields(siteId)({ + pstgPrdSeCd: value, + pstgBgngDt: from, + pstgEndDt: to, + }); + }; + + const handlePeriodDateChange = + (siteId: string) => (value: DateRange | undefined) => { + const from = dayjs(value?.from).format("YYYY-MM-DD 00:00:00"); + const to = dayjs(value?.to).format("YYYY-MM-DD 23:59:59"); + setRememberPeriodDate((prev) => ({ + ...prev, + [siteId]: { from, to }, + })); + handleUpdateFields(siteId)({ + pstgBgngDt: from, + pstgEndDt: to, + }); + }; + + const handleSave = async () => { + if (await confirm("저장하시겠습니까?")) { + const data = form.getValues("pstg"); + await savePosting(data); + } + }; + + return ( + + + + + + + 사이트 반영 + +
+

+ 선택된 {POSTING_TYPE_LABEL[postingType]} +

+
+ {postingTargets.map((target) => ( + + {target.pstgTrgtNm} + + ))} +
+
+
+ + + + + + + + + + + + 사이트명 + 게시 구분 + + + + {siteList?.content.map((site, i) => ( + + + handleSelectRow(i)} + /> + + {site.siteNm} + +
+ + {!!Number(rememberPeriodCode[site.siteId]) && ( + + 게시 기간: {postingPeriod(site.siteId)} + + )} + {Number(rememberPeriodCode[site.siteId]) === 0 && ( + + handlePeriodDateChange(site.siteId)( + date as DateRange | undefined, + ) + } + disabled={!selectedRows.includes(i)} + /> + )} +
+
+
+ ))} +
+
+
+ + + + + + +
+
+ ); +} + +function PostingPeriodSelect({ + ...props +}: React.ComponentPropsWithoutRef) { + return ; +} + +export { PostingDialog }; diff --git a/apps/cs/src/entities/site/@x/posting.ts b/apps/cs/src/entities/site/@x/posting.ts new file mode 100644 index 000000000..6470b3cb2 --- /dev/null +++ b/apps/cs/src/entities/site/@x/posting.ts @@ -0,0 +1,2 @@ +export { SiteApi } from "../api/site-service"; +export type { Site } from "../model/site-interface"; diff --git a/apps/cs/src/entities/site/api/site-service.ts b/apps/cs/src/entities/site/api/site-service.ts index 37fd7e49a..a5d9d7cd6 100644 --- a/apps/cs/src/entities/site/api/site-service.ts +++ b/apps/cs/src/entities/site/api/site-service.ts @@ -10,7 +10,7 @@ import { } from "../model/site-interface"; export class SiteApiService implements SiteService { - public async getSiteList(params: SiteParams): Promise { + public async getSiteList(params?: SiteParams): Promise { const response = await client.get( `${ENDPOINT.CMS_SERVICE.SITES}`, { params }, @@ -44,10 +44,7 @@ export class SiteApiService implements SiteService { } public async deleteSite(siteId: string): Promise { - const response = await client.delete( - `${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`, - ); - return response.data; + await client.delete(`${ENDPOINT.CMS_SERVICE.SITES}/${siteId}`); } } diff --git a/apps/cs/src/entities/site/model/site-interface.ts b/apps/cs/src/entities/site/model/site-interface.ts index 85b1f802f..8846f59b9 100644 --- a/apps/cs/src/entities/site/model/site-interface.ts +++ b/apps/cs/src/entities/site/model/site-interface.ts @@ -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; diff --git a/apps/cs/src/pages/banner/api/banner-service.ts b/apps/cs/src/pages/banner/api/banner-service.ts new file mode 100644 index 000000000..93646c770 --- /dev/null +++ b/apps/cs/src/pages/banner/api/banner-service.ts @@ -0,0 +1,51 @@ +import { client } from "@/shared/api"; +import { ENDPOINT } from "@/shared/config"; +import type { + BannerFormData, + BannerInfo, + BannerList, + BannerParams, + BannerService, +} from "../model/banner-interface"; + +export class BannerApiService implements BannerService { + public async getBannerList(params: BannerParams): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.BANNERS}`, + { params }, + ); + return response.data; + } + + public async getBannerInfo(bnrSn: string): Promise { + const response = await client.get( + `${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`, + ); + return response.data; + } + + public async createBanner(data: BannerFormData): Promise { + const response = await client.post( + ENDPOINT.CMS_SERVICE.BANNERS, + data, + ); + return response.data; + } + + public async updateBanner( + bnrSn: string, + data: BannerFormData, + ): Promise { + const response = await client.put( + `${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`, + data, + ); + return response.data; + } + + public async deleteBanner(bnrSn: string): Promise { + await client.delete(`${ENDPOINT.CMS_SERVICE.BANNERS}/${bnrSn}`); + } +} + +export const BannerApi = new BannerApiService(); diff --git a/apps/cs/src/pages/banner/index.ts b/apps/cs/src/pages/banner/index.ts new file mode 100644 index 000000000..b2f0c6a55 --- /dev/null +++ b/apps/cs/src/pages/banner/index.ts @@ -0,0 +1,3 @@ +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; +export { BannerPage } from "./ui/banner-page"; diff --git a/apps/cs/src/pages/image/lib/utils.ts b/apps/cs/src/pages/banner/lib/utils.ts similarity index 100% rename from apps/cs/src/pages/image/lib/utils.ts rename to apps/cs/src/pages/banner/lib/utils.ts diff --git a/apps/cs/src/pages/banner/model/banner-interface.ts b/apps/cs/src/pages/banner/model/banner-interface.ts new file mode 100644 index 000000000..1226f63e6 --- /dev/null +++ b/apps/cs/src/pages/banner/model/banner-interface.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; +import type { PageableList, PageableParams } from "@/shared/model"; + +export const BANNER_TYPE = { + BNRMAIN: "BNRMAIN", + BNRFTER: "BNRFTER", +} as const; + +export type BannerType = (typeof BANNER_TYPE)[keyof typeof BANNER_TYPE]; + +export interface BannerParams extends PageableParams { + bnrTypeCd?: BannerType; + bnrTtl?: string; + useYn?: string; +} + +export interface Banner { + bnrSn: number; + bnrTypeCd: BannerType; + bnrTypeNm: string; + bnrTtl: string; + lnkgUrlAddr: string; + useYn: string; + atflCd: string; + mdfcnDt: string; + mdfr: string; + mdfrNm: string; +} + +export interface BannerInfo extends Omit { + bnrExpln: string; + newWndwYn: string; + atflCd: string; + regDt: string; + rgTrNm: string; +} + +export const bannerFormSchema = z.object({ + bnrTypeCd: z.string(), + bnrTtl: z.string(), + bnrExpln: z.string().nullish(), + lnkgUrlAddr: z.string().nullish(), + newWndwYn: z.string(), + atflCd: z.string().nullish(), + useYn: z.string(), +}); + +export type BannerFormData = z.infer; +export type BannerList = PageableList; + +export interface BannerService { + getBannerList: (params: BannerParams) => Promise; + getBannerInfo: (bnrSn: string) => Promise; + createBanner: (data: BannerFormData) => Promise; + updateBanner: (bnrSn: string, data: BannerFormData) => Promise; + deleteBanner: (bnrSn: string) => Promise; +} diff --git a/apps/cs/src/pages/banner/model/banner-table-columns.tsx b/apps/cs/src/pages/banner/model/banner-table-columns.tsx new file mode 100644 index 000000000..9b36a54fc --- /dev/null +++ b/apps/cs/src/pages/banner/model/banner-table-columns.tsx @@ -0,0 +1,100 @@ +"use client"; + +import dayjs from "dayjs"; +import Image from "next/image"; +import { useQuery } from "@tanstack/react-query"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox, Skeleton } from "@common/components/ui"; +import { AttachmentApi } from "@/entities/attachment"; +import { ENDPOINT } from "@/shared/config"; +import type { Banner } from "./banner-interface"; + +export const columns = ( + imageRefs: React.MutableRefObject>, +): ColumnDef[] => [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + header: "배너 이미지", + cell: ({ row }) => ( + + ), + }, + { + accessorKey: "bnrTtl", + header: "배너명", + }, + { + accessorKey: "bnrTypeNm", + header: "배너구분", + }, + { + accessorKey: "useYn", + header: "사용여부", + }, + { + accessorKey: "mdfrNm", + header: "최종수정자", + }, + { + accessorKey: "mdfcnDt", + header: "최종수정일", + cell: ({ row }) => { + const date = row.getValue("mdfcnDt") as string; + return date ? dayjs(date).format("YYYY-MM-DD") : ""; + }, + }, +]; + +type BannerImageProps = { + atflCd: string; + bnrSn: number; + imageRefs: React.MutableRefObject>; +}; + +const BannerImage = ({ atflCd, bnrSn, imageRefs }: BannerImageProps) => { + const prevImage = imageRefs.current.get(bnrSn); + + const { data: imageUrl, isLoading } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], + queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd), + select: (url) => { + if (url) imageRefs.current.set(bnrSn, url); + return url; + }, + enabled: !prevImage && !!atflCd, + }); + + if (isLoading) return ; + + if (!imageUrl) return null; + + return ( +
+ 배너 이미지 +
+ ); +}; diff --git a/apps/cs/src/pages/banner/model/posting-table-columns.tsx b/apps/cs/src/pages/banner/model/posting-table-columns.tsx new file mode 100644 index 000000000..a43cf8693 --- /dev/null +++ b/apps/cs/src/pages/banner/model/posting-table-columns.tsx @@ -0,0 +1,44 @@ +"use client"; + +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Checkbox } from "@common/components/ui"; +import type { Post } from "@/entities/posting"; + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + className="h-4 w-4 rounded border-gray-300" + /> + ), + cell: ({ row }) => ( + e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + ), + }, + { + accessorKey: "siteNm", + header: "사이트", + }, + { + accessorKey: "pstgPrdSeNm", + header: "게시기간", + cell: ({ row }) => { + const pstgPrdSeNm = row.original.pstgPrdSeNm; + const pstgBgngDt = row.original.pstgBgngDt; + const pstgEndDt = row.original.pstgEndDt; + return `${pstgPrdSeNm} (${dayjs(pstgBgngDt).format("YYYY-MM-DD")} ~ ${dayjs(pstgEndDt).format("YYYY-MM-DD")})`; + }, + }, +]; diff --git a/apps/cs/src/pages/banner/ui/banner-form.tsx b/apps/cs/src/pages/banner/ui/banner-form.tsx new file mode 100644 index 000000000..b0a31d775 --- /dev/null +++ b/apps/cs/src/pages/banner/ui/banner-form.tsx @@ -0,0 +1,297 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import type { UseFormReturn } from "react-hook-form"; +import type { HttpResponse, Upload } from "tus-js-client"; +import { + FileUploader, + Input, + Label, + Progress, + RadioGroup, + RadioGroupItem, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Textarea, +} 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 { setCookie } from "@/shared/lib/cookie"; +import { FormControl, FormField, FormItem } from "@/shared/ui/form"; +import type { BannerFormData } from "../model/banner-interface"; +import { handleUpload } from "../lib/utils"; + +type BannerFormProps = { + form: UseFormReturn; + bnrSn?: number; + handleSave: (data: BannerFormData) => Promise; + handleDelete?: () => Promise; +}; + +function BannerForm({ form, bnrSn, ...props }: BannerFormProps) { + const atflCd = form.watch("atflCd"); + + const [imageUrl, setImageUrl] = useState(""); + const [upload, setUpload] = useState(null); + const [progress, setProgress] = useState(0); + + const { data: attachmentData } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, atflCd], + queryFn: () => AttachmentApi.getAttachments(atflCd!), + select: (data) => data?.[data.length - 1], + enabled: !!atflCd, + }); + + const handleProgress = (percentage: string) => { + setProgress(Number(percentage)); + }; + + const handleUploadResponse = async (res: HttpResponse) => { + const fileInfo = res.getHeader("X-File-Info"); + if (fileInfo === undefined || fileInfo === null) return; + + const { atflCd, atflId } = JSON.parse(fileInfo) ?? {}; + if (atflCd) form.setValue("atflCd", atflCd); + if (atflId) setImageUrl(await AttachmentApi.getImageUrl(atflId)); + if (attachmentData) + await AttachmentApi.deleteAttachmentByAtflCd(attachmentData.atflCd); + }; + + useEffect(() => { + if (!attachmentData) return; + + (async () => { + setImageUrl(await AttachmentApi.getImageUrl(attachmentData.atflId)); + })(); + }, [attachmentData]); + + return ( + +
+ + + + + + + + 배너명 + + ( + + + + + + )} + /> + + + + 배너구분 + + ( + + + + + + )} + /> + + + + 배너 이미지 + + ( + + + { + const files = e.target.files; + if (!files) return; + setCookie("atfl-cd-key", ""); + handleUpload(files, { + handleUploadStart: setUpload, + handleProgress, + handleSuccess: setUpload, + handleResponse: handleUploadResponse, + }); + }} + customPreview={ + <> + +
+
    +
  • + 이미지 파일(jpg, jpeg, png, gif)만 업로드 + 가능합니다. +
  • +
+
+ + } + /> +
+
+ )} + /> +
+
+ + 링크 URL + + ( + + + + + + )} + /> + + + + 링크 열기 + + ( + + + +
+ + +
+
+ + +
+
+
+
+ )} + /> +
+
+ + 배너 설명 + + ( + + +