[as] feat: 보완요청 파일 첨부 로직 수정

This commit is contained in:
2026-09-08 17:01:02 +09:00
parent c47c130fc3
commit c79824e358
8 changed files with 287 additions and 180 deletions
@@ -3,7 +3,13 @@ import { AxiosResponse } from "axios";
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import type { AttachService } from "../model/attach-interface";
import type { Attach, AttachService } from "../model/attach-interface";
async function findByAtflCd(atflCd: string): Promise<Attach[]> {
if (!atflCd) return [];
const response = await client.get<Attach[]>(`${ENDPOINT.ATTACH_SERVICE.ATTACHMENTS}/${atflCd}`);
return response.data;
}
async function deleteAttachmentByAtflId(atflId: string): Promise<void> {
if (!atflId) return;
@@ -23,6 +29,7 @@ async function downloadAttachmentByAtflId(atflId: string): Promise<AxiosResponse
}
export const attachService = {
findByAtflCd,
deleteAttachmentByAtflId,
downloadAttachmentByAtflCd,
downloadAttachmentByAtflId,
@@ -17,6 +17,7 @@ export interface Attach {
}
export interface AttachService {
findByAtflCd: (atflCd: string) => Promise<Attach[]>;
deleteAttachmentByAtflId: (atflId: string) => Promise<void>;
downloadAttachmentByAtflCd: (atflCd: string) => Promise<AxiosResponse>;
downloadAttachmentByAtflId: (atflId: string) => Promise<AxiosResponse>;
@@ -3,9 +3,9 @@ const AI_PENDING_STATUS_CODES = ["FV", "AC", "AV"] as const;
type AiPendingStatusCode = (typeof AI_PENDING_STATUS_CODES)[number];
const AI_PENDING_MESSAGES: Record<AiPendingStatusCode, string> = {
FV: "파일 검증이 진행 중입니다",
AC: "실무협의서 초안 생성이 진행 중입니다",
AV: "금액 검증이 진행 중입니다",
FV: "AI 파일 검증이 진행 중입니다",
AC: "AI 실무협의서 초안 생성이 진행 중입니다",
AV: "AI 금액 검증이 진행 중입니다",
};
function isAiPendingStatus(prgrsSttsComCd: string): boolean {
@@ -322,7 +322,7 @@ function FloatingActionButtons({
dmndTtl: data.detDto?.bizNm ?? "",
dmndCn: data.detDto?.bizCn ?? "",
elaprCycl: data.detDto?.prgrsSttsComCd === "RK" ? 2 : 1,
docPrgrsSttsComCd: data.detDto?.prgrsSttsComCd,
docPrgrsSttsComCd: "AC",
elaprPrgrsSttsList: buildElaprPrgrsSttsListFromAplnRows(aplnRows),
};
await executeAction({
@@ -88,7 +88,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
function buildFormPayload(
data: FnrsCnsltnRegistFormData,
prgrsSttsComCd: "SV" | "ST" | "TE",
prgrsSttsComCd: string,
): FnrsCnsltnApplySaveRequestDto | undefined {
const withTotals = recalculateFnrsSharTotals(data.fnrsSharList, sharItems);
const validationMessage = validateBizCstSharBalance(
@@ -123,7 +123,7 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
({ splmntAtchFileNm, splmntAtchFileAtflId, splmntDmndAtchFileNm, ...item }) => ({
...item,
...(prgrsSttsComCd === "TE" ? { cmptnYn: "Y" } : null),
...(prgrsSttsComCd !== "SV" && prgrsSttsComCd !== "ST" && { cmptnYn: "Y" }),
}),
),
};
@@ -139,12 +139,13 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
}
async function submitSupplementForm(data: FnrsCnsltnRegistFormData): Promise<string | undefined> {
const beforePrgrsSttsComCd = initialData?.detDto?.beforePrgrsSttsComCd;
if (!beforePrgrsSttsComCd) return undefined;
const docNo = initialData?.detDto?.fnrsCnsltnDocNo;
if (!docNo) return undefined;
const payload = buildFormPayload(data, "TE");
if (!payload) return undefined;
const payload = buildFormPayload(data, beforePrgrsSttsComCd);
if (!docNo || !payload) return undefined;
debugger;
return fnrsCnsltnMngService.submitSupplement(docNo, payload);
}
@@ -102,9 +102,15 @@ function FnrsViewPage({ id }: FnrsViewPageProps) {
<div className="sticky top-[6rem] z-30 flex items-center gap-4 bg-container px-4 py-2">
<SectionCollapsibleToggle />
{isAiPending && (
<TextShimmer active className="text-xl">
{getAiPendingMessage(detDto.prgrsSttsComCd || "")}
</TextShimmer>
<div className="flex items-center gap-2">
<span className="relative flex size-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex size-3 rounded-full bg-primary"></span>
</span>
<TextShimmer active className="text-xl">
{getAiPendingMessage(detDto.prgrsSttsComCd || "")}
</TextShimmer>
</div>
)}
</div>
<div className="space-y-4">
@@ -2,6 +2,8 @@
import { useEffect, useId, useState } from "react";
import { Paperclip, X } from "lucide-react";
import { getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { attachService } from "@/entities/attach";
@@ -36,7 +38,7 @@ interface SplmntDmndRow {
splmntArtclComCd: string;
splmntDmndCn: string;
splmntDmndAtchFileGroupId: string;
uploadedFile: UploadedSplmntFile | null;
uploadedFiles: UploadedSplmntFile[];
}
interface SplmntDmndDialogConfirmItem {
@@ -67,7 +69,7 @@ function createEmptyRow(): SplmntDmndRow {
splmntArtclComCd: "",
splmntDmndCn: "",
splmntDmndAtchFileGroupId: "",
uploadedFile: null,
uploadedFiles: [],
};
}
@@ -105,11 +107,13 @@ function SplmntDmndDialog({
setRows((prev) =>
prev.map((row) => {
if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row;
if (row.rowKey !== rowKey) return row;
return {
...row,
splmntDmndAtchFileGroupId: atflCd,
uploadedFile: { ...row.uploadedFile, atflCd, atflId },
uploadedFiles: row.uploadedFiles.map((file) =>
file.fileKey === fileKey ? { ...file, atflCd, atflId } : file,
),
};
}),
);
@@ -120,8 +124,11 @@ function SplmntDmndDialog({
setRows((prev) =>
prev.map((row) => {
if (row.rowKey !== rowKey || row.uploadedFile?.fileKey !== fileKey) return row;
return { ...row, uploadedFile: null };
if (row.rowKey !== rowKey) return row;
return {
...row,
uploadedFiles: row.uploadedFiles.filter((file) => file.fileKey !== fileKey),
};
}),
);
},
@@ -132,8 +139,8 @@ function SplmntDmndDialog({
setRows([createEmptyRow()]);
}, [open]);
const isAnyUploading = rows.some(
(row) => row.uploadedFile != null && (isUploading[row.uploadedFile.fileKey] ?? false),
const isAnyUploading = rows.some((row) =>
row.uploadedFiles.some((file) => isUploading[file.fileKey] ?? false),
);
const canSubmit = isRowsValid(rows) && !isAnyUploading && !isSubmitting;
@@ -152,30 +159,42 @@ function SplmntDmndDialog({
async function handleFileUpload(rowKey: string, files: FileList | null) {
if (!files || files.length === 0) return;
const file = files[0];
const fileKey = createUploadKey(rowKey, file);
const targetRow = rows.find((row) => row.rowKey === rowKey);
const existingAtflCd = targetRow?.splmntDmndAtchFileGroupId;
let existingAtflCd = targetRow?.splmntDmndAtchFileGroupId ?? "";
updateRow(rowKey, {
uploadedFile: { fileName: file.name, fileKey },
});
for (const file of Array.from(files)) {
const fileKey = createUploadKey(rowKey, file);
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
setRows((prev) =>
prev.map((row) =>
row.rowKey === rowKey
? {
...row,
uploadedFiles: [...row.uploadedFiles, { fileName: file.name, fileKey }],
}
: row,
),
);
try {
const atflCd = await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
if (!existingAtflCd) existingAtflCd = atflCd;
} catch {
// onError callback handles cleanup
}
}
}
async function handleFileRemove(rowKey: string) {
async function handleFileRemove(rowKey: string, fileIndex: number) {
const targetRow = rows.find((row) => row.rowKey === rowKey);
if (!targetRow?.uploadedFile) return;
const targetFile = targetRow?.uploadedFiles[fileIndex];
if (!targetRow || !targetFile) return;
const { fileKey, atflId } = targetRow.uploadedFile;
const { fileKey, atflId } = targetFile;
uploads[fileKey]?.abort();
if (atflId) {
@@ -186,9 +205,10 @@ function SplmntDmndDialog({
}
}
const nextFiles = targetRow.uploadedFiles.filter((_, index) => index !== fileIndex);
updateRow(rowKey, {
uploadedFile: null,
splmntDmndAtchFileGroupId: "",
uploadedFiles: nextFiles,
splmntDmndAtchFileGroupId: nextFiles.length > 0 ? targetRow.splmntDmndAtchFileGroupId : "",
});
}
@@ -237,9 +257,9 @@ function SplmntDmndDialog({
</TableHeader>
<TableBody>
{rows.map((row) => {
const uploading = row.uploadedFile
? (isUploading[row.uploadedFile.fileKey] ?? false)
: false;
const isRowUploading = row.uploadedFiles.some(
(file) => isUploading[file.fileKey] ?? false,
);
const fileInputId = `${formId}-${row.rowKey}-file`;
return (
@@ -266,40 +286,58 @@ function SplmntDmndDialog({
className="min-h-[8rem] !text-xl"
/>
</TableCell>
<TableCell className="align-top">
<div className="flex items-center gap-2">
{row.uploadedFile ? (
<div className="flex items-stretch gap-2">
<TextShimmer className="text-xl" active={uploading}>
{row.uploadedFile.fileName}
</TextShimmer>
<Button
type="button"
size="icon"
className="size-8 shrink-0 !bg-destructive"
disabled={uploading}
onClick={() => void handleFileRemove(row.rowKey)}
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
</div>
) : (
<Button type="button" size="sm" disabled={uploading} asChild>
<label htmlFor={fileInputId} className="cursor-pointer">
파일추가
<input
id={fileInputId}
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(row.rowKey, event.target.files);
event.target.value = "";
}}
/>
</label>
</Button>
)}
</div>
<TableCell className="space-y-2 align-top">
{row.uploadedFiles.length > 0 ? (
<div className="flex flex-wrap gap-2">
{row.uploadedFiles.map((file, fileIndex) => {
const uploading = isUploading[file.fileKey] ?? false;
return (
<div
key={file.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}>
{file.fileName}
</TextShimmer>
<button
type="button"
disabled={uploading}
onClick={() => void handleFileRemove(row.rowKey, fileIndex)}
>
<X
className="size-5 shrink-0 text-destructive"
aria-label="삭제"
/>
</button>
</div>
);
})}
</div>
) : null}
<label htmlFor={fileInputId} className="cursor-pointer">
<input
id={fileInputId}
type="file"
multiple
disabled={isRowUploading}
className="peer sr-only"
onChange={(event) => {
void handleFileUpload(row.rowKey, event.target.files);
event.target.value = "";
}}
/>
<Button
size="sm"
variant="outline"
className="peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2"
disabled={isRowUploading}
asChild
>
<span>파일추가</span>
</Button>
</label>
</TableCell>
{rows.length > 1 ? (
<TableCell align="center" className="align-top">
@@ -3,12 +3,12 @@
import { useEffect, useId, useRef, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { Paperclip } from "lucide-react";
import { Paperclip, X } from "lucide-react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
import { TextShimmer } from "@/shared/ui";
import { SectionCollapsible } from "@/shared/ui";
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
import { attachService } from "@/entities/attach";
import {
Badge,
Button,
@@ -42,8 +42,6 @@ interface SplmntDmndFormItem {
splmntPicNm?: string;
splmntCmptnDt?: string;
splmntAtchFileGroupId?: string;
splmntAtchFileNm?: string;
splmntAtchFileAtflId?: string;
cmptnYn?: string;
}
@@ -75,9 +73,10 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
const formId = useId();
const { control, setValue, getValues } = useFormContext<SplmntDmndFormData>();
const splmntDmndList = useWatch({ control, name: "splmntDmndList" }) ?? [];
const [uploadedFiles, setUploadedFiles] = useState<Record<string, UploadedSplmntFile | null>>({});
const [uploadedFiles, setUploadedFiles] = useState<Record<string, UploadedSplmntFile[]>>({});
const initialFilesMappedRef = useRef(false);
const { mutate: downloadAttachment } = useDownloadAttachmentByAtflCd();
const isMountedRef = useRef(true);
const atflCdByRowRef = useRef<Record<string, string>>({});
const { uploadFileAsync, isUploading, uploads } = useTus({
onSuccess: (atflCd, atflId, _file, fileKey) => {
@@ -86,55 +85,93 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
if (!rowKey) return;
setUploadedFiles((prev) => {
const current = prev[rowKey];
if (!current || current.fileKey !== fileKey) return prev;
const current = prev[rowKey] ?? [];
return {
...prev,
[rowKey]: { ...current, atflCd, atflId },
[rowKey]: current.map((item) =>
item.fileKey === fileKey ? { ...item, atflCd, atflId } : item,
),
};
});
atflCdByRowRef.current[rowKey] = atflCd;
const list = getValues("splmntDmndList") ?? [];
const index = list.findIndex((item, idx) => getRowKey(item, idx) === rowKey);
if (index < 0) return;
setValue(`splmntDmndList.${index}.splmntAtchFileGroupId`, atflCd, { shouldDirty: true });
setValue(`splmntDmndList.${index}.splmntAtchFileAtflId`, atflId, { shouldDirty: true });
},
onError: (_error, _file, fileKey) => {
const rowKey = getRowKeyFromFileKey(fileKey);
if (!rowKey) return;
setUploadedFiles((prev) => {
const current = prev[rowKey];
if (!current || current.fileKey !== fileKey) return prev;
return { ...prev, [rowKey]: null };
const nextFiles = (prev[rowKey] ?? []).filter((item) => item.fileKey !== fileKey);
return { ...prev, [rowKey]: nextFiles };
});
},
});
useEffect(
() => () => {
isMountedRef.current = false;
},
[],
);
useEffect(() => {
if (initialFilesMappedRef.current) return;
if (splmntDmndList.length === 0) return;
initialFilesMappedRef.current = true;
const rowsWithFiles = splmntDmndList.flatMap((item, index) => {
if (!item.splmntAtchFileGroupId) return [];
const initial: Record<string, UploadedSplmntFile | null> = {};
for (let index = 0; index < splmntDmndList.length; index += 1) {
const item = splmntDmndList[index];
const rowKey = getRowKey(item, index);
if (item.splmntAtchFileGroupId && item.splmntAtchFileNm) {
initial[rowKey] = {
fileName: item.splmntAtchFileNm,
fileKey: `${SPLMNT_CMPTN_FILE_KEY_PREFIX}${rowKey}::initial`,
atflCd: item.splmntAtchFileGroupId,
atflId: item.splmntAtchFileAtflId,
};
} else {
initial[rowKey] = null;
}
atflCdByRowRef.current[rowKey] = item.splmntAtchFileGroupId;
return [{ rowKey, atflCd: item.splmntAtchFileGroupId }];
});
async function mapInitialFiles() {
const results = await Promise.all(
rowsWithFiles.map(async ({ rowKey, atflCd }) => {
try {
const files = await attachService.findByAtflCd(atflCd);
return {
rowKey,
files: files.map((file) => ({
fileName: file.orgnlFileNm,
fileKey: `${SPLMNT_CMPTN_FILE_KEY_PREFIX}${rowKey}::server::${file.atflId}`,
atflCd: file.atflCd,
atflId: file.atflId,
})),
};
} catch (error) {
console.error("보완완료 첨부파일 조회 실패:", error);
return { rowKey, files: [] };
}
}),
);
if (!isMountedRef.current) return;
setUploadedFiles((prev) => {
const next = { ...prev };
for (const { rowKey, files } of results) {
const localFiles = prev[rowKey] ?? [];
const serverAtflIds = new Set(files.map((file) => file.atflId));
next[rowKey] = [
...files,
...localFiles.filter((file) => !file.atflId || !serverAtflIds.has(file.atflId)),
];
}
return next;
});
}
setUploadedFiles(initial);
initialFilesMappedRef.current = true;
void mapInitialFiles();
}, [splmntDmndList]);
if (splmntDmndList.length === 0) return null;
@@ -144,30 +181,37 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
const item = splmntDmndList[index];
const rowKey = getRowKey(item, index);
const file = files[0];
const fileKey = createUploadKey(rowKey, file);
const existingAtflCd = item.splmntAtchFileGroupId;
let existingAtflCd = atflCdByRowRef.current[rowKey] || item.splmntAtchFileGroupId || "";
setUploadedFiles((prev) => ({
...prev,
[rowKey]: { fileName: file.name, fileKey },
}));
setValue(`splmntDmndList.${index}.splmntAtchFileNm`, file.name, { shouldDirty: true });
for (const file of Array.from(files)) {
const fileKey = createUploadKey(rowKey, file);
try {
await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
} catch {
// onError callback handles cleanup
setUploadedFiles((prev) => ({
...prev,
[rowKey]: [...(prev[rowKey] ?? []), { fileName: file.name, fileKey }],
}));
try {
const atflCd = await uploadFileAsync(file, {
fileKey,
metadata: existingAtflCd ? { atflCd: existingAtflCd } : undefined,
});
if (!existingAtflCd) {
existingAtflCd = atflCd;
atflCdByRowRef.current[rowKey] = atflCd;
}
} catch {
// onError callback handles cleanup
}
}
}
async function handleFileRemove(index: number) {
async function handleFileRemove(index: number, fileIndex: number) {
const item = splmntDmndList[index];
const rowKey = getRowKey(item, index);
const uploaded = uploadedFiles[rowKey];
const currentFiles = uploadedFiles[rowKey] ?? [];
const uploaded = currentFiles[fileIndex];
if (!uploaded) return;
const { fileKey, atflId } = uploaded;
@@ -181,10 +225,13 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
}
}
setUploadedFiles((prev) => ({ ...prev, [rowKey]: null }));
const nextFiles = currentFiles.filter((_, currentIndex) => currentIndex !== fileIndex);
setUploadedFiles((prev) => ({ ...prev, [rowKey]: nextFiles }));
if (nextFiles.length > 0) return;
delete atflCdByRowRef.current[rowKey];
setValue(`splmntDmndList.${index}.splmntAtchFileGroupId`, "", { shouldDirty: true });
setValue(`splmntDmndList.${index}.splmntAtchFileNm`, "", { shouldDirty: true });
setValue(`splmntDmndList.${index}.splmntAtchFileAtflId`, "", { shouldDirty: true });
}
return (
@@ -212,8 +259,8 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
<TableBody>
{splmntDmndList.map((item, index) => {
const rowKey = getRowKey(item, index);
const uploaded = uploadedFiles[rowKey] ?? null;
const uploading = uploaded ? (isUploading[uploaded.fileKey] ?? false) : false;
const rowFiles = uploadedFiles[rowKey] ?? [];
const isRowUploading = rowFiles.some((file) => isUploading[file.fileKey] ?? false);
const fileInputId = `${formId}-${rowKey}-file`;
const isRowReadOnly = readOnly || item.cmptnYn === "Y";
@@ -229,13 +276,10 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
<div className="space-y-2">
<div className="whitespace-pre-wrap">{item.splmntDmndCn || "-"}</div>
{item.splmntDmndAtchFileGroupId ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
onClick={() => downloadAttachment(item.splmntDmndAtchFileGroupId!)}
>
<span className="inline-flex items-start gap-1">
<Paperclip className="my-1 size-5 shrink-0" />
{item.splmntDmndAtchFileNm || "다운로드"}
</a>
{item.splmntDmndAtchFileNm || "첨부파일"}
</span>
) : null}
</div>
</TableCell>
@@ -258,54 +302,64 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
/>
)}
</TableCell>
<TableCell className="align-top">
{uploaded ? (
<div className="flex items-stretch gap-2">
{uploaded.atflCd || item.splmntAtchFileGroupId ? (
<a
className="inline-flex cursor-pointer items-start gap-1 text-blue-600 hover:underline"
onClick={() =>
downloadAttachment((uploaded.atflCd || item.splmntAtchFileGroupId)!)
}
>
<Paperclip className="my-1 size-5 shrink-0" />
{uploaded.fileName}
</a>
) : (
<TextShimmer className="text-xl" active={uploading}>
{uploaded.fileName}
</TextShimmer>
)}
{!isRowReadOnly ? (
<Button
type="button"
size="icon"
className="size-8 shrink-0 !bg-destructive"
disabled={uploading}
onClick={() => void handleFileRemove(index)}
>
<i className="diveicon di-x text-xs" aria-label="삭제" />
</Button>
) : null}
<TableCell className="space-y-2 align-top">
{rowFiles.length > 0 ? (
<div className="flex flex-wrap gap-2">
{rowFiles.map((file, fileIndex) => {
const uploading = isUploading[file.fileKey] ?? false;
return (
<div
key={file.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}>
{file.fileName}
</TextShimmer>
{!isRowReadOnly ? (
<button
type="button"
disabled={uploading}
onClick={() => void handleFileRemove(index, fileIndex)}
>
<X
className="size-5 shrink-0 text-destructive"
aria-label="삭제"
/>
</button>
) : null}
</div>
);
})}
</div>
) : isRowReadOnly ? (
) : null}
{!isRowReadOnly ? (
<label htmlFor={fileInputId} className="block cursor-pointer">
<input
id={fileInputId}
type="file"
multiple
disabled={isRowUploading}
className="peer sr-only"
onChange={(event) => {
void handleFileUpload(index, event.target.files);
event.target.value = "";
}}
/>
<Button
size="sm"
variant="outline"
className="peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2"
disabled={isRowUploading}
asChild
>
<span>파일추가</span>
</Button>
</label>
) : rowFiles.length === 0 ? (
<span>-</span>
) : (
<Button type="button" size="sm" disabled={uploading} asChild>
<label htmlFor={fileInputId} className="cursor-pointer">
파일추가
<input
id={fileInputId}
type="file"
className="hidden"
onChange={(event) => {
void handleFileUpload(index, event.target.files);
event.target.value = "";
}}
/>
</label>
</Button>
)}
) : null}
</TableCell>
<TableCell align="center" className="align-top">
<Badge size="lg" color={item.cmptnYn === "Y" ? "green" : "gray"}>