diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 607d7c3..879778b 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,5 +1,5 @@ import type { Preview } from '@storybook/react'; -import '../src/app/globals.css'; +import './storybook-globals.css'; const preview: Preview = { parameters: { diff --git a/.storybook/storybook-globals.css b/.storybook/storybook-globals.css new file mode 100644 index 0000000..bd74514 --- /dev/null +++ b/.storybook/storybook-globals.css @@ -0,0 +1,14 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --layout-max-w: 100%; +} + +@layer base { + .layout { + @apply mx-auto min-h-[100dvh] w-full; + max-width: var(--layout-max-w); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index 9aa8a28..a69f454 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,8 +2,13 @@ @tailwind components; @tailwind utilities; +:root { + --layout-max-w: 480px; +} + @layer base { .layout { - @apply max-w-[480px] min-h-[100dvh] w-full mx-auto; + @apply mx-auto min-h-[100dvh] w-full; + max-width: var(--layout-max-w); } } diff --git a/src/assets/close_20.svg b/src/assets/close_20.svg new file mode 100644 index 0000000..8515bbb --- /dev/null +++ b/src/assets/close_20.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/plus.svg b/src/assets/plus_20.svg similarity index 100% rename from src/assets/plus.svg rename to src/assets/plus_20.svg diff --git a/src/components/atoms/bottomSheet/style.css b/src/components/atoms/bottomSheet/style.css index c5b03ff..609f9af 100644 --- a/src/components/atoms/bottomSheet/style.css +++ b/src/components/atoms/bottomSheet/style.css @@ -2,11 +2,10 @@ --rsbs-max-w: var(--layout-max-w); --rsbs-content-opacity: 1; --rsbs-backdrop-bg: rgba(0, 0, 0, 0.6); - --rsbs-bg: #1F2028; - --rsbs-handle-bg: #8C8EA6; - --rsbs-max-w: auto; - --rsbs-ml: env(safe-area-inset-left); - --rsbs-mr: env(safe-area-inset-right); + --rsbs-bg: #2c2e3a; + --rsbs-handle-bg: #8c8ea6; + --rsbs-ml: auto; + --rsbs-mr: auto; --rsbs-overlay-rounded: 16px; /* 스크롤바 없음 */ diff --git a/src/features/comment/component/BottomContent.tsx b/src/features/comment/component/BottomContent.tsx new file mode 100644 index 0000000..dd179b9 --- /dev/null +++ b/src/features/comment/component/BottomContent.tsx @@ -0,0 +1,195 @@ +import React, { useEffect, useRef, ChangeEvent } from 'react'; +import { BottomSheet } from '@/components/atoms/bottomSheet'; +import Close from '@/assets/close_20.svg'; + +const DEFAULT_BOTTOM_SHEET_HEIGHT = 290; +const MAX_IMAGE = 9; + +interface BottomContentProps { + images: string[]; + setImages: (images: string[]) => void; + showGallery: boolean; + setShowGallery: (value: boolean) => void; + setBottomSheetHeight: (height: number) => void; +} + +export const BottomContent = ({ + images, + showGallery, + setImages, + setShowGallery, + setBottomSheetHeight, +}: BottomContentProps) => { + const observerRef = useRef(null); + const isFirstOpenRef = useRef(true); + + useEffect(() => { + if (!showGallery && observerRef.current) { + isFirstOpenRef.current = true; + observerRef.current.disconnect(); + observerRef.current = null; + } + }, [showGallery]); + + const observeHeightChanges = () => { + const bottomSheetElement = document.querySelector( + '[data-rsbs-root="true"]', + ) as HTMLElement; + + if (!bottomSheetElement) { + return; + } + + observerRef.current?.disconnect(); + + observerRef.current = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'style') { + if (isFirstOpenRef.current) { + setBottomSheetHeight(DEFAULT_BOTTOM_SHEET_HEIGHT); + return; + } + + const computedStyle = getComputedStyle(bottomSheetElement); + const newHeight = parseInt( + computedStyle.getPropertyValue('--rsbs-overlay-h'), + 10, + ); + const translateY = parseInt( + computedStyle.getPropertyValue('--rsbs-overlay-translate-y'), + 10, + ); + + setBottomSheetHeight( + newHeight === DEFAULT_BOTTOM_SHEET_HEIGHT + ? newHeight - translateY + : newHeight, + ); + } + }); + }); + + observerRef.current.observe(bottomSheetElement, { + attributes: true, + attributeOldValue: true, + }); + }; + + const handleSpringStart = () => { + observeHeightChanges(); + }; + + const handleSpringEnd = () => { + if (showGallery) { + isFirstOpenRef.current = false; + } + }; + + return ( + setShowGallery(false)} + snapPoints={({ maxHeight }) => [DEFAULT_BOTTOM_SHEET_HEIGHT, maxHeight]} + defaultSnap={() => DEFAULT_BOTTOM_SHEET_HEIGHT} + blocking={false} + header={ + + } + > + + + ); +}; + +const BottomSheetHeader = ({ + images, + setImages, + setShowGallery, +}: { + images: string[]; + setImages: (images: string[]) => void; + setShowGallery: (value: boolean) => void; +}) => { + const inputRef = useRef(null); + const openNativeGallery = () => { + inputRef.current?.click(); + }; + + const handleImageUpload = (event: ChangeEvent) => { + if (!event.target.files) return; + + const fileArray = Array.from(event.target.files).map((file) => + URL.createObjectURL(file), + ); + setImages(images ? [...images, ...fileArray] : [...fileArray]); + setShowGallery(true); + }; + + return ( +
+ + +
+ 사진 + + {images?.length ?? 0}/ + {MAX_IMAGE} + +
+ + +
+ ); +}; + +const BottomSheetContent = ({ + images, + setImages, +}: { + images: string[]; + setImages: (images: string[]) => void; +}) => { + return images.length ? ( +
+ {images.map((src, index) => ( +
+ {`User + +
+ ))} +
+ ) : ( +
+ + 앨범에서 사진을 찾아보세요. + +
+ ); +}; diff --git a/src/features/comment/component/CommentBox.stories.tsx b/src/features/comment/component/CommentBox.stories.tsx index f2522e6..f01a8ed 100644 --- a/src/features/comment/component/CommentBox.stories.tsx +++ b/src/features/comment/component/CommentBox.stories.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { CommentBox } from './CommentBox'; @@ -14,11 +13,7 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: { - text: '', - }, - render: (args) => { - const [text, setText] = useState(args.text); - return ; + render: () => { + return ; }, }; diff --git a/src/features/comment/component/CommentBox.tsx b/src/features/comment/component/CommentBox.tsx index fb9779a..33b2b77 100644 --- a/src/features/comment/component/CommentBox.tsx +++ b/src/features/comment/component/CommentBox.tsx @@ -1,38 +1,44 @@ -import Upload from '@/assets/upload.svg'; -import UploadActive from '@/assets/upload_active.svg'; -import { CommentInput } from '@/components/molecules/comment'; -import { ImageUploader } from './ImageUploader'; +import { useState } from 'react'; +import { BottomContent } from './BottomContent'; +import { TextFieldBottom } from './TextFieldBottom'; -interface CommentBoxProps { - text: string; - setText: (text: string) => void; -} +export const CommentBox = () => { + const [text, setText] = useState(''); + const [images, setImages] = useState([]); + const [showGallery, setShowGallery] = useState(false); + const [bottomSheetHeight, setBottomSheetHeight] = useState(0); -export const CommentBox = ({ text, setText }: CommentBoxProps) => { - const handleUploadText = () => { - // text upload - setText(''); + const handleUpload = () => { + // text, image upload + if (text.trim()) { + setText(''); + } + if (showGallery && images.length > 0) { + setImages([]); + setShowGallery(false); + } }; return ( -
- - - + -
- {text.trim() ? : } -
+
); }; diff --git a/src/features/comment/component/ImageUploader.tsx b/src/features/comment/component/ImageUploader.tsx deleted file mode 100644 index 6764ab6..0000000 --- a/src/features/comment/component/ImageUploader.tsx +++ /dev/null @@ -1,98 +0,0 @@ -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) => ( -
- 업로드 하고자 하는 사진 - -
- ))} -
-
- )} - - ); -}; diff --git a/src/features/comment/component/TextFieldBottom.tsx b/src/features/comment/component/TextFieldBottom.tsx new file mode 100644 index 0000000..d4b7282 --- /dev/null +++ b/src/features/comment/component/TextFieldBottom.tsx @@ -0,0 +1,48 @@ +import { CommentInput } from '@/components/molecules/comment'; +import Close from '@/assets/close_20.svg'; +import Plus from '@/assets/plus_20.svg'; +import Upload from '@/assets/upload.svg'; +import UploadActive from '@/assets/upload_active.svg'; + +interface TextFieldBottomProps { + text: string; + setText: (text: string) => void; + showGallery: boolean; + setShowGallery: (value: boolean) => void; + handleUpload: () => void; +} + +export const TextFieldBottom = ({ + text, + setText, + showGallery, + setShowGallery, + handleUpload, +}: TextFieldBottomProps) => { + return ( +
+ + + + + +
+ ); +};