[as] feat: 첨부파일 컴포넌트 생성 및 적용
This commit is contained in:
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user