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
Expand Up @@ -50,6 +50,26 @@ export const Content = styled.div`
flex: 1;
overflow-y: auto;
min-height: 0;

scrollbar-width: thin;
scrollbar-color: ${colors.grey[100]} transparent;

&::-webkit-scrollbar {
width: 4px;
}

&::-webkit-scrollbar-track {
background: transparent;
}

&::-webkit-scrollbar-thumb {
background: ${colors.grey[100]};
border-radius: 2px;
}

&::-webkit-scrollbar-thumb:hover {
background: ${colors.white};
}
`;

export const LoadingState = styled.div`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useParams } from 'react-router-dom';
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/replyStore';
Expand All @@ -16,44 +15,23 @@ import {
Header,
Title,
Content,
LoadingState,
InputSection,
} from './GlobalCommentBottomSheet.styled';

const GlobalCommentBottomSheet = () => {
const { roomId } = useParams<{ roomId: string }>();
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 [roomCompleted, setRoomCompleted] = useState(false);
const [replyReloadKey, setReplyReloadKey] = useState(0);
const contentRef = useRef<HTMLDivElement | null>(null);

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

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;

Expand All @@ -71,7 +49,7 @@ const GlobalCommentBottomSheet = () => {
if (response.isSuccess) {
setInputValue('');
cancelReply();
await loadComments();
setReplyReloadKey(prev => prev + 1);
} else {
openSnackbar({
message: response.message || '댓글 작성 중 오류가 발생했습니다.',
Expand Down Expand Up @@ -121,17 +99,11 @@ const GlobalCommentBottomSheet = () => {
}
}, [isOpen, roomId]);

useEffect(() => {
if (isOpen) {
loadComments();
}
}, [isOpen, postId, loadComments]);

useEffect(() => {
if (!isOpen) {
setInputValue('');
cancelReply();
setCommentList([]);
setReplyReloadKey(0);
}
}, [isOpen, cancelReply]);

Expand All @@ -144,12 +116,16 @@ const GlobalCommentBottomSheet = () => {
<Title>댓글</Title>
</Header>

<Content>
{isLoading ? (
<LoadingState>댓글을 불러오는 중...</LoadingState>
) : (
<ReplyList commentList={commentList} onReload={loadComments} />
)}
<Content ref={contentRef}>
{postId && postType ? (
<ReplyList
postId={postId}
postType={postType}
reloadKey={`${replyReloadKey}-${isOpen ? 'open' : 'closed'}`}
rootRef={contentRef}
disableBottomMargin={true}
/>
) : null}
</Content>

{!roomCompleted && (
Expand Down
5 changes: 3 additions & 2 deletions src/components/common/Post/PostBody.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ export const PostContent = styled.div<{ hasImage: boolean }>`
font-weight: var(--string-weight-regular, 400);
line-height: var(--string-lineheight-feedcontent_height20, 20px);
cursor: pointer;
white-space: pre-wrap; // 개행문자 유지
/* word-wrap: break-word; // 긴 텍스트 줄바꿈 */
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;

display: -webkit-box;
-webkit-box-orient: vertical;
Expand Down
8 changes: 4 additions & 4 deletions src/components/common/Post/ReplyList.styled.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import styled from '@emotion/styled';
import { typography, colors } from '@/styles/global/global';

export const Container = styled.div`
export const Container = styled.div<{ disableBottomMargin?: boolean }>`
display: flex;
flex-direction: column;
width: 100%;
/* min-width: 360px;
max-width: 540px; */
padding: 40px 20px;
margin: 0 auto;
margin-bottom: 56px;
margin-bottom: ${({ disableBottomMargin }) => (disableBottomMargin ? '0' : '56px')};
gap: 24px;
flex: 1;

Expand All @@ -20,7 +20,7 @@ export const Container = styled.div`
}
`;

export const EmptyState = styled.div`
export const EmptyState = styled.div<{ disableBottomMargin?: boolean }>`
display: flex;
flex-direction: column;
align-items: center;
Expand All @@ -30,7 +30,7 @@ export const EmptyState = styled.div`
max-width: 540px;
padding: 40px 20px;
margin: 0 auto;
margin-bottom: 56px;
margin-bottom: ${({ disableBottomMargin }) => (disableBottomMargin ? '0' : '56px')};
gap: 8px;
flex: 1;

Expand Down
78 changes: 68 additions & 10 deletions src/components/common/Post/ReplyList.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,95 @@
import { useCallback } from 'react';
import type { RefObject } from 'react';
import Reply from './Reply';
import SubReply from './SubReply';
import type { CommentData } from '@/api/comments/getComments';
import { getComments, type CommentData } from '@/api/comments/getComments';
import { useInifinieScroll } from '@/hooks/useInifinieScroll';
import LoadingSpinner from '../LoadingSpinner';
import { Container, EmptyState } from './ReplyList.styled';

interface ReplyListProps {
commentList: CommentData[];
onReload: () => void;
commentList?: CommentData[];
onReload?: () => void;
postId?: number;
postType?: 'FEED' | 'RECORD' | 'VOTE';
reloadKey?: string;
rootRef?: RefObject<HTMLElement | null>;
disableBottomMargin?: boolean;
}

const ReplyList = ({ commentList, onReload }: ReplyListProps) => {
const hasComments = commentList.length > 0;
const ReplyList = ({
commentList = [],
onReload,
postId,
postType,
reloadKey = '',
rootRef,
disableBottomMargin = false,
}: ReplyListProps) => {
const isInfiniteMode = Boolean(postId && postType);
const commentFeed = useInifinieScroll<CommentData>({
enabled: isInfiniteMode,
reloadKey: `${postId ?? ''}-${postType ?? ''}-${reloadKey}`,
fetchPage: async cursor => {
if (!postId || !postType) {
return { items: [], nextCursor: null, isLast: true };
}

const response = await getComments(postId, {
postType,
size: 20,
cursor,
});

return {
items: response.data.commentList,
nextCursor: response.data.nextCursor || null,
isLast: response.data.isLast,
};
},
rootRef,
rootMargin: '120px 0px',
threshold: 0.1,
});

const list = isInfiniteMode ? commentFeed.items : commentList;
const hasComments = list.length > 0;

const handleReload = useCallback(() => {
if (isInfiniteMode) {
void commentFeed.reload();
return;
}
onReload?.();
}, [commentFeed, isInfiniteMode, onReload]);

return (
<Container>
<Container disableBottomMargin={disableBottomMargin}>
{isInfiniteMode && commentFeed.isLoading && list.length === 0 && (
<LoadingSpinner size="small" fullHeight={false} />
)}
{hasComments ? (
commentList.map((comment, commentIndex) => (
list.map((comment, commentIndex) => (
<div className="comment-group" key={comment.commentId || `comment-${commentIndex}`}>
<Reply {...comment} onDelete={onReload} />
<Reply {...comment} onDelete={handleReload} />
{comment.replyList.map((sub, replyIndex) => (
<SubReply
key={sub.commentId || `reply-${comment.commentId || commentIndex}-${replyIndex}`}
{...sub}
onDelete={onReload}
onDelete={handleReload}
/>
))}
</div>
))
) : (
<EmptyState>
<EmptyState disableBottomMargin={disableBottomMargin}>
<div className="title">아직 댓글이 없어요</div>
<div className="sub-title">첫번째 댓글을 남겨보세요</div>
</EmptyState>
Comment on lines +67 to 88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

초기 로딩 중 빈 상태 문구가 함께 노출됩니다.

무한 모드에서 Line 68 조건으로 스피너가 보일 때, Line 85의 EmptyState도 동시에 렌더링되어 잘못된 안내가 잠깐 노출될 수 있습니다. 초기 로딩 시에는 EmptyState를 숨기는 게 자연스럽습니다.

🛠️ 제안 수정
-      ) : (
-        <EmptyState disableBottomMargin={disableBottomMargin}>
+      ) : isInfiniteMode && commentFeed.isLoading ? null : (
+        <EmptyState disableBottomMargin={disableBottomMargin}>
           <div className="title">아직 댓글이 없어요</div>
           <div className="sub-title">첫번째 댓글을 남겨보세요</div>
         </EmptyState>
       )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Container disableBottomMargin={disableBottomMargin}>
{isInfiniteMode && commentFeed.isLoading && list.length === 0 && (
<LoadingSpinner size="small" fullHeight={false} />
)}
{hasComments ? (
commentList.map((comment, commentIndex) => (
list.map((comment, commentIndex) => (
<div className="comment-group" key={comment.commentId || `comment-${commentIndex}`}>
<Reply {...comment} onDelete={onReload} />
<Reply {...comment} onDelete={handleReload} />
{comment.replyList.map((sub, replyIndex) => (
<SubReply
key={sub.commentId || `reply-${comment.commentId || commentIndex}-${replyIndex}`}
{...sub}
onDelete={onReload}
onDelete={handleReload}
/>
))}
</div>
))
) : (
<EmptyState>
<EmptyState disableBottomMargin={disableBottomMargin}>
<div className="title">아직 댓글이 없어요</div>
<div className="sub-title">첫번째 댓글을 남겨보세요</div>
</EmptyState>
<Container disableBottomMargin={disableBottomMargin}>
{isInfiniteMode && commentFeed.isLoading && list.length === 0 && (
<LoadingSpinner size="small" fullHeight={false} />
)}
{hasComments ? (
list.map((comment, commentIndex) => (
<div className="comment-group" key={comment.commentId || `comment-${commentIndex}`}>
<Reply {...comment} onDelete={handleReload} />
{comment.replyList.map((sub, replyIndex) => (
<SubReply
key={sub.commentId || `reply-${comment.commentId || commentIndex}-${replyIndex}`}
{...sub}
onDelete={handleReload}
/>
))}
</div>
))
) : isInfiniteMode && commentFeed.isLoading ? null : (
<EmptyState disableBottomMargin={disableBottomMargin}>
<div className="title">아직 댓글이 없어요</div>
<div className="sub-title">첫번째 댓글을 남겨보세요</div>
</EmptyState>
)}
</Container>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/Post/ReplyList.tsx` around lines 67 - 88, The
EmptyState is shown during initial infinite-mode loading because the EmptyState
render (when hasComments is false) doesn't account for the initial loading flag;
update ReplyList rendering logic to suppress EmptyState while isInfiniteMode &&
commentFeed.isLoading && list.length === 0 (e.g., compute isInitialLoading =
isInfiniteMode && commentFeed.isLoading && list.length === 0) and only render
EmptyState when !isInitialLoading and !hasComments, leaving the LoadingSpinner
behavior unchanged; adjust the conditional around EmptyState (and any
hasComments check) accordingly so EmptyState is not displayed during initial
load.

)}

{isInfiniteMode && !commentFeed.isLast && <div ref={commentFeed.sentinelRef} style={{ height: 20 }} />}
{isInfiniteMode && commentFeed.isLoadingMore && <LoadingSpinner size="small" fullHeight={false} />}
</Container>
);
};
Expand Down
Loading