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
17 changes: 17 additions & 0 deletions src/app/(pages)/albaTalk/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { Suspense } from "react";

export default function AlbaTalkLayout({ children }: { children: React.ReactNode }) {
return (
<div className="mx-auto max-w-screen-xl px-4 py-8">
<Suspense
fallback={
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<div>로딩 중...</div>
</div>
}
>
{children}
</Suspense>
</div>
);
}
134 changes: 133 additions & 1 deletion src/app/(pages)/albaTalk/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,137 @@
"use client";

import React, { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { usePosts } from "@/hooks/queries/post/usePosts";
import { usePathname, useSearchParams } from "next/navigation";
import SortSection from "@/app/components/layout/posts/SortSection";
import SearchSection from "@/app/components/layout/posts/SearchSection";
import { useUser } from "@/hooks/queries/user/me/useUser";
import Link from "next/link";
import { RiEdit2Fill } from "react-icons/ri";
import FloatingBtn from "@/app/components/button/default/FloatingBtn";
import CardBoard from "@/app/components/card/board/CardBoard";

const POSTS_PER_PAGE = 10;

export default function AlbaTalk() {
return <div>AlbaTalk</div>;
const pathname = usePathname();
const searchParams = useSearchParams();
const { user } = useUser();

// URL 쿼리 파라미터에서 키워드와 정렬 기준 가져오기
const keyword = searchParams.get("keyword");
const orderBy = searchParams.get("orderBy");

// 무한 스크롤을 위한 Intersection Observer 설정
const { ref, inView } = useInView({
threshold: 0.1,
triggerOnce: false,
rootMargin: "100px",
});

// 게시글 목록 조회
const { data, isLoading, error, hasNextPage, fetchNextPage, isFetchingNextPage } = usePosts({
limit: POSTS_PER_PAGE,
keyword: keyword || undefined,
orderBy: orderBy || undefined,
});

// 스크롤이 하단에 도달하면 다음 페이지 로드
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage, isFetchingNextPage]);

// 에러 상태 처리
if (error) {
return (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<p className="text-red-500">게시글 목록을 불러오는데 실패했습니다.</p>
</div>
);
}

// 로딩 상태 처리
if (isLoading) {
return (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<div>로딩 중...</div>
</div>
);
}

return (
<div className="flex min-h-screen flex-col items-center">
{/* 검색 섹션과 정렬 옵션을 고정 위치로 설정 */}
<div className="fixed left-0 right-0 top-16 z-40 bg-white shadow-sm">
{/* 검색 섹션 */}
<div className="w-full border-b border-grayscale-100">
<div className="mx-auto flex max-w-screen-2xl flex-col gap-4 px-4 py-4 md:px-6 lg:px-8">
<div className="flex items-center justify-between">
<SearchSection />
</div>
</div>
</div>

{/* 정렬 옵션 섹션 */}
<div className="w-full border-b border-grayscale-100">
<div className="mx-auto flex max-w-screen-2xl items-center justify-end gap-2 px-4 py-4 md:px-6 lg:px-8">
<div className="flex items-center gap-4">
<SortSection pathname={pathname} searchParams={searchParams} />
</div>
</div>
</div>
</div>

{/* 메인 콘텐츠 영역 */}
<div className="w-full pt-[132px]">
{/* 글쓰기 버튼 - 고정 위치 */}
{user && (
<Link href="/albatalk/addtalk" className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
<FloatingBtn icon={<RiEdit2Fill className="size-6" />} variant="orange" />
</Link>
)}

{!data?.pages?.[0]?.data?.length ? (
<div className="flex h-[calc(100vh-200px)] flex-col items-center justify-center">
<p className="text-grayscale-500">등록된 게시글이 없습니다.</p>
</div>
) : (
<div className="mx-auto mt-4 w-full max-w-screen-xl px-3">
<div className="flex flex-col gap-4">
{data?.pages.map((page) => (
<React.Fragment key={page.nextCursor}>
{page.data.map((post) => (
<div key={post.id} className="rounded-lg border border-grayscale-100 p-4 hover:bg-grayscale-50">
<Link href={`/albatalk/${post.id}`}>
<CardBoard
title={post.title}
content={post.content}
nickname={post.writer.nickname}
updatedAt={post.updatedAt}
commentCount={post.commentCount}
likeCount={post.likeCount}
/>
</Link>
</div>
))}
</React.Fragment>
))}
</div>

{/* 무한 스크롤 트리거 영역 */}
<div ref={ref} className="h-4 w-full">
{isFetchingNextPage && (
<div className="flex justify-center py-4">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary-orange-300 border-t-transparent" />
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}
30 changes: 19 additions & 11 deletions src/app/(pages)/mypage/components/sections/CommentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import React from "react";
import { useState } from "react";
import { useMyComments } from "@/hooks/queries/user/me/useMyComments";
import Pagination from "@/app/components/pagination/Pagination";
import type { MyCommentType } from "@/types/response/user";
import Comment from "@/app/components/card/board/Comment";
import Link from "next/link";
import LoadingSpinner from "@/app/components/loading-spinner/LoadingSpinner";
import { useUser } from "@/hooks/queries/user/me/useUser";

// 한 페이지당 댓글 수
const COMMENTS_PER_PAGE = 10;

export default function CommentsSection() {
const { user } = useUser();

// 현재 페이지 상태 관리
const [currentPage, setCurrentPage] = useState(1);

Expand Down Expand Up @@ -57,18 +61,22 @@ export default function CommentsSection() {
}

return (
<div className="space-y-4">
<div className="mx-auto w-full max-w-screen-xl space-y-4 px-3">
{/* 댓글 목록 렌더링 */}
{data.data.map((comment: MyCommentType) => (
<div key={comment.id} className="rounded-lg border p-4">
<h3 className="text-grayscale-900 mb-2 font-medium">{comment.post.title}</h3>
<p className="text-grayscale-600">{comment.content}</p>
<div className="mt-2 text-sm text-grayscale-500">
<time>{new Date(comment.createdAt).toLocaleDateString()}</time>
{comment.updatedAt !== comment.createdAt && <span className="ml-2 text-grayscale-400">(수정됨)</span>}
<div className="flex flex-col gap-4">
{data.data.map((comment) => (
<div key={comment.id} className="rounded-lg border border-grayscale-100 p-4 hover:bg-grayscale-50">
<Link href={`/albatalk/${comment.post.id}`}>
<Comment
nickname={user?.nickname || ""}
updatedAt={comment.updatedAt}
content={comment.content}
onKebabClick={() => console.log("케밥 메뉴 클릭", comment.id)}
/>
</Link>
</div>
</div>
))}
))}
</div>

{/* 페이지네이션 */}
{totalPages > 1 && (
Expand Down
60 changes: 29 additions & 31 deletions src/app/(pages)/mypage/components/sections/PostsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,31 @@ import React, { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import { useMyPosts } from "@/hooks/queries/user/me/useMyPosts";
import { useMySortStore } from "@/store/mySortStore";
import type { PostListType } from "@/types/response/post";
import { useProfileStringValue } from "@/hooks/queries/user/me/useProfileStringValue";
import CardBoard from "@/app/components/card/board/CardBoard";
import Link from "next/link";
import LoadingSpinner from "@/app/components/loading-spinner/LoadingSpinner";

// 한 페이지당 게시글 수
const POSTS_PER_PAGE = 10;

// 컴포넌트
// 상태 메시지 컴포넌트
const StatusMessage = ({ message, className = "text-grayscale-500" }: { message: string; className?: string }) => (
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
<p className={className}>{message}</p>
</div>
);

const PostCard = ({ post }: { post: PostListType }) => (
<div className="rounded-lg border p-4 transition-all hover:border-primary-orange-200">
<h3 className="font-bold">{post.title}</h3>
<p className="text-grayscale-600">{post.content}</p>
<div className="mt-2 text-sm text-grayscale-500">
<span>댓글 {post.commentCount}</span>
<span className="mx-2">•</span>
<span>좋아요 {post.likeCount}</span>
</div>
</div>
);

const PostList = ({ pages }: { pages: any[] }) => (
<>
{pages.map((page, index) => (
<React.Fragment key={index}>
{page.data.map((post: PostListType) => (
<PostCard key={post.id} post={post} />
))}
</React.Fragment>
))}
</>
);

export default function PostsSection() {
// 정렬 상태 관리
const { orderBy } = useMySortStore();
useProfileStringValue();

// 무한 스크롤을 위한 Intersection Observer 설정
const { ref, inView } = useInView({
threshold: 0.1, // 10% 정도 보이면 트리거
triggerOnce: true, // 한 번만 트리거 (불필요한 API 호출 방지)
rootMargin: "100px", // 하단 100px 전에 미리 로드
threshold: 0.1,
triggerOnce: false,
rootMargin: "100px",
});

// 내가 작성한 게시글 목록 조회
Expand All @@ -74,8 +51,29 @@ export default function PostsSection() {
if (!data?.pages[0]?.data?.length) return <StatusMessage message="작성한 게시글이 없습니다." />;

return (
<div className="mt-10 space-y-4">
<PostList pages={data.pages} />
<div className="mx-auto w-full max-w-screen-xl space-y-4 px-3">
{/* 게시글 목록 렌더링 */}
<div className="flex flex-col gap-4">
{data.pages.map((page) => (
<React.Fragment key={page.nextCursor}>
{page.data.map((post) => (
<div key={post.id} className="rounded-lg border border-grayscale-100 p-4 hover:bg-grayscale-50">
<Link href={`/albatalk/${post.id}`}>
<CardBoard
title={post.title}
content={post.content}
nickname={post.writer.nickname}
updatedAt={post.updatedAt}
commentCount={post.commentCount}
likeCount={post.likeCount}
onKebabClick={() => console.log("케밥 메뉴 클릭", post.id)}
/>
</Link>
</div>
))}
</React.Fragment>
))}
</div>

{/* 무한 스크롤 트리거 영역 */}
<div ref={ref} className="h-4 w-full">
Expand Down
Loading
Loading