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 92800794a..406cca549 100644 --- a/apps/common/features/src/entities/attachment/api/attachment-service.ts +++ b/apps/common/features/src/entities/attachment/api/attachment-service.ts @@ -1,3 +1,4 @@ +import type { AxiosResponse } from "axios"; import { client } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { @@ -54,6 +55,16 @@ export class AttachmentApiService implements AttachmentService { const blob = await response.blob(); return URL.createObjectURL(blob); } + + public async downloadAttachmentByAtflCd( + atflCd: string, + ): Promise { + const response = await client.get( + `${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`, + { responseType: "blob" }, + ); + return response; + } } export const AttachmentApi = new AttachmentApiService(); 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 f9dd31c16..00340e9ae 100644 --- a/apps/common/features/src/entities/attachment/model/attachment-interface.ts +++ b/apps/common/features/src/entities/attachment/model/attachment-interface.ts @@ -1,3 +1,4 @@ +import type { AxiosResponse } from "axios"; import type { PageableList, PageableParams } from "@/shared/model"; export interface AttachmentParams extends PageableParams { @@ -40,4 +41,5 @@ export interface AttachmentService { deleteAttachmentByAtflCd: (atflCd: string) => Promise; getImageUrl: (atflId: string) => Promise; getImageUrlByAtflCd: (atflCd: string) => Promise; + downloadAttachmentByAtflCd: (atflCd: string) => Promise; } diff --git a/apps/common/features/src/pages/notice/model/notice-table-columns.tsx b/apps/common/features/src/pages/notice/model/notice-table-columns.tsx index d926b5f5d..2a9794919 100644 --- a/apps/common/features/src/pages/notice/model/notice-table-columns.tsx +++ b/apps/common/features/src/pages/notice/model/notice-table-columns.tsx @@ -1,7 +1,8 @@ import dayjs from "dayjs"; import type { ColumnDef } from "@tanstack/react-table"; -import type { Post } from "@/entities/post"; import { Badge, Button } from "@common/components"; +import { AttachmentApi } from "@/entities/attachment"; +import type { Post } from "@/entities/post"; export const columns: ColumnDef[] = [ { @@ -51,9 +52,39 @@ export const columns: ColumnDef[] = [ ]; const AttachmentButton = ({ atflCd }: { atflCd: string }) => { - if (!atflCd) return null; + 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/common/features/src/shared/config/api.ts b/apps/common/features/src/shared/config/api.ts index b82a8eb23..aea8fbcbf 100644 --- a/apps/common/features/src/shared/config/api.ts +++ b/apps/common/features/src/shared/config/api.ts @@ -8,6 +8,7 @@ export const ENDPOINT = { EDITOR_UPLOAD: "/attach-service/api/v1/upload/editor", IMAGES: "/attach-service/api/v1/images", ATTACHMENTS: "/attach-service/api/v1/atfl/attachments", + DOWNLOAD: "/attach-service/api/v1/atflcd/download", }, USER_SERVICE: { LOGIN: "/user-service/login", diff --git a/apps/common/features/src/widgets/post/ui/post.tsx b/apps/common/features/src/widgets/post/ui/post.tsx index 5ed0235f3..c3419792b 100644 --- a/apps/common/features/src/widgets/post/ui/post.tsx +++ b/apps/common/features/src/widgets/post/ui/post.tsx @@ -1,17 +1,73 @@ "use client"; import dayjs from "dayjs"; +import type { AxiosResponse } from "axios"; +import { useEffect, useState, useTransition } from "react"; import { useQuery } from "@tanstack/react-query"; +import { Button, Skeleton } from "@common/components"; import { PostApi } from "@/entities/post"; +import { AttachmentApi } from "@/entities/attachment"; import { ENDPOINT } from "@/shared/config"; import { PostAction } from "./post-action"; function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { + const [isPending, startTransition] = useTransition(); + const [filename, setFilename] = useState(""); + const [downloadResponse, setDownloadResponse] = + useState(null); + const { data: postData } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS], queryFn: () => PostApi.getPostInfo(bbsId, pstNo), }); + useEffect(() => { + if (!postData?.atflCd) return; + startTransition(async () => { + try { + let filename = ""; + const response = await AttachmentApi.downloadAttachmentByAtflCd( + postData.atflCd, + ); + setDownloadResponse(response); + + const contentDisposition = response.headers["content-disposition"]; + if (contentDisposition) { + const filenameMatch = contentDisposition.match( + /filename\*=UTF-8''([^;]+)/, + ); + if (filenameMatch && filenameMatch[1]) { + filename = decodeURIComponent(filenameMatch[1]); + } + } + setFilename(filename); + } catch { + setFilename(""); + } + }); + }, [postData?.atflCd]); + + const handleDownload = async () => { + if (isPending || !downloadResponse) return; + try { + startTransition(() => { + 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); + } + }; + return (
@@ -36,7 +92,23 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) { />
- 첨부파일 + 첨부파일 + {isPending ? ( + + ) : ( + filename && ( + + ) + )}