diff --git a/package-lock.json b/package-lock.json
index 958ff80..bcea2d9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.1.0",
"license": "ISC",
"dependencies": {
+ "@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"gsap": "^3.13.0",
"lottie-react": "^2.4.1",
@@ -3297,6 +3298,33 @@
"tailwindcss": "4.1.13"
}
},
+ "node_modules/@tanstack/react-virtual": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
+ "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.13.12"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tanstack/virtual-core": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
+ "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
diff --git a/package.json b/package.json
index 061e194..f98fce8 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app/community/[id]/loading.tsx b/src/app/community/[id]/loading.tsx
index 6fca4e1..f8db77a 100644
--- a/src/app/community/[id]/loading.tsx
+++ b/src/app/community/[id]/loading.tsx
@@ -1,30 +1,7 @@
-function Loading() {
- return (
-
- {/* 메인 콘텐츠 */}
-
- {/* DetailHeader 자리 */}
-
-
- {/* Title 자리 */}
-
-
+import DetailSkeleton from '@/domains/community/detail/DetailSkeleton';
- {/* Content 자리 */}
-
-
- {/* 댓글 */}
-
-
- {[...Array(2)].map((_, i) => (
-
- ))}
-
-
-
- );
+function Loading() {
+ return ;
}
export default Loading;
diff --git a/src/app/community/[id]/page.tsx b/src/app/community/[id]/page.tsx
index 6d22101..c80776f 100644
--- a/src/app/community/[id]/page.tsx
+++ b/src/app/community/[id]/page.tsx
@@ -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(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 게시글을 불러오지 못했습니다.
;
+ if (isLoading) return ;
+ if (!postDetail) return null;
const {
categoryName,
diff --git a/src/domains/community/api/fetchComment.ts b/src/domains/community/api/fetchComment.ts
index 492d0a6..2eb8302 100644
--- a/src/domains/community/api/fetchComment.ts
+++ b/src/domains/community/api/fetchComment.ts
@@ -5,11 +5,84 @@ export const fetchComment = async (postId: number): Promise 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 {
+ 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 {
+ 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}`);
+ }
+}
diff --git a/src/domains/community/api/fetchPost.ts b/src/domains/community/api/fetchPost.ts
index 74e9586..29fdb67 100644
--- a/src/domains/community/api/fetchPost.ts
+++ b/src/domains/community/api/fetchPost.ts
@@ -4,7 +4,7 @@ import { ParamValue } from 'next/dist/server/request/params';
export const fetchPost = async (): Promise => {
try {
- const res = await fetch(`${getApi}/posts`, {
+ const res = await fetch(`${getApi}/posts?postSortStatus=LATEST`, {
method: 'GET',
cache: 'no-store',
});
diff --git a/src/domains/community/api/fetchView.ts b/src/domains/community/api/fetchView.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/domains/community/components/post-info/PostInfo.tsx b/src/domains/community/components/post-info/PostInfo.tsx
index 8ef3b62..d30403b 100644
--- a/src/domains/community/components/post-info/PostInfo.tsx
+++ b/src/domains/community/components/post-info/PostInfo.tsx
@@ -28,9 +28,9 @@ function PostInfo({
)}
{elapsedTime(createdAt)}
|
- 조회 {viewCount}
+ 조회 {viewCount || 0}
|
- 댓글 {commentCount}
+ 댓글 {commentCount + 1 || 0}
);
}
diff --git a/src/domains/community/components/textarea/AutoGrowingTextarea.tsx b/src/domains/community/components/textarea/AutoGrowingTextarea.tsx
new file mode 100644
index 0000000..8f12a29
--- /dev/null
+++ b/src/domains/community/components/textarea/AutoGrowingTextarea.tsx
@@ -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) => void;
+ rowVirtualize: Virtualizer;
+};
+
+function AutoGrowingTextarea({ value, onChange, rowVirtualize }: Props) {
+ const textareaRef = useRef(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 (
+