Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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>
);
}
27 changes: 16 additions & 11 deletions src/app/(pages)/mypage/components/sections/CommentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ 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";

// 한 페이지당 댓글 수
const COMMENTS_PER_PAGE = 10;
Expand Down Expand Up @@ -56,18 +57,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={comment.writer.nickname}
updatedAt={comment.updatedAt}
content={comment.content}
onKebabClick={() => console.log("케밥 메뉴 클릭", comment.id)}
/> */}
</Link>
</div>
</div>
))}
))}
</div>

{/* 페이지네이션 */}
{totalPages > 1 && (
Expand Down
62 changes: 30 additions & 32 deletions src/app/(pages)/mypage/components/sections/PostsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,37 @@ 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";

// 한 페이지당 게시글 수
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 LoadingSpinner = () => (
<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>
);

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 @@ -75,12 +53,32 @@ export default function PostsSection() {
// 에러 상태 처리
if (error) return <StatusMessage message="게시글을 불러오는데 실패했습니다." className="text-red-500" />;
if (isLoading) return <StatusMessage message="로딩 중..." />;
// 데이터가 없는 경우 처리
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
35 changes: 19 additions & 16 deletions src/app/components/card/board/CardBoard.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

댓글 본문에 줄넘김 허용을 위해서 className에 whitespace-pre-wrap 추가하면 좋겠습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네~ 추가할께요.

Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,32 @@

import React, { useEffect, useState } from "react";
import Image from "next/image";
import { formatLocalDate } from "@/utils/workDayFormatter";

export interface CardBoardProps {
title: string;
content: string;
userName: string;
date: string;
comments: number;
likes: number;
nickname: string;
updatedAt: Date;
commentCount: number;
likeCount: number;
variant?: "default" | "primary";
onKebabClick?: () => void; // 케밥 버튼 클릭 핸들러
}

const CardBoard: React.FC<CardBoardProps> = ({
title,
content,
userName,
date,
comments,
likes,
nickname,
updatedAt,
commentCount,
likeCount,
variant = "default",
onKebabClick,
}) => {
const [isLargeScreen, setIsLargeScreen] = useState(false);
const [isLiked, setIsLiked] = useState(false);
const [likeCount, setLikeCount] = useState(likes);
const [likeDisplayCount, setLikeDisplayCount] = useState(likeCount);

useEffect(() => {
const handleResize = () => {
Expand All @@ -41,9 +42,9 @@ const CardBoard: React.FC<CardBoardProps> = ({

const handleLikeClick = () => {
if (isLiked) {
setLikeCount((prev) => prev - 1); // 좋아요 취소 시 감소
setLikeDisplayCount((prev) => prev - 1); // 좋아요 취소 시 감소
} else {
setLikeCount((prev) => prev + 1); // 좋아요 클릭 시 증가
setLikeDisplayCount((prev) => prev + 1); // 좋아요 클릭 시 증가
}
setIsLiked((prev) => !prev); // 좋아요 상태 토글
};
Expand Down Expand Up @@ -94,14 +95,14 @@ const CardBoard: React.FC<CardBoardProps> = ({
width={28}
height={28}
/>
{/* 이름 + 날짜 */}
{/* 닉네임 + 수정일 */}
<div className="flex items-center gap-1 truncate">
<span className="truncate font-nexon text-[14px] font-normal text-grayscale-500 sm:text-[16px]">
{userName}
{nickname}
</span>
<span className="text-grayscale-500">|</span>
<span className="whitespace-nowrap font-nexon text-[14px] font-normal text-grayscale-500 sm:text-[16px]">
{date}
{formatLocalDate(updatedAt)}
</span>
</div>
</div>
Expand All @@ -116,7 +117,7 @@ const CardBoard: React.FC<CardBoardProps> = ({
width={22}
height={22}
/>
<span className="font-nexon text-[14px] font-normal text-grayscale-500 sm:text-[16px]">{comments}</span>
<span className="font-nexon text-[14px] font-normal text-grayscale-500 sm:text-[16px]">{commentCount}</span>
</div>
{/* 좋아요 */}
<div className="flex items-center gap-1">
Expand All @@ -136,7 +137,9 @@ const CardBoard: React.FC<CardBoardProps> = ({
className="cursor-pointer"
onClick={handleLikeClick}
/>
<span className="font-nexon text-[14px] font-normal text-grayscale-500 sm:text-[16px]">{likeCount}</span>
<span className="font-nexon text-[14px] font-normal text-grayscale-500 sm:text-[16px]">
Copy link
Collaborator

Choose a reason for hiding this comment

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

sm: 은 따로 적어주지 않아도 될것같아요
14px과 16px을 각각 언제 적용하고싶으신걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이건 제가 만든 컴포넌트는 아니라서 잘 모르겠어요.

{likeDisplayCount}
</span>
</div>
</div>
</div>
Expand Down
Loading
Loading