diff --git a/src/assets/plus.svg b/src/assets/plus.svg new file mode 100644 index 0000000..f987f5d --- /dev/null +++ b/src/assets/plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/upload.svg b/src/assets/upload.svg new file mode 100644 index 0000000..cd1a085 --- /dev/null +++ b/src/assets/upload.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/upload_active.svg b/src/assets/upload_active.svg new file mode 100644 index 0000000..1393336 --- /dev/null +++ b/src/assets/upload_active.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/molecules/comment/CommentInput.tsx b/src/components/molecules/comment/CommentInput.tsx new file mode 100644 index 0000000..62abf23 --- /dev/null +++ b/src/components/molecules/comment/CommentInput.tsx @@ -0,0 +1,91 @@ +import { + ChangeEvent, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'react'; +import { textAreaVariants } from '@/components/atoms/textArea/TextArea'; + +interface CommentInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + minHeight: number; + maxHeight?: number; + maxLineCount?: number; +} + +export const CommentInput = ({ + value, + onChange, + placeholder, + minHeight, + maxHeight = minHeight, + maxLineCount, + ...props +}: CommentInputProps) => { + const wrapperRef = useRef(null); + const textAreaRef = useRef(null); + const [paddingTop, setPaddingTop] = useState(null); + + const handleTextChange = (event: ChangeEvent) => { + onChange(event.target.value); + }; + + useEffect(() => { + if (!wrapperRef.current) return; + const computedStyle = window.getComputedStyle(wrapperRef.current); + setPaddingTop(parseInt(computedStyle.paddingTop, 10) || null); + }, []); + + useLayoutEffect(() => { + const textArea = textAreaRef.current; + const wrapper = wrapperRef.current; + if (!textArea || !wrapper || paddingTop === null) return; + + const { scrollHeight, clientHeight } = textArea; + textArea.style.height = `${minHeight}px`; + + // 한 줄만 있을 때는 minHeight 유지 + if (scrollHeight === clientHeight) { + wrapper.style.paddingTop = `${paddingTop}px`; + return; + } + + const lineCount = Math.floor( + scrollHeight / parseInt(window.getComputedStyle(textArea).lineHeight, 10), + ); + + if (maxLineCount && lineCount >= maxLineCount) { + textArea.style.height = `${maxHeight + paddingTop}px`; + wrapper.style.paddingTop = '0px'; + } else { + textArea.style.height = `${lineCount === 1 ? minHeight : maxHeight}px`; + wrapper.style.paddingTop = `${paddingTop}px`; + } + }, [value, paddingTop]); + + return ( + + + + ); +}; diff --git a/src/components/molecules/comment/index.ts b/src/components/molecules/comment/index.ts new file mode 100644 index 0000000..c354da4 --- /dev/null +++ b/src/components/molecules/comment/index.ts @@ -0,0 +1 @@ +export { CommentInput } from '@/components/molecules/comment/CommentInput'; diff --git a/src/features/comment/component/CommentBox.stories.tsx b/src/features/comment/component/CommentBox.stories.tsx new file mode 100644 index 0000000..f2522e6 --- /dev/null +++ b/src/features/comment/component/CommentBox.stories.tsx @@ -0,0 +1,24 @@ +import { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { CommentBox } from './CommentBox'; + +const meta: Meta = { + title: 'Components/CommentBox', + component: CommentBox, + parameters: { + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + text: '', + }, + render: (args) => { + const [text, setText] = useState(args.text); + return ; + }, +}; diff --git a/src/features/comment/component/CommentBox.tsx b/src/features/comment/component/CommentBox.tsx new file mode 100644 index 0000000..fb9779a --- /dev/null +++ b/src/features/comment/component/CommentBox.tsx @@ -0,0 +1,38 @@ +import Upload from '@/assets/upload.svg'; +import UploadActive from '@/assets/upload_active.svg'; +import { CommentInput } from '@/components/molecules/comment'; +import { ImageUploader } from './ImageUploader'; + +interface CommentBoxProps { + text: string; + setText: (text: string) => void; +} + +export const CommentBox = ({ text, setText }: CommentBoxProps) => { + const handleUploadText = () => { + // text upload + setText(''); + }; + + return ( + + + + + + + {text.trim() ? : } + + + ); +}; diff --git a/src/features/comment/component/ImageUploader.tsx b/src/features/comment/component/ImageUploader.tsx new file mode 100644 index 0000000..6764ab6 --- /dev/null +++ b/src/features/comment/component/ImageUploader.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import Plus from '@/assets/plus.svg'; + +export const ImageUploader = () => { + const [images, setImages] = useState([]); + const [showGallery, setShowGallery] = useState(false); + + const handleImageUpload = (event: React.ChangeEvent) => { + if (!event.target.files) return; + + const files = Array.from(event.target.files); + const fileArray = files.map((file) => URL.createObjectURL(file)); + + setImages((prev) => [...prev, ...fileArray]); + setShowGallery(true); + }; + + const openNativeGallery = () => { + const fileInput = document.getElementById('fileInput') as HTMLInputElement; + if (fileInput) { + fileInput.click(); + } + }; + + const handleDeleteImage = (index: number) => { + setImages((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleGalleryClose = () => { + // 선택한 사진 닫는 기능 + setImages([]); + setShowGallery(false); + }; + + const handleUploadImage = () => { + // upload Image : images 변수에 저장되어 있음 + setImages([]); + setShowGallery(false); + }; + + return ( + <> + + + + + + + {showGallery && ( + + + + 닫기 + + 선택한 사진 + + 전송 + + + + + {images.map((src, index) => ( + + + handleDeleteImage(index)} + > + ✕ + + + ))} + + + )} + > + ); +};