[as] feat: 행정지원 문서 파일 첨부 로직 수정
This commit is contained in:
@@ -2,8 +2,10 @@ export {
|
||||
atchFileGroupIdPath,
|
||||
createEtcUploadKey,
|
||||
createUploadKey,
|
||||
getEsntlAtchFileGroupId,
|
||||
isEtcFileKey,
|
||||
isFormEsntlAtchComplete,
|
||||
toEsntlAtchFileSaveList,
|
||||
} from "./lib/attachments-form-utils";
|
||||
export {
|
||||
buildAcntgCostItems,
|
||||
@@ -30,6 +32,7 @@ export {
|
||||
} from "./lib/fnrs-shar-form-utils";
|
||||
export { calcDiffPercent, fmtNumber, indentPx } from "./lib/format-utils";
|
||||
export { AttachmentsFormSection } from "./ui/attachments-form-section";
|
||||
export { AttachmentsViewSection } from "./ui/attachments-view-section";
|
||||
export { BusinessCostFormSection } from "./ui/business-cost-form-section";
|
||||
export { BusinessScaleFormSection } from "./ui/business-scale-form-section";
|
||||
export { ChecklistFormSection } from "./ui/checklist-form-section";
|
||||
|
||||
@@ -10,7 +10,7 @@ export type FieldPrefix =
|
||||
|
||||
export const ETC_FILE_KEY_PREFIX = "etc::";
|
||||
|
||||
interface EsntlAtchFileListItem {
|
||||
export interface EsntlAtchFileListItem {
|
||||
esntlAtchFileId: string;
|
||||
atchFileGroupId: string;
|
||||
}
|
||||
@@ -42,6 +42,27 @@ export function toFileAccept(rmrk: string | null | undefined): string {
|
||||
.join(",");
|
||||
}
|
||||
|
||||
export function getEsntlAtchFileGroupId(item: EsntlAtchFileMngTreeResponseDto): string {
|
||||
return item.atchFileList?.[0]?.atflCd ?? "";
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileSaveList(
|
||||
esntlFileList?: EsntlAtchFileMngTreeResponseDto[],
|
||||
): EsntlAtchFileListItem[] {
|
||||
const esntlMap = new Map<string, string>();
|
||||
|
||||
for (const item of esntlFileList ?? []) {
|
||||
const atchFileGroupId = getEsntlAtchFileGroupId(item);
|
||||
if (!item.esntlAtchFileId || !atchFileGroupId) continue;
|
||||
esntlMap.set(item.esntlAtchFileId, atchFileGroupId);
|
||||
}
|
||||
|
||||
return Array.from(esntlMap.entries()).map(([esntlAtchFileId, atchFileGroupId]) => ({
|
||||
esntlAtchFileId,
|
||||
atchFileGroupId,
|
||||
}));
|
||||
}
|
||||
|
||||
export function isFormEsntlAtchComplete(
|
||||
essentialFiles: EsntlAtchFileMngTreeResponseDto[],
|
||||
esntlAtchFileList: EsntlAtchFileListItem[],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Paperclip, X } from "lucide-react";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { useTus } from "@/shared/lib";
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
type FieldPrefix,
|
||||
isEtcFileKey,
|
||||
toFileAccept,
|
||||
} from "../lib/attachments-form-utils";
|
||||
} from "../lib/attachments-form-utils";
|
||||
|
||||
interface AttachmentsFormData {
|
||||
esntlAtchFileList: Array<{
|
||||
@@ -103,22 +104,22 @@ function AttachmentsFormSection({
|
||||
if (initialEsntlFiles?.length) {
|
||||
const nextUploadedFiles: Record<string, UploadedFileItem[]> = {};
|
||||
|
||||
for (const file of initialEsntlFiles) {
|
||||
if (!file.esntlAtchFileId || !file.atflCd || !file.atflId) continue;
|
||||
for (const slot of initialEsntlFiles) {
|
||||
if (!slot.esntlAtchFileId) continue;
|
||||
|
||||
const fileKey = `server::${file.esntlAtchFileId}::${file.atflId}`;
|
||||
const fileName = file.orgnlFileNm ?? "첨부파일";
|
||||
const files = (slot.atchFileList ?? []).filter((file) => file.atflId && file.atflCd);
|
||||
if (files.length === 0) continue;
|
||||
|
||||
nextUploadedFiles[file.esntlAtchFileId] = [
|
||||
...(nextUploadedFiles[file.esntlAtchFileId] ?? []),
|
||||
{
|
||||
fileName,
|
||||
fileKey,
|
||||
atflId: file.atflId,
|
||||
atflCd: file.atflCd,
|
||||
},
|
||||
];
|
||||
atflCdBySlotRef.current[file.esntlAtchFileId] = file.atflCd;
|
||||
const [firstFile] = files;
|
||||
if (!firstFile) continue;
|
||||
|
||||
nextUploadedFiles[slot.esntlAtchFileId] = files.map((file) => ({
|
||||
fileName: file.orgnlFileNm ?? "첨부파일",
|
||||
fileKey: `server::${slot.esntlAtchFileId}::${file.atflId}`,
|
||||
atflId: file.atflId,
|
||||
atflCd: file.atflCd,
|
||||
}));
|
||||
atflCdBySlotRef.current[slot.esntlAtchFileId] = firstFile.atflCd;
|
||||
}
|
||||
|
||||
setUploadedFiles(nextUploadedFiles);
|
||||
@@ -372,7 +373,7 @@ function AttachmentsFormSection({
|
||||
render={() => (
|
||||
<div className="px-2">
|
||||
<h3 className="mb-4 text-xl font-semibold">필수 첨부파일</h3>
|
||||
<Table variant="secondary">
|
||||
<Table variant="secondary" className="table-fixed">
|
||||
<colgroup>
|
||||
<col width="40%" />
|
||||
<col width="15%" />
|
||||
@@ -406,36 +407,43 @@ function AttachmentsFormSection({
|
||||
</TableHead>
|
||||
<TableCell align="center">{file.esntlAtchFileRmrk}</TableCell>
|
||||
<TableCell className="space-y-2">
|
||||
{uploadedFiles[file.esntlAtchFileId]?.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{uploadedFiles[file.esntlAtchFileId]?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{uploadedFiles[file.esntlAtchFileId].map((item, idx) => {
|
||||
const uploading = isUploading[item.fileKey] ?? false;
|
||||
|
||||
return (
|
||||
<div key={item.fileKey} className="flex items-center gap-2">
|
||||
<div
|
||||
key={item.fileKey}
|
||||
className="inline-flex items-center justify-between gap-2 rounded-sm border bg-accent p-2"
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0" />
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{item.file?.name ?? item.fileName}
|
||||
</TextShimmer>
|
||||
{!uploading && (
|
||||
<Button
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEssentialFile(file.esntlAtchFileId, idx);
|
||||
}}
|
||||
className="size-8 !bg-destructive"
|
||||
>
|
||||
<i className="diveicon di-x text-xs" aria-label="삭제" />
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEssentialFile(file.esntlAtchFileId, idx);
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
<X
|
||||
className="size-5 shrink-0 text-destructive"
|
||||
aria-label="삭제"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
)}
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
|
||||
onChange={(e) => {
|
||||
@@ -447,10 +455,10 @@ function AttachmentsFormSection({
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<span>파일선택</span>
|
||||
<span>파일추가</span>
|
||||
</Button>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
@@ -475,35 +483,35 @@ function AttachmentsFormSection({
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" variant="outline" asChild>
|
||||
<span>파일 추가</span>
|
||||
<span>파일추가</span>
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rounded border p-4">
|
||||
{etcFiles.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{etcFiles.map((item, idx) => {
|
||||
const uploading = isUploading[item.fileKey] ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.fileKey}
|
||||
className="flex items-center justify-between rounded-sm border p-2"
|
||||
className="inline-flex items-center justify-between gap-2 rounded-sm border bg-accent p-2"
|
||||
>
|
||||
<TextShimmer className="px-2 text-xl" active={uploading}>
|
||||
<Paperclip className="size-5 shrink-0" />
|
||||
<TextShimmer className="text-xl" active={uploading}>
|
||||
{item.file?.name ?? item.fileName}
|
||||
</TextShimmer>
|
||||
<Button
|
||||
size="sm"
|
||||
color="gray"
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
void removeEtcFile(idx);
|
||||
}}
|
||||
disabled={uploading}
|
||||
>
|
||||
삭제
|
||||
</Button>
|
||||
<X className="size-5 shrink-0 text-destructive" aria-label="삭제" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { Paperclip } from "lucide-react";
|
||||
|
||||
import { formatFileSize } from "@/shared/lib/file-utils";
|
||||
import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
|
||||
import { SectionCollapsible } from "@/shared/ui";
|
||||
import { useDownloadAttachmentByAtflCd, useDownloadAttachmentByAtflId } from "@/entities/attach";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
|
||||
|
||||
interface AttachmentsViewSectionProps {
|
||||
esntlFileList: EsntlAtchFileMngTreeResponseDto[];
|
||||
etcFileList: AtflMngClientResponseDto[];
|
||||
}
|
||||
|
||||
function AttachmentsViewSection({ esntlFileList, etcFileList }: AttachmentsViewSectionProps) {
|
||||
const { mutate: downloadByAtflCd } = useDownloadAttachmentByAtflCd();
|
||||
const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId();
|
||||
|
||||
return (
|
||||
<>
|
||||
{esntlFileList.length > 0 && (
|
||||
<SectionCollapsible title="필수 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>필수 자료명</TableHead>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{esntlFileList.map((slot) => {
|
||||
const files = slot.atchFileList ?? [];
|
||||
|
||||
return (
|
||||
<TableRow key={slot.esntlAtchFileId}>
|
||||
<TableCell className="pl-2 text-left">
|
||||
{slot.esntlAtchFileNm}
|
||||
{slot.esntlAtchYn === "Y" ? (
|
||||
<span className="ml-1 text-red-500">*</span>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-left">
|
||||
{files.length === 0 ? (
|
||||
<span className="text-gray-500">첨부없음</span>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{files.map((file) => (
|
||||
<a
|
||||
key={file.atflId}
|
||||
className="flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadByAtflId(file.atflId)}
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0 text-blue-600" />
|
||||
{file.orgnlFileNm || "다운로드"}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{files.length === 0 ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{files.map((file) => (
|
||||
<div key={file.atflId}>{formatFileSize(file.atflFlieSz)}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
|
||||
{etcFileList.length > 0 && (
|
||||
<SectionCollapsible title="기타 첨부파일">
|
||||
<div className="overflow-x-auto">
|
||||
<Table variant="secondary">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>파일명</TableHead>
|
||||
<TableHead className="w-[8rem] text-right">크기</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{etcFileList.map((file) => (
|
||||
<TableRow key={file.atflId}>
|
||||
<TableCell className="text-left">
|
||||
{file.atflCd ? (
|
||||
<a
|
||||
className="flex cursor-pointer items-center gap-1 text-blue-600 hover:underline"
|
||||
onClick={() => downloadByAtflCd(file.atflCd)}
|
||||
>
|
||||
<Paperclip className="size-5 shrink-0 text-blue-600" />
|
||||
{file.orgnlFileNm || file.physFileNm || file.atflId}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.orgnlFileNm || file.physFileNm || file.atflId}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-gray-500">
|
||||
{formatFileSize(file.atflFlieSz)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SectionCollapsible>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { AttachmentsViewSection };
|
||||
Reference in New Issue
Block a user