This commit is contained in:
2026-06-29 16:54:03 +09:00
10 changed files with 125 additions and 43 deletions
@@ -0,0 +1,22 @@
import { AxiosResponse } from "axios";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { AttachService } from "../model/attach-interface";
async function deleteAttachmentByAtflId(atflId: string): Promise<void> {
if (!atflId) return;
await client.delete(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`);
}
async function downloadAttachmentByAtflCd(atflCd: string): Promise<AxiosResponse> {
return await client.get<Blob>(`${ENDPOINT.ATTACH_SERVICE.DOWNLOAD}/${atflCd}`, {
responseType: "blob",
});
}
export const attachService = {
deleteAttachmentByAtflId,
downloadAttachmentByAtflCd,
} satisfies AttachService;
@@ -0,0 +1,39 @@
import { useMutation } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { parseFilenameFromResponse } from "../lib/parse-filename-from-response";
import { attachService } from "./attach-service";
const attachBaseKey = [ENDPOINT.ATTACH_SERVICE.ATTACHMENTS] as const;
const attachDownloadBaseKey = [ENDPOINT.ATTACH_SERVICE.DOWNLOAD] as const;
export function useDeleteAttachment() {
return useMutation({
mutationKey: [...attachBaseKey, "delete"],
mutationFn: (atflId: string) => attachService.deleteAttachmentByAtflId(atflId),
});
}
export function useDownloadAttachmentByAtflCd() {
return useMutation({
mutationKey: [...attachDownloadBaseKey, "download"],
mutationFn: async (atflCd: string) => {
const response = await attachService.downloadAttachmentByAtflCd(atflCd);
const filename = parseFilenameFromResponse(response);
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);
},
});
}
+3
View File
@@ -0,0 +1,3 @@
export { attachService } from "./api/attach-service";
export { useDeleteAttachment, useDownloadAttachmentByAtflCd } from "./api/queries";
export type { Attach, AttachService } from "./model/attach-interface";
@@ -0,0 +1,11 @@
import type { AxiosResponse } from "axios";
export function parseFilenameFromResponse(response: AxiosResponse): string {
const contentDisposition = response.headers["content-disposition"];
if (!contentDisposition) return "download";
const filenameMatch = contentDisposition.match(/filename\*=UTF-8''([^;]+)/);
if (!filenameMatch?.[1]) return "download";
return decodeURIComponent(filenameMatch[1]);
}
@@ -0,0 +1,22 @@
import { AxiosResponse } from "axios";
/** 백엔드 AtflMngResponseDto */
export interface Attach {
atflCd: string;
atflSn: number;
atflId: string;
physFileNm: string;
orgnlFileNm: string;
atflFlieSz: number;
atflType: string;
dwnldNocs: number;
delYn: string;
lnkgDmnId: string;
lnkgDmnNm: string;
regDt: string;
}
export interface AttachService {
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
}
@@ -10,10 +10,10 @@ import { type HttpResponse, Upload } from "tus-js-client";
import { AUTH_USER_ID, CLAIM_NAME, ENDPOINT, PROXY_HOST } from "@/shared/config";
import { getCookie, setCookie, useAlertStore, useUserStore } from "@/shared/lib";
import { deleteAttachment } from "@/shared/lib/attach";
import { formatFileSize } from "@/shared/lib/file-utils";
import type { AcntgSeMngTreeDto } from "@/entities/acntg";
import { acntgSeMngService } from "@/entities/acntg";
import { attachService } from "@/entities/attach";
import type { CklstTreeNode } from "@/entities/cklst";
import { cklstMngService } from "@/entities/cklst";
import type { CnstpSeMngTreeDto } from "@/entities/cnstp";
@@ -509,7 +509,7 @@ function CnfmRegistPage({ initialData }: { initialData?: any } = {}) {
const handleDeleteEtcFile = async (fileId: string, atflId: string) => {
try {
if (atflId) {
await deleteAttachment(atflId);
await attachService.deleteAttachmentByAtflId(atflId);
}
} catch (error) {
console.error("기타 첨부파일 삭제 실패", error);
@@ -7,7 +7,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { ENDPOINT } from "@/shared/config";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { deleteAttachment } from "@/shared/lib/attach";
import { attachService } from "@/entities/attach";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import type {
AtflMngClientResponseDto,
@@ -242,7 +242,7 @@ function AttachmentsFormSection({
if (fileToRemove.atflId) {
try {
await deleteAttachment(fileToRemove.atflId);
await attachService.deleteAttachmentByAtflId(fileToRemove.atflId);
await invalidateAttachmentQueries();
} catch (error) {
console.error("기타 첨부 파일 삭제 실패:", error);
@@ -267,14 +267,16 @@ function AttachmentsFormSection({
if (atflId) {
try {
await deleteAttachment(atflId);
await attachService.deleteAttachmentByAtflId(atflId);
await invalidateAttachmentQueries();
} catch (error) {
console.error("파일 삭제 실패:", error);
}
}
const updatedList = (uploadedFiles[esntlAtchFileId] ?? []).filter((_, index) => index !== fileIndex);
const updatedList = (uploadedFiles[esntlAtchFileId] ?? []).filter(
(_, index) => index !== fileIndex,
);
const isSlotEmpty = updatedList.length === 0;
setUploadedFiles((prev) => {
@@ -312,7 +314,10 @@ function AttachmentsFormSection({
setUploadedFiles((prev) => ({
...prev,
[esntlAtchFileId]: [...(prev[esntlAtchFileId] ?? []), { file, fileName: file.name, fileKey }],
[esntlAtchFileId]: [
...(prev[esntlAtchFileId] ?? []),
{ file, fileName: file.name, fileKey },
],
}));
uploadMappingRef.current.set(fileKey, { esntlAtchFileId, file });
@@ -391,7 +396,9 @@ function AttachmentsFormSection({
return (
<div key={item.fileKey} className="flex items-center gap-2">
<span className="text-xl">{item.file?.name ?? item.fileName}</span>
<span className="text-xl">
{item.file?.name ?? item.fileName}
</span>
<Button
size="icon"
onClick={(e) => {
@@ -413,7 +420,10 @@ function AttachmentsFormSection({
type="file"
className="hidden"
onChange={(e) => {
void handleEssentialFileUpload(file.esntlAtchFileId, e.target.files);
void handleEssentialFileUpload(
file.esntlAtchFileId,
e.target.files,
);
e.target.value = "";
}}
/>
@@ -1,19 +1,12 @@
"use client";
import { PROXY_HOST } from "@/shared/config";
import { formatFileSize } from "@/shared/lib/file-utils";
import { useDownloadAttachmentByAtflCd } from "@/entities/attach";
import type {
AtflMngClientResponseDto,
EsntlAtchFileMngTreeResponseDto,
} from "@/entities/instl-sncn";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@common/components";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import { SectionCollapsible } from "../components/section-collapsible";
@@ -22,11 +15,9 @@ interface AttachmentsSectionProps {
etcFileList: AtflMngClientResponseDto[];
}
function getDownloadUrl(atflCd: string) {
return `${PROXY_HOST}/attach-service/api/v1/atflcd/download/${atflCd}`;
}
function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionProps) {
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
return (
<>
<SectionCollapsible title="필수 첨부파일">
@@ -58,8 +49,8 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
<TableCell className="text-left">
{file.atflCd ? (
<a
className="text-blue-600 hover:underline"
href={getDownloadUrl(file.atflCd)}
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd ?? "")}
target="_blank"
rel="noreferrer"
>
@@ -100,8 +91,8 @@ function AttachmentsSection({ esntlFileList, etcFileList }: AttachmentsSectionPr
<TableCell className="text-left">
{file.atflCd ? (
<a
className="text-blue-600 hover:underline"
href={getDownloadUrl(file.atflCd)}
className="cursor-pointer text-blue-600 hover:underline"
onClick={() => downloadAttachment(file.atflCd)}
target="_blank"
rel="noreferrer"
>
+1
View File
@@ -2,6 +2,7 @@ export const ENDPOINT = {
ATTACH_SERVICE: {
TUS_UPLOAD: "/attach-service/api/v1/tus/upload",
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
DOWNLOAD: "/attach-service/api/v1/atflcd/download",
},
AS_SERVICE: {
ACNTG_SE: "/as-service/api/v1/acntg-se",
-17
View File
@@ -1,17 +0,0 @@
import { getCookie } from "@/shared/lib";
import { CLAIM_NAME, AUTH_USER_ID } from "@/shared/config/env";
import axios from "axios";
import { PROXY_HOST, ENDPOINT } from "@/shared/config";
/**
* Common helper to delete attachment using cookies for auth headers.
*/
export async function deleteAttachment(atflId: string): Promise<void> {
if (!atflId) return;
const headers: Record<string, string> = {
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
};
const url = `${PROXY_HOST}${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflId}`;
await axios.delete(url, { headers });
}