[as] feat: 첨부파일 컴포넌트 생성 및 적용

This commit is contained in:
2026-09-11 17:23:45 +09:00
parent d1ccddc1d3
commit a7e0d8f826
10 changed files with 270 additions and 384 deletions
@@ -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<string, boolean>;
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 (
<div className="space-y-2">
{files.length > 0 ? (
<div className="flex flex-wrap gap-2">
{files.map((file, index) => (
<UploadedFileChip
key={file.fileKey}
fileName={file.fileName}
isUploading={isUploadingByKey[file.fileKey] ?? false}
onRemove={onRemove ? () => onRemove(index) : undefined}
/>
))}
</div>
) : null}
{showAddButton ? (
<FileAddButton
inputId={inputId}
accept={accept}
multiple={multiple}
disabled={addDisabled}
onFilesSelected={onAdd}
/>
) : null}
</div>
);
}
export { AttachmentFileField };
export type { AttachmentFileFieldItem, AttachmentFileFieldProps };
+55
View File
@@ -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 (
<label htmlFor={inputId} className={cn("cursor-pointer", disabled && "pointer-events-none")}>
<input
id={inputId}
type="file"
multiple={multiple}
accept={accept}
disabled={disabled}
className="peer sr-only"
aria-label={label}
onChange={(event) => {
onFilesSelected(event.target.files);
event.target.value = "";
}}
/>
<Button
type="button"
size="sm"
variant="outline"
disabled={disabled}
className={cn(
"peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2",
disabled && "pointer-events-none opacity-50",
)}
asChild
>
<span>{label}</span>
</Button>
</label>
);
}
export { FileAddButton };
export type { FileAddButtonProps };
+6
View File
@@ -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";
@@ -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 (
<div 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={isUploading}>
{fileName}
</TextShimmer>
{onRemove ? (
<button
type="button"
aria-label="삭제"
onClick={(event) => {
event.preventDefault();
onRemove();
}}
disabled={isUploading}
>
<X className="size-5 shrink-0 text-destructive" />
</button>
) : null}
</div>
);
}
export { UploadedFileChip };
export type { UploadedFileChipProps };