diff --git a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx
index 58ee1ca6b..710d6c93d 100644
--- a/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx
+++ b/apps/as/src/pages/esntl-atch-file-mng/ui/esntl-atch-file-form-dialog.tsx
@@ -3,10 +3,8 @@
import { useEffect, useId, useRef, useState } from "react";
import { useForm } from "react-hook-form";
-import { Paperclip, X } from "lucide-react";
-
import { getDefaultFileKey, useTus } from "@/shared/lib";
-import { TextShimmer } from "@/shared/ui";
+import { AttachmentFileField } from "@/shared/ui";
import { attachService } from "@/entities/attach";
import { CodeSelect } from "@/entities/code";
import type { EsntlAtchFileMng } from "@/entities/esntl";
@@ -217,57 +215,16 @@ function EsntlAtchFileFormDialog({
-
- {uploadedFiles.length > 0 ? (
-
- {uploadedFiles.map((item, index) => {
- const uploading = isUploading[item.fileKey] ?? false;
-
- return (
-
-
-
- {item.fileName}
-
-
-
- );
- })}
-
- ) : null}
-
-
+
({
+ fileKey: item.fileKey,
+ fileName: item.fileName,
+ }))}
+ inputId={fileInputId}
+ isUploadingByKey={isUploading}
+ onAdd={(fileList) => void handleFileUpload(fileList)}
+ onRemove={(index) => void handleFileRemove(index)}
+ />
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
index 59ae0fe47..be1694755 100644
--- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
+++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx
@@ -4,15 +4,10 @@ import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { Paperclip, X } from "lucide-react";
import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib";
-import { TextShimmer } from "@/shared/ui";
-import {
- attachService,
- parseFilenameFromResponse,
- useDownloadAttachmentByAtflId,
-} from "@/entities/attach";
+import { AttachmentFileField } from "@/shared/ui";
+import { attachService, parseFilenameFromResponse } from "@/entities/attach";
import {
pwrkCnsltnDocCrtRsltSaveRequestSchema,
pwrkCnsltnDocCrtRsltService,
@@ -70,7 +65,6 @@ function PwrkCnsltnDocMngDialog({
}: PwrkCnsltnDocMngDialogProps) {
const queryClient = useQueryClient();
const alert = useAlertStore((state) => state.setMessage);
- const downloadByAtflId = useDownloadAttachmentByAtflId();
const [atchFileGroupId, setAtchFileGroupId] = useState("");
const [uploadedFiles, setUploadedFiles] = useState
([]);
@@ -237,11 +231,6 @@ function PwrkCnsltnDocMngDialog({
}
}
- function handleAtchFileDownload(atflId?: string) {
- if (!atflId) return;
- downloadByAtflId.mutate(atflId);
- }
-
const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]);
const canSave = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code));
@@ -281,69 +270,21 @@ function PwrkCnsltnDocMngDialog({
{completedDraft ? (
-
-
첨부파일
-
-
- {uploadedFiles.length > 0 ? (
-
- {uploadedFiles.map((item, index) => {
- const uploading = isUploading[item.fileKey] ?? false;
-
- return (
-
-
-
-
-
- );
- })}
-
- ) : (
+ 첨부파일
+ {uploadedFiles.length === 0 ? (
등록된 첨부파일이 없습니다.
- )}
+ ) : null}
+ ({
+ fileKey: item.fileKey,
+ fileName: item.fileName,
+ }))}
+ inputId="pwrk-cnsltn-doc-atch-file-input"
+ isUploadingByKey={isUploading}
+ addDisabled={isAnyAtchUploading}
+ onAdd={(files) => void handleAtchFileUpload(files)}
+ onRemove={(index) => void handleAtchFileRemove(index)}
+ />
) : null}
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx
index bdbd4e622..08f2651f4 100644
--- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx
+++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx
@@ -6,13 +6,11 @@ import { Paperclip } from "lucide-react";
import { PROXY_HOST } from "@/shared/config";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
-import { TextShimmer } from "@/shared/ui";
-import { SectionCollapsible } from "@/shared/ui";
+import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { Field, FieldLabel, FieldSet } from "@/shared/ui/field";
import { attachService } from "@/entities/attach";
import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn";
import {
- Button,
RadioGroup,
RadioGroupItem,
Table,
@@ -246,38 +244,20 @@ const TechReviewOpinionSection = forwardRef<
첨부파일
- {uploadedFile ? (
-
-
- {uploadedFile.fileName}
-
-
-
- ) : (
-
- )}
+ void handleFileUpload(fileList)}
+ onRemove={() => void handleFileRemove()}
+ />
diff --git a/apps/as/src/shared/ui/attachment-file-field.tsx b/apps/as/src/shared/ui/attachment-file-field.tsx
new file mode 100644
index 000000000..6867a8323
--- /dev/null
+++ b/apps/as/src/shared/ui/attachment-file-field.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import { FileAddButton } from "./file-add-button";
+import { UploadedFileChip } from "./uploaded-file-chip";
+
+interface AttachmentFileFieldItem {
+ fileKey: string;
+ fileName: string;
+}
+
+interface AttachmentFileFieldProps {
+ files: AttachmentFileFieldItem[];
+ inputId: string;
+ isUploadingByKey: Record;
+ onAdd: (files: FileList | null) => void;
+ onRemove?: (index: number) => void;
+ accept?: string;
+ multiple?: boolean;
+ addDisabled?: boolean;
+ showAddButton?: boolean;
+}
+
+function AttachmentFileField({
+ files,
+ inputId,
+ isUploadingByKey,
+ onAdd,
+ onRemove,
+ accept,
+ multiple = true,
+ addDisabled = false,
+ showAddButton = true,
+}: AttachmentFileFieldProps) {
+ return (
+
+ {files.length > 0 ? (
+
+ {files.map((file, index) => (
+ onRemove(index) : undefined}
+ />
+ ))}
+
+ ) : null}
+ {showAddButton ? (
+
+ ) : null}
+
+ );
+}
+
+export { AttachmentFileField };
+export type { AttachmentFileFieldItem, AttachmentFileFieldProps };
diff --git a/apps/as/src/shared/ui/file-add-button.tsx b/apps/as/src/shared/ui/file-add-button.tsx
new file mode 100644
index 000000000..24bfd6ecf
--- /dev/null
+++ b/apps/as/src/shared/ui/file-add-button.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { Button, cn } from "@common/components";
+
+interface FileAddButtonProps {
+ inputId: string;
+ onFilesSelected: (files: FileList | null) => void;
+ accept?: string;
+ multiple?: boolean;
+ disabled?: boolean;
+ label?: string;
+}
+
+function FileAddButton({
+ inputId,
+ onFilesSelected,
+ accept,
+ multiple = true,
+ disabled = false,
+ label = "파일추가",
+}: FileAddButtonProps) {
+ return (
+
+ );
+}
+
+export { FileAddButton };
+export type { FileAddButtonProps };
diff --git a/apps/as/src/shared/ui/index.ts b/apps/as/src/shared/ui/index.ts
index 813bcafd5..71cd9a481 100644
--- a/apps/as/src/shared/ui/index.ts
+++ b/apps/as/src/shared/ui/index.ts
@@ -1,3 +1,7 @@
+export type { AttachmentFileFieldItem, AttachmentFileFieldProps } from "./attachment-file-field";
+export { AttachmentFileField } from "./attachment-file-field";
+export type { FileAddButtonProps } from "./file-add-button";
+export { FileAddButton } from "./file-add-button";
export { NoData } from "./no-data";
export { PageLayout } from "./page-layout";
export { SectionCollapsible } from "./section-collapsible";
@@ -7,3 +11,5 @@ export {
} from "./section-collapsible-group";
export type { TextShimmerProps } from "./text-shimmer";
export { TEXT_SHIMMER_CLASS, TextShimmer } from "./text-shimmer";
+export type { UploadedFileChipProps } from "./uploaded-file-chip";
+export { UploadedFileChip } from "./uploaded-file-chip";
diff --git a/apps/as/src/shared/ui/uploaded-file-chip.tsx b/apps/as/src/shared/ui/uploaded-file-chip.tsx
new file mode 100644
index 000000000..a7eb6fa22
--- /dev/null
+++ b/apps/as/src/shared/ui/uploaded-file-chip.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { Paperclip, X } from "lucide-react";
+
+import { TextShimmer } from "./text-shimmer";
+
+interface UploadedFileChipProps {
+ fileName: string;
+ isUploading?: boolean;
+ onRemove?: () => void;
+}
+
+function UploadedFileChip({ fileName, isUploading = false, onRemove }: UploadedFileChipProps) {
+ return (
+
+
+
+ {fileName}
+
+ {onRemove ? (
+
+ ) : null}
+
+ );
+}
+
+export { UploadedFileChip };
+export type { UploadedFileChipProps };
diff --git a/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx
index ff62679b6..d3507d8ba 100644
--- a/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx
+++ b/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx
@@ -4,13 +4,17 @@ import { useEffect, useRef, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useQueryClient } from "@tanstack/react-query";
-import { Download, Paperclip, X } from "lucide-react";
+import { Download } from "lucide-react";
import { ENDPOINT } from "@/shared/config";
import { useTus } from "@/shared/lib";
import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model";
-import { TextShimmer } from "@/shared/ui";
-import { SectionCollapsible } from "@/shared/ui";
+import {
+ AttachmentFileField,
+ FileAddButton,
+ SectionCollapsible,
+ UploadedFileChip,
+} from "@/shared/ui";
import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach";
import { useEsntlAtchFileMngTree } from "@/entities/esntl";
import {
@@ -423,64 +427,21 @@ function AttachmentsFormSection({
{file.esntlAtchFileRmrk}
- {uploadedFiles[file.esntlAtchFileId]?.length > 0 && (
-
- {uploadedFiles[file.esntlAtchFileId].map((item, idx) => {
- const uploading = isUploading[item.fileKey] ?? false;
-
- return (
-
-
-
- {item.file?.name ?? item.fileName}
-
-
-
- );
- })}
-
- )}
-
-
-
+ ({
+ fileKey: item.fileKey,
+ fileName: item.file?.name ?? item.fileName,
+ }))}
+ inputId={`${file.esntlAtchFileId}-file-input`}
+ isUploadingByKey={isUploading}
+ accept={toFileAccept(file.esntlAtchFileRmrk) || undefined}
+ onAdd={(fileList) =>
+ void handleEssentialFileUpload(file.esntlAtchFileId, fileList)
+ }
+ onRemove={(index) =>
+ void removeEssentialFile(file.esntlAtchFileId, index)
+ }
+ />
);
@@ -495,55 +456,22 @@ function AttachmentsFormSection({
기타 첨부파일
-
+ void handleEtcFileUpload(files)}
+ />
{etcFiles.length > 0 ? (
- {etcFiles.map((item, idx) => {
- const uploading = isUploading[item.fileKey] ?? false;
-
- return (
-
-
-
- {item.file?.name ?? item.fileName}
-
-
-
- );
- })}
+ {etcFiles.map((item, idx) => (
+
void removeEtcFile(idx)}
+ />
+ ))}
) : (
diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx
index 95aaaede2..752898721 100644
--- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx
+++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx
@@ -2,10 +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 { AttachmentFileField } from "@/shared/ui";
import { attachService } from "@/entities/attach";
import { CodeSelect } from "@/entities/code";
import {
@@ -287,57 +285,17 @@ function SplmntDmndDialog({
/>
- {row.uploadedFiles.length > 0 ? (
-
- {row.uploadedFiles.map((file, fileIndex) => {
- const uploading = isUploading[file.fileKey] ?? false;
-
- return (
-
-
-
- {file.fileName}
-
-
-
- );
- })}
-
- ) : null}
-
+ ({
+ fileKey: file.fileKey,
+ fileName: file.fileName,
+ }))}
+ inputId={fileInputId}
+ isUploadingByKey={isUploading}
+ addDisabled={isRowUploading}
+ onAdd={(fileList) => void handleFileUpload(row.rowKey, fileList)}
+ onRemove={(fileIndex) => void handleFileRemove(row.rowKey, fileIndex)}
+ />
{rows.length > 1 ? (
diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx
index a8f38ecb4..eea7fb7af 100644
--- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx
+++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx
@@ -3,15 +3,13 @@
import { useEffect, useId, useRef, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
-import { Paperclip, X } from "lucide-react";
+import { Paperclip } from "lucide-react";
import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib";
-import { TextShimmer } from "@/shared/ui";
-import { SectionCollapsible } from "@/shared/ui";
+import { AttachmentFileField, SectionCollapsible } from "@/shared/ui";
import { attachService } from "@/entities/attach";
import {
Badge,
- Button,
Table,
TableBody,
TableCell,
@@ -303,63 +301,26 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps)
)}
- {rowFiles.length > 0 ? (
-
- {rowFiles.map((file, fileIndex) => {
- const uploading = isUploading[file.fileKey] ?? false;
-
- return (
-
-
-
- {file.fileName}
-
- {!isRowReadOnly ? (
-
- ) : null}
-
- );
- })}
-
- ) : null}
- {!isRowReadOnly ? (
-
- ) : rowFiles.length === 0 ? (
+ {isRowReadOnly && rowFiles.length === 0 ? (
-
- ) : null}
+ ) : (
+ ({
+ fileKey: file.fileKey,
+ fileName: file.fileName,
+ }))}
+ inputId={fileInputId}
+ isUploadingByKey={isUploading}
+ addDisabled={isRowUploading}
+ showAddButton={!isRowReadOnly}
+ onAdd={(fileList) => void handleFileUpload(index, fileList)}
+ onRemove={
+ isRowReadOnly
+ ? undefined
+ : (fileIndex) => void handleFileRemove(index, fileIndex)
+ }
+ />
+ )}