From a7e0d8f8268666b3de831d6fde757d22ea2f65d3 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 11 Sep 2026 17:23:45 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EC=B2=A8=EB=B6=80=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/esntl-atch-file-form-dialog.tsx | 65 ++------ .../components/pwrk-cnsltn-doc-mng-dialog.tsx | 91 ++---------- .../sections/tech-review-opinion-section.tsx | 50 ++----- .../src/shared/ui/attachment-file-field.tsx | 62 ++++++++ apps/as/src/shared/ui/file-add-button.tsx | 55 +++++++ apps/as/src/shared/ui/index.ts | 6 + apps/as/src/shared/ui/uploaded-file-chip.tsx | 38 +++++ .../ui/attachments-form-section.tsx | 140 +++++------------- .../pbadms-sprt/ui/splmnt-dmnd-dialog.tsx | 66 ++------- .../ui/splmnt-dmnd-form-section.tsx | 81 +++------- 10 files changed, 270 insertions(+), 384 deletions(-) create mode 100644 apps/as/src/shared/ui/attachment-file-field.tsx create mode 100644 apps/as/src/shared/ui/file-add-button.tsx create mode 100644 apps/as/src/shared/ui/uploaded-file-chip.tsx 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) + } + /> + )}