feat: 첨부파일 다운로드 작업

This commit is contained in:
2025-05-07 13:22:16 +09:00
parent 1bf2919a09
commit f5e7601d54
5 changed files with 121 additions and 4 deletions
@@ -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<AxiosResponse> {
const response = await client.get(
`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`,
{ responseType: "blob" },
);
return response;
}
}
export const AttachmentApi = new AttachmentApiService();
@@ -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<void>;
getImageUrl: (atflId: string) => Promise<string>;
getImageUrlByAtflCd: (atflCd: string) => Promise<string>;
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
}
@@ -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<Post>[] = [
{
@@ -51,9 +52,39 @@ export const columns: ColumnDef<Post>[] = [
];
const AttachmentButton = ({ atflCd }: { atflCd: string }) => {
if (!atflCd) return null;
const handleDownload = async (e: React.MouseEvent<HTMLButtonElement>) => {
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 (
<Button size="icon" color="white">
<Button size="icon" color="white" onClick={handleDownload}>
<i className="diveicon di-download" />
</Button>
);
@@ -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",
@@ -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<AxiosResponse | null>(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 (
<PostAction bbsId={bbsId} pstNo={pstNo}>
<div className="card card-border !p-8">
@@ -36,7 +92,23 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
/>
<hr className="mt-8" />
<div className="flex items-center gap-4 p-8">
<span>첨부파일</span>
<span className="font-bold">첨부파일</span>
{isPending ? (
<Skeleton className="h-[2.5rem] w-[20rem]" />
) : (
filename && (
<Button
size="sm"
color="white"
className="flex w-fit items-center gap-2"
onClick={handleDownload}
disabled={isPending}
>
<i className="diveicon di-download" />
<span>{filename}</span>
</Button>
)
)}
</div>
</div>
</PostAction>