Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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: 20px 20px 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;
padding-bottom: 10px;
flex-shrink: 0;
`;

export const Title = styled.h2`
color: ${colors.white};
font-size: 20px;
font-weight: ${typography.fontWeight.bold};
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};
`;
146 changes: 146 additions & 0 deletions src/components/common/CommentBottomSheet/GlobalCommentBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
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,
BottomSheet,
Header,
Title,
Content,
LoadingState,
InputSection,
} from './GlobalCommentBottomSheet.styled';

const GlobalCommentBottomSheet = () => {
const { isOpen, postId, postType, closeCommentBottomSheet } = useCommentBottomSheetStore();

const [commentList, setCommentList] = useState<CommentData[]>([]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isSending, setIsSending] = useState(false);

const { nickname, isReplying, cancelReply } = useReplyActions();
const { parentId } = useReplyStore();

// 댓글 목록 로드
const loadComments = useCallback(async () => {
if (!isOpen || !postId || !postType) 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);
}
}, [isOpen, postId, postType]);

// 댓글 전송
const handleSendComment = async () => {
if (!inputValue.trim() || isSending || !postId || !postType) return;

setIsSending(true);
try {
const requestData = {
content: inputValue.trim(),
isReplyRequest: isReplying,
parentId: isReplying ? parentId : null,
postType: postType as 'FEED' | 'RECORD' | 'VOTE'
};

const response = await postReply(postId, requestData);

if (response.isSuccess) {
setInputValue('');
cancelReply(); // 답글 상태 초기화
// 댓글 목록 새로고침
await loadComments();
}
} catch (error) {
console.error('댓글 전송 실패:', error);
} finally {
setIsSending(false);
}
};

// 답글 취소
const handleCancelReply = () => {
cancelReply();
};

// Overlay 클릭 처리
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
closeCommentBottomSheet();
}
};

// 바텀시트가 열릴 때 댓글 로드
useEffect(() => {
if (isOpen) {
loadComments();
}
}, [isOpen, postId, loadComments]);

// 바텀시트가 닫힐 때 상태 초기화
useEffect(() => {
if (!isOpen) {
setInputValue('');
cancelReply();
setCommentList([]);
}
}, [isOpen, cancelReply]);

if (!isOpen) return null;

return (
<Overlay isOpen={isOpen} onClick={handleOverlayClick}>
<BottomSheet isOpen={isOpen}>
<Header>
<Title>댓글</Title>
</Header>

<Content>
{isLoading ? (
<LoadingState>댓글을 불러오는 중...</LoadingState>
) : (
<ReplyList
commentList={commentList}
onReload={loadComments}
/>
)}
</Content>

<InputSection>
<MessageInput
placeholder={
isReplying ? `@${nickname}님에게 답글을 남겨보세요` : '댓글을 남겨보세요'
}
value={inputValue}
onChange={setInputValue}
onSend={handleSendComment}
isReplying={isReplying}
onCancelReply={handleCancelReply}
nickname={nickname}
disabled={isSending}
/>
</InputSection>
</BottomSheet>
</Overlay>
);
};

export default GlobalCommentBottomSheet;
15 changes: 14 additions & 1 deletion src/components/memory/RecordItem/RecordItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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';
Expand Down Expand Up @@ -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 { openCommentBottomSheet } = useCommentBottomSheetStore();

// 길게 누르기 상태 관리
const [isPressed, setIsPressed] = useState(false);
Expand Down Expand Up @@ -254,6 +258,11 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
});
}, [openConfirm, handlePinRecord]);

// 댓글 버튼 클릭 핸들러
const handleCommentClick = useCallback(() => {
openCommentBottomSheet(parseInt(id), type === 'poll' ? 'VOTE' : 'RECORD');
}, [openCommentBottomSheet, id, type]);

// 길게 누르기 이벤트 핸들러
const handleTouchStart = useCallback(
(e: React.TouchEvent) => {
Expand Down Expand Up @@ -367,7 +376,10 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
/>
<span>{currentLikeCount}</span>
</ActionButton>
<ActionButton>
<ActionButton onClick={(e) => {
e.stopPropagation();
handleCommentClick();
}}>
<img src={commentIcon} alt="댓글" />
<span>{commentCount}</span>
</ActionButton>
Expand All @@ -382,6 +394,7 @@ const RecordItem = ({ record, shouldBlur = false }: RecordItemProps) => {
</ActionButton>
)}
</ActionSection>

</Container>
);
};
Expand Down
3 changes: 3 additions & 0 deletions src/pages/memory/Memory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -295,6 +296,8 @@ const Memory = () => {
onClose={() => setShowSnackbar(false)}
/>
)}

<GlobalCommentBottomSheet />
</Container>
);
};
Expand Down
26 changes: 26 additions & 0 deletions src/stores/useCommentBottomSheetStore.ts
Original file line number Diff line number Diff line change
@@ -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<CommentBottomSheetState & CommentBottomSheetActions>((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 }),
}));