From c76e5aa052f03909dd00508f2be51a8f890523e0 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 19 Aug 2025 11:21:08 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20UI=20=EB=B0=8F=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/CommentModal/CommentModal.tsx | 192 ++++++++++++++++++ .../memory/RecordItem/RecordItem.tsx | 27 ++- 2 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 src/components/memory/CommentModal/CommentModal.tsx diff --git a/src/components/memory/CommentModal/CommentModal.tsx b/src/components/memory/CommentModal/CommentModal.tsx new file mode 100644 index 00000000..b8d8b492 --- /dev/null +++ b/src/components/memory/CommentModal/CommentModal.tsx @@ -0,0 +1,192 @@ +import { useState, useEffect } from 'react'; +import styled from '@emotion/styled'; +import { colors, typography } from '@/styles/global/global'; +import MessageInput from '@/components/today-words/MessageInput'; +import ReplyList from '@/components/common/Post/ReplyList'; +import { getComments, type CommentData } from '@/api/comments/getComments'; +import { postReply } from '@/api/comments/postReply'; +import { useReplyActions } from '@/hooks/useReplyActions'; + +interface CommentModalProps { + isOpen: boolean; + onClose: () => void; + postId: number; + postType: 'RECORD' | 'VOTE'; +} + +const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) => { + const [commentList, setCommentList] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isSending, setIsSending] = useState(false); + + const { replyTarget, isReplying, endReply } = useReplyActions(); + + // 댓글 목록 로드 + const loadComments = async () => { + if (!isOpen) return; + + setIsLoading(true); + try { + const response = await getComments(postId, { + postType, + size: 20, + }); + + if (response.data) { + setCommentList(response.data.commentList); + } + } catch (error) { + console.error('댓글 로드 실패:', error); + } finally { + setIsLoading(false); + } + }; + + // 댓글 전송 + const handleSendComment = async () => { + if (!inputValue.trim() || isSending) return; + + setIsSending(true); + try { + const requestData = { + content: inputValue.trim(), + isReplyRequest: isReplying, + parentId: isReplying ? replyTarget?.commentId || null : null, + postType, + }; + + const response = await postReply(postId, requestData); + + if (response.isSuccess) { + setInputValue(''); + endReply(); + // 댓글 목록 새로고침 + await loadComments(); + } + } catch (error) { + console.error('댓글 전송 실패:', error); + } finally { + setIsSending(false); + } + }; + + // 답글 취소 + const handleCancelReply = () => { + endReply(); + }; + + // 모달이 열릴 때 댓글 로드 + useEffect(() => { + if (isOpen) { + loadComments(); + } + }, [isOpen, postId]); + + // 모달이 닫힐 때 상태 초기화 + useEffect(() => { + if (!isOpen) { + setInputValue(''); + endReply(); + setCommentList([]); + } + }, [isOpen]); + + if (!isOpen) return null; + + return ( + + e.stopPropagation()}> +
+ 댓글 +
+ + + {isLoading ? ( + 댓글을 불러오는 중... + ) : ( + + )} + + + + + +
+
+ ); +}; + +const Overlay = styled.div` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: flex-end; +`; + +const ModalContainer = styled.div` + width: 100%; + max-width: 540px; + min-width: 360px; + margin: 0 auto; + background: ${colors.grey[400]}; + border-radius: 16px 16px 0 0; + display: flex; + flex-direction: column; + max-height: 80vh; + min-height: 50vh; +`; + +const Header = styled.div` + padding: 20px; + border-bottom: 1px solid ${colors.grey[200]}; + flex-shrink: 0; +`; + +const Title = styled.h2` + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; + margin: 0; +`; + +const Content = styled.div` + flex: 1; + overflow-y: auto; + min-height: 0; +`; + +const LoadingState = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 200px; + color: ${colors.grey[100]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; +`; + +const InputSection = styled.div` + padding: 20px; + border-top: 1px solid ${colors.grey[400]}; + flex-shrink: 0; +`; + +export default CommentModal; diff --git a/src/components/memory/RecordItem/RecordItem.tsx b/src/components/memory/RecordItem/RecordItem.tsx index c65458d0..0a4a9f5a 100644 --- a/src/components/memory/RecordItem/RecordItem.tsx +++ b/src/components/memory/RecordItem/RecordItem.tsx @@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import type { Record } from '../../../types/memory'; import TextRecord from './TextRecord'; import PollRecord from './PollRecord'; +import CommentModal from '../CommentModal/CommentModal'; import heartIcon from '../../../assets/memory/heart.svg'; import heartFilledIcon from '../../../assets/memory/heart-filled.svg'; import commentIcon from '../../../assets/memory/comment.svg'; @@ -52,6 +53,9 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { // 좋아요 상태 관리 - record 객체에서 isLiked 속성 가져오기 const [isLiked, setIsLiked] = useState(record.isLiked || false); const [currentLikeCount, setCurrentLikeCount] = useState(likeCount); + + // 댓글 모달 상태 관리 + const [isCommentModalOpen, setIsCommentModalOpen] = useState(false); // 길게 누르기 상태 관리 const [isPressed, setIsPressed] = useState(false); @@ -254,6 +258,16 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { }); }, [openConfirm, handlePinRecord]); + // 댓글 버튼 클릭 핸들러 + const handleCommentClick = useCallback(() => { + setIsCommentModalOpen(true); + }, []); + + // 댓글 모달 닫기 핸들러 + const handleCommentModalClose = useCallback(() => { + setIsCommentModalOpen(false); + }, []); + // 길게 누르기 이벤트 핸들러 const handleTouchStart = useCallback( (e: React.TouchEvent) => { @@ -367,7 +381,10 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { /> {currentLikeCount} - + { + e.stopPropagation(); + handleCommentClick(); + }}> 댓글 {commentCount} @@ -382,6 +399,14 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { )} + + {/* 댓글 모달 */} + ); }; From 8ab41c79e6422d2e5997f0589732c49482d7e5b8 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 19 Aug 2025 11:24:44 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20TypeScript=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../memory/CommentModal/CommentModal.tsx | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/memory/CommentModal/CommentModal.tsx b/src/components/memory/CommentModal/CommentModal.tsx index b8d8b492..59df5c35 100644 --- a/src/components/memory/CommentModal/CommentModal.tsx +++ b/src/components/memory/CommentModal/CommentModal.tsx @@ -1,10 +1,9 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import styled from '@emotion/styled'; import { colors, typography } from '@/styles/global/global'; import MessageInput from '@/components/today-words/MessageInput'; import ReplyList from '@/components/common/Post/ReplyList'; import { getComments, type CommentData } from '@/api/comments/getComments'; -import { postReply } from '@/api/comments/postReply'; import { useReplyActions } from '@/hooks/useReplyActions'; interface CommentModalProps { @@ -20,10 +19,10 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); - const { replyTarget, isReplying, endReply } = useReplyActions(); + const { nickname, isReplying, cancelReply, setReplyContent, submitComment } = useReplyActions(); // 댓글 목록 로드 - const loadComments = async () => { + const loadComments = useCallback(async () => { if (!isOpen) return; setIsLoading(true); @@ -41,7 +40,7 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) } finally { setIsLoading(false); } - }; + }, [isOpen, postId, postType]); // 댓글 전송 const handleSendComment = async () => { @@ -49,21 +48,19 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) setIsSending(true); try { - const requestData = { - content: inputValue.trim(), - isReplyRequest: isReplying, - parentId: isReplying ? replyTarget?.commentId || null : null, + // useReplyActions의 replyContent를 현재 inputValue로 설정 + setReplyContent(inputValue.trim()); + + // submitComment 사용 + await submitComment({ + postId, postType, - }; - - const response = await postReply(postId, requestData); - - if (response.isSuccess) { - setInputValue(''); - endReply(); - // 댓글 목록 새로고침 - await loadComments(); - } + onSuccess: async () => { + setInputValue(''); + // 댓글 목록 새로고침 + await loadComments(); + } + }); } catch (error) { console.error('댓글 전송 실패:', error); } finally { @@ -73,7 +70,7 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) // 답글 취소 const handleCancelReply = () => { - endReply(); + cancelReply(); }; // 모달이 열릴 때 댓글 로드 @@ -81,16 +78,16 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) if (isOpen) { loadComments(); } - }, [isOpen, postId]); + }, [isOpen, postId, loadComments]); // 모달이 닫힐 때 상태 초기화 useEffect(() => { if (!isOpen) { setInputValue(''); - endReply(); + cancelReply(); setCommentList([]); } - }, [isOpen]); + }, [isOpen, cancelReply]); if (!isOpen) return null; @@ -112,14 +109,14 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) From edddf87fd43869eaf1d24d3ce090d6d347c1ba1f Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 19 Aug 2025 11:34:02 +0900 Subject: [PATCH 3/7] fix: modal -> bottomsheet --- .../CommentBottomSheet.styled.ts | 70 ++++++++++++ .../CommentBottomSheet.tsx} | 106 +++++------------- .../memory/RecordItem/RecordItem.tsx | 6 +- 3 files changed, 104 insertions(+), 78 deletions(-) create mode 100644 src/components/memory/CommentBottomSheet/CommentBottomSheet.styled.ts rename src/components/memory/{CommentModal/CommentModal.tsx => CommentBottomSheet/CommentBottomSheet.tsx} (63%) diff --git a/src/components/memory/CommentBottomSheet/CommentBottomSheet.styled.ts b/src/components/memory/CommentBottomSheet/CommentBottomSheet.styled.ts new file mode 100644 index 00000000..b5c9e047 --- /dev/null +++ b/src/components/memory/CommentBottomSheet/CommentBottomSheet.styled.ts @@ -0,0 +1,70 @@ +import styled from '@emotion/styled'; +import { colors, typography } from '@/styles/global/global'; + +export const Overlay = styled.div<{ isOpen: boolean }>` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, ${props => props.isOpen ? '0.5' : '0'}); + z-index: 1000; + display: flex; + align-items: flex-end; + justify-content: center; + transition: background-color 0.3s ease; + opacity: ${props => props.isOpen ? 1 : 0}; + visibility: ${props => props.isOpen ? 'visible' : 'hidden'}; +`; + +export const BottomSheet = styled.div<{ isOpen: boolean }>` + width: 100%; + max-width: 540px; + min-width: 360px; + background: ${colors.darkgrey.main}; + border-radius: 16px 16px 0 0; + display: flex; + flex-direction: column; + max-height: 80vh; + min-height: 50vh; + transform: translateY(${props => props.isOpen ? '0' : '100%'}); + transition: transform 0.3s ease; + overflow: hidden; +`; + +export const Header = styled.div` + padding: 20px; + border-bottom: 1px solid ${colors.grey[400]}; + flex-shrink: 0; +`; + +export const Title = styled.h2` + color: ${colors.white}; + font-size: ${typography.fontSize.lg}; + font-weight: ${typography.fontWeight.semibold}; + line-height: 24px; + margin: 0; +`; + +export const Content = styled.div` + flex: 1; + overflow-y: auto; + min-height: 0; +`; + +export const LoadingState = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 200px; + color: ${colors.grey[100]}; + font-size: ${typography.fontSize.sm}; + font-weight: ${typography.fontWeight.regular}; +`; + +export const InputSection = styled.div` + padding: 20px; + border-top: 1px solid ${colors.grey[400]}; + flex-shrink: 0; + background: ${colors.darkgrey.main}; +`; \ No newline at end of file diff --git a/src/components/memory/CommentModal/CommentModal.tsx b/src/components/memory/CommentBottomSheet/CommentBottomSheet.tsx similarity index 63% rename from src/components/memory/CommentModal/CommentModal.tsx rename to src/components/memory/CommentBottomSheet/CommentBottomSheet.tsx index 59df5c35..85e41c86 100644 --- a/src/components/memory/CommentModal/CommentModal.tsx +++ b/src/components/memory/CommentBottomSheet/CommentBottomSheet.tsx @@ -1,24 +1,31 @@ import { useState, useEffect, useCallback } from 'react'; -import styled from '@emotion/styled'; -import { colors, typography } from '@/styles/global/global'; import MessageInput from '@/components/today-words/MessageInput'; import ReplyList from '@/components/common/Post/ReplyList'; import { getComments, type CommentData } from '@/api/comments/getComments'; import { useReplyActions } from '@/hooks/useReplyActions'; - -interface CommentModalProps { +import { + Overlay, + BottomSheet, + Header, + Title, + Content, + LoadingState, + InputSection, +} from './CommentBottomSheet.styled'; + +interface CommentBottomSheetProps { isOpen: boolean; onClose: () => void; postId: number; postType: 'RECORD' | 'VOTE'; } -const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) => { +const CommentBottomSheet = ({ isOpen, onClose, postId, postType }: CommentBottomSheetProps) => { const [commentList, setCommentList] = useState([]); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); - + const { nickname, isReplying, cancelReply, setReplyContent, submitComment } = useReplyActions(); // 댓글 목록 로드 @@ -73,14 +80,21 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) cancelReply(); }; - // 모달이 열릴 때 댓글 로드 + // Overlay 클릭 처리 + const handleOverlayClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + onClose(); + } + }; + + // 바텀시트가 열릴 때 댓글 로드 useEffect(() => { if (isOpen) { loadComments(); } }, [isOpen, postId, loadComments]); - // 모달이 닫힐 때 상태 초기화 + // 바텀시트가 닫힐 때 상태 초기화 useEffect(() => { if (!isOpen) { setInputValue(''); @@ -92,17 +106,20 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) if (!isOpen) return null; return ( - - e.stopPropagation()}> + +
댓글
- + {isLoading ? ( 댓글을 불러오는 중... ) : ( - + )} @@ -120,70 +137,9 @@ const CommentModal = ({ isOpen, onClose, postId, postType }: CommentModalProps) disabled={isSending} /> -
+
); }; -const Overlay = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 1000; - display: flex; - align-items: flex-end; -`; - -const ModalContainer = styled.div` - width: 100%; - max-width: 540px; - min-width: 360px; - margin: 0 auto; - background: ${colors.grey[400]}; - border-radius: 16px 16px 0 0; - display: flex; - flex-direction: column; - max-height: 80vh; - min-height: 50vh; -`; - -const Header = styled.div` - padding: 20px; - border-bottom: 1px solid ${colors.grey[200]}; - flex-shrink: 0; -`; - -const Title = styled.h2` - color: ${colors.white}; - font-size: ${typography.fontSize.lg}; - font-weight: ${typography.fontWeight.semibold}; - line-height: 24px; - margin: 0; -`; - -const Content = styled.div` - flex: 1; - overflow-y: auto; - min-height: 0; -`; - -const LoadingState = styled.div` - display: flex; - align-items: center; - justify-content: center; - height: 200px; - color: ${colors.grey[100]}; - font-size: ${typography.fontSize.sm}; - font-weight: ${typography.fontWeight.regular}; -`; - -const InputSection = styled.div` - padding: 20px; - border-top: 1px solid ${colors.grey[400]}; - flex-shrink: 0; -`; - -export default CommentModal; +export default CommentBottomSheet; \ No newline at end of file diff --git a/src/components/memory/RecordItem/RecordItem.tsx b/src/components/memory/RecordItem/RecordItem.tsx index 0a4a9f5a..e7e7b4f6 100644 --- a/src/components/memory/RecordItem/RecordItem.tsx +++ b/src/components/memory/RecordItem/RecordItem.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import type { Record } from '../../../types/memory'; import TextRecord from './TextRecord'; import PollRecord from './PollRecord'; -import CommentModal from '../CommentModal/CommentModal'; +import CommentBottomSheet from '../CommentBottomSheet/CommentBottomSheet'; import heartIcon from '../../../assets/memory/heart.svg'; import heartFilledIcon from '../../../assets/memory/heart-filled.svg'; import commentIcon from '../../../assets/memory/comment.svg'; @@ -400,8 +400,8 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { )} - {/* 댓글 모달 */} - Date: Tue, 19 Aug 2025 11:39:44 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EC=A0=84=EC=97=AD=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 ++ .../GlobalCommentBottomSheet.styled.ts} | 0 .../GlobalCommentBottomSheet.tsx} | 22 +++++++--------- .../memory/RecordItem/RecordItem.tsx | 22 ++++------------ src/stores/useCommentBottomSheetStore.ts | 26 +++++++++++++++++++ 5 files changed, 42 insertions(+), 30 deletions(-) rename src/components/{memory/CommentBottomSheet/CommentBottomSheet.styled.ts => common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts} (100%) rename src/components/{memory/CommentBottomSheet/CommentBottomSheet.tsx => common/CommentBottomSheet/GlobalCommentBottomSheet.tsx} (88%) create mode 100644 src/stores/useCommentBottomSheetStore.ts diff --git a/src/App.tsx b/src/App.tsx index 2af98459..7a29b80a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import Router from './pages'; import { Global } from '@emotion/react'; import { globalStyles } from './styles/global/global'; import PopupContainer from './components/common/Modal/PopupContainer'; +import GlobalCommentBottomSheet from './components/common/CommentBottomSheet/GlobalCommentBottomSheet'; const App = () => { return ( @@ -9,6 +10,7 @@ const App = () => { + ); }; diff --git a/src/components/memory/CommentBottomSheet/CommentBottomSheet.styled.ts b/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts similarity index 100% rename from src/components/memory/CommentBottomSheet/CommentBottomSheet.styled.ts rename to src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts diff --git a/src/components/memory/CommentBottomSheet/CommentBottomSheet.tsx b/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx similarity index 88% rename from src/components/memory/CommentBottomSheet/CommentBottomSheet.tsx rename to src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx index 85e41c86..8e5c9492 100644 --- a/src/components/memory/CommentBottomSheet/CommentBottomSheet.tsx +++ b/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx @@ -3,6 +3,7 @@ import MessageInput from '@/components/today-words/MessageInput'; import ReplyList from '@/components/common/Post/ReplyList'; import { getComments, type CommentData } from '@/api/comments/getComments'; import { useReplyActions } from '@/hooks/useReplyActions'; +import { useCommentBottomSheetStore } from '@/stores/useCommentBottomSheetStore'; import { Overlay, BottomSheet, @@ -11,16 +12,11 @@ import { Content, LoadingState, InputSection, -} from './CommentBottomSheet.styled'; +} from './GlobalCommentBottomSheet.styled'; -interface CommentBottomSheetProps { - isOpen: boolean; - onClose: () => void; - postId: number; - postType: 'RECORD' | 'VOTE'; -} - -const CommentBottomSheet = ({ isOpen, onClose, postId, postType }: CommentBottomSheetProps) => { +const GlobalCommentBottomSheet = () => { + const { isOpen, postId, postType, closeCommentBottomSheet } = useCommentBottomSheetStore(); + const [commentList, setCommentList] = useState([]); const [inputValue, setInputValue] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -30,7 +26,7 @@ const CommentBottomSheet = ({ isOpen, onClose, postId, postType }: CommentBottom // 댓글 목록 로드 const loadComments = useCallback(async () => { - if (!isOpen) return; + if (!isOpen || !postId || !postType) return; setIsLoading(true); try { @@ -51,7 +47,7 @@ const CommentBottomSheet = ({ isOpen, onClose, postId, postType }: CommentBottom // 댓글 전송 const handleSendComment = async () => { - if (!inputValue.trim() || isSending) return; + if (!inputValue.trim() || isSending || !postId || !postType) return; setIsSending(true); try { @@ -83,7 +79,7 @@ const CommentBottomSheet = ({ isOpen, onClose, postId, postType }: CommentBottom // Overlay 클릭 처리 const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { - onClose(); + closeCommentBottomSheet(); } }; @@ -142,4 +138,4 @@ const CommentBottomSheet = ({ isOpen, onClose, postId, postType }: CommentBottom ); }; -export default CommentBottomSheet; \ No newline at end of file +export default GlobalCommentBottomSheet; \ No newline at end of file diff --git a/src/components/memory/RecordItem/RecordItem.tsx b/src/components/memory/RecordItem/RecordItem.tsx index e7e7b4f6..cea788b9 100644 --- a/src/components/memory/RecordItem/RecordItem.tsx +++ b/src/components/memory/RecordItem/RecordItem.tsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import type { Record } from '../../../types/memory'; import TextRecord from './TextRecord'; import PollRecord from './PollRecord'; -import CommentBottomSheet from '../CommentBottomSheet/CommentBottomSheet'; +import { useCommentBottomSheetStore } from '@/stores/useCommentBottomSheetStore'; import heartIcon from '../../../assets/memory/heart.svg'; import heartFilledIcon from '../../../assets/memory/heart-filled.svg'; import commentIcon from '../../../assets/memory/comment.svg'; @@ -54,8 +54,8 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { const [isLiked, setIsLiked] = useState(record.isLiked || false); const [currentLikeCount, setCurrentLikeCount] = useState(likeCount); - // 댓글 모달 상태 관리 - const [isCommentModalOpen, setIsCommentModalOpen] = useState(false); + // 전역 댓글 바텀시트 + const { openCommentBottomSheet } = useCommentBottomSheetStore(); // 길게 누르기 상태 관리 const [isPressed, setIsPressed] = useState(false); @@ -260,13 +260,8 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { // 댓글 버튼 클릭 핸들러 const handleCommentClick = useCallback(() => { - setIsCommentModalOpen(true); - }, []); - - // 댓글 모달 닫기 핸들러 - const handleCommentModalClose = useCallback(() => { - setIsCommentModalOpen(false); - }, []); + openCommentBottomSheet(parseInt(id), type === 'poll' ? 'VOTE' : 'RECORD'); + }, [openCommentBottomSheet, id, type]); // 길게 누르기 이벤트 핸들러 const handleTouchStart = useCallback( @@ -400,13 +395,6 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => { )} - {/* 댓글 바텀시트 */} - ); }; diff --git a/src/stores/useCommentBottomSheetStore.ts b/src/stores/useCommentBottomSheetStore.ts new file mode 100644 index 00000000..8a458e5a --- /dev/null +++ b/src/stores/useCommentBottomSheetStore.ts @@ -0,0 +1,26 @@ +import { create } from 'zustand'; + +interface CommentBottomSheetState { + isOpen: boolean; + postId: number | null; + postType: 'RECORD' | 'VOTE' | null; +} + +interface CommentBottomSheetActions { + openCommentBottomSheet: (postId: number, postType: 'RECORD' | 'VOTE') => void; + closeCommentBottomSheet: () => void; +} + +export const useCommentBottomSheetStore = create((set) => ({ + // 상태 + isOpen: false, + postId: null, + postType: null, + + // 액션 + openCommentBottomSheet: (postId: number, postType: 'RECORD' | 'VOTE') => + set({ isOpen: true, postId, postType }), + + closeCommentBottomSheet: () => + set({ isOpen: false, postId: null, postType: null }), +})); \ No newline at end of file From e583630472192885e6288071295d764755f14a02 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 19 Aug 2025 11:47:07 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20=EC=A0=84=EC=97=AD=EC=9D=B4=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=B3=84=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 -- src/pages/memory/Memory.tsx | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7a29b80a..2af98459 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,6 @@ import Router from './pages'; import { Global } from '@emotion/react'; import { globalStyles } from './styles/global/global'; import PopupContainer from './components/common/Modal/PopupContainer'; -import GlobalCommentBottomSheet from './components/common/CommentBottomSheet/GlobalCommentBottomSheet'; const App = () => { return ( @@ -10,7 +9,6 @@ const App = () => { - ); }; diff --git a/src/pages/memory/Memory.tsx b/src/pages/memory/Memory.tsx index baf4cf7d..083ae705 100644 --- a/src/pages/memory/Memory.tsx +++ b/src/pages/memory/Memory.tsx @@ -5,6 +5,7 @@ import MemoryHeader from '../../components/memory/MemoryHeader/MemoryHeader'; import MemoryContent from '../../components/memory/MemoryContent/MemoryContent'; import MemoryAddButton from '../../components/memory/MemoryAddButton/MemoryAddButton'; import Snackbar from '../../components/common/Modal/Snackbar'; +import GlobalCommentBottomSheet from '../../components/common/CommentBottomSheet/GlobalCommentBottomSheet'; import { Container, FixedHeader, ScrollableContent, FloatingElements } from './Memory.styled'; import { getMemoryPosts } from '../../api/memory/getMemoryPosts'; import type { GetMemoryPostsParams, Post, Record } from '../../types/memory'; @@ -295,6 +296,8 @@ const Memory = () => { onClose={() => setShowSnackbar(false)} /> )} + + ); }; From 1321bc2fffe10ef6d8ed9b081524a982a66be601 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 19 Aug 2025 11:50:38 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=9E=85=EB=A0=A5=20=EC=A7=80?= =?UTF-8?q?=EC=97=B0=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useReplyActions 대신 postReply API 직접 사용 - 상태 동기화 지연으로 인한 댓글 전송 버그 수정 - 댓글 작성 시 즉시 업로드되도록 개선 - BookSearchBottomSheet 패턴으로 Memory 페이지에서만 렌더링 --- .../GlobalCommentBottomSheet.tsx | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx b/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx index 8e5c9492..58b274d1 100644 --- a/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx +++ b/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx @@ -2,7 +2,9 @@ import { useState, useEffect, useCallback } from 'react'; import MessageInput from '@/components/today-words/MessageInput'; import ReplyList from '@/components/common/Post/ReplyList'; import { getComments, type CommentData } from '@/api/comments/getComments'; +import { postReply } from '@/api/comments/postReply'; import { useReplyActions } from '@/hooks/useReplyActions'; +import { useReplyStore } from '@/stores/useReplyStore'; import { useCommentBottomSheetStore } from '@/stores/useCommentBottomSheetStore'; import { Overlay, @@ -22,7 +24,8 @@ const GlobalCommentBottomSheet = () => { const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); - const { nickname, isReplying, cancelReply, setReplyContent, submitComment } = useReplyActions(); + const { nickname, isReplying, cancelReply } = useReplyActions(); + const { parentId } = useReplyStore(); // 댓글 목록 로드 const loadComments = useCallback(async () => { @@ -51,19 +54,21 @@ const GlobalCommentBottomSheet = () => { setIsSending(true); try { - // useReplyActions의 replyContent를 현재 inputValue로 설정 - setReplyContent(inputValue.trim()); + const requestData = { + content: inputValue.trim(), + isReplyRequest: isReplying, + parentId: isReplying ? parentId : null, + postType: postType as 'FEED' | 'RECORD' | 'VOTE' + }; + + const response = await postReply(postId, requestData); - // submitComment 사용 - await submitComment({ - postId, - postType, - onSuccess: async () => { - setInputValue(''); - // 댓글 목록 새로고침 - await loadComments(); - } - }); + if (response.isSuccess) { + setInputValue(''); + cancelReply(); // 답글 상태 초기화 + // 댓글 목록 새로고침 + await loadComments(); + } } catch (error) { console.error('댓글 전송 실패:', error); } finally { From c51a04da91cf6301d35d627668c68496d24fdb75 Mon Sep 17 00:00:00 2001 From: fr0gydev Date: Tue, 19 Aug 2025 12:02:01 +0900 Subject: [PATCH 7/7] =?UTF-8?q?style:=20=EC=9D=BC=EB=B6=80=20=EC=8A=A4?= =?UTF-8?q?=ED=83=80=EC=9D=BC=EB=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GlobalCommentBottomSheet.styled.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts b/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts index b5c9e047..350e6589 100644 --- a/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts +++ b/src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.styled.ts @@ -7,14 +7,14 @@ export const Overlay = styled.div<{ isOpen: boolean }>` left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, ${props => props.isOpen ? '0.5' : '0'}); + background: rgba(0, 0, 0, ${props => (props.isOpen ? '0.5' : '0')}); z-index: 1000; display: flex; align-items: flex-end; justify-content: center; transition: background-color 0.3s ease; - opacity: ${props => props.isOpen ? 1 : 0}; - visibility: ${props => props.isOpen ? 'visible' : 'hidden'}; + opacity: ${props => (props.isOpen ? 1 : 0)}; + visibility: ${props => (props.isOpen ? 'visible' : 'hidden')}; `; export const BottomSheet = styled.div<{ isOpen: boolean }>` @@ -22,26 +22,26 @@ export const BottomSheet = styled.div<{ isOpen: boolean }>` max-width: 540px; min-width: 360px; background: ${colors.darkgrey.main}; - border-radius: 16px 16px 0 0; + border-radius: 20px 20px 0 0; display: flex; flex-direction: column; max-height: 80vh; min-height: 50vh; - transform: translateY(${props => props.isOpen ? '0' : '100%'}); + transform: translateY(${props => (props.isOpen ? '0' : '100%')}); transition: transform 0.3s ease; overflow: hidden; `; export const Header = styled.div` padding: 20px; - border-bottom: 1px solid ${colors.grey[400]}; + padding-bottom: 10px; flex-shrink: 0; `; export const Title = styled.h2` color: ${colors.white}; - font-size: ${typography.fontSize.lg}; - font-weight: ${typography.fontWeight.semibold}; + font-size: 20px; + font-weight: ${typography.fontWeight.bold}; line-height: 24px; margin: 0; `; @@ -67,4 +67,4 @@ export const InputSection = styled.div` border-top: 1px solid ${colors.grey[400]}; flex-shrink: 0; background: ${colors.darkgrey.main}; -`; \ No newline at end of file +`;