diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/[pstNo]/page.tsx new file mode 100644 index 000000000..9d3f6c0e3 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/[pstNo]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage } from "@/pages/(pst)/faq"; + +export default async function PstBbsFaqPostModifyPage({ + params, +}: { + params: Promise<{ bbsId: string; pstNo: string }>; +}) { + const { bbsId, pstNo } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/page.tsx new file mode 100644 index 000000000..34dd522bf --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/page.tsx @@ -0,0 +1,10 @@ +import { FaqPage } from "@/pages/(pst)/faq"; + +export default async function PstBbsFaqPage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/register/page.tsx new file mode 100644 index 000000000..6a044f876 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsfaq/[bbsId]/register/page.tsx @@ -0,0 +1,10 @@ +import { CreatePage } from "@/pages/(pst)/faq"; + +export default async function PstBbsFaqRegisterPage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx new file mode 100644 index 000000000..76f2d06c3 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/modify/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage } from "@/pages/(pst)/movie"; + +export default async function PstBbsMoviePostModifyPage({ + params, +}: { + params: Promise<{ bbsId: string; pstNo: string }>; +}) { + const { bbsId, pstNo } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/page.tsx new file mode 100644 index 000000000..e0983bd61 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/[pstNo]/page.tsx @@ -0,0 +1,10 @@ +import { MoviePost } from "@/pages/(pst)/movie"; + +export default async function PstBbsMoviePostPage({ + params, +}: { + params: Promise<{ bbsId: string; pstNo: string }>; +}) { + const { bbsId, pstNo } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/page.tsx new file mode 100644 index 000000000..bbd6b2619 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/page.tsx @@ -0,0 +1,10 @@ +import { MoviePage } from "@/pages/(pst)/movie"; + +export default async function PstBbsMoviePage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/register/page.tsx b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/register/page.tsx new file mode 100644 index 000000000..5cfffb031 --- /dev/null +++ b/apps/common/features/app/(with-base-layout)/pst/bbsmovie/[bbsId]/register/page.tsx @@ -0,0 +1,10 @@ +import { CreatePage } from "@/pages/(pst)/movie"; + +export default async function PstBbsMovieRegisterPage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/common/features/src/entities/attachment/api/attachment-service.ts b/apps/common/features/src/entities/attachment/api/attachment-service.ts index a5352c712..fc2dcab05 100644 --- a/apps/common/features/src/entities/attachment/api/attachment-service.ts +++ b/apps/common/features/src/entities/attachment/api/attachment-service.ts @@ -52,7 +52,15 @@ export class AttachmentApiService implements AttachmentService { return URL.createObjectURL(blob); } - public async getMovieThumbnailUrl(atflId: string): Promise { + public async getStreamVideoUrlByAtflId(atflId: string): Promise { + const response = await fetch( + `${ENDPOINT.ATTACH_SERVICE.STREAM_VIDEO}/${atflId}`, + ); + const blob = await response.blob(); + return URL.createObjectURL(blob); + } + + public async getMovieThumbnailUrlByAtflId(atflId: string): Promise { const response = await fetch( `${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/${atflId}`, ); @@ -60,6 +68,14 @@ export class AttachmentApiService implements AttachmentService { return URL.createObjectURL(blob); } + public async getMovieThumbnailUrlByAtflCd(atflCd: string): Promise { + const response = await fetch( + `${ENDPOINT.ATTACH_SERVICE.MOVIE_THUMBNAIL}/atflCd/${atflCd}`, + ); + const blob = await response.blob(); + return URL.createObjectURL(blob); + } + public async downloadAttachmentByAtflCd( atflCd: string, ): Promise { diff --git a/apps/common/features/src/entities/attachment/model/attachment-interface.ts b/apps/common/features/src/entities/attachment/model/attachment-interface.ts index 5ad0fc3ea..0f1b5d1ee 100644 --- a/apps/common/features/src/entities/attachment/model/attachment-interface.ts +++ b/apps/common/features/src/entities/attachment/model/attachment-interface.ts @@ -40,6 +40,8 @@ export interface AttachmentService { deleteAttachmentByAtflCd: (atflCd: string) => Promise; getImageUrlByAtflId: (atflId: string) => Promise; getImageUrlByAtflCd: (atflCd: string) => Promise; - getMovieThumbnailUrl: (atflId: string) => Promise; + getStreamVideoUrlByAtflId: (atflId: string) => Promise; + getMovieThumbnailUrlByAtflId: (atflId: string) => Promise; + getMovieThumbnailUrlByAtflCd: (atflCd: string) => Promise; downloadAttachmentByAtflCd: (atflCd: string) => Promise; } diff --git a/apps/common/features/src/entities/post/api/post-service.ts b/apps/common/features/src/entities/post/api/post-service.ts index a07fc4d2a..1e5ac5146 100644 --- a/apps/common/features/src/entities/post/api/post-service.ts +++ b/apps/common/features/src/entities/post/api/post-service.ts @@ -14,7 +14,7 @@ import { export class PostApiService implements PostService { public async getPostList( - bbsId: string, + bbsId: number, params: PostParams, ): Promise { const response = await client.get( @@ -24,7 +24,7 @@ export class PostApiService implements PostService { return response.data; } - public async getPostInfo(bbsId: string, pstNo: string): Promise { + public async getPostInfo(bbsId: number, pstNo: number): Promise { const response = await client.get( `${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`, ); @@ -41,8 +41,8 @@ export class PostApiService implements PostService { } public async updatePost( - bbsId: string, - pstNo: string, + bbsId: number, + pstNo: number, data: PostUpdateFormData, ): Promise { const validatedData = postUpdateFormSchema.parse(data); @@ -54,8 +54,8 @@ export class PostApiService implements PostService { } public async deletePost( - bbsId: string, - pstNo: string, + bbsId: number, + pstNo: number, pstPswd?: string, ): Promise { await client.delete( diff --git a/apps/common/features/src/entities/post/model/post-interface.ts b/apps/common/features/src/entities/post/model/post-interface.ts index 9cb97ef92..11778002f 100644 --- a/apps/common/features/src/entities/post/model/post-interface.ts +++ b/apps/common/features/src/entities/post/model/post-interface.ts @@ -8,13 +8,15 @@ export interface PostParams extends PageableParams { upndFixYn?: string; prdPstgYn?: string; siteId?: string; + bbsTypeCd?: string; } export interface Post { - bbsId: number; rowNo: number; + bbsId: number; pstNo: number; pstTtl: string; + pstCn: string; inqNocs: number; rfrncUrl: string; popupYn: string; @@ -73,13 +75,13 @@ export type PostFormData = z.infer; export type PostUpdateFormData = z.infer; export interface PostService { - getPostList(bbsId: string, params: PostParams): Promise; - getPostInfo(bbsId: string, pstNo: string): Promise; + getPostList(bbsId: number, params: PostParams): Promise; + getPostInfo(bbsId: number, pstNo: number): Promise; createPost(data: PostFormData): Promise; updatePost( - bbsId: string, - pstNo: string, + bbsId: number, + pstNo: number, data: PostUpdateFormData, ): Promise; - deletePost(bbsId: string, pstNo: string, pstPswd?: string): Promise; + deletePost(bbsId: number, pstNo: number, pstPswd?: string): Promise; } diff --git a/apps/common/features/src/pages/(pst)/faq/index.ts b/apps/common/features/src/pages/(pst)/faq/index.ts new file mode 100644 index 000000000..350df198c --- /dev/null +++ b/apps/common/features/src/pages/(pst)/faq/index.ts @@ -0,0 +1,3 @@ +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; +export { FaqPage } from "./ui/faq-page"; diff --git a/apps/common/features/src/pages/(pst)/faq/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/faq/ui/create-page.tsx new file mode 100644 index 000000000..8b564cad4 --- /dev/null +++ b/apps/common/features/src/pages/(pst)/faq/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 ( +
+
+

FAQ 게시글 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/common/features/src/pages/(pst)/faq/ui/faq-page.tsx b/apps/common/features/src/pages/(pst)/faq/ui/faq-page.tsx new file mode 100644 index 000000000..a5d7129d0 --- /dev/null +++ b/apps/common/features/src/pages/(pst)/faq/ui/faq-page.tsx @@ -0,0 +1,316 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + Checkbox, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Skeleton, +} from "@common/components"; +import { AttachmentApi } from "@/entities/attachment"; +import { PostApi, type PostParams } from "@/entities/post"; +import { useAlertStore, useMenuStore, useUserStore } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; +import dayjs from "dayjs"; +import React from "react"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; + +function FaqPage({ bbsId }: { bbsId: string }) { + const router = useRouter(); + const pathname = usePathname(); + const { confirm } = useConfirm(); + const { userInfo } = useUserStore((state) => state); + const { setMessage: alert } = useAlertStore((state) => state); + const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); + + const [postParams, setPostParams] = useState({ + keywordType: "title", + keyword: "", + }); + const [selectedPostNumbers, setSelectedPostNumbers] = useState([]); + + const { + data: postList, + isFetched, + refetch, + } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsInfo?.bbsTypeCd], + queryFn: () => + PostApi.getPostList(parseInt(bbsId), { + ...postParams, + bbsTypeCd: bbsInfo?.bbsTypeCd, + }), + enabled: !!bbsInfo?.bbsTypeCd, + }); + + const { mutate: deletePost } = useMutation({ + mutationFn: ({ pstNo, pstPswd }: { pstNo: number; pstPswd?: string }) => + PostApi.deletePost(parseInt(bbsId), pstNo, pstPswd), + onSuccess: () => { + alert("삭제되었습니다."); + refetch(); + }, + }); + + const handleDeletePost = async (pstNo: number) => { + if (await confirm("삭제하시겠습니까?")) { + deletePost({ pstNo, pstPswd: userInfo?.userUnqId }); + } + }; + + const onSelectedPostDelete = async () => { + if (await confirm("삭제하시겠습니까?")) { + await Promise.all( + selectedPostNumbers.map((pstNo) => + PostApi.deletePost(parseInt(bbsId), pstNo, userInfo?.userUnqId), + ), + ); + refetch(); + setSelectedPostNumbers([]); + alert("삭제되었습니다."); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleCheckboxChange = (pstNo: number, checked: boolean | string) => { + setSelectedPostNumbers( + checked === true + ? [...selectedPostNumbers, pstNo] + : selectedPostNumbers.filter((num) => num !== pstNo), + ); + }; + + const handleAllCheckboxChange = (checked: boolean | string) => { + setSelectedPostNumbers( + checked === true ? postList?.content.map((item) => item.pstNo) || [] : [], + ); + }; + + return ( +
+
+
+ + + + setPostParams({ ...postParams, keyword: e.target.value }) + } + className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" + /> + +
+
+
+
+
+

콘텐츠 목록

+
+ {pageMenuAuthority?.regAuthrtYn === "Y" && ( + + )} + {pageMenuAuthority?.delAuthrtYn === "Y" && ( + + )} +
+
+
+
+
+ e.stopPropagation()} + onCheckedChange={handleAllCheckboxChange} + /> + +
+ {isFetched ? ( + + {postList?.content.map((item) => ( +
+ e.stopPropagation()} + onCheckedChange={(checked) => + handleCheckboxChange(item.pstNo, checked) + } + className="mx-2 my-6" + /> + + +
+
+ Q + {item.pstTtl} +
+ + 등록일: {dayjs(item.regDt).format("YYYY-MM-DD")} + +
+
+ +
+ + A + +
+
+                            {item.pstCn}
+                          
+ +
+
+ + +
+
+
+
+
+ ))} +
+ ) : ( + + )} +
+
+
+ ); +} + +function Attachment({ atflCd }: { atflCd: string }) { + const [filename, setFilename] = useState(""); + + const { data: downloadResponse, isFetching } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, atflCd], + queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(atflCd), + enabled: !!atflCd, + }); + + const handleDownload = async () => { + if (!downloadResponse) return; + try { + const blob = new Blob([downloadResponse.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = filename || "download"; + document.body.appendChild(link); + link.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(link); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + if (!downloadResponse) return; + try { + let filename = ""; + const contentDisposition = + downloadResponse.headers["content-disposition"]; + if (contentDisposition) { + const filenameMatch = contentDisposition.match( + /filename\*=UTF-8''([^;]+)/, + ); + if (filenameMatch && filenameMatch[1]) { + filename = decodeURIComponent(filenameMatch[1]); + } + } + setFilename(filename); + } catch { + setFilename(""); + } + }, [downloadResponse]); + + return isFetching ? ( +
+ +
+ ) : ( + filename && ( +
+ 첨부파일 + +
+ ) + ); +} + +export { FaqPage }; diff --git a/apps/common/features/src/pages/(pst)/faq/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/faq/ui/modify-page.tsx new file mode 100644 index 000000000..7f56031d9 --- /dev/null +++ b/apps/common/features/src/pages/(pst)/faq/ui/modify-page.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { PostForm } from "@/widgets/post"; +import { + PostApi, + postUpdateFormSchema, + type PostUpdateFormData, +} from "@/entities/post"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useUserStore } from "@/shared/lib"; + +function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { + const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); + const { userInfo } = useUserStore((state) => state); + + const { data: postInfo } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), + }); + + const { mutateAsync: updatePost } = useMutation({ + mutationFn: (data: PostUpdateFormData) => + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), + onSuccess: () => { + alert("수정되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(postUpdateFormSchema), + values: { + ...postInfo, + pstTtl: postInfo?.pstTtl ?? "", + pstPswd: userInfo?.userUnqId ?? "", + }, + }); + + return ( +
+
+

FAQ 게시글 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx index c721286e4..8b09be8c3 100644 --- a/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/image/ui/modify-page.tsx @@ -20,12 +20,12 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { mutateAsync: updatePost } = useMutation({ mutationFn: (data: PostUpdateFormData) => - PostApi.updatePost(bbsId, pstNo, data), + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { alert("수정되었습니다.", router.back); }, diff --git a/apps/common/features/src/pages/(pst)/movie/index.ts b/apps/common/features/src/pages/(pst)/movie/index.ts new file mode 100644 index 000000000..c746cff6c --- /dev/null +++ b/apps/common/features/src/pages/(pst)/movie/index.ts @@ -0,0 +1,4 @@ +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; +export { MoviePage } from "./ui/movie-page"; +export { MoviePost } from "./ui/movie-post"; diff --git a/apps/common/features/src/pages/(pst)/movie/ui/create-page.tsx b/apps/common/features/src/pages/(pst)/movie/ui/create-page.tsx new file mode 100644 index 000000000..f7e2e837f --- /dev/null +++ b/apps/common/features/src/pages/(pst)/movie/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)/movie/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/movie/ui/modify-page.tsx new file mode 100644 index 000000000..6bbdd8d94 --- /dev/null +++ b/apps/common/features/src/pages/(pst)/movie/ui/modify-page.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { PostForm } from "@/widgets/post"; +import { + PostApi, + postUpdateFormSchema, + type PostUpdateFormData, +} from "@/entities/post"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useUserStore } from "@/shared/lib"; + +function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { + const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); + const { userInfo } = useUserStore((state) => state); + + const { data: postInfo } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), + }); + + const { mutateAsync: updatePost } = useMutation({ + mutationFn: (data: PostUpdateFormData) => + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), + onSuccess: () => { + alert("수정되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(postUpdateFormSchema), + values: { + ...postInfo, + pstTtl: postInfo?.pstTtl ?? "", + pstPswd: userInfo?.userUnqId ?? "", + }, + }); + + return ( +
+
+

동영상 게시글 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/common/features/src/pages/(pst)/movie/ui/movie-page.tsx b/apps/common/features/src/pages/(pst)/movie/ui/movie-page.tsx new file mode 100644 index 000000000..5ba24e94b --- /dev/null +++ b/apps/common/features/src/pages/(pst)/movie/ui/movie-page.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { PostPage as MoviePage } from "@/widgets/post"; diff --git a/apps/common/features/src/pages/(pst)/movie/ui/movie-post.tsx b/apps/common/features/src/pages/(pst)/movie/ui/movie-post.tsx new file mode 100644 index 000000000..8a5bf6996 --- /dev/null +++ b/apps/common/features/src/pages/(pst)/movie/ui/movie-post.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { Post as MoviePost } from "@/widgets/post"; diff --git a/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx index d6114ab44..e0d1834d4 100644 --- a/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/normal/ui/modify-page.tsx @@ -20,12 +20,12 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { mutateAsync: updatePost } = useMutation({ mutationFn: (data: PostUpdateFormData) => - PostApi.updatePost(bbsId, pstNo, data), + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { alert("수정되었습니다.", router.back); }, diff --git a/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx b/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx index bd014917f..9fe65782d 100644 --- a/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx +++ b/apps/common/features/src/pages/(pst)/notice/ui/modify-page.tsx @@ -20,12 +20,12 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { data: postInfo } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { mutateAsync: updatePost } = useMutation({ mutationFn: (data: PostUpdateFormData) => - PostApi.updatePost(bbsId, pstNo, data), + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { alert("수정되었습니다.", router.back); }, diff --git a/apps/common/features/src/shared/config/api.ts b/apps/common/features/src/shared/config/api.ts index 243afca6d..b6191979e 100644 --- a/apps/common/features/src/shared/config/api.ts +++ b/apps/common/features/src/shared/config/api.ts @@ -10,6 +10,7 @@ export const ENDPOINT = { MOVIE_THUMBNAIL: "/attach-service/api/v1/movie/thumbnail", ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", DOWNLOAD: "/attach-service/api/v1/atflcd/download", + STREAM_VIDEO: "/attach-service/api/v1/stream/video", }, USER_SERVICE: { LOGIN: "/user-service/login", diff --git a/apps/common/features/src/shared/lib/use-prompt.tsx b/apps/common/features/src/shared/lib/use-prompt.tsx index da9c4b386..f36858eb0 100644 --- a/apps/common/features/src/shared/lib/use-prompt.tsx +++ b/apps/common/features/src/shared/lib/use-prompt.tsx @@ -7,18 +7,18 @@ export const usePrompt = () => { const prompt = (message: string) => { setMessage(message); - return new Promise((resolve) => { + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { setResolve(() => resolve); }); }; const onSubmit = (value: string) => { - resolve?.(value); + resolve?.({ resolved: true, value }); setMessage(""); }; const onCancel = () => { - resolve?.(""); + resolve?.({ resolved: false }); setMessage(""); }; diff --git a/apps/common/features/src/shared/providers/prompt-provider.tsx b/apps/common/features/src/shared/providers/prompt-provider.tsx index 267b7aafc..10338c338 100644 --- a/apps/common/features/src/shared/providers/prompt-provider.tsx +++ b/apps/common/features/src/shared/providers/prompt-provider.tsx @@ -2,9 +2,15 @@ import { createContext, useState } from "react"; interface PromptContext { message?: string; - resolve?: (value: string) => void; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; setMessage: (message: string) => void; - setResolve: (resolve: (value: string) => void) => void; + setResolve: (resolve: () => void) => void; } export const PromptContext = createContext({ @@ -16,7 +22,10 @@ export const PromptContext = createContext({ export const PromptProvider = ({ children }: { children: React.ReactNode }) => { const [message, setMessage] = useState(); - const [resolve, setResolve] = useState<(value: string) => void>(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); return ( ({ key={row.id} onClick={() => onRowClick?.(row.original)} className={cn( - "rounded-md border border-border bg-background p-4 shadow-md", + "overflow-hidden rounded-md border border-border bg-background p-4 shadow-md", onRowClick && "cursor-pointer hover:bg-accent", )} > 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 index 242c7ee2f..34f2fc237 100644 --- 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 @@ -1,11 +1,19 @@ import dayjs from "dayjs"; import Image from "next/image"; import type { ColumnDef } from "@tanstack/react-table"; -import { Badge, Checkbox, Skeleton } from "@common/components"; +import { Badge, Checkbox, Skeleton, cn } from "@common/components"; +import { AttachmentApi } from "@/entities/attachment"; import type { Post } from "@/entities/post"; import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import { AttachmentApi } from "@/entities/attachment"; +import type { BbsType } from "@/shared/store/menu-store"; + +declare module "@tanstack/react-table" { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface TableMeta { + bbsTypeCd?: BbsType; + } +} export const createColumns = ( thumbRefs: React.MutableRefObject>, @@ -22,68 +30,120 @@ export const createColumns = ( 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" + cell: ({ row, table }) => { + const { atflCd, pstNo, pstTtl, inqNocs, rgtrNm, regDt } = row.original; + const bbsTypeCd = table.options.meta?.bbsTypeCd; + + if (bbsTypeCd === "BBSIMAGE") { + 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} + +
+ - {inqNocs >= 100 && HOT} - {dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && ( - NEW - )} +
+ + {rgtrNm} + + + {dayjs(regDt).format("YYYY-MM-DD")} + +
- - {pstTtl} - -
- ); + ); + } + + if (bbsTypeCd === "BBSMOVIE") { + return ( +
+ +
+
+
+ e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + {inqNocs >= 100 && HOT} + {dayjs().diff(dayjs(regDt), "hour") < 24 && ( + NEW + )} + + {pstTtl} + +
+
+ + {rgtrNm} + + + {dayjs(regDt).format("YYYY-MM-DD")} + +
+
+
+
+ ); + } }, }, - { - id: "atflCd", - cell: ({ row }) => { - const { atflCd, pstNo } = row.original; - return ; - }, - }, - { - id: "rgtrNm", - cell: ({ row }) => { - return ( - - {row.original.rgtrNm} - - ); - }, - }, - { - id: "regDt", - cell: ({ row }) => ( - - {dayjs(row.original.regDt).format("YYYY-MM-DD")} - - ), - }, ]; type ThumbImageProps = { atflCd: string; pstNo: number; thumbRefs: React.MutableRefObject>; + bbsTypeCd?: string; + classes?: { + image?: string; + }; }; -const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => { +const ThumbImage = ({ + atflCd, + pstNo, + thumbRefs, + bbsTypeCd, + classes, +}: ThumbImageProps) => { const prevImage = thumbRefs.current.get(pstNo); const { data: imageUrl, isLoading } = useQuery({ queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], - queryFn: () => AttachmentApi.getImageUrlByAtflCd(atflCd), + queryFn: () => { + if (bbsTypeCd === "BBSIMAGE") { + return AttachmentApi.getImageUrlByAtflCd(atflCd); + } else if (bbsTypeCd === "BBSMOVIE") { + return AttachmentApi.getMovieThumbnailUrlByAtflCd(atflCd); + } + }, select: (url) => { if (url) thumbRefs.current.set(pstNo, url); return url; @@ -93,17 +153,17 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => { if (isLoading) return ( -
- +
+
); if (imageUrl) return ( -
+
썸네일 이미지 @@ -111,7 +171,7 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => { ); return ( -
+
); diff --git a/apps/common/features/src/widgets/post/model/post-table-columns.tsx b/apps/common/features/src/widgets/post/model/post-table-columns.tsx index 862538896..973ce9315 100644 --- a/apps/common/features/src/widgets/post/model/post-table-columns.tsx +++ b/apps/common/features/src/widgets/post/model/post-table-columns.tsx @@ -34,7 +34,7 @@ export const columns: ColumnDef[] = [ return ( diff --git a/apps/common/features/src/widgets/post/ui/post-action.tsx b/apps/common/features/src/widgets/post/ui/post-action.tsx index c74c76482..0169e313c 100644 --- a/apps/common/features/src/widgets/post/ui/post-action.tsx +++ b/apps/common/features/src/widgets/post/ui/post-action.tsx @@ -35,7 +35,7 @@ function PostAction({ children, handleDelete }: PostActionProps) { }; return ( -
+ <> {children}
-
+ ); } diff --git a/apps/common/features/src/widgets/post/ui/post-comment.tsx b/apps/common/features/src/widgets/post/ui/post-comment.tsx index f87a90fca..1800b2581 100644 --- a/apps/common/features/src/widgets/post/ui/post-comment.tsx +++ b/apps/common/features/src/widgets/post/ui/post-comment.tsx @@ -89,7 +89,8 @@ function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) { if (userInfo?.userUnqId) { cmntPswd = userInfo?.userUnqId; } else { - cmntPswd = await prompt("비밀번호를 입력해주세요."); + const { resolved, value } = await prompt("비밀번호를 입력해주세요."); + if (resolved) cmntPswd = value ?? ""; } return cmntPswd; }; 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 262226a48..c4bf33576 100644 --- a/apps/common/features/src/widgets/post/ui/post-page.tsx +++ b/apps/common/features/src/widgets/post/ui/post-page.tsx @@ -24,11 +24,10 @@ import { } from "@common/components"; import { PostApi, type Post, type PostParams } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import type { BbsType } from "@/shared/store/menu-store"; 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"; +import { columns as tableColumns } from "../model/post-table-columns"; +import { createColumns as createGridViewColumns } from "../model/grid-view-table-columns"; function PostPage({ bbsId }: { bbsId: string }) { const router = useRouter(); @@ -36,7 +35,6 @@ function PostPage({ bbsId }: { bbsId: string }) { const { confirm } = useConfirm(); const { userInfo } = useUserStore((state) => state); const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); - const bbsType = bbsInfo?.bbsTypeCd as BbsType; const [postParams, setPostParams] = useState({ keywordType: "title", @@ -53,7 +51,7 @@ function PostPage({ bbsId }: { bbsId: string }) { const { data: upndFixPostList } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo], queryFn: () => - PostApi.getPostList(bbsId, { + PostApi.getPostList(parseInt(bbsId), { ...postParams, upndFixYn: bbsInfo?.upndFixUseYn, }), @@ -77,7 +75,7 @@ function PostPage({ bbsId }: { bbsId: string }) { } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId], queryFn: () => - PostApi.getPostList(bbsId, { + PostApi.getPostList(parseInt(bbsId), { ...postParams, page: pagination.pageIndex, size: pagination.pageSize, @@ -85,9 +83,14 @@ function PostPage({ bbsId }: { bbsId: string }) { enabled: false, }); + const isGridViewPage = + bbsInfo?.bbsTypeCd === "BBSIMAGE" || bbsInfo?.bbsTypeCd === "BBSMOVIE"; + const table = useReactTable({ - data: [...(upndFixPostList?.content ?? []), ...(postList?.content ?? [])], - columns, + data: isGridViewPage + ? (postList?.content ?? []) + : [...(upndFixPostList?.content ?? []), ...(postList?.content ?? [])], + columns: isGridViewPage ? createGridViewColumns(thumbRefs) : tableColumns, state: { pagination, rowSelection }, rowCount: postList?.totalElements ?? 0, manualPagination: true, @@ -95,19 +98,10 @@ function PostPage({ bbsId }: { bbsId: string }) { getCoreRowModel: getCoreRowModel(), onPaginationChange: setPagination, onRowSelectionChange: setRowSelection, + meta: { bbsTypeCd: bbsInfo?.bbsTypeCd }, }); - 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 DataTableComponent = isGridViewPage ? GridViewDataTable : DataTable; const onPostDelete = async () => { const selectedPosts = table @@ -118,7 +112,7 @@ function PostPage({ bbsId }: { bbsId: string }) { setRowSelection({}); await Promise.all( selectedPosts.map((pstNo) => - PostApi.deletePost(bbsId, pstNo.toString(), userInfo?.userUnqId), + PostApi.deletePost(parseInt(bbsId), pstNo, userInfo?.userUnqId), ), ); refetch(); @@ -211,19 +205,11 @@ 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 379bcdd38..44e549a70 100644 --- a/apps/common/features/src/widgets/post/ui/post.tsx +++ b/apps/common/features/src/widgets/post/ui/post.tsx @@ -29,14 +29,17 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { data: postData } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { data: downloadResponse, isFetching } = useQuery({ queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, postData?.atflCd], queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(postData?.atflCd as string), - enabled: !!postData?.atflCd && bbsInfo?.bbsTypeCd !== "BBSIMAGE", + enabled: + !!postData?.atflCd && + bbsInfo?.bbsTypeCd !== "BBSIMAGE" && + bbsInfo?.bbsTypeCd !== "BBSMOVIE", }); const { data: imageUrl, isLoading } = useQuery({ @@ -46,8 +49,25 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { enabled: !!postData?.atflCd, }); + const { data: videoAttachment } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS, postData?.atflCd], + queryFn: () => AttachmentApi.getAttachmentsInfo(postData?.atflCd as string), + select: (data) => data[0], + enabled: !!postData?.atflCd && bbsInfo?.bbsTypeCd === "BBSMOVIE", + }); + + const { data: videoUrl } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.STREAM_VIDEO, videoAttachment?.atflId], + queryFn: () => + AttachmentApi.getStreamVideoUrlByAtflId( + videoAttachment?.atflId as string, + ), + enabled: !!videoAttachment?.atflId, + }); + const { mutate: deletePost } = useMutation({ - mutationFn: (pstPswd: string) => PostApi.deletePost(bbsId, pstNo, pstPswd), + mutationFn: (pstPswd: string) => + PostApi.deletePost(parseInt(bbsId), parseInt(pstNo), pstPswd), onSuccess: () => { alert("삭제되었습니다.", router.back); }, @@ -58,7 +78,8 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { if (userInfo?.userUnqId) { pstPswd = userInfo?.userUnqId; } else { - pstPswd = await prompt("비밀번호를 입력해주세요."); + const { resolved, value } = await prompt("비밀번호를 입력해주세요."); + if (resolved) pstPswd = value ?? ""; } deletePost(pstPswd); }; @@ -142,6 +163,11 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { )} )} + {bbsInfo?.bbsTypeCd === "BBSMOVIE" && ( +
+
+ )}
             {postData?.pstCn ?? ""}
           
diff --git a/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx b/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx index 618108017..9496280e6 100644 --- a/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx +++ b/apps/common/features/src/widgets/post/ui/with-preview-file-uploader.tsx @@ -50,7 +50,7 @@ function WithPreviewFileUploader({ imageUrl = await AttachmentApi.getImageUrlByAtflId(atflId); } if (bbsInfo?.bbsTypeCd === "BBSMOVIE") { - imageUrl = await AttachmentApi.getMovieThumbnailUrl(atflId); + imageUrl = await AttachmentApi.getMovieThumbnailUrlByAtflId(atflId); } setThumbnailUrl(imageUrl); } diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/[pstNo]/page.tsx b/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/[pstNo]/page.tsx new file mode 100644 index 000000000..7c8d6ee86 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/[pstNo]/page.tsx @@ -0,0 +1,10 @@ +import { ModifyPage } from "@/pages/(post)/faq"; + +export default async function PstBbsFaqPostModifyPage({ + params, +}: { + params: Promise<{ bbsId: string; pstNo: string }>; +}) { + const { bbsId, pstNo } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/page.tsx b/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/page.tsx new file mode 100644 index 000000000..12fe535a5 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/page.tsx @@ -0,0 +1,10 @@ +import { FaqPage } from "@/pages/(post)/faq"; + +export default async function PstBbsFaqPage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/register/page.tsx b/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/register/page.tsx new file mode 100644 index 000000000..e768a48db --- /dev/null +++ b/apps/cs/app/(with-base-layout)/bbs/(post)/bbsfaq/[bbsId]/register/page.tsx @@ -0,0 +1,10 @@ +import { CreatePage } from "@/pages/(post)/faq"; + +export default async function PstBbsFaqRegisterPage({ + params, +}: { + params: Promise<{ bbsId: string }>; +}) { + const { bbsId } = await params; + return ; +} diff --git a/apps/cs/app/(with-base-layout)/recent/page.tsx b/apps/cs/app/(with-base-layout)/recent/page.tsx new file mode 100644 index 000000000..02ed5aba7 --- /dev/null +++ b/apps/cs/app/(with-base-layout)/recent/page.tsx @@ -0,0 +1,2 @@ +import { RecentPage } from "@/pages/recent"; +export default RecentPage; diff --git a/apps/cs/src/app/providers/app-provider.tsx b/apps/cs/src/app/providers/app-provider.tsx index 713bc172c..7732d34ba 100644 --- a/apps/cs/src/app/providers/app-provider.tsx +++ b/apps/cs/src/app/providers/app-provider.tsx @@ -6,8 +6,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ConfirmProvider } from "@frontend-opensource/use-react-hooks"; import { AuthWrapper } from "@/entities/auth"; import { MenuWrapper } from "@/entities/menu"; -import { StoreProvider } from "@/shared/providers"; +import { PromptProvider, StoreProvider } from "@/shared/providers"; import { AlertDialog } from "@/shared/ui/alert-dialog"; +import { PromptDialog } from "@/shared/ui/prompt-dialog"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { initializeNextClientConfig } from "@common/config"; @@ -19,17 +20,23 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { return ( - - - - - {children} - - - - - - + + + + + + {children} + + + + + + + + ); diff --git a/apps/cs/src/entities/board/api/board-service.ts b/apps/cs/src/entities/board/api/board-service.ts index 150ff677a..dc474b3ad 100644 --- a/apps/cs/src/entities/board/api/board-service.ts +++ b/apps/cs/src/entities/board/api/board-service.ts @@ -11,7 +11,7 @@ import type { } from "../model/board-interface"; export class BoardApiService implements BoardService { - public async getBoardList(params: BoardParams): Promise { + public async getBoardList(params?: BoardParams): Promise { const response = await client.get( `${ENDPOINT.BOARD_SERVICE.BBS}`, { params }, diff --git a/apps/cs/src/entities/post/api/post-service.ts b/apps/cs/src/entities/post/api/post-service.ts index a07fc4d2a..471da5d81 100644 --- a/apps/cs/src/entities/post/api/post-service.ts +++ b/apps/cs/src/entities/post/api/post-service.ts @@ -4,29 +4,43 @@ import { postFormSchema, postUpdateFormSchema, type Post, + type MovePost, + type DeletePost, type PostInfo, type PostList, + type RecentPostList, type PostParams, - type PostService, + type RecentPostParams, type PostFormData, type PostUpdateFormData, + type PostService, } from "../model/post-interface"; export class PostApiService implements PostService { public async getPostList( - bbsId: string, + bbsId: number, params: PostParams, ): Promise { const response = await client.get( - `${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}`, + `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}`, { params }, ); return response.data; } - public async getPostInfo(bbsId: string, pstNo: string): Promise { + public async getRecentPostList( + params?: RecentPostParams, + ): Promise { + const response = await client.get( + `${ENDPOINT.BOARD_SERVICE.POSTS}/recentPosts`, + { params }, + ); + return response.data; + } + + public async getPostInfo(bbsId: number, pstNo: number): Promise { const response = await client.get( - `${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`, + `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, ); return response.data; } @@ -34,34 +48,49 @@ export class PostApiService implements PostService { public async createPost(data: PostFormData): Promise { const validatedData = postFormSchema.parse(data); const response = await client.post( - `${ENDPOINT.BOARD_SERVICE.POSTS_USER}`, + `${ENDPOINT.BOARD_SERVICE.POSTS}`, validatedData, ); return response.data; } public async updatePost( - bbsId: string, - pstNo: string, + bbsId: number, + pstNo: number, data: PostUpdateFormData, ): Promise { const validatedData = postUpdateFormSchema.parse(data); const response = await client.put( - `${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`, + `${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, validatedData, ); return response.data; } public async deletePost( - bbsId: string, - pstNo: string, - pstPswd?: string, + bbsId: number, + pstNo: number, + delRsn?: string, ): Promise { - await client.delete( - `${ENDPOINT.BOARD_SERVICE.POSTS_USER}/${bbsId}/${pstNo}`, - { params: { pstPswd } }, - ); + await client.delete(`${ENDPOINT.BOARD_SERVICE.POSTS}/${bbsId}/${pstNo}`, { + params: { delRsn }, + }); + } + + public async softDeletePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/softDeleteSels`, data); + } + + public async hardDeletePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/hardDeleteSels`, data); + } + + public async movePost(data: MovePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/movePstByBbsId`, data); + } + + public async restorePost(data: DeletePost[]): Promise { + await client.post(`${ENDPOINT.BOARD_SERVICE.POSTS}/restoreSels`, data); } } diff --git a/apps/cs/src/entities/post/index.ts b/apps/cs/src/entities/post/index.ts index d2b94ba84..0e19b54e2 100644 --- a/apps/cs/src/entities/post/index.ts +++ b/apps/cs/src/entities/post/index.ts @@ -3,9 +3,13 @@ export { postFormSchema, postUpdateFormSchema, type Post, + type RecentPost, + type MovePost, + type DeletePost, type PostList, type PostParams, - type PostService, + type RecentPostParams, type PostFormData, type PostUpdateFormData, + type PostService, } from "./model/post-interface"; diff --git a/apps/cs/src/entities/post/model/post-interface.ts b/apps/cs/src/entities/post/model/post-interface.ts index 9cb97ef92..86e27afd5 100644 --- a/apps/cs/src/entities/post/model/post-interface.ts +++ b/apps/cs/src/entities/post/model/post-interface.ts @@ -8,13 +8,24 @@ export interface PostParams extends PageableParams { upndFixYn?: string; prdPstgYn?: string; siteId?: string; + bbsTypeCd?: string; +} + +export interface RecentPostParams extends PageableParams { + keywordType?: string; + keyword?: string; + bbsId?: number | null; + delYn?: string; + frDateYmd?: string; + toDateYmd?: string; } export interface Post { - bbsId: number; rowNo: number; + bbsId: number; pstNo: number; pstTtl: string; + pstCn: string; inqNocs: number; rfrncUrl: string; popupYn: string; @@ -31,6 +42,11 @@ export interface Post { cmntCnt: number; } +export interface RecentPost extends Post { + bbsNm: string; + bbsTypeCd: string; +} + export interface PostInfo extends Post { pstCn: string; rgtr: string; @@ -38,6 +54,18 @@ export interface PostInfo extends Post { mdfr: string; } +export interface MovePost { + fromBbsId: number; + toBbsId?: number; + pstNo: number; +} + +export interface DeletePost { + bbsId: number; + pstNo: number; + delRsn?: string; +} + const postEtcSchema = z.object({ bbsPstgTypeCd: z.string(), pstgPrdSeCd: z.string(), @@ -69,17 +97,22 @@ export const postUpdateFormSchema = postFormSchema.omit({ }); export type PostList = PageableList; +export type RecentPostList = PageableList; export type PostFormData = z.infer; export type PostUpdateFormData = z.infer; export interface PostService { - getPostList(bbsId: string, params: PostParams): Promise; - getPostInfo(bbsId: string, pstNo: string): Promise; + getPostList(bbsId: number, params: PostParams): Promise; + getRecentPostList(params: RecentPostParams): Promise; + getPostInfo(bbsId: number, pstNo: number): Promise; createPost(data: PostFormData): Promise; updatePost( - bbsId: string, - pstNo: string, + bbsId: number, + pstNo: number, data: PostUpdateFormData, ): Promise; - deletePost(bbsId: string, pstNo: string, pstPswd?: string): Promise; + deletePost(bbsId: number, pstNo: number, delRsn?: string): Promise; + softDeletePost(data: DeletePost[]): Promise; + hardDeletePost(data: DeletePost[]): Promise; + movePost(data: MovePost[]): Promise; } diff --git a/apps/cs/src/entities/posting/ui/posting-dialog.tsx b/apps/cs/src/entities/posting/ui/posting-dialog.tsx index b742eee28..ac31ad0dc 100644 --- a/apps/cs/src/entities/posting/ui/posting-dialog.tsx +++ b/apps/cs/src/entities/posting/ui/posting-dialog.tsx @@ -287,7 +287,7 @@ function PostingDialog({ postingType, postingTargets }: PostingDialogProps) { {parseInt(rememberPeriodCode[site.siteId] ?? "") === 0 && ( + onValueChange={(date) => handlePeriodDateChange(site.siteId)( date as DateRange | undefined, ) diff --git a/apps/cs/src/pages/(post)/faq/index.ts b/apps/cs/src/pages/(post)/faq/index.ts new file mode 100644 index 000000000..350df198c --- /dev/null +++ b/apps/cs/src/pages/(post)/faq/index.ts @@ -0,0 +1,3 @@ +export { CreatePage } from "./ui/create-page"; +export { ModifyPage } from "./ui/modify-page"; +export { FaqPage } from "./ui/faq-page"; diff --git a/apps/cs/src/pages/(post)/faq/ui/create-page.tsx b/apps/cs/src/pages/(post)/faq/ui/create-page.tsx new file mode 100644 index 000000000..8b564cad4 --- /dev/null +++ b/apps/cs/src/pages/(post)/faq/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 ( +
+
+

FAQ 게시글 등록

+
+ +
+ ); +} + +export { CreatePage }; diff --git a/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx new file mode 100644 index 000000000..24ffada45 --- /dev/null +++ b/apps/cs/src/pages/(post)/faq/ui/faq-page.tsx @@ -0,0 +1,317 @@ +"use client"; + +import dayjs from "dayjs"; +import React, { useEffect, useState } from "react"; +import { usePathname, useRouter } from "next/navigation"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + Button, + Checkbox, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Skeleton, +} from "@common/components"; +import { AttachmentApi } from "@/entities/attachment"; +import { PostApi, type PostParams } from "@/entities/post"; +import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; +import { ENDPOINT } from "@/shared/config"; + +function FaqPage({ bbsId }: { bbsId: string }) { + const router = useRouter(); + const pathname = usePathname(); + const { prompt } = usePrompt(); + const { setMessage: alert } = useAlertStore((state) => state); + const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); + + const [postParams, setPostParams] = useState({ + keywordType: "title", + keyword: "", + }); + const [selectedPostNumbers, setSelectedPostNumbers] = useState([]); + + const { + data: postList, + isFetched, + refetch, + } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsInfo?.bbsTypeCd], + queryFn: () => + PostApi.getPostList(parseInt(bbsId), { + ...postParams, + bbsTypeCd: bbsInfo?.bbsTypeCd, + }), + enabled: !!bbsInfo?.bbsTypeCd, + }); + + const { mutate: deletePost } = useMutation({ + mutationFn: ({ pstNo, delRsn }: { pstNo: number; delRsn?: string }) => + PostApi.deletePost(parseInt(bbsId), pstNo, delRsn), + onSuccess: () => { + alert("삭제되었습니다."); + refetch(); + }, + }); + + const getPromptValue = async (message: string) => { + const { resolved, value } = await prompt(message); + if (!resolved) return ""; + return value; + }; + + const handleDeletePost = async (pstNo: number) => { + const delRsn = await getPromptValue("삭제 사유를 입력해주세요."); + deletePost({ pstNo, delRsn }); + }; + + const onSelectedPostDelete = async () => { + const delRsn = await getPromptValue("삭제 사유를 입력해주세요."); + await Promise.all( + selectedPostNumbers.map((pstNo) => + PostApi.deletePost(parseInt(bbsId), pstNo, delRsn), + ), + ); + refetch(); + setSelectedPostNumbers([]); + alert("삭제되었습니다."); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + refetch(); + }; + + const handleCheckboxChange = (pstNo: number, checked: boolean | string) => { + setSelectedPostNumbers( + checked === true + ? [...selectedPostNumbers, pstNo] + : selectedPostNumbers.filter((num) => num !== pstNo), + ); + }; + + const handleAllCheckboxChange = (checked: boolean | string) => { + setSelectedPostNumbers( + checked === true ? postList?.content.map((item) => item.pstNo) || [] : [], + ); + }; + + return ( +
+
+
+ + + + setPostParams({ ...postParams, keyword: e.target.value }) + } + className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" + /> + +
+
+
+
+
+

콘텐츠 목록

+
+ {pageMenuAuthority?.regAuthrtYn === "Y" && ( + + )} + {pageMenuAuthority?.delAuthrtYn === "Y" && ( + + )} +
+
+
+
+
+ e.stopPropagation()} + onCheckedChange={handleAllCheckboxChange} + /> + +
+ {isFetched ? ( + + {postList?.content.map((item) => ( +
+ e.stopPropagation()} + onCheckedChange={(checked) => + handleCheckboxChange(item.pstNo, checked) + } + className="mx-2 my-6" + /> + + +
+
+ Q + {item.pstTtl} +
+ + 등록일: {dayjs(item.regDt).format("YYYY-MM-DD")} + +
+
+ +
+ + A + +
+
+                            {item.pstCn}
+                          
+ +
+
+ + +
+
+
+
+
+ ))} +
+ ) : ( + + )} +
+
+
+ ); +} + +function Attachment({ atflCd }: { atflCd: string }) { + const [filename, setFilename] = useState(""); + + const { data: downloadResponse, isFetching } = useQuery({ + queryKey: [ENDPOINT.ATTACH_SERVICE.DOWNLOAD, atflCd], + queryFn: () => AttachmentApi.downloadAttachmentByAtflCd(atflCd), + enabled: !!atflCd, + }); + + const handleDownload = async () => { + if (!downloadResponse) return; + try { + const blob = new Blob([downloadResponse.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = filename || "download"; + document.body.appendChild(link); + link.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(link); + } catch (error) { + console.error(error); + } + }; + + useEffect(() => { + if (!downloadResponse) return; + try { + let filename = ""; + const contentDisposition = + downloadResponse.headers["content-disposition"]; + if (contentDisposition) { + const filenameMatch = contentDisposition.match( + /filename\*=UTF-8''([^;]+)/, + ); + if (filenameMatch && filenameMatch[1]) { + filename = decodeURIComponent(filenameMatch[1]); + } + } + setFilename(filename); + } catch { + setFilename(""); + } + }, [downloadResponse]); + + return isFetching ? ( +
+ +
+ ) : ( + filename && ( +
+ 첨부파일 + +
+ ) + ); +} + +export { FaqPage }; diff --git a/apps/cs/src/pages/(post)/faq/ui/modify-page.tsx b/apps/cs/src/pages/(post)/faq/ui/modify-page.tsx new file mode 100644 index 000000000..1a0ed8006 --- /dev/null +++ b/apps/cs/src/pages/(post)/faq/ui/modify-page.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { PostForm } from "@/widgets/post"; +import { + PostApi, + postUpdateFormSchema, + type PostUpdateFormData, +} from "@/entities/post"; +import { ENDPOINT } from "@/shared/config"; +import { useAlertStore, useUserStore } from "@/shared/lib"; + +function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { + const router = useRouter(); + const { setMessage: alert } = useAlertStore((state) => state); + const { userInfo } = useUserStore((state) => state); + + const { data: postInfo } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), + }); + + const { mutateAsync: updatePost } = useMutation({ + mutationFn: (data: PostUpdateFormData) => + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), + onSuccess: () => { + alert("수정되었습니다.", router.back); + }, + }); + + const form = useForm({ + resolver: zodResolver(postUpdateFormSchema), + values: { + ...postInfo, + pstTtl: postInfo?.pstTtl ?? "", + pstPswd: userInfo?.userUnqId ?? "", + }, + }); + + return ( +
+
+

FAQ 게시글 수정

+
+ +
+ ); +} + +export { ModifyPage }; diff --git a/apps/cs/src/pages/(post)/image/ui/modify-page.tsx b/apps/cs/src/pages/(post)/image/ui/modify-page.tsx index c721286e4..c2426c134 100644 --- a/apps/cs/src/pages/(post)/image/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/image/ui/modify-page.tsx @@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { userInfo } = useUserStore((state) => state); const { data: postInfo } = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { mutateAsync: updatePost } = useMutation({ mutationFn: (data: PostUpdateFormData) => - PostApi.updatePost(bbsId, pstNo, data), + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { alert("수정되었습니다.", router.back); }, diff --git a/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx b/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx index 2f67e313c..bee473938 100644 --- a/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/movie/ui/modify-page.tsx @@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { userInfo } = useUserStore((state) => state); const { data: postInfo } = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { mutateAsync: updatePost } = useMutation({ mutationFn: (data: PostUpdateFormData) => - PostApi.updatePost(bbsId, pstNo, data), + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { alert("수정되었습니다.", router.back); }, diff --git a/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx b/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx index d6114ab44..2e23f62a0 100644 --- a/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/normal/ui/modify-page.tsx @@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { userInfo } = useUserStore((state) => state); const { data: postInfo } = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { mutateAsync: updatePost } = useMutation({ mutationFn: (data: PostUpdateFormData) => - PostApi.updatePost(bbsId, pstNo, data), + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { alert("수정되었습니다.", router.back); }, diff --git a/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx b/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx index bd014917f..fa12dbcce 100644 --- a/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx +++ b/apps/cs/src/pages/(post)/notice/ui/modify-page.tsx @@ -19,13 +19,13 @@ function ModifyPage({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const { userInfo } = useUserStore((state) => state); const { data: postInfo } = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { mutateAsync: updatePost } = useMutation({ mutationFn: (data: PostUpdateFormData) => - PostApi.updatePost(bbsId, pstNo, data), + PostApi.updatePost(parseInt(bbsId), parseInt(pstNo), data), onSuccess: () => { alert("수정되었습니다.", router.back); }, diff --git a/apps/cs/src/pages/bbs/model/table-columns.tsx b/apps/cs/src/pages/bbs/model/table-columns.tsx index 5a6cb62f8..09933fb7c 100644 --- a/apps/cs/src/pages/bbs/model/table-columns.tsx +++ b/apps/cs/src/pages/bbs/model/table-columns.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import dayjs from "dayjs"; import { usePathname, useRouter } from "next/navigation"; import type { ColumnDef, Row } from "@tanstack/react-table"; @@ -61,6 +62,7 @@ const ManageButton = ({ row }: { row: Row }) => { onClick={(e) => { e.stopPropagation(); router.push(`${pathname}/${bbsTypeCd.toLowerCase()}/${bbsId}`); + // router.push(`/pst/${bbsTypeCd.toLowerCase()}/${bbsId}`); // 공통 개발용 setBbsInfo(JSON.stringify(row.original)); setStoreBbsInfo(row.original); }} diff --git a/apps/cs/src/pages/recent/index.ts b/apps/cs/src/pages/recent/index.ts new file mode 100644 index 000000000..72b8579e1 --- /dev/null +++ b/apps/cs/src/pages/recent/index.ts @@ -0,0 +1 @@ +export { RecentPage } from "./ui/recent-page"; diff --git a/apps/cs/src/pages/recent/model/table-columns.tsx b/apps/cs/src/pages/recent/model/table-columns.tsx new file mode 100644 index 000000000..272a7d149 --- /dev/null +++ b/apps/cs/src/pages/recent/model/table-columns.tsx @@ -0,0 +1,117 @@ +import dayjs from "dayjs"; +import type { ColumnDef } from "@tanstack/react-table"; +import { Button, Checkbox } from "@common/components"; +import { AttachmentApi } from "@/entities/attachment"; +import type { RecentPost } from "@/entities/post"; + +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" + /> + ), + meta: { + width: 40, + }, + }, + { + accessorKey: "rowNo", + header: "NO", + meta: { + width: 80, + }, + }, + { + accessorKey: "bbsNm", + header: "게시판", + meta: { + width: 200, + }, + }, + { + accessorKey: "pstTtl", + header: "제목", + cell: ({ row }) =>
{row.original.pstTtl}
, + }, + { + accessorKey: "atflCd", + header: "첨부파일", + cell: ({ row }) => { + const atflCd = row.original.atflCd; + if (!atflCd) return null; + return ; + }, + meta: { + width: 100, + }, + }, + { + accessorKey: "rgtrNm", + header: "작성자", + meta: { + width: 120, + }, + }, + { + accessorKey: "regDt", + header: "등록일", + cell: ({ row }) => dayjs(row.original.regDt).format("YYYY-MM-DD"), + meta: { + width: 120, + }, + }, +]; + +const AttachmentButton = ({ atflCd }: { atflCd: string }) => { + const handleDownload = async (e: React.MouseEvent) => { + e.stopPropagation(); + try { + let filename = ""; + const response = await AttachmentApi.downloadAttachmentByAtflCd(atflCd); + const contentDisposition = response.headers["content-disposition"]; + if (contentDisposition) { + const filenameMatch = contentDisposition.match( + /filename\*=UTF-8''([^;]+)/, + ); + if (filenameMatch) { + filename = decodeURIComponent(filenameMatch[1]); + } + } + + const blob = new Blob([response.data]); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + + window.URL.revokeObjectURL(url); + document.body.removeChild(link); + } catch (error) { + console.error(error); + } + }; + + return ( + + ); +}; diff --git a/apps/cs/src/pages/recent/ui/move-post-dialog.tsx b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx new file mode 100644 index 000000000..64afef317 --- /dev/null +++ b/apps/cs/src/pages/recent/ui/move-post-dialog.tsx @@ -0,0 +1,68 @@ +import React, { useRef } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogTrigger, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@common/components"; +import type { Board } from "@/entities/board"; +import type { MovePost } from "@/entities/post"; + +type MovePostDialogProps = { + selectedPosts: MovePost[]; + boardList: Board[]; + onMovePost: (data: MovePost[]) => void; +}; + +function MovePostDialog({ + selectedPosts, + boardList, + onMovePost, +}: MovePostDialogProps) { + const selectedPostsRef = useRef(selectedPosts); + + const onSelectValueChange = (toBbsId: string) => { + selectedPostsRef.current = selectedPosts.map((post) => ({ + ...post, + toBbsId: parseInt(toBbsId), + })); + }; + + return ( + + + + + +

+ 이동할 게시판을 선택해 주세요. +

+ + + +
+
+ ); +} + +export { MovePostDialog }; diff --git a/apps/cs/src/pages/recent/ui/recent-page.tsx b/apps/cs/src/pages/recent/ui/recent-page.tsx new file mode 100644 index 000000000..407b90d49 --- /dev/null +++ b/apps/cs/src/pages/recent/ui/recent-page.tsx @@ -0,0 +1,342 @@ +"use client"; + +import dayjs from "dayjs"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query"; +import { + getCoreRowModel, + useReactTable, + type PaginationState, + type RowSelectionState, +} from "@tanstack/react-table"; +import { + Button, + DateRangePicker, + Input, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + DataTable, +} from "@common/components"; +import { + PostApi, + type DeletePost, + type MovePost, + type RecentPost, + type RecentPostParams, +} from "@/entities/post"; +import { useAlertStore, useMenuStore, usePrompt } from "@/shared/lib"; +import { BoardApi } from "@/entities/board"; +import { ENDPOINT } from "@/shared/config"; +import { columns } from "../model/table-columns"; +import { MovePostDialog } from "./move-post-dialog"; + +type DateRange = { + from: Date | undefined; + to?: Date | undefined; +}; + +function RecentPage() { + const router = useRouter(); + const { prompt } = usePrompt(); + const { confirm } = useConfirm(); + const { setMessage: alert } = useAlertStore((state) => state); + const { pageMenuAuthority } = useMenuStore((state) => state); + const [dateRange, setDateRange] = useState(); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const [rowSelection, setRowSelection] = useState({}); + const [recentPostParams, setRecentPostParams] = useState({ + keywordType: "", + keyword: "", + bbsId: null, + delYn: "N", + frDateYmd: "", + toDateYmd: "", + }); + const [isDeletePostData, setIsDeletePostData] = useState(false); + + const { data: recentPostList, refetch } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, pagination], + queryFn: () => + PostApi.getRecentPostList({ + ...recentPostParams, + page: pagination.pageIndex, + size: pagination.pageSize, + }), + placeholderData: keepPreviousData, + }); + + const { data: boardList } = useQuery({ + queryKey: [ENDPOINT.BOARD_SERVICE.BBS], + queryFn: () => BoardApi.getBoardList(), + }); + + const { mutate: movePost } = useMutation({ + mutationFn: (data: MovePost[]) => PostApi.movePost(data), + onSuccess: () => { + alert("이동이 완료되었습니다.", refetch); + }, + }); + + const table = useReactTable({ + data: recentPostList?.content ?? [], + columns, + state: { + pagination, + rowSelection, + }, + rowCount: recentPostList?.totalElements ?? 0, + manualPagination: true, + enableMultiRowSelection: true, + getCoreRowModel: getCoreRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + }); + + const isSelectedRow = table.getFilteredSelectedRowModel().rows.length > 0; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setIsDeletePostData(recentPostParams.delYn === "Y"); + refetch(); + }; + + const handleRowClick = (row: RecentPost) => { + router.push( + `/bbs/${row.bbsTypeCd.toLowerCase()}/${row.bbsId}/${row.pstNo}`, + ); + }; + + const handleRecentPostParamsChange = + (key: keyof RecentPostParams) => (value: unknown) => { + setRecentPostParams({ ...recentPostParams, [key]: value }); + }; + + const handlePresetDateRange = (value: string) => { + let presetDateRange: DateRange | undefined; + switch (value) { + case "1W": + presetDateRange = { + from: dayjs().subtract(7, "day").toDate(), + to: dayjs().toDate(), + }; + break; + case "1M": + presetDateRange = { + from: dayjs().subtract(30, "day").toDate(), + to: dayjs().toDate(), + }; + break; + case "3M": + presetDateRange = { + from: dayjs().subtract(90, "day").toDate(), + to: dayjs().toDate(), + }; + break; + default: + presetDateRange = undefined; + } + setDateRange(presetDateRange); + }; + + const onSoftDeletePost = async () => { + const { resolved, value: delRsn } = + await prompt("삭제 사유를 입력해주세요."); + if (!resolved) return; + + const selectedPosts: DeletePost[] = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + bbsId: row.original.bbsId, + pstNo: row.original.pstNo, + delRsn, + })); + PostApi.softDeletePost(selectedPosts); + alert("삭제가 완료되었습니다.", refetch); + setRowSelection({}); + }; + + const onHardDeletePost = async () => { + if ( + await confirm( + "게시물을 완전 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.", + ) + ) { + const selectedPosts: DeletePost[] = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + bbsId: row.original.bbsId, + pstNo: row.original.pstNo, + })); + PostApi.hardDeletePost(selectedPosts); + alert("삭제가 완료되었습니다.", refetch); + setRowSelection({}); + } + }; + + const onRestorePost = async () => { + if (await confirm("게시물을 복원하시겠습니까?")) { + const selectedPosts: DeletePost[] = table + .getFilteredSelectedRowModel() + .rows.map((row) => ({ + bbsId: row.original.bbsId, + pstNo: row.original.pstNo, + })); + PostApi.restorePost(selectedPosts); + alert("복원이 완료되었습니다.", refetch); + setRowSelection({}); + } + }; + + return ( +
+
+
+ + +
+ +
+
+ + + + + + + + + + + handleRecentPostParamsChange("keyword")(e.target.value) + } + className="h-[3.2rem] w-[24rem] rounded-sm !text-[1.3rem]" + /> + +
+
+
+
+
+

콘텐츠 목록

+
+ {pageMenuAuthority?.mngAuthrtYn === "Y" && + (isDeletePostData ? ( + <> + + + + ) : ( + <> + ({ + fromBbsId: row.original.bbsId, + pstNo: row.original.pstNo, + }))} + boardList={boardList?.content ?? []} + onMovePost={movePost} + /> + + + ))} + +
+
+
+
+ +
+
+
+ ); +} + +export { RecentPage }; diff --git a/apps/cs/src/shared/config/api.ts b/apps/cs/src/shared/config/api.ts index 094bb3776..7aef8df12 100644 --- a/apps/cs/src/shared/config/api.ts +++ b/apps/cs/src/shared/config/api.ts @@ -42,7 +42,7 @@ export const ENDPOINT = { BOARD_SERVICE: { BBS: "/board-service/api/v1/bbs", BBS_TEMPLATES: "/board-service/api/v1/bbsTmplt", - POSTS_USER: "/board-service/api/v1/posts/user", + POSTS: "/board-service/api/v1/posts", POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts", }, }; diff --git a/apps/cs/src/shared/lib/use-prompt.tsx b/apps/cs/src/shared/lib/use-prompt.tsx index da9c4b386..f36858eb0 100644 --- a/apps/cs/src/shared/lib/use-prompt.tsx +++ b/apps/cs/src/shared/lib/use-prompt.tsx @@ -7,18 +7,18 @@ export const usePrompt = () => { const prompt = (message: string) => { setMessage(message); - return new Promise((resolve) => { + return new Promise<{ resolved: boolean; value?: string }>((resolve) => { setResolve(() => resolve); }); }; const onSubmit = (value: string) => { - resolve?.(value); + resolve?.({ resolved: true, value }); setMessage(""); }; const onCancel = () => { - resolve?.(""); + resolve?.({ resolved: false }); setMessage(""); }; diff --git a/apps/cs/src/shared/providers/index.ts b/apps/cs/src/shared/providers/index.ts index f2e092170..fa8320fe7 100644 --- a/apps/cs/src/shared/providers/index.ts +++ b/apps/cs/src/shared/providers/index.ts @@ -1 +1,2 @@ export { StoreProvider } from "./store-provider"; +export { PromptProvider } from "./prompt-provider"; diff --git a/apps/cs/src/shared/providers/prompt-provider.tsx b/apps/cs/src/shared/providers/prompt-provider.tsx index 267b7aafc..10338c338 100644 --- a/apps/cs/src/shared/providers/prompt-provider.tsx +++ b/apps/cs/src/shared/providers/prompt-provider.tsx @@ -2,9 +2,15 @@ import { createContext, useState } from "react"; interface PromptContext { message?: string; - resolve?: (value: string) => void; + resolve?: ({ + resolved, + value, + }: { + resolved: boolean; + value?: string; + }) => void; setMessage: (message: string) => void; - setResolve: (resolve: (value: string) => void) => void; + setResolve: (resolve: () => void) => void; } export const PromptContext = createContext({ @@ -16,7 +22,10 @@ export const PromptContext = createContext({ export const PromptProvider = ({ children }: { children: React.ReactNode }) => { const [message, setMessage] = useState(); - const [resolve, setResolve] = useState<(value: string) => void>(); + const [resolve, setResolve] = + useState< + ({ resolved, value }: { resolved: boolean; value?: string }) => void + >(); return ( ({ key={row.id} onClick={() => onRowClick?.(row.original)} className={cn( - "rounded-md border border-border bg-background p-4 shadow-md", + "overflow-hidden rounded-md border border-border bg-background p-4 shadow-md", onRowClick && "cursor-pointer hover:bg-accent", )} > diff --git a/apps/cs/src/shared/ui/prompt-dialog.tsx b/apps/cs/src/shared/ui/prompt-dialog.tsx new file mode 100644 index 000000000..54c2f3fcc --- /dev/null +++ b/apps/cs/src/shared/ui/prompt-dialog.tsx @@ -0,0 +1,45 @@ +import { useState } from "react"; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogClose, + Input, +} from "@common/components"; +import { usePrompt } from "@/shared/lib/use-prompt"; + +function PromptDialog() { + const { message, isOpen, onSubmit, onCancel } = usePrompt(); + const [value, setValue] = useState(""); + + return ( + + +
+

+ {message} +

+ setValue(e.target.value)} + /> +
+ + + + + + + + +
+
+ ); +} + +export { PromptDialog }; diff --git a/apps/cs/src/widgets/post/model/grid-view-table-columns.tsx b/apps/cs/src/widgets/post/model/grid-view-table-columns.tsx index 96f7866db..dbe3b88eb 100644 --- a/apps/cs/src/widgets/post/model/grid-view-table-columns.tsx +++ b/apps/cs/src/widgets/post/model/grid-view-table-columns.tsx @@ -1,12 +1,19 @@ import dayjs from "dayjs"; import Image from "next/image"; import type { ColumnDef } from "@tanstack/react-table"; -import { Badge, Checkbox, Skeleton } from "@common/components"; +import { Badge, Checkbox, Skeleton, cn } from "@common/components"; import { AttachmentApi } from "@/entities/attachment"; import type { Post } from "@/entities/post"; import { useQuery } from "@tanstack/react-query"; import { ENDPOINT } from "@/shared/config"; -import { useMenuStore } from "@/shared/lib"; +import type { BbsType } from "@/shared/store/menu-store"; + +declare module "@tanstack/react-table" { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface TableMeta { + bbsTypeCd?: BbsType; + } +} export const createColumns = ( thumbRefs: React.MutableRefObject>, @@ -23,73 +30,121 @@ export const createColumns = ( 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" + cell: ({ row, table }) => { + const { atflCd, pstNo, pstTtl, pstCn, inqNocs, rgtrNm, regDt } = + row.original; + const bbsTypeCd = table.options.meta?.bbsTypeCd; + + if (bbsTypeCd === "BBSIMAGE") { + 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} + +
+ - {inqNocs >= 100 && HOT} - {dayjs().diff(dayjs(row.original.regDt), "hour") < 24 && ( - NEW - )} +
+ + {rgtrNm} + + + {dayjs(regDt).format("YYYY-MM-DD")} + +
- - {pstTtl} - -
- ); + ); + } + + if (bbsTypeCd === "BBSMOVIE") { + return ( +
+ +
+
+
+ e.stopPropagation()} + onCheckedChange={(checked) => row.toggleSelected(!!checked)} + className="h-4 w-4 rounded border-gray-300" + /> + {inqNocs >= 100 && HOT} + {dayjs().diff(dayjs(regDt), "hour") < 24 && ( + NEW + )} + + {pstTtl} + +
+

+ {pstCn} +

+
+ + {rgtrNm} + + + {dayjs(regDt).format("YYYY-MM-DD")} + +
+
+
+
+ ); + } }, }, - { - id: "atflCd", - cell: ({ row }) => { - const { atflCd, pstNo } = row.original; - return ; - }, - }, - { - id: "rgtrNm", - cell: ({ row }) => { - return ( - - {row.original.rgtrNm} - - ); - }, - }, - { - id: "regDt", - cell: ({ row }) => ( - - {dayjs(row.original.regDt).format("YYYY-MM-DD")} - - ), - }, ]; type ThumbImageProps = { atflCd: string; pstNo: number; thumbRefs: React.MutableRefObject>; + bbsTypeCd?: string; + classes?: { + image?: string; + }; }; -const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => { +const ThumbImage = ({ + atflCd, + pstNo, + thumbRefs, + bbsTypeCd, + classes, +}: ThumbImageProps) => { const prevImage = thumbRefs.current.get(pstNo); - const { bbsInfo } = useMenuStore((state) => state); - - const { data: imageUrl, isLoading } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: [ENDPOINT.ATTACH_SERVICE.IMAGES, atflCd], queryFn: () => { - if (bbsInfo?.bbsTypeCd === "BBSIMAGE") { + if (bbsTypeCd === "BBSIMAGE") { return AttachmentApi.getImageUrlByAtflCd(atflCd); - } else if (bbsInfo?.bbsTypeCd === "BBSMOVIE") { + } else if (bbsTypeCd === "BBSMOVIE") { return AttachmentApi.getMovieThumbnailUrlByAtflCd(atflCd); } }, @@ -100,19 +155,21 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => { enabled: !prevImage && !!atflCd, }); + const imageUrl = prevImage ?? data; + if (isLoading) return ( -
- +
+
); if (imageUrl) return ( -
+
썸네일 이미지 @@ -120,7 +177,7 @@ const ThumbImage = ({ atflCd, pstNo, thumbRefs }: ThumbImageProps) => { ); return ( -
+
); diff --git a/apps/cs/src/widgets/post/model/post-table-columns.tsx b/apps/cs/src/widgets/post/model/post-table-columns.tsx index 862538896..973ce9315 100644 --- a/apps/cs/src/widgets/post/model/post-table-columns.tsx +++ b/apps/cs/src/widgets/post/model/post-table-columns.tsx @@ -34,7 +34,7 @@ export const columns: ColumnDef[] = [ return ( diff --git a/apps/cs/src/widgets/post/ui/post-action.tsx b/apps/cs/src/widgets/post/ui/post-action.tsx index c74c76482..acaa60bf5 100644 --- a/apps/cs/src/widgets/post/ui/post-action.tsx +++ b/apps/cs/src/widgets/post/ui/post-action.tsx @@ -1,24 +1,24 @@ import { useRouter, usePathname } from "next/navigation"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button } from "@common/components"; -import { useMenuStore, useUserStore } from "@/shared/lib"; +import { useMenuStore, usePrompt, useUserStore } from "@/shared/lib"; type PostActionProps = { bbsId: string; pstNo: string; children: React.ReactNode; - handleDelete: () => Promise; + handleDelete: (delRsn?: string) => Promise; }; function PostAction({ children, handleDelete }: PostActionProps) { const router = useRouter(); const pathname = usePathname(); - const { confirm } = useConfirm(); + const { prompt } = usePrompt(); const { bbsInfo } = useMenuStore((state) => state); const { userInfo } = useUserStore((state) => state); const onDelete = async () => { - if (await confirm("삭제하시겠습니까?")) handleDelete(); + const { resolved, value } = await prompt("삭제 사유를 입력해주세요."); + if (resolved) handleDelete(value); }; const checkAnsUseAuthrt = () => { @@ -35,7 +35,7 @@ function PostAction({ children, handleDelete }: PostActionProps) { }; return ( -
+ <> {children}
-
+ ); } diff --git a/apps/cs/src/widgets/post/ui/post-comment.tsx b/apps/cs/src/widgets/post/ui/post-comment.tsx index f87a90fca..1800b2581 100644 --- a/apps/cs/src/widgets/post/ui/post-comment.tsx +++ b/apps/cs/src/widgets/post/ui/post-comment.tsx @@ -89,7 +89,8 @@ function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) { if (userInfo?.userUnqId) { cmntPswd = userInfo?.userUnqId; } else { - cmntPswd = await prompt("비밀번호를 입력해주세요."); + const { resolved, value } = await prompt("비밀번호를 입력해주세요."); + if (resolved) cmntPswd = value ?? ""; } return cmntPswd; }; diff --git a/apps/cs/src/widgets/post/ui/post-page.tsx b/apps/cs/src/widgets/post/ui/post-page.tsx index 6389bc799..6bbc4de03 100644 --- a/apps/cs/src/widgets/post/ui/post-page.tsx +++ b/apps/cs/src/widgets/post/ui/post-page.tsx @@ -1,8 +1,7 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; -import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { useQuery } from "@tanstack/react-query"; import { getCoreRowModel, @@ -24,7 +23,7 @@ import { } from "@common/components"; import { PostApi, type Post, type PostParams } from "@/entities/post"; import { ENDPOINT } from "@/shared/config"; -import { useMenuStore, useUserStore } from "@/shared/lib"; +import { useMenuStore, usePrompt } from "@/shared/lib"; import { GridViewDataTable } from "@/shared/ui/grid-view-data-table"; import { columns as tableColumns } from "../model/post-table-columns"; import { createColumns as createGridViewColumns } from "../model/grid-view-table-columns"; @@ -32,8 +31,7 @@ import { createColumns as createGridViewColumns } from "../model/grid-view-table function PostPage({ bbsId }: { bbsId: string }) { const router = useRouter(); const pathname = usePathname(); - const { confirm } = useConfirm(); - const { userInfo } = useUserStore((state) => state); + const { prompt } = usePrompt(); const { bbsInfo, pageMenuAuthority } = useMenuStore((state) => state); const thumbRefs = useRef>(new Map()); @@ -48,9 +46,9 @@ function PostPage({ bbsId }: { bbsId: string }) { const [rowSelection, setRowSelection] = useState({}); const { data: upndFixPostList } = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, bbsInfo], + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, bbsInfo], queryFn: () => - PostApi.getPostList(bbsId, { + PostApi.getPostList(parseInt(bbsId), { ...postParams, upndFixYn: bbsInfo?.upndFixUseYn, }), @@ -69,14 +67,15 @@ function PostPage({ bbsId }: { bbsId: string }) { isFetched, refetch, } = useQuery({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId], + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsInfo?.bbsTypeCd], queryFn: () => - PostApi.getPostList(bbsId, { + PostApi.getPostList(parseInt(bbsId), { ...postParams, + bbsTypeCd: bbsInfo?.bbsTypeCd, page: pagination.pageIndex, size: pagination.pageSize, }), - enabled: false, + enabled: !!bbsInfo?.bbsTypeCd, }); const isGridViewPage = @@ -94,6 +93,7 @@ function PostPage({ bbsId }: { bbsId: string }) { getCoreRowModel: getCoreRowModel(), onPaginationChange: setPagination, onRowSelectionChange: setRowSelection, + meta: { bbsTypeCd: bbsInfo?.bbsTypeCd }, }); const DataTableComponent = isGridViewPage ? GridViewDataTable : DataTable; @@ -103,15 +103,16 @@ function PostPage({ bbsId }: { bbsId: string }) { .getFilteredSelectedRowModel() .rows.map((row) => row.original.pstNo); - if (await confirm("삭제하시겠습니까?")) { - setRowSelection({}); - await Promise.all( - selectedPosts.map((pstNo) => - PostApi.deletePost(bbsId, pstNo.toString(), userInfo?.userUnqId), - ), - ); - refetch(); - } + const { resolved, value: delRsn } = + await prompt("삭제 사유를 입력해주세요."); + if (!resolved) return; + setRowSelection({}); + await Promise.all( + selectedPosts.map((pstNo) => + PostApi.deletePost(parseInt(bbsId), pstNo, delRsn), + ), + ); + refetch(); }; const handleSubmit = (e: React.FormEvent) => { @@ -123,10 +124,6 @@ function PostPage({ bbsId }: { bbsId: string }) { router.push(`${pathname}/${row.pstNo}`); }; - useEffect(() => { - refetch(); - }, [pagination]); - return (
diff --git a/apps/cs/src/widgets/post/ui/post.tsx b/apps/cs/src/widgets/post/ui/post.tsx index 3cac19401..370c9cc1d 100644 --- a/apps/cs/src/widgets/post/ui/post.tsx +++ b/apps/cs/src/widgets/post/ui/post.tsx @@ -8,28 +8,21 @@ 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, - useMenuStore, - usePrompt, -} from "@/shared/lib"; +import { useAlertStore, useMenuStore } from "@/shared/lib"; import { ENDPOINT } from "@/shared/config"; import { PostAction } from "./post-action"; import { PostComment } from "./post-comment"; function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { const router = useRouter(); - 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({ - queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_USER, bbsId, pstNo], - queryFn: () => PostApi.getPostInfo(bbsId, pstNo), + queryKey: [ENDPOINT.BOARD_SERVICE.POSTS, bbsId, pstNo], + queryFn: () => PostApi.getPostInfo(parseInt(bbsId), parseInt(pstNo)), }); const { data: downloadResponse, isFetching } = useQuery({ @@ -65,23 +58,14 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { enabled: !!videoAttachment?.atflId, }); - const { mutate: deletePost } = useMutation({ - mutationFn: (pstPswd: string) => PostApi.deletePost(bbsId, pstNo, pstPswd), + const { mutateAsync: deletePost } = useMutation({ + mutationFn: (delRsn?: string) => + PostApi.deletePost(parseInt(bbsId), parseInt(pstNo), delRsn), onSuccess: () => { alert("삭제되었습니다.", router.back); }, }); - const handleDelete = async () => { - let pstPswd = ""; - if (userInfo?.userUnqId) { - pstPswd = userInfo?.userUnqId; - } else { - pstPswd = await prompt("비밀번호를 입력해주세요."); - } - deletePost(pstPswd); - }; - const handleDownload = async () => { if (!downloadResponse) return; try { @@ -123,7 +107,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { return ( <> - +

{postData?.pstTtl} diff --git a/apps/cs/src/widgets/post/ui/posting-period-selector.tsx b/apps/cs/src/widgets/post/ui/posting-period-selector.tsx index 1673df8c9..9c23995a6 100644 --- a/apps/cs/src/widgets/post/ui/posting-period-selector.tsx +++ b/apps/cs/src/widgets/post/ui/posting-period-selector.tsx @@ -76,9 +76,7 @@ function PostingPeriodSelector({ ) )} - {periodCode === "0" && ( - - )} + {periodCode === "0" && }

); } diff --git a/packages/common/components/src/ui/button-group/button-group.stories.tsx b/packages/common/components/src/ui/button-group/button-group.stories.tsx index 17e2f3664..fa52df4be 100644 --- a/packages/common/components/src/ui/button-group/button-group.stories.tsx +++ b/packages/common/components/src/ui/button-group/button-group.stories.tsx @@ -2,11 +2,11 @@ import "./button-group.css"; import type { Meta, StoryObj } from "@storybook/react"; -import { RadioGroup, RadioGroupItem } from "../radio-group"; +import { ButtonGroup, ButtonGroupItem } from "../button-group"; -const meta: Meta = { +const meta: Meta = { title: "Elements/ButtonGroup", - component: RadioGroup, + component: ButtonGroup, parameters: { docs: { description: { @@ -19,49 +19,49 @@ ButtonGroup 컴포넌트는 RadioGroup 컴포넌트를 기반으로 버튼 모 }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { render: () => ( - - Default - Comfortable - Compact - + + Default + Comfortable + Compact + ), }; export const Secondary: Story = { render: () => ( - - Default - Comfortable - Compact - + + Default + Comfortable + Compact + ), }; export const Rounded: Story = { render: () => ( - - Default - Comfortable - Compact - + + Default + Comfortable + Compact + ), }; export const SecondaryRounded: Story = { render: () => ( - - Default - Comfortable - Compact - + Default + Comfortable + Compact + ), }; diff --git a/packages/common/components/src/ui/button-group/index.tsx b/packages/common/components/src/ui/button-group/index.tsx new file mode 100644 index 000000000..86d44b9c9 --- /dev/null +++ b/packages/common/components/src/ui/button-group/index.tsx @@ -0,0 +1,39 @@ +import "./button-group.css"; + +import * as React from "react"; +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; + +const ButtonGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + variant?: "radio" | "button"; + color?: "primary" | "secondary"; + shape?: "square" | "rounded"; + } +>( + ( + { variant = "radio", color = "primary", shape = "square", ...props }, + ref, + ) => { + return ( + + ); + }, +); +ButtonGroup.displayName = RadioGroupPrimitive.Root.displayName; + +const ButtonGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => { + return ; +}); +ButtonGroupItem.displayName = RadioGroupPrimitive.Item.displayName; + +export { ButtonGroup, ButtonGroupItem }; diff --git a/packages/common/components/src/ui/data-table/index.tsx b/packages/common/components/src/ui/data-table/index.tsx index bcb072bb1..2b9879879 100644 --- a/packages/common/components/src/ui/data-table/index.tsx +++ b/packages/common/components/src/ui/data-table/index.tsx @@ -16,6 +16,7 @@ import type { HTMLAttributes } from "react"; declare module "@tanstack/react-table" { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { + width?: number; align?: "left" | "center" | "right"; rowSpan?: number; } diff --git a/packages/common/components/src/ui/date-range-picker/index.tsx b/packages/common/components/src/ui/date-range-picker/index.tsx index 2c99347a2..3572ffb7b 100644 --- a/packages/common/components/src/ui/date-range-picker/index.tsx +++ b/packages/common/components/src/ui/date-range-picker/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { format } from "date-fns"; import { CalendarIcon } from "lucide-react"; import type { DateRange } from "react-day-picker"; @@ -11,20 +11,38 @@ import { Calendar } from "../calendar"; import { Popover, PopoverContent, PopoverTrigger } from "../popover"; type DateRangePickerProps = React.HTMLAttributes & { + value?: DateRange; + defaultValue?: DateRange; disabled?: boolean; - handleDateChange?: (date: DateRange | undefined) => void; + onValueChange?: (date: DateRange | undefined) => void; }; export function DateRangePicker({ + value, + defaultValue, disabled, className, - handleDateChange, + children, + onValueChange, }: DateRangePickerProps) { - const [date, setDate] = useState(); + const [dateState, setDateState] = useState( + defaultValue, + ); + + const isControlled = value !== undefined; + const dateValue = isControlled ? value : dateState; + + useEffect(() => { + if (isControlled) { + setDateState(value); + } + }, [isControlled, value]); const handleSelect = (date: DateRange | undefined) => { - setDate(date); - handleDateChange?.(date); + if (!isControlled) { + setDateState(date); + } + onValueChange?.(date); }; return ( @@ -36,19 +54,19 @@ export function DateRangePicker({ variant="outline" className={cn( "h-[3.2rem] min-w-[12rem] justify-start border-input !bg-background px-4 py-2 text-left text-[1.3rem] font-normal text-muted-foreground hover:!bg-accent", - !date && "text-muted-foreground", + !dateValue && "text-muted-foreground", )} disabled={disabled} > - {date?.from ? ( - date.to ? ( + {dateValue?.from ? ( + dateValue.to ? ( <> - {format(date.from, "yyyy-MM-dd")} ~{" "} - {format(date.to, "yyyy-MM-dd")} + {format(dateValue.from, "yyyy-MM-dd")} ~{" "} + {format(dateValue.to, "yyyy-MM-dd")} ) : ( - format(date.from, "yyyy-MM-dd") + format(dateValue.from, "yyyy-MM-dd") ) ) : ( 기간 선택 @@ -56,14 +74,17 @@ export function DateRangePicker({ - +
+ {children} + +
diff --git a/packages/common/components/src/ui/index.ts b/packages/common/components/src/ui/index.ts index 134b76d21..2e2cea547 100644 --- a/packages/common/components/src/ui/index.ts +++ b/packages/common/components/src/ui/index.ts @@ -1,6 +1,7 @@ export * from "./accordion"; export * from "./badge"; export * from "./button"; +export * from "./button-group"; export * from "./calendar"; export * from "./carousel"; export * from "./checkbox";