프레임워크 공부하면서 한참동안 블로그를 작성하지 못했다..
굳이 연습하는 코드를 블로그에 적으면 뭐하나 싶기도 했고..
export default function MyEditor() {
// 텍스트 영역
const [text, setText] = useState<string>("HelloWorld");
const [files, setFiles] = useState<File[]>([]);
// 파일 추가 시 확장되는 높이
const [isOpen, setIsOpen] = useState<boolean>(false);
const editorRef = useRef<HTMLDivElement>(null);
const editor = useEditor({
immediatelyRender: false,
extensions: [
StarterKit,
Link.configure({
openOnClick: false,
}),
],
content: text,
onUpdate: ({ editor }) => {
setText(editor.getText());
},
});
return (
<div className="rounded-t-lg rounded-b-lg border-input border">
<EditorContext.Provider value={{ editor }}>
<div className="p-2 bg-muted/50">
<div className="tiptap-button-group" data-orientation="horizontal">
<MarkButton type="bold" />
<MarkButton type="italic" />
<MarkButton type="strike" />
<LinkPopover />
<ListButton type="orderedList" />
<ListButton type="bulletList" />
<BlockquoteButton />
<MarkButton type="code" />
<CodeBlockButton />
</div>
</div>
<div className={`transition-all duration-300 ease-in`} ref={editorRef} style={{}}>
<EditorContent
editor={editor}
role="presentation"
className="bg-transparent rounded-b-md px-3 py-2 text-sm min-h-16 w-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors resize-none"
/>
</div>
<div>
<CustomButton
files={files}
setFiles={setFiles}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
</div>
</EditorContext.Provider>
</div>
);
}
export default function ImagePreview({ files }: ImagePreviewProps) {
const [previews, setPreviews] = useState<string[]>([]);
// 파일이 변경될 때마다 미리보기 생성
useEffect(() => {
const generatePreviews = async () => {
const newPreviews = await Promise.all(
files.map((file) => {
return new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target?.result as string);
reader.readAsDataURL(file);
});
}),
);
setPreviews(newPreviews);
};
generatePreviews();
}, [files]);
return (
<div className="p-4">
<div className="grid grid-cols-8 gap-4">
{files.map((file, index) => (
<div key={index} className="border rounded-lg">
<img
src={previews[index]}
alt={file.name}
className="w-20 h-20 object-cover rounded"
/>
</div>
))}
</div>
</div>
);
}
export default function CustomButton({
files,
setFiles,
setIsOpen,
isOpen,
}: {
files: File[];
setFiles: (files: File[]) => void;
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}) {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = Array.from(e.target.files || []);
setFiles([...files, ...selected]);
setIsOpen(true);
};
return (
<div className="ml-5 mt-3">
{/* 숨겨진 파일 입력 */}
<input
ref={fileInputRef}
type="file"
multiple
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
{/* 동그란 회색 버튼 */}
<div>
<button
onClick={() => fileInputRef.current?.click()}
className="w-[25px] h-[25px] rounded-full bg-gray-300 hover:bg-gray-500 active:bg-gray-700 transition-colors duration-200 shadow-md"
title="파일 선택"
>
<Plus className="h-6 w-6 text-white" />
</button>
</div>
{/* 이미지 미리보기 */}
{files.length > 0 && <ImagePreview files={files} />}
</div>
);
}