diff --git a/src/assets/images/comment_image.png b/src/assets/images/comment_image.png new file mode 100644 index 0000000..c9e3f09 Binary files /dev/null and b/src/assets/images/comment_image.png differ diff --git a/src/assets/images/default_profile.png b/src/assets/images/default_profile.png new file mode 100644 index 0000000..57f68fc Binary files /dev/null and b/src/assets/images/default_profile.png differ diff --git a/src/assets/images/profile_heart.png b/src/assets/images/profile_heart.png new file mode 100644 index 0000000..409f19c Binary files /dev/null and b/src/assets/images/profile_heart.png differ diff --git a/src/assets/three_dots.svg b/src/assets/three_dots.svg new file mode 100644 index 0000000..b86fbc4 --- /dev/null +++ b/src/assets/three_dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/molecules/commentCard/CommentCard.tsx b/src/components/molecules/commentCard/CommentCard.tsx new file mode 100644 index 0000000..71aa721 --- /dev/null +++ b/src/components/molecules/commentCard/CommentCard.tsx @@ -0,0 +1,47 @@ +'use client'; + +import ThreeDot from '@/assets/three_dots.svg'; +import { detailDate } from '@/utils/time'; +import { CommentDropdown } from '@/features/comment/components'; +import { useDropdown } from '@/hooks/useDropdown'; + +interface CommentCardProps { + name: string; + text: string; + time: Date; +} + +export const CommentCard = ({ name, text, time }: CommentCardProps) => { + const { isDropdownOpen, handleToggleDropdown, dropdownRef, triggerRef } = + useDropdown(); + + const handleClick = (e: React.MouseEvent) => { + handleToggleDropdown(); + e.stopPropagation(); + }; + return ( +
+
+
+
+ {name} + {detailDate(time)} +
+
+ +
+
+ {isDropdownOpen && ( +
+ +
+ )} +
+ {text} +
+ ); +}; diff --git a/src/components/molecules/commentCard/index.ts b/src/components/molecules/commentCard/index.ts new file mode 100644 index 0000000..5d20360 --- /dev/null +++ b/src/components/molecules/commentCard/index.ts @@ -0,0 +1 @@ +export { CommentCard } from './CommentCard'; diff --git a/src/components/molecules/foldableSpan/FoldableImage.tsx b/src/components/molecules/foldableSpan/FoldableImage.tsx new file mode 100644 index 0000000..73d6fa3 --- /dev/null +++ b/src/components/molecules/foldableSpan/FoldableImage.tsx @@ -0,0 +1,54 @@ +'use client'; + +import DownArrow from '@/assets/down_arrow.svg'; +import TopArrow from '@/assets/top_arrow.svg'; +import { StaticImageData } from 'next/image'; +import { useState } from 'react'; +import Image from 'next/image'; + +interface FoldableImageProps { + spanText: string; + images: StaticImageData[]; +} + +export const FoldableImage = ({ spanText, images }: FoldableImageProps) => { + const [isOpen, setIsOpen] = useState(false); + + if (images.length <= 3) return null; + + if (isOpen) + return ( +
+
+ {images?.map((image) => ( + 이미지 + ))} +
+
setIsOpen(false)} + > + 접기 +
+
+ ); + + return ( +
+
+ {images + ?.slice(0, 3) + .map((image) => ( + 이미지 + ))} +
+
setIsOpen(true)} + > + {spanText} + +
+
+ ); +}; diff --git a/src/components/molecules/foldableSpan/index.ts b/src/components/molecules/foldableSpan/index.ts new file mode 100644 index 0000000..e66af13 --- /dev/null +++ b/src/components/molecules/foldableSpan/index.ts @@ -0,0 +1 @@ +export { FoldableImage } from './FoldableImage'; diff --git a/src/features/comment/components/Comment.stories.tsx b/src/features/comment/components/Comment.stories.tsx new file mode 100644 index 0000000..064b198 --- /dev/null +++ b/src/features/comment/components/Comment.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import CommentImage from '@/assets/images/comment_image.png'; +import DefaultProfile from '@/assets/images/default_profile.png'; +import { Comment } from './'; + +const meta: Meta = { + title: 'Components/Comment', + component: Comment, +}; + +export default meta; +type Story = StoryObj; + +// 기본 카드 +export const Default: Story = { + args: { + name: '예신', + text: '여기 완전 좋다 그치? 여기로 가볼까?', + time: new Date(), + profile: DefaultProfile, + images: [ + CommentImage, + CommentImage, + CommentImage, + CommentImage, + CommentImage, + CommentImage, + CommentImage, + CommentImage, + CommentImage, + ], + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; + +Default.parameters = { + design: { + type: 'figma', + url: 'https://www.figma.com/design/cr2DuY0vceiMI5LlqWdKR2/Wedvice_%EB%94%94%EC%9E%90%EC%9D%B8?node-id=1493-6711&t=C1C6AY6noGiQkf3h-4', + }, +}; diff --git a/src/features/comment/components/Comment.tsx b/src/features/comment/components/Comment.tsx new file mode 100644 index 0000000..a45f0e1 --- /dev/null +++ b/src/features/comment/components/Comment.tsx @@ -0,0 +1,37 @@ +import Image, { StaticImageData } from 'next/image'; +import { FoldableImage } from '../../../components/molecules/foldableSpan'; +import { CommentCard } from '@/components/molecules/commentCard'; + +interface CommentProps { + name: string; + profile: StaticImageData; + text: string; + time: Date; + images?: StaticImageData[]; +} + +export const Comment = ({ + name, + profile, + text, + time, + images = [], +}: CommentProps) => { + return ( +
+
+
+ 프로필 +
+
+ + + +
+
+
+ ); +}; diff --git a/src/features/comment/components/CommentDropdown.tsx b/src/features/comment/components/CommentDropdown.tsx new file mode 100644 index 0000000..beb4eb2 --- /dev/null +++ b/src/features/comment/components/CommentDropdown.tsx @@ -0,0 +1,9 @@ +export const CommentDropdown = () => { + return ( +
+ +
+ +
+ ); +}; diff --git a/src/features/comment/components/index.ts b/src/features/comment/components/index.ts new file mode 100644 index 0000000..5cbc54c --- /dev/null +++ b/src/features/comment/components/index.ts @@ -0,0 +1,2 @@ +export { CommentDropdown } from './CommentDropdown'; +export { Comment } from './Comment'; diff --git a/src/hooks/useDropdown.ts b/src/hooks/useDropdown.ts new file mode 100644 index 0000000..4d4aff2 --- /dev/null +++ b/src/hooks/useDropdown.ts @@ -0,0 +1,36 @@ +import { useState, useEffect, useRef } from 'react'; + +export const useDropdown = () => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + const triggerRef = useRef(null); + + const handleToggleDropdown = (e?: React.MouseEvent) => { + if (e) e.stopPropagation(); + setIsDropdownOpen((prev) => !prev); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + // 여기 조건문을 통해 클릭한 영역이 ThreeDot이 아니면 dropdown 컴포넌트가 사라지도록 만듭니다. + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + triggerRef.current && + !triggerRef.current.contains(event.target as Node) + ) { + setIsDropdownOpen(false); + } + }; + + if (isDropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isDropdownOpen]); + + return { isDropdownOpen, handleToggleDropdown, dropdownRef, triggerRef }; +}; diff --git a/src/utils/time.ts b/src/utils/time.ts index e2c6928..197721e 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -26,3 +26,22 @@ export const getFormattedYearMonth = (time: string) => { return `${year}년 ${month}월`; }; + +export const detailDate = (pastTime: Date) => { + const milliSeconds = + new Date().getMilliseconds() - pastTime.getMilliseconds(); + const seconds = milliSeconds / 1000; + if (seconds < 60) return `방금 전`; + const minutes = seconds / 60; + if (minutes < 60) return `${Math.floor(minutes)}분 전`; + const hours = minutes / 60; + if (hours < 24) return `${Math.floor(hours)}시간 전`; + const days = hours / 24; + if (days < 7) return `${Math.floor(days)}일 전`; + const weeks = days / 7; + if (weeks < 5) return `${Math.floor(weeks)}주 전`; + const months = days / 30; + if (months < 12) return `${Math.floor(months)}개월 전`; + const years = days / 365; + return `${Math.floor(years)}년 전`; +};