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
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
]
},
"dependencies": {
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"gsap": "^3.13.0",
"lottie-react": "^2.4.1",
Expand Down
29 changes: 3 additions & 26 deletions src/app/community/[id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,7 @@
function Loading() {
return (
<div className="w-full mb-10 flex relative animate-pulse">
{/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  */}
<article className="page-layout max-w-[824px] flex-1 z-5 space-y-6 mt-15">
{/* DetailHeader ์ž๋ฆฌ */}
<div className="h-6 w-15 bg-gray rounded-md" />

{/* Title ์ž๋ฆฌ */}
<div className="h-12 w-full bg-gray rounded-md" />
<div className="h-7 w-20 -mt-2 bg-gray rounded-md" />
import DetailSkeleton from '@/domains/community/detail/DetailSkeleton';

{/* Content ์ž๋ฆฌ */}
<div className="space-y-2 mt-5">
<div className="h-70 w-full bg-gray rounded-md" />
</div>

{/* ๋Œ“๊ธ€ */}
<div className="h-9 w-full bg-gray rounded-md mt-4" />
<div className="space-y-3">
{[...Array(2)].map((_, i) => (
<div key={i} className="h-16 w-full bg-gray rounded-md" />
))}
</div>
</article>
</div>
);
function Loading() {
return <DetailSkeleton />;
}

export default Loading;
7 changes: 6 additions & 1 deletion src/app/community/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,28 @@ import Comment from '@/domains/community/detail/Comment';
import StarBg from '@/domains/shared/components/star-bg/StarBg';
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import DetailSkeleton from '@/domains/community/detail/DetailSkeleton';

function Page() {
const params = useParams();
const [postDetail, setPostDetail] = useState<Post | null>(null);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
const postId = params.id;
const fetchData = async () => {
setIsLoading(true);
const data = await fetchPostById(postId);
if (!data) return;

setPostDetail(data);
setIsLoading(false);
};
fetchData();
}, [params.id, setPostDetail]);

if (!postDetail) return <div>๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.</div>;
if (isLoading) return <DetailSkeleton />;
if (!postDetail) return null;

const {
categoryName,
Expand Down
75 changes: 74 additions & 1 deletion src/domains/community/api/fetchComment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,84 @@ export const fetchComment = async (postId: number): Promise<CommentType[] | null
try {
const res = await fetch(`${getApi}/posts/${postId}/comments`, {
method: 'GET',
cache: 'no-store', // ์บ์‹œ ๋น„ํ™œ์„ฑํ™”
});
const data = await res.json();
return data.data;

//์‚ญ์ œ๋œ ๋Œ“๊ธ€์€ ์ œ์™ธ
const filteredComments = data.data.filter(
(comment: CommentType) => comment.status !== 'DELETED'
);

return filteredComments;
} catch (err) {
console.error('ํ•ด๋‹น ๊ธ€์˜ ๋Œ“๊ธ€ ์กฐํšŒ ์‹คํŒจ', err);
return null;
}
};

export const postComments = async (postId: number, content: string) => {
try {
const res = await fetch(`${getApi}/posts/${postId}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ content }),
});

const text = await res.text();

if (!res.ok) {
console.error(`๋Œ“๊ธ€ ์ž‘์„ฑ ์‹คํŒจ: ${res.status}`, text);
return null;
}
const data = JSON.parse(text);
return data;
} catch (err) {
console.error('ํ•ด๋‹น ๊ธ€์˜ ๋Œ“๊ธ€ ์ž‘์„ฑ ์‹คํŒจ', err);
return null;
}
};

export async function updateComment(
accessToken: string | null,
postId: number,
commentId: number,
content: string
): Promise<void> {
const response = await fetch(`${getApi}/posts/${postId}/comments/${commentId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ content }),
});

if (!response.ok) {
const errorText = await response.text(); // ๐Ÿ‘ˆ ์‘๋‹ต ๋ณธ๋ฌธ์„ ํ…์ŠคํŠธ๋กœ ์ฝ๊ธฐ
console.error('์„œ๋ฒ„ ์‘๋‹ต ์—๋Ÿฌ:', errorText);
throw new Error(`๋Œ“๊ธ€ ์ˆ˜์ • ์‹คํŒจ: ${response.status}`);
}
}

export async function deleteComment(
accessToken: string | null,
postId: number,
commentId: number
): Promise<void> {
const response = await fetch(`${getApi}/posts/${postId}/comments/${commentId}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

if (!response.ok) {
const errorText = await response.text(); // ๐Ÿ‘ˆ ์‘๋‹ต ๋ณธ๋ฌธ์„ ํ…์ŠคํŠธ๋กœ ์ฝ๊ธฐ
console.error('์„œ๋ฒ„ ์‘๋‹ต ์—๋Ÿฌ:', errorText);
throw new Error(`๋Œ“๊ธ€ ์ˆ˜์ • ์‹คํŒจ: ${response.status}`);
}
}
2 changes: 1 addition & 1 deletion src/domains/community/api/fetchPost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ParamValue } from 'next/dist/server/request/params';

export const fetchPost = async (): Promise<Post[] | null> => {
try {
const res = await fetch(`${getApi}/posts`, {
const res = await fetch(`${getApi}/posts?postSortStatus=LATEST`, {
method: 'GET',
cache: 'no-store',
});
Expand Down
Empty file.
4 changes: 2 additions & 2 deletions src/domains/community/components/post-info/PostInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ function PostInfo({
)}
<li>{elapsedTime(createdAt)}</li>
<li aria-hidden="true">|</li>
<li>์กฐํšŒ {viewCount}</li>
<li>์กฐํšŒ {viewCount || 0}</li>
<li aria-hidden="true">|</li>
<li>๋Œ“๊ธ€ {commentCount}</li>
<li>๋Œ“๊ธ€ {commentCount + 1 || 0}</li>
</ul>
);
}
Expand Down
64 changes: 64 additions & 0 deletions src/domains/community/components/textarea/AutoGrowingTextarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Virtualizer } from '@tanstack/react-virtual';
import gsap from 'gsap';
import { useEffect, useRef } from 'react';

type Props = {
value: string;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
rowVirtualize: Virtualizer<HTMLElement, Element>;
};

function AutoGrowingTextarea({ value, onChange, rowVirtualize }: Props) {
const textareaRef = useRef<HTMLTextAreaElement | null>(null);

useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;

textarea.style.height = 'auto'; // ๋†’์ด ์ดˆ๊ธฐํ™”
textarea.style.height = textarea.scrollHeight + 'px'; // ์Šคํฌ๋กค ๋†’์ด๋งŒํผ ๋Š˜๋ฆฌ๊ธฐ

const handleInput = () => {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
};

textarea.addEventListener('input', handleInput);
return () => {
textarea.removeEventListener('input', handleInput);
};
}, []);

useEffect(() => {
if (textareaRef.current) {
requestAnimationFrame(() => {
const li = textareaRef.current?.closest('li') as HTMLElement | null;
if (li) {
rowVirtualize.measureElement(li);
}
});
}
}, [value]);

useEffect(() => {
if (!textareaRef.current) return;
gsap.fromTo(
textareaRef.current,
{ autoAlpha: 0, y: -15 },
{ duration: 0.4, autoAlpha: 1, y: 0, ease: 'power2.out' }
);
}, []);

return (
<textarea
ref={textareaRef}
value={value}
className="w-full resize-none overflow-hidden bg-white py-2 px-4 text-primary rounded-lg outline-none"
rows={1}
spellCheck={false}
onChange={(e) => onChange(e)}
/>
);
}

export default AutoGrowingTextarea;
69 changes: 52 additions & 17 deletions src/domains/community/detail/Comment.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,65 @@
import React, { useEffect, useState } from 'react';
import CommentHeader from '../../shared/components/comment/CommentHeader';
import CommentList from '../../shared/components/comment/CommentList';
import { fetchComment } from '../api/fetchComment';
import { CommentType } from '../types/post';
import { postComments } from '../api/fetchComment';
import { useAuthStore } from '@/domains/shared/store/auth';
import { useShallow } from 'zustand/shallow';
import ConfirmModal from '@/shared/components/modal-pop/ConfirmModal';
import { useComments } from '../hook/useComment';

type Props = {
postId: number;
};

function Comment({ postId }: Props) {
const [comments, setComments] = useState<CommentType[] | null>(null);

useEffect(() => {
const fetchData = async () => {
const data = await fetchComment(postId);
if (!data) return;
setComments(data);
};
fetchData();
}, [postId]);
const { user, accessToken } = useAuthStore(
useShallow((state) => ({
user: state.user,
accessToken: state.accessToken,
}))
);
const {
comments,
isEnd,
isLoading,
deleteTarget,
setDeleteTarget,
fetchData,
handleUpdateComment,
handleAskDeleteComment,
handleConfirmDelete,
loadMoreComments,
} = useComments(postId, user, accessToken);

return (
<section className="mb-10 border-t-1 border-gray ">
<CommentHeader />
<CommentList comments={comments} />
</section>
<>
<section className="mb-10 border-t-1 border-gray ">
<CommentHeader
postId={postId}
comments={comments}
onCommentAdded={fetchData}
postCommentsApi={postComments}
/>
<CommentList
comments={comments}
currentUserNickname={user?.nickname}
onUpdateComment={handleUpdateComment}
onDeleteComment={handleAskDeleteComment}
onLoadMore={loadMoreComments}
isEnd={isEnd}
isLoading={isLoading}
/>
</section>
{deleteTarget && (
<ConfirmModal
open={!!deleteTarget}
onConfirm={handleConfirmDelete}
onCancel={() => setDeleteTarget(null)}
onClose={() => setDeleteTarget(null)}
title="๋Œ“๊ธ€ ์‚ญ์ œ"
description="์ •๋ง ์ด ๋Œ“๊ธ€์„ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?"
/>
)}
</>
);
}

Expand Down
Loading