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
3 changes: 0 additions & 3 deletions apps/web/src/apis/reports/postReport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useMutation } from "@tanstack/react-query";

import type { AxiosError } from "axios";
import { useRouter } from "next/navigation";

import { toast } from "@/lib/zustand/useToastStore";
import { reportsApi, type UsePostReportsRequest } from "./api";
Expand All @@ -10,12 +9,10 @@ import { reportsApi, type UsePostReportsRequest } from "./api";
* @description 신고 등록 훅
*/
const usePostReports = () => {
const router = useRouter();
return useMutation<void, AxiosError<{ message: string }>, UsePostReportsRequest>({
mutationFn: reportsApi.postReport,
onSuccess: () => {
toast.success("신고가 성공적으로 등록되었습니다.");
router.back();
},
onError: (_error) => {
toast.error("신고 등록에 실패했습니다. 잠시 후 다시 시도해주세요.");
Expand Down
38 changes: 35 additions & 3 deletions apps/web/src/app/community/[boardCode]/CommunityPageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,60 @@
"use client";

import { useRouter } from "next/navigation";
import { useState } from "react";
import { useMemo, useState } from "react";
import { useGetPostList } from "@/apis/community";
import ButtonTab from "@/components/ui/ButtonTab";
import { COMMUNITY_BOARDS, COMMUNITY_CATEGORIES } from "@/constants/community";
import useReportedPostsStore from "@/lib/zustand/useReportedPostsStore";
import type { ListPost } from "@/types/community";
import CommunityRegionSelector from "./CommunityRegionSelector";
import PostCards from "./PostCards";
import PostWriteButton from "./PostWriteButton";

type ListPostWithAuthor = ListPost & {
postFindSiteUserResponse?: {
id: number;
};
};

interface CommunityPageContentProps {
boardCode: string;
}

const CommunityPageContent = ({ boardCode }: CommunityPageContentProps) => {
const router = useRouter();
const [category, setCategory] = useState<string | null>("전체");
const reportedPostIds = useReportedPostsStore((state) => state.reportedPostIds);
const blockedUserIds = useReportedPostsStore((state) => state.blockedUserIds);

// HydrationBoundary로부터 자동으로 prefetch된 데이터 사용
const { data: posts = [] } = useGetPostList({
boardCode,
category,
});

const visiblePosts = useMemo(() => {
if (reportedPostIds.length === 0 && blockedUserIds.length === 0) {
return posts;
}

const reportedIdSet = new Set(reportedPostIds);
const blockedUserIdSet = new Set(blockedUserIds);

return posts.filter((post) => {
if (reportedIdSet.has(post.id)) {
return false;
}

const authorId = (post as ListPostWithAuthor).postFindSiteUserResponse?.id;

if (typeof authorId === "number" && blockedUserIdSet.has(authorId)) {
return false;
}

return true;
});
}, [posts, reportedPostIds, blockedUserIds]);

const handleBoardChange = (newBoard: string) => {
router.push(`/community/${newBoard}`);
};
Expand All @@ -49,7 +81,7 @@ const CommunityPageContent = ({ boardCode }: CommunityPageContentProps) => {
setChoice={setCategory}
style={{ padding: "10px 0 10px 18px" }}
/>
{<PostCards posts={posts} boardCode={boardCode} />}
{<PostCards posts={visiblePosts} boardCode={boardCode} />}
<PostWriteButton onClick={postWriteHandler} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ type KebabMenuProps = {
postId: number;
boardCode: string;
isOwner?: boolean;
authorId?: number;
};

const KebabMenu = ({ postId, boardCode, isOwner = false }: KebabMenuProps) => {
const KebabMenu = ({ postId, boardCode, isOwner = false, authorId }: KebabMenuProps) => {
const dropdownRef = useRef<HTMLDivElement>(null);
const { mutate: deletePost } = useDeletePost();
const router = useRouter();
Expand Down Expand Up @@ -87,7 +88,7 @@ const KebabMenu = ({ postId, boardCode, isOwner = false }: KebabMenuProps) => {
<div className="absolute right-0 top-full z-10 mt-2 w-40 origin-top-right rounded-lg border border-gray-100 bg-white shadow-lg">
<ul className="p-1">
<li>
<ReportPanel idx={postId} />
<ReportPanel idx={postId} blockUserId={authorId} />
</li>
<li key={"URL 복사"}>
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"use client";

import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useGetPostDetail } from "@/apis/community";
import TopDetailNavigation from "@/components/layout/TopDetailNavigation";
import CloudSpinnerPage from "@/components/ui/CloudSpinnerPage";
import useReportedPostsStore from "@/lib/zustand/useReportedPostsStore";
import CommentSection from "./CommentSection";
import Content from "./Content";
import KebabMenu from "./KebabMenu";
Expand All @@ -15,8 +17,28 @@ interface PostPageContentProps {

const PostPageContent = ({ boardCode, postId }: PostPageContentProps) => {
const router = useRouter();
const reportedPostIds = useReportedPostsStore((state) => state.reportedPostIds);
const blockedUserIds = useReportedPostsStore((state) => state.blockedUserIds);
const isReportedPost = reportedPostIds.includes(postId);

const { data: post, isLoading, isError, refetch } = useGetPostDetail(postId);
const isBlockedUserPost = post ? blockedUserIds.includes(post.postFindSiteUserResponse.id) : false;

useEffect(() => {
if (isReportedPost) {
router.replace(`/community/${boardCode}`);
}
}, [boardCode, isReportedPost, router]);

useEffect(() => {
if (isBlockedUserPost) {
router.replace(`/community/${boardCode}`);
}
}, [boardCode, isBlockedUserPost, router]);

if (isReportedPost || isBlockedUserPost) {
return null;
}

if (isLoading) {
return <CloudSpinnerPage />;
Expand All @@ -41,7 +63,14 @@ const PostPageContent = ({ boardCode, postId }: PostPageContentProps) => {
handleBack={() => {
router.push(`/community/${boardCode}`);
}}
icon={<KebabMenu isOwner={post.isOwner} postId={postId} boardCode={boardCode} />}
icon={
<KebabMenu
isOwner={post.isOwner}
postId={postId}
boardCode={boardCode}
authorId={post.isOwner ? undefined : post.postFindSiteUserResponse.id}
/>
}
/>
<Content post={post} postId={postId} />
<CommentSection comments={post.postFindCommentResponses} postId={postId} refresh={refetch} />
Expand Down
38 changes: 3 additions & 35 deletions apps/web/src/app/community/[boardCode]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,24 @@
import { dehydrate, HydrationBoundary, QueryClient } from "@tanstack/react-query";
import type { Metadata } from "next";
import { CommunityQueryKeys } from "@/apis/community/api";
import { getPostListServer } from "@/apis/community/server";
import TopDetailNavigation from "@/components/layout/TopDetailNavigation";
import { COMMUNITY_BOARDS } from "@/constants/community";
import CommunityPageContent from "./CommunityPageContent";

export const metadata: Metadata = {
title: "커뮤니티",
};

// ISR: 정적 경로 생성
export async function generateStaticParams() {
return COMMUNITY_BOARDS.map((board) => ({
boardCode: board.code,
}));
}

// ISR: 자동 재생성 비활성화 (수동 revalidate만 사용)
export const revalidate = false;

interface CommunityPageProps {
params: {
boardCode: string;
};
}

const CommunityPage = async ({ params }: CommunityPageProps) => {
const CommunityPage = ({ params }: CommunityPageProps) => {
const { boardCode } = params;

// QueryClient 생성 (서버 컴포넌트에서만 사용)
const queryClient = new QueryClient();

// 기본 카테고리
const defaultCategory = "전체";

// 서버에서 데이터 prefetch (ISR - 수동 revalidate만)
const result = await getPostListServer({ boardCode, category: defaultCategory, revalidate: false });

if (result.ok) {
// React Query 캐시에 데이터 설정 (서버 fetch와 동일한 category 사용)
queryClient.setQueryData([CommunityQueryKeys.postList, boardCode, defaultCategory], {
data: result.data,
});
}

return (
<div className="w-full">
<HydrationBoundary state={dehydrate(queryClient)}>
<TopDetailNavigation title="커뮤니티" />
<CommunityPageContent boardCode={boardCode} />
</HydrationBoundary>
<TopDetailNavigation title="커뮤니티" />
<CommunityPageContent boardCode={boardCode} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { usePostReports } from "@/apis/reports";
import { postBlockUser } from "@/apis/users";
import { reportReasons } from "@/constants/report";
import { customConfirm } from "@/lib/zustand/useConfirmModalStore";
import useReportedPostsStore from "@/lib/zustand/useReportedPostsStore";
import { toast } from "@/lib/zustand/useToastStore";
import { IconReport } from "@/public/svgs/mentor";
import type { ReportType } from "@/types/reports";

Expand All @@ -10,9 +14,22 @@ interface UseSelectReportHandlerReturn {
handleReasonSelect: (reason: ReportType) => Promise<void>;
}

const useSelectReportHandler = (chatId: number): UseSelectReportHandlerReturn => {
interface UseSelectReportHandlerOptions {
blockUserId?: number;
}

const useSelectReportHandler = (
chatId: number,
{ blockUserId }: UseSelectReportHandlerOptions = {},
): UseSelectReportHandlerReturn => {
const [selectedReason, setSelectedReason] = useState<ReportType | null>(null);
const { mutate: postReports } = usePostReports();
const { mutateAsync: postReports } = usePostReports();
const { mutateAsync: blockUser } = postBlockUser();
const addBlockedUser = useReportedPostsStore((state) => state.addBlockedUser);
const removeBlockedUser = useReportedPostsStore((state) => state.removeBlockedUser);
const addReportedPost = useReportedPostsStore((state) => state.addReportedPost);
const pathname = usePathname();
const router = useRouter();

const handleReasonSelect = async (reason: ReportType) => {
setSelectedReason(reason);
Expand All @@ -24,11 +41,36 @@ const useSelectReportHandler = (chatId: number): UseSelectReportHandlerReturn =>
icon: IconReport,
});
if (ok) {
postReports({
targetType: "POST",
targetId: chatId,
reportType: reason,
});
try {
await postReports({
targetType: "POST",
targetId: chatId,
reportType: reason,
});

if (pathname.startsWith("/community/")) {
addReportedPost(chatId);

if (blockUserId) {
addBlockedUser(blockUserId);

try {
await blockUser({
blockedId: blockUserId,
data: {},
});
} catch {
removeBlockedUser(blockUserId);
toast.error("사용자 차단에 실패했습니다. 다시 시도해주세요.");
return;
}
}
}

router.back();
} catch {
setSelectedReason(null);
}
} else {
setSelectedReason(null);
}
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/ui/ReportPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import useSelectReportHandler from "./_hooks/useSelectReportHandler";

interface ReportPanelProps {
idx: number;
blockUserId?: number;
}

const ReportPanel = ({ idx }: ReportPanelProps) => {
const ReportPanel = ({ idx, blockUserId }: ReportPanelProps) => {
const [isExpanded, setIsExpanded] = useState<boolean>(false);
const { selectedReason, handleReasonSelect } = useSelectReportHandler(idx);
const { selectedReason, handleReasonSelect } = useSelectReportHandler(idx, { blockUserId });

return (
<>
Expand Down
55 changes: 55 additions & 0 deletions apps/web/src/lib/zustand/useReportedPostsStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";

interface ReportedPostsState {
reportedPostIds: number[];
blockedUserIds: number[];
addReportedPost: (postId: number) => void;
addBlockedUser: (userId: number) => void;
removeBlockedUser: (userId: number) => void;
}

const useReportedPostsStore = create<ReportedPostsState>()(
persist(
(set) => ({
reportedPostIds: [],
blockedUserIds: [],
addReportedPost: (postId) => {
set((state) => {
if (state.reportedPostIds.includes(postId)) {
return state;
}

return {
reportedPostIds: [...state.reportedPostIds, postId],
};
});
},
addBlockedUser: (userId) => {
set((state) => {
if (state.blockedUserIds.includes(userId)) {
return state;
}

return {
blockedUserIds: [...state.blockedUserIds, userId],
};
});
},
removeBlockedUser: (userId) => {
set((state) => ({
blockedUserIds: state.blockedUserIds.filter((id) => id !== userId),
}));
},
}),
{
name: "reported-community-posts-storage",
partialize: (state) => ({
reportedPostIds: state.reportedPostIds,
blockedUserIds: state.blockedUserIds,
}),
},
),
);

export default useReportedPostsStore;
Loading