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)}년 전`;
+};