From 902f7ac3bf07a5348a5bbd2ef6fbe5ad4f273b8c Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 29 Sep 2025 21:08:46 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20input=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/ui/group-study-thumbnail-input.tsx | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 src/features/study/group/ui/group-study-thumbnail-input.tsx diff --git a/src/features/study/group/ui/group-study-thumbnail-input.tsx b/src/features/study/group/ui/group-study-thumbnail-input.tsx new file mode 100644 index 00000000..319193e2 --- /dev/null +++ b/src/features/study/group/ui/group-study-thumbnail-input.tsx @@ -0,0 +1,136 @@ +'use client'; + +import Image from 'next/image'; +import { useState, DragEvent, ChangeEvent, useRef } from 'react'; +import Button from '@/shared/ui/button'; + +const inputStyles = { + base: 'rounded-100 flex w-full flex-col items-center justify-center rounded-lg border-2 p-500', + dragging: 'border-border-brand bg-fill-brand-subtle-hover', + notDragging: 'border-gray-300 border-gray-300 border-dashed', +}; + +export default function GroupStudyThumbnailInput({ + image, + onChangeImage, +}: { + image?: string; + onChangeImage: (image?: string) => void; +}) { + const fileInputRef = useRef(null); + + const handleOpenFileDialog = () => { + fileInputRef.current?.click(); + }; + + const [isDragging, setIsDragging] = useState(false); + + // 영역 안에 드래그 들어왔을 때 + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + // 영역 밖으로 드래그 나갈 때 + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + console.log('drag leave'); + setIsDragging(false); + }; + // 영역 안에서 드래그 중일 때 + const handleDragOver = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + console.log('drag over'); + + if (e.dataTransfer.files) { + setIsDragging(true); + } + }; + // 영역 안에서 drop 했을 때 + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + console.log('drop'); + setIsDragging(false); + + const file = e.dataTransfer.files[0]; // 1장만 허용 + + if (file && file.type.startsWith('image/')) { + onChangeImage(URL.createObjectURL(file)); + } + }; + + // 파일 업로드 버튼으로 파일 선택했을 때 preview 설정 + const handleFileChange = (e: ChangeEvent) => { + const file = e.target.files?.[0]; + + if (file && file.type.startsWith('image/')) { + onChangeImage(URL.createObjectURL(file)); + } + }; + + const handleRemove = () => { + onChangeImage(undefined); + }; + + return ( +
+ {!image ? ( +
+
+ 파일 업로드 + + 드래그하여 파일 업로드 + +
+ + +
+ ) : ( +
+ preview + +
+ )} +
+ ); +} From 468e3f6a2b71701dd6114a23e84ad1980983d69b Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 29 Sep 2025 21:17:15 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=B9=B4=EB=A9=94=EB=9D=BC=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/icons/camera.svg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 public/icons/camera.svg diff --git a/public/icons/camera.svg b/public/icons/camera.svg new file mode 100644 index 00000000..cc6a70ac --- /dev/null +++ b/public/icons/camera.svg @@ -0,0 +1,4 @@ + + + + From 2ebdf884754a900dbcbd247911a23ca255b0e7f0 Mon Sep 17 00:00:00 2001 From: aken-you Date: Mon, 29 Sep 2025 21:17:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EA=B7=B8=EB=A3=B9=20=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=94=94=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20input=20stor?= =?UTF-8?q?ybook=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group-study-thumbnail-input.stories.tsx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/stories/ui/group-study-thumbnail-input.stories.tsx diff --git a/src/stories/ui/group-study-thumbnail-input.stories.tsx b/src/stories/ui/group-study-thumbnail-input.stories.tsx new file mode 100644 index 00000000..948fd35d --- /dev/null +++ b/src/stories/ui/group-study-thumbnail-input.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; +import GroupStudyThumbnailInput from '@/features/study/group/ui/group-study-thumbnail-input'; + +const meta: Meta = { + component: GroupStudyThumbnailInput, + argTypes: { + image: { + description: + '이미지 URL 상태 입니다. (undefined인 경우 이미지가 없는 상태를 의미합니다.)', + }, + onChangeImage: { + description: + '이미지 URL 상태를 변경하는 함수입니다. 파라미터는 이미지 URL 또는 undefined 입니다. (undefined인 경우 이미지가 제거된 상태를 의미합니다.)', + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const [image, setImage] = useState(undefined); + + return ; + }, +};