diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/[pstNo]/page.tsx new file mode 100644 index 000000000..9ca687f78 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/[pstNo]/page.tsx @@ -0,0 +1,10 @@ +import { ImagePost } from "@/pages/(pst)/image"; + +export default async function PstBbsImagePostPage({ + params, +}: { + params: Promise<{ bbsId: string; pstNo: string }>; +}) { + const { bbsId, pstNo } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/page.tsx new file mode 100644 index 000000000..520cfc83a --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/page.tsx @@ -0,0 +1,10 @@ +import { ImagePage } from "@/pages/(pst)/image"; + +export default async function PstBbsImagePage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/register/page.tsx new file mode 100644 index 000000000..c5e3e68c0 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsimage/[bbsId]/register/page.tsx @@ -0,0 +1,10 @@ +import { CreatePage } from "@/pages/(pst)/image"; + +export default async function PstBbsImageRegisterPage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/layout.tsx b/apps/common/features/app/(with-base-layout)/pst/layout.tsx index 19d7de4cb..e0333f2d2 100644 --- a/apps/common/features/app/(with-base-layout)/pst/layout.tsx +++ b/apps/common/features/app/(with-base-layout)/pst/layout.tsx @@ -1,21 +1,14 @@ "use client"; -import { useEffect, useState } from "react"; -import type { Board } from "@/entities/board"; +import { useMenuStore } from "@/shared/lib"; import { PageLayout } from "@/shared/ui/page-layout"; -import { useLocalStorage } from "@frontend-opensource/use-react-hooks"; export default function PostLayout({ children, }: { children: React.ReactNode; }) { - const [bbsInfo, setBbsInfo] = useState(null); - const [storedBbsInfo] = useLocalStorage("bbs-info", ""); - - useEffect(() => { - if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo)); - }, [storedBbsInfo]); + const { bbsInfo } = useMenuStore((state) => state); return ( { const pathname = usePathname(); const { setMessage: alert } = useAlertStore((state) => state); - const { pageMenuAuthority, setCurrentMenuInfo, setPageMenuAuthority } = - useMenuStore((state) => state); + const { + pageMenuAuthority, + setBbsInfo, + setCurrentMenuInfo, + setPageMenuAuthority, + } = useMenuStore((state) => state); + + const [storedBbsInfo] = useLocalStorage("bbs-info", ""); const getCurrentMenuInfo = async () => { const menuRoles = await MenuApi.getMenuRoles(SITE_ID); @@ -53,6 +60,11 @@ const MenuWrapper = ({ children }: { children: React.ReactNode }) => { alert("페이지에 대한 조회 권한이 없습니다.", () => router.push("/")); }, [pageMenuAuthority]); + useEffect(() => { + if (!storedBbsInfo) return; + setBbsInfo(JSON.parse(storedBbsInfo)); + }, [storedBbsInfo]); + return <>{children}; }; diff --git a/apps/common/features/src/pages/(pst)/image/index.ts b/apps/common/features/src/pages/(pst)/image/index.ts new file mode 100644 index 000000000..b3a860dfe --- /dev/null +++ b/apps/common/features/src/pages/(pst)/image/index.ts @@ -0,0 +1,3 @@ +export { CreatePage } from "./ui/create-page"; +export { ImagePage } from "./ui/image-page"; +export { ImagePost } from "./ui/image-post"; diff --git a/apps/common/features/src/pages/(pst)/image/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/image/ui/create-page.tsx new file mode 100644 index 000000000..0efcd2c30 --- /dev/null +++ b/apps/common/features/src/pages/(pst)/image/ui/create-page.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import { PostForm } from "@/widgets/post"; +import { PostApi, postFormSchema, type PostFormData } from "@/entities/post"; +import { useAlertStore, useUserStore } from "@/shared/lib"; + +function CreatePage({ bbsId }: { bbsId: string }) { + const router = useRouter(); + const { userInfo } = useUserStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); + + const { mutateAsync: createPost } = useMutation({ + mutationFn: (data: PostFormData) => PostApi.createPost(data), + onSuccess: () => { + alert("등록되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(postFormSchema), + values: { + bbsId: parseInt(bbsId), + pstTtl: "", + wrtrNm: userInfo?.userName ?? "", + pstPswd: userInfo?.userUnqId ?? "", + }, + }); + + return ( +
+
+

이미지 게시글 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/common/features/src/pages/(pst)/image/ui/image-page.tsx b/apps/common/features/src/pages/(pst)/image/ui/image-page.tsx new file mode 100644 index 000000000..613d8bb5e --- /dev/null +++ b/apps/common/features/src/pages/(pst)/image/ui/image-page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { PostPage as ImagePage } from "@/widgets/post"; diff --git a/apps/common/features/src/pages/(pst)/image/ui/image-post.tsx b/apps/common/features/src/pages/(pst)/image/ui/image-post.tsx new file mode 100644 index 000000000..77eb96bc1 --- /dev/null +++ b/apps/common/features/src/pages/(pst)/image/ui/image-post.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { Post as ImagePost } from "@/widgets/post"; diff --git a/apps/common/features/src/shared/store/menu-store.ts b/apps/common/features/src/shared/store/menu-store.ts index 139c37890..8536011ee 100644 --- a/apps/common/features/src/shared/store/menu-store.ts +++ b/apps/common/features/src/shared/store/menu-store.ts @@ -1,5 +1,39 @@ import { createStore } from "zustand/vanilla"; +export const BBS_TYPE = { + BBSNOTICE: "BBSNOTICE", + BBSNORMAL: "BBSNORMAL", + BBSFAQ: "BBSFAQ", + BBSIMAGE: "BBSIMAGE", + BBSMOVIE: "BBSMOVIE", +} as const; + +export type BbsType = (typeof BBS_TYPE)[keyof typeof BBS_TYPE]; + +export interface IBbs { + bbsId: number; + bbsTypeCd: BbsType; + bbsTypeNm: string; + bbsNm: string; + bbsExpln: string; + rfrncUseYn: string; + atflUseYn: string; + atflMaxCnt: number; + atflMaxFileSz: number; + ansUseYn: string; + ansUseAuthrt: string; + cmntUseYn: string; + dgstfnEvlUseYn: string; + smsUseYn: string; + upndFixUseYn: string; + pstgPrdUseYn: string; + popupUseYn: string; + delYn: string; + useYn: string; + mdfcnDt: string; + mdfrNm: string; +} + interface IMenu { menuCd: number; menuKornNm: string; @@ -29,11 +63,13 @@ interface IAuthority { } export type MenuState = { + bbsInfo: IBbs | null; currentMenuInfo: IMenu | null; pageMenuAuthority: IAuthority | null; }; export type MenuActions = { + setBbsInfo: (bbs: IBbs) => void; setCurrentMenuInfo: (menu: IMenu) => void; setPageMenuAuthority: (authority: IAuthority) => void; }; @@ -42,12 +78,14 @@ export type MenuStore = MenuState & MenuActions; export const defaultInitState: MenuState = { currentMenuInfo: null, + bbsInfo: null, pageMenuAuthority: null, }; export const createMenuStore = (initState: MenuState = defaultInitState) => { return createStore()((set) => ({ ...initState, + setBbsInfo: (bbs: IBbs) => set({ bbsInfo: bbs }), setCurrentMenuInfo: (menu: IMenu) => set({ currentMenuInfo: menu }), setPageMenuAuthority: (authority: IAuthority) => set({ pageMenuAuthority: authority }), diff --git a/apps/common/features/src/shared/ui/grid-view-data-table.tsx b/apps/common/features/src/shared/ui/grid-view-data-table.tsx new file mode 100644 index 000000000..b68ec6895 --- /dev/null +++ b/apps/common/features/src/shared/ui/grid-view-data-table.tsx @@ -0,0 +1,158 @@ +import type { HTMLAttributes } from "react"; +import { + flexRender, + type RowData, + type Table as TableInstance, +} from "@tanstack/react-table"; +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, + DoubleArrowRightIcon, +} from "@radix-ui/react-icons"; +import { Button, Checkbox, Label, cn } from "@common/components"; + +declare module "@tanstack/react-table" { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface ColumnMeta { + align?: "left" | "center" | "right"; + rowSpan?: number; + } +} + +type GridViewDataTableProps = { + table: TableInstance; + variant?: "default" | "secondary" | "line"; + className?: HTMLAttributes["className"]; + viewPagination?: boolean; + onRowClick?: (row: T) => void; +}; + +function GridViewDataTable({ + table, + className, + viewPagination, + onRowClick, +}: GridViewDataTableProps) { + return ( + <> +
+ e.stopPropagation()} + onCheckedChange={(checked) => + table.toggleAllPageRowsSelected(!!checked) + } + /> + +
+
+
+ {table.getRowModel().rows.map((row) => ( +
onRowClick?.(row.original)} + className={cn( + "rounded-md border border-border p-4 shadow-md", + onRowClick && "cursor-pointer hover:bg-accent", + )} + > + {row.getVisibleCells().map((cell) => { + const { meta } = cell.column.columnDef; + return ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ); + })} +
+ ))} +
+
+ {viewPagination && } + + ); +} + +function Pagination({ table }: { table: TableInstance }) { + const pageCount = table.getPageCount(); + const currentPage = table.getState().pagination.pageIndex; + const maxVisiblePages = 10; // 한 번에 보여줄 최대 페이지 수 + + let startPage = Math.max(0, currentPage - maxVisiblePages / 2); // 시작 페이지 + const endPage = Math.min(pageCount, startPage + maxVisiblePages); // 끝 페이지 + + if (endPage - startPage < maxVisiblePages) { + startPage = Math.max(0, endPage - maxVisiblePages); + } + + const pageNumbers = Array(endPage - startPage) + .fill(0) + .map((_, i) => startPage + i); + + return ( +
+ + +
+ {pageNumbers.map((pageNumber) => ( + + ))} +
+ + +
+ ); +} + +export { GridViewDataTable }; diff --git a/apps/common/features/src/widgets/post/model/grid-view-table-columns.tsx b/apps/common/features/src/widgets/post/model/grid-view-table-columns.tsx new file mode 100644 index 000000000..66c144053 --- /dev/null +++ b/apps/common/features/src/widgets/post/model/grid-view-table-columns.tsx @@ -0,0 +1,117 @@ +import dayjs from "dayjs"; +import Image from "next/image"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Badge, Checkbox, Skeleton } from "@common/components"; +import type { Post } from "@/entities/post"; +import { useQuery } from "@tanstack/react-query"; +import { ENDPOINT } from "@/shared/config"; +import { AttachmentApi } from "@/entities/attachment"; + +export const createColumns = ( + thumbRefs: 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 }) => { + const { pstTtl, inqNocs } = row.original; + return ( +
+
+ e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + {inqNocs >= 100 && HOT} + {dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && ( + NEW + )} +
+
+ {pstTtl} +
+
+ ); + }, + }, + { + id: "atflCd", + cell: ({ row }) => { + const atflCd = row.original.atflCd; + if (!atflCd) return null; + return ( + + ); + }, + }, + { + id: "rgtrNm", + cell: ({ row }) => { + const { rgtrNm } = row.original; + return {rgtrNm}; + }, + }, + { + id: "regDt", + cell: ({ row }) => ( + + {dayjs(row.original.regDt).format("YYYY-MM-DD")} + + ), + }, +]; + +type ThumbImageProps = { + atflCd: string; + pstNo: number; + thumbRefs: React.MutableRefObject>; +}; + +const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => { + const prevImage = thumbRefs.current.get(pstNo); + + const { data: imageUrl, isLoading } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], + queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd), + select: (url) => { + if (url) thumbRefs.current.set(pstNo, url); + return url; + }, + enabled: !prevImage && !!atflCd, + }); + + if (isLoading) + return ( +
+ +
+ ); + + if (!imageUrl) return null; + + return ( +
+ 썸네일 이미지 +
+ ); +}; diff --git a/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx b/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx index b28614587..99088aa3e 100644 --- a/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx +++ b/apps/common/features/src/widgets/post/ui/general-file-uploader.tsx @@ -1,13 +1,11 @@ import { useEffect, useState } from "react"; import type { FieldPath, UseFormReturn } from "react-hook-form"; import type { HttpResponse, Upload } from "tus-js-client"; -import { useLocalStorage } from "@frontend-opensource/use-react-hooks"; import { Button, FileUploader } from "@common/components"; -import type { Board } from "@/entities/board"; import { handleTusUpload } from "@/entities/tus"; import { type Attachment } from "@/entities/attachment"; import type { PostFormData, PostUpdateFormData } from "@/entities/post"; -import { setCookie, useAlertStore } from "@/shared/lib"; +import { setCookie, useAlertStore, useMenuStore } from "@/shared/lib"; function GeneralFileUploader({ form, @@ -20,10 +18,9 @@ function GeneralFileUploader({ deleteAttachmentIds: string[]; setDeleteAttachmentIds: (deleteAttachmentIds: string[]) => void; }) { - const [storedBbsInfo] = useLocalStorage("bbs-info", ""); const { setMessage: alert } = useAlertStore((state) => state); + const { bbsInfo } = useMenuStore((state) => state); - const [bbsInfo, setBbsInfo] = useState(null); const [attachments, setAttachments] = useState>( {}, ); @@ -88,10 +85,6 @@ function GeneralFileUploader({ if (attachmentData) setAttachments(mapper(attachmentData)); }, [attachmentData]); - useEffect(() => { - if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo)); - }, [storedBbsInfo]); - return ( (null); + const { bbsInfo } = useMenuStore((state) => state); const onDelete = async () => { if (await confirm("삭제하시겠습니까?")) handleDelete(); }; - useEffect(() => { - if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo)); - }, [storedBbsInfo]); - return (
{children} diff --git a/apps/common/features/src/widgets/post/ui/post-form.tsx b/apps/common/features/src/widgets/post/ui/post-form.tsx index 895309ed5..d84cb179a 100644 --- a/apps/common/features/src/widgets/post/ui/post-form.tsx +++ b/apps/common/features/src/widgets/post/ui/post-form.tsx @@ -2,10 +2,7 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import type { UseFormReturn, FieldPath } from "react-hook-form"; -import { - useConfirm, - useLocalStorage, -} from "@frontend-opensource/use-react-hooks"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Input, Label, @@ -19,14 +16,15 @@ import { Textarea, Button, } from "@common/components"; -import { BOARD_POSTING_CODE, type Board } from "@/entities/board"; +import { BOARD_POSTING_CODE } from "@/entities/board"; import { AttachmentApi } from "@/entities/attachment"; import type { Post, PostFormData, PostUpdateFormData } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import { useUserStore, setCookie } from "@/shared/lib"; +import { useUserStore, setCookie, useMenuStore } from "@/shared/lib"; import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form"; import { PostingPeriodSelector } from "./posting-period-selector"; import { GeneralFileUploader } from "./general-file-uploader"; +import { WithPreviewFileUploader } from "./with-preview-file-uploader"; type PostFormProps = { form: UseFormReturn; @@ -42,10 +40,9 @@ function PostForm({ const router = useRouter(); const { confirm } = useConfirm(); + const { bbsInfo } = useMenuStore((state) => state); const { userInfo } = useUserStore((state) => state); - const [storedBbsInfo] = useLocalStorage("bbs-info", ""); - const [bbsInfo, setBbsInfo] = useState(null); const [deleteAttachmentIds, setDeleteAttachmentIds] = useState([]); const { data: attachmentData } = useQuery({ @@ -65,10 +62,6 @@ function PostForm({ } }; - useEffect(() => { - if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo)); - }, [storedBbsInfo]); - useEffect(() => { setCookie("atfl-cd-key", ""); }, []); @@ -419,12 +412,19 @@ function PostForm({ 첨부파일 - + {bbsInfo?.bbsTypeCd === "BBSIMAGE" ? ( + + ) : ( + + )} )} diff --git a/apps/common/features/src/widgets/post/ui/post-page.tsx b/apps/common/features/src/widgets/post/ui/post-page.tsx index 25c08e685..776940266 100644 --- a/apps/common/features/src/widgets/post/ui/post-page.tsx +++ b/apps/common/features/src/widgets/post/ui/post-page.tsx @@ -1,11 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { - useConfirm, - useLocalStorage, -} from "@frontend-opensource/use-react-hooks"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, @@ -23,22 +20,24 @@ import { SelectItem, SelectTrigger, SelectValue, + Skeleton, } from "@common/components"; import { PostApi, type Post, type PostParams } from "@/entities/post"; -import type { Board } from "@/entities/board"; +import type { BoardType } from "@/entities/board"; import { ENDPOINT } from "@/shared/config"; import { useMenuStore, useUserStore } from "@/shared/lib"; +import { GridViewDataTable } from "@/shared/ui/grid-view-data-table"; import { columns } from "../model/post-table-columns"; +import { createColumns } from "../model/grid-view-table-columns"; function PostPage({ bbsId }: { bbsId: string }) { const router = useRouter(); const pathname = usePathname(); const { confirm } = useConfirm(); - const [storedBbsInfo] = useLocalStorage("bbs-info", ""); - const { pageMenuAuthority } = useMenuStore((state) => state); const { userInfo } = useUserStore((state) => state); + const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); + const bbsType = bbsInfo?.bbsTypeCd as BoardType; - const [bbsInfo, setBbsInfo] = useState(null); const [postParams, setPostParams] = useState({ keywordType: "title", keyword: "", @@ -49,6 +48,8 @@ function PostPage({ bbsId }: { bbsId: string }) { }); const [rowSelection, setRowSelection] = useState({}); + const thumbRefs = useRef>(new Map()); + const { data: upndFixPostList } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo], queryFn: () => @@ -69,7 +70,11 @@ function PostPage({ bbsId }: { bbsId: string }) { enabled: bbsInfo?.upndFixUseYn === "Y", }); - const { data: postList, refetch } = useQuery({ + const { + data: postList, + isFetched, + refetch, + } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId], queryFn: () => PostApi.getPostList(bbsId, { @@ -92,6 +97,18 @@ function PostPage({ bbsId }: { bbsId: string }) { onRowSelectionChange: setRowSelection, }); + const gridViewTable = useReactTable({ + data: postList?.content ?? [], + columns: createColumns(thumbRefs), + state: { pagination, rowSelection }, + rowCount: postList?.totalElements ?? 0, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }); + const onPostDelete = async () => { const selectedPosts = table .getFilteredSelectedRowModel() @@ -117,10 +134,6 @@ function PostPage({ bbsId }: { bbsId: string }) { router.push(`${pathname}/${row.pstNo}`); }; - useEffect(() => { - if (storedBbsInfo) setBbsInfo(JSON.parse(storedBbsInfo)); - }, [storedBbsInfo]); - useEffect(() => { refetch(); }, [pagination]); @@ -197,7 +210,23 @@ function PostPage({ bbsId }: { bbsId: string }) {
- + {isFetched ? ( + bbsType === "BBSIMAGE" ? ( + + ) : ( + + ) + ) : ( + + )}
diff --git a/apps/common/features/src/widgets/post/ui/post.tsx b/apps/common/features/src/widgets/post/ui/post.tsx index 572103c42..f000c52ae 100644 --- a/apps/common/features/src/widgets/post/ui/post.tsx +++ b/apps/common/features/src/widgets/post/ui/post.tsx @@ -1,13 +1,19 @@ "use client"; import dayjs from "dayjs"; +import Image from "next/image"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useMutation, useQuery } from "@tanstack/react-query"; import { Button, Skeleton } from "@common/components"; import { AttachmentApi } from "@/entities/attachment"; import { PostApi } from "@/entities/post"; -import { useAlertStore, useUserStore, usePrompt } from "@/shared/lib"; +import { + useAlertStore, + useUserStore, + useMenuStore, + usePrompt, +} from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { PostAction } from "./post-action"; import { PostComment } from "./post-comment"; @@ -17,6 +23,8 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { prompt } = usePrompt(); const { setMessage: alert } = useAlertStore((state) => state); const { userInfo } = useUserStore((state) => state); + const { bbsInfo } = useMenuStore((state) => state); + const [filename, setFilename] = useState(""); const { data: postData } = useQuery({ @@ -28,6 +36,13 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, postData?.atflCd], queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(postData?.atflCd as string), + enabled: !!postData?.atflCd && bbsInfo?.bbsTypeCd !== "BBSIMAGE", + }); + + const { data: imageUrl, isLoading } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, postData?.atflCd], + queryFn: () => + AttachmentApi.getImageUrlByAtflCd(postData?.atflCd as string), enabled: !!postData?.atflCd, }); @@ -106,6 +121,19 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
+ {bbsInfo?.bbsTypeCd === "BBSIMAGE" && ( +
+ {isLoading && } + {imageUrl && ( + 썸네일 이미지 + )} +
+ )}
({ + form, +}: { + form: UseFormReturn; + attachmentData?: Attachment[]; +}) { + const { setMessage: alert } = useAlertStore((state) => state); + const { bbsInfo } = useMenuStore((state) => state); + + const [thumbnailUrl, setThumbnailUrl] = useState(""); + const [progress, setProgress] = useState(0); + + const handleTusResponse = async (res: HttpResponse) => { + const fileInfo = res.getHeader("X-File-Info"); + if ( + typeof fileInfo !== "string" || + fileInfo === undefined || + fileInfo === null + ) + return; + + const { atflCd, atflId } = JSON.parse(fileInfo) ?? {}; + if (atflCd) form.setValue("atflCd" as FieldPath, atflCd); + if (atflId) { + setThumbnailUrl(await AttachmentApi.getImageUrl(atflId)); + } + }; + + const handleProgress = (percentage: string) => { + setProgress(parseInt(percentage)); + }; + + const onUpload = (e: React.ChangeEvent) => { + const atflCd = form.getValues("atflCd" as FieldPath); + if (atflCd) setCookie("atfl-cd-key", atflCd as string); + + const files = e.target.files; + if (!files) return; + if (bbsInfo?.atflMaxCnt && files.length > bbsInfo.atflMaxCnt) { + alert("첨부파일 최대 개수를 초과했습니다."); + return; + } + + for (const file of files) { + const fileSize = Math.round(file.size / 1024); // Byte -> KB + if (bbsInfo?.atflMaxFileSz && fileSize > bbsInfo.atflMaxFileSz) { + alert("첨부파일 최대 크기를 초과했습니다."); + return; + } + handleTusUpload(file, { + handleResponse: handleTusResponse, + handleProgress, + }); + } + }; + + return ( + +
+
+ {thumbnailUrl && ( + 업로드된 이미지 + )} +
+ +
+ + } + /> + ); +} + +export { WithPreviewFileUploader }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4988f297b..98c9b9026 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@minoru/react-dnd-treeview': specifier: ^3.5.0 version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)