Skip to content
Open
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
45 changes: 41 additions & 4 deletions apps/web/src/apis/community/deletePost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,64 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError, AxiosResponse } from "axios";
import { useRouter } from "next/navigation";

import useAuthStore from "@/lib/zustand/useAuthStore";
import { toast } from "@/lib/zustand/useToastStore";
import { CommunityQueryKeys, communityApi, type DeletePostResponse } from "./api";

interface DeletePostVariables {
postId: number;
boardCode?: string;
}

/**
* @description ISR ํŽ˜์ด์ง€๋ฅผ revalidateํ•˜๋Š” ํ•จ์ˆ˜
* @param boardCode - ๊ฒŒ์‹œํŒ ์ฝ”๋“œ
* @param accessToken - ์‚ฌ์šฉ์ž ์ธ์ฆ ํ† ํฐ
*/
const revalidateCommunityPage = async (boardCode: string, accessToken: string) => {
try {
if (!accessToken) {
console.warn("Revalidation skipped: No access token available");
return;
}

await fetch("/api/revalidate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ boardCode }),
});
Comment on lines +20 to +34
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

ISR revalidate๊ฐ€ โ€˜์กฐ์šฉํžˆ ์‹คํŒจโ€™ํ•  ์ˆ˜ ์žˆ์–ด์š”.
fetch๋Š” 4xx/5xx์—์„œ๋„ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€์ง€ ์•Š์•„์„œ, ํ˜„์žฌ๋Š” ์‹คํŒจ๊ฐ€ ๋กœ๊ทธ ์—†์ด ํ†ต๊ณผ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์‘๋‹ต ok ์ฒดํฌ๋ฅผ ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”ง ์ˆ˜์ • ์ œ์•ˆ
-    await fetch("/api/revalidate", {
+    const response = await fetch("/api/revalidate", {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
         Authorization: `Bearer ${accessToken}`,
       },
       body: JSON.stringify({ boardCode }),
     });
+    if (!response.ok) {
+      throw new Error(`Revalidate failed: ${response.status}`);
+    }
๐Ÿค– Prompt for AI Agents
In `@apps/web/src/apis/community/deletePost.ts` around lines 20 - 34, The
revalidation call in revalidateCommunityPage currently awaits fetch but doesn't
check HTTP status, so 4xx/5xx responses silently succeed; update
revalidateCommunityPage to capture the fetch response, check response.ok, and if
false log or throw an error including response.status and response.statusText
(or response.text()) so failed ISR revalidations are visible; ensure this
handling occurs after the fetch and before returning from
revalidateCommunityPage.

} catch (error) {
console.error("Revalidate failed:", error);
}
};

/**
* @description ๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ๋ฅผ ์œ„ํ•œ useMutation ์ปค์Šคํ…€ ํ›…
*/
const useDeletePost = () => {
const router = useRouter();
const queryClient = useQueryClient();
const { accessToken } = useAuthStore();

return useMutation<AxiosResponse<DeletePostResponse>, AxiosError, number>({
mutationFn: communityApi.deletePost,
onSuccess: () => {
return useMutation<AxiosResponse<DeletePostResponse>, AxiosError, DeletePostVariables>({
mutationFn: ({ postId }) => communityApi.deletePost(postId),
onSuccess: async (_result, variables) => {
// 'posts' ์ฟผ๋ฆฌ ํ‚ค๋ฅผ ๊ฐ€์ง„ ๋ชจ๋“  ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ
// ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ๋‹ค์‹œ ๋ถˆ๋Ÿฌ์˜ค๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] });

// ISR ํŽ˜์ด์ง€ revalidate
if (variables.boardCode && accessToken) {
await revalidateCommunityPage(variables.boardCode, accessToken);
}

toast.success("๊ฒŒ์‹œ๊ธ€์ด ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");

// ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€ ์ด๋™
router.replace("/community/FREE");
router.replace(`/community/${variables.boardCode || "FREE"}`);
},
onError: (error) => {
console.error("๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ ์‹คํŒจ:", error);
Expand Down
36 changes: 35 additions & 1 deletion apps/web/src/apis/community/patchUpdatePost.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,60 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";

import useAuthStore from "@/lib/zustand/useAuthStore";
import { toast } from "@/lib/zustand/useToastStore";
import { CommunityQueryKeys, communityApi, type PostIdResponse, type PostUpdateRequest } from "./api";

interface UpdatePostVariables {
postId: number;
data: PostUpdateRequest;
boardCode?: string;
}

/**
* @description ISR ํŽ˜์ด์ง€๋ฅผ revalidateํ•˜๋Š” ํ•จ์ˆ˜
* @param boardCode - ๊ฒŒ์‹œํŒ ์ฝ”๋“œ
* @param accessToken - ์‚ฌ์šฉ์ž ์ธ์ฆ ํ† ํฐ
*/
const revalidateCommunityPage = async (boardCode: string, accessToken: string) => {
try {
if (!accessToken) {
console.warn("Revalidation skipped: No access token available");
return;
}

await fetch("/api/revalidate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ boardCode }),
});
Comment on lines +19 to +33
Copy link

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐ŸŸ  Major

์—ฌ๊ธฐ๋„ revalidate ์‹คํŒจ๊ฐ€ ์ˆจ๊ฒจ์งˆ ์ˆ˜ ์žˆ์–ด์š”.
fetch๋Š” 4xx/5xx์—์„œ๋„ ์„ฑ๊ณต์œผ๋กœ resolve๋˜๋ฏ€๋กœ, ์‘๋‹ต ok ์ฒดํฌ๋กœ ์‹คํŒจ๋ฅผ ๋ช…์‹œํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”ง ์ˆ˜์ • ์ œ์•ˆ
-    await fetch("/api/revalidate", {
+    const response = await fetch("/api/revalidate", {
       method: "POST",
       headers: {
         "Content-Type": "application/json",
         Authorization: `Bearer ${accessToken}`,
       },
       body: JSON.stringify({ boardCode }),
     });
+    if (!response.ok) {
+      throw new Error(`Revalidate failed: ${response.status}`);
+    }
๐Ÿ“ Committable suggestion

โ€ผ๏ธ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const revalidateCommunityPage = async (boardCode: string, accessToken: string) => {
try {
if (!accessToken) {
console.warn("Revalidation skipped: No access token available");
return;
}
await fetch("/api/revalidate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ boardCode }),
});
const revalidateCommunityPage = async (boardCode: string, accessToken: string) => {
try {
if (!accessToken) {
console.warn("Revalidation skipped: No access token available");
return;
}
const response = await fetch("/api/revalidate", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ boardCode }),
});
if (!response.ok) {
throw new Error(`Revalidate failed: ${response.status}`);
}
๐Ÿค– Prompt for AI Agents
In `@apps/web/src/apis/community/patchUpdatePost.ts` around lines 19 - 33, The
fetch in revalidateCommunityPage currently treats all HTTP responses as success;
update the function to check the Response.ok after the await fetch (in
revalidateCommunityPage) and handle non-ok responses by throwing or logging a
descriptive error (include response.status and response text) so the existing
try/catch can surface failures; ensure the Authorization/header/body logic
remains unchanged and that failures end up logged or rethrown for callers to
observe.

} catch (error) {
console.error("Revalidate failed:", error);
}
};

/**
* @description ๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •์„ ์œ„ํ•œ useMutation ์ปค์Šคํ…€ ํ›…
*/
const useUpdatePost = () => {
const queryClient = useQueryClient();
const { accessToken } = useAuthStore();

return useMutation<PostIdResponse, AxiosError, UpdatePostVariables>({
mutationFn: ({ postId, data }) => communityApi.updatePost(postId, data),
onSuccess: (_result, variables) => {
onSuccess: async (_result, variables) => {
// ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ์ฟผ๋ฆฌ์™€ ๋ชฉ๋ก ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํšจํ™”
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts, variables.postId] });
queryClient.invalidateQueries({ queryKey: [CommunityQueryKeys.posts] });

// ISR ํŽ˜์ด์ง€ revalidate
if (variables.boardCode && accessToken) {
await revalidateCommunityPage(variables.boardCode, accessToken);
}

toast.success("๊ฒŒ์‹œ๊ธ€์ด ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
},
onError: (error) => {
Expand Down
13 changes: 8 additions & 5 deletions apps/web/src/app/community/[boardCode]/[postId]/KebabMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
"use client";

import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, type RefObject } from "react";
import { useDeletePost } from "@/apis/community";
import ReportPanel from "@/components/ui/ReportPanel";
import { toast } from "@/lib/zustand/useToastStore";
import { IconSetting } from "@/public/svgs/mentor";

const useClickOutside = (ref, handler) => {
const useClickOutside = (ref: RefObject<HTMLDivElement>, handler: (event: MouseEvent | TouchEvent) => void) => {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) return;
const listener = (event: MouseEvent | TouchEvent) => {
const current = ref.current;
if (!current) return;
if (!(event.target instanceof Node)) return;
if (current.contains(event.target)) return;
handler(event);
};
document.addEventListener("mousedown", listener);
Expand Down Expand Up @@ -113,7 +116,7 @@ const KebabMenu = ({ postId, boardCode, isOwner = false }: KebabMenuProps) => {
<button
onClick={() => {
if (confirm("์ •๋ง๋กœ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?")) {
deletePost(postId);
deletePost({ postId, boardCode });
}
}}
className={`flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-gray-700 typo-regular-2 hover:bg-k-50`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react";

import { useUpdatePost } from "@/apis/community";
import { toast } from "@/lib/zustand/useToastStore";
import { IconArrowBackFilled, IconImage, IconPostCheckboxFilled, IconPostCheckboxOutlined } from "@/public/svgs";

type PostModifyFormProps = {
Expand All @@ -25,6 +26,7 @@ const PostModifyForm = ({
}: PostModifyFormProps) => {
const [title, setTitle] = useState<string>(defaultTitle);
const [content, setContent] = useState<string>(defaultContent);
const [isQuestion, setIsQuestion] = useState<boolean>(defaultIsQuestion);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const titleRef = useRef<HTMLDivElement>(null);
const imageUploadRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -60,12 +62,23 @@ const PostModifyForm = ({
}, []);

const submitPost = async () => {
if (!title.trim()) {
toast.error("์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.");
return;
}

if (!content.trim()) {
toast.error("๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.");
return;
}

updatePostMutation.mutate(
{
postId,
boardCode,
data: {
postUpdateRequest: {
postCategory: defaultPostCategory,
postCategory: isQuestion ? "์งˆ๋ฌธ" : "์ž์œ ",
title,
content,
},
Expand Down Expand Up @@ -108,8 +121,8 @@ const PostModifyForm = ({
</div>
<div className="flex h-[42px] items-center justify-between border-b border-b-gray-c-100 px-5 py-2.5">
<div className="text-gray-250/87 flex items-center gap-1 typo-regular-2">
<button type="button">
{defaultIsQuestion ? <IconPostCheckboxFilled /> : <IconPostCheckboxOutlined />}
<button onClick={() => setIsQuestion(!isQuestion)} type="button">
{isQuestion ? <IconPostCheckboxFilled /> : <IconPostCheckboxOutlined />}
</button>
์งˆ๋ฌธ์œผ๋กœ ์—…๋กœ๋“œ ํ•˜๊ธฐ
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const useModifyHookForm = (myMentorProfile: MentorCardPreview | null): UseModify
reset({
channels,
introduction: myMentorProfile.introduction,
passTip: "",
passTip: myMentorProfile.passTip ?? "",
});
} else {
// myMentorProfile์ด ์—†์„ ๋•Œ๋„ 4๊ฐœ์˜ ๋นˆ ์ฑ„๋„ ์Šฌ๋กฏ ์ œ๊ณต
Expand All @@ -35,6 +35,6 @@ const useModifyHookForm = (myMentorProfile: MentorCardPreview | null): UseModify
});
}
}, [myMentorProfile, reset]);
return { ...method };
return method;
};
export default useModifyHookForm;
6 changes: 4 additions & 2 deletions apps/web/src/app/mentor/modify/_ui/ModifyContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ const ModifyContent = () => {
<h2 className="mt-10 text-primary-1 typo-sb-5">๋ฉ˜ํ†  ํ•œ๋งˆ๋””</h2>
<textarea
{...register("introduction")}
className="mt-2.5 h-30 w-full rounded-lg bg-k-50 p-5 text-k-300 typo-regular-2"
className="mt-2.5 h-30 w-full rounded-lg bg-k-50 p-5 text-k-900 placeholder:text-k-300 typo-regular-2"
placeholder="์ตœ๋Œ€ 200์ž ์ด๋‚ด"
maxLength={200}
/>
{errors.introduction && (
<p className="mt-1 text-red-500 typo-regular-2">
Expand All @@ -74,8 +75,9 @@ const ModifyContent = () => {
<h2 className="mt-10 text-primary-1 typo-sb-5">ํ•ฉ๊ฒฉ ๋ ˆ์‹œํ”ผ</h2>
<textarea
{...register("passTip")}
className="mt-2.5 h-30 w-full rounded-lg bg-k-50 p-5 text-k-300 typo-regular-2"
className="mt-2.5 h-30 w-full rounded-lg bg-k-50 p-5 text-k-900 placeholder:text-k-300 typo-regular-2"
placeholder="์ตœ๋Œ€ 200์ž ์ด๋‚ด"
maxLength={200}
/>
{errors.passTip && (
<p className="mt-1 text-red-500 typo-regular-2">
Expand Down
34 changes: 17 additions & 17 deletions apps/web/src/app/mentor/waiting/_ui/WaitingContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const WaitingContent = () => {
<div className="flex items-center">
<h2 className="mr-2 text-k-900 typo-sb-5">๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ˜ํ† ๋ง</h2>
{totalLength > DEFAULT_VISIBLE_ITEMS && (
<span className="rounded-2xl bg-primary-1 px-2 text-k-0">{totalLength - 2}+</span>
<span className="rounded-2xl bg-primary-1 px-2 text-k-0">{totalLength - DEFAULT_VISIBLE_ITEMS}+</span>
)}
</div>
</div>
Expand All @@ -36,15 +36,15 @@ const WaitingContent = () => {
</div>
) : (
(isExpanded ? approveList : approveList.slice(0, DEFAULT_VISIBLE_ITEMS)).map((item) => (
<MentorExpandChatCard
key={item.mentoringId}
isChecked={item.isChecked}
mentoringId={item.mentoringId}
profileImageUrl={item.profileImageUrl}
nickname={item.nickname}
message={`๋‹˜์ด ๋ฉ˜ํ‹ฐ ์‹ ์ฒญ์„ ์ˆ˜๋ฝํ–ˆ์–ด์š”.`}
date={item.createdAt}
/>
<MentorExpandChatCard
key={item.mentoringId}
isChecked={item.isChecked}
mentoringId={item.mentoringId}
profileImageUrl={item.profileImageUrl}
nickname={item.nickname}
message="๋ฉ˜ํ† ๊ฐ€ ๋ฉ˜ํ‹ฐ ์‹ ์ฒญ์„ ์ˆ˜๋ฝํ–ˆ์–ด์š”."
date={item.createdAt}
/>
))
)}
</div>
Expand All @@ -56,19 +56,19 @@ const WaitingContent = () => {
{({ isExpanded }) => (
<div className="space-y-2">
<h3 className="mt-3 px-5 text-k-900 typo-sb-5">์ˆ˜๋ฝ ๋Œ€๊ธฐ์ค‘</h3>
<div className="space-y-2 p-4">
<div className="space-y-2">
{pendingList.length === 0 ? (
<div className="px-4 py-3">
<EmptySdwBCards message="๋ฉ˜ํ†  ์‹ ์ฒญ ๋‚ด์—ญ์ด ์—†์Šต๋‹ˆ๋‹ค." />
</div>
) : (
(isExpanded ? pendingList : pendingList.slice(0, DEFAULT_VISIBLE_ITEMS)).map((item) => (
<MentorChatCard
key={item.mentoringId}
profileImageUrl={item.profileImageUrl}
nickname={item.nickname}
description={`๋‹˜์—๊ฒŒ ๋ฉ˜ํ‹ฐ ์‹ ์ฒญ์„ ๋ณด๋ƒˆ์–ด์š”.`}
/>
<MentorChatCard
key={item.mentoringId}
profileImageUrl={item.profileImageUrl}
nickname={item.nickname}
description="๋ฉ˜ํ† ์—๊ฒŒ ๋ฉ˜ํ‹ฐ ์‹ ์ฒญ์„ ๋ณด๋ƒˆ์–ด์š”."
/>
))
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,23 @@ const useArticleSchema = ({

const handleFormSubmit = (data: ArticleFormData) => {
if (isEdit) {
putModifyArticle({ body: data, articleId });
putModifyArticle(
{ body: data, articleId },
{
onSuccess: () => {
handleClose();
reset();
},
}
);
} else {
postAddArticle(data);
postAddArticle(data, {
onSuccess: () => {
handleClose();
reset();
},
});
}
handleClose();
reset();
};

const handleModalClose = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { z } from "zod";
export const articleSchema = z.object({
title: z.string().min(1, "์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”").max(20, "์ œ๋ชฉ์€ 20์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"),
description: z.string().min(1, "๋‚ด์šฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”").max(300, "๋‚ด์šฉ์€ 300์ž ์ดํ•˜๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"),
url: z.string().url("์˜ฌ๋ฐ”๋ฅธ ๋งํฌ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”").optional().or(z.literal("")),
url: z.union([z.string().url("์˜ฌ๋ฐ”๋ฅธ ๋งํฌ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"), z.literal("")]).optional(),
resetToDefaultImage: z.boolean().optional(),
// file์€ optional๋กœ ์„ค์ • (์—…๋กœ๋“œํ•˜์ง€ ์•Š์„ ์ˆ˜๋„ ์žˆ์œผ๋ฏ€๋กœ)
file: z.instanceof(File).optional(),
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/mentor/MentorCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const MentorCard = ({ mentor, observeRef, isMine = false }: MentorCardProps) =>
onClick={() => id && handlePostApplyMentor(id)}
className="flex h-10 w-1/2 flex-shrink-0 items-center justify-center gap-3 rounded-[20px] bg-primary px-5 py-2.5 text-white typo-medium-2"
>
๋ฉ˜ํ‹ฐ ์‹ ์ฒญํ•˜๊ธฐ
๋ฉ˜ํ† ๋ง ์‹ ์ฒญํ•˜๊ธฐ
</button>
</>
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/types/mentor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface Mentor {
isApplied: boolean;
}
/** ๋ฆฌ์ŠคํŠธ(๋ฏธ๋ฆฌ๋ณด๊ธฐ) ์šฉ โ€“ passTip / isApplied ์—†์ด ์‚ฌ์šฉ */
export type MentorCardPreview = (MentorCardBase & { term: string }) | null; // ํ•™์—… ํ•™๊ธฐ (์˜ˆ: "2026-1")
export type MentorCardPreview = (MentorCardBase & { term: string; passTip?: string }) | null; // ํ•™์—… ํ•™๊ธฐ (์˜ˆ: "2026-1")

/** ์ƒ์„ธ ๋ทฐ ์šฉ โ€“ ์ถ”๊ฐ€ ์ •๋ณด ํฌํ•จ */
export interface MentorCardDetail extends MentorCardBase {
Expand Down
Loading