-
Notifications
You must be signed in to change notification settings - Fork 0
Feature#337 문의 삭제 및 정렬 추가 #363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds server-driven pagination/filtering/sorting for inquiry lists by introducing a params interface and updating the API, hook, and UI usage. Implements reply deletion via a new mutation, exposes deleteReply in the reply actions hook, and wires it into the inquiry detail page UI. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Admin as Admin UI
participant Comp as InquiryTabContent
participant Hook as useInquiryList
participant API as getInquiryList
participant Server as Backend
Admin->>Comp: Change filters/sort/pagination
Comp->>Hook: useInquiryList({ page, size, status?, sort, asc })
Hook->>API: getInquiryList(params)
API->>Server: GET /inquiries?{page,size,status?,sort,asc}
Server-->>API: 200 { content, totalPages, ... }
API-->>Hook: data
Hook-->>Comp: data
Comp-->>Admin: Render table with data.content
note over API,Server: Server-side filtering/sorting/paging
sequenceDiagram
autonumber
actor Admin as Admin UI
participant Detail as InquiryDetailPage
participant Actions as useInquiryReplyActions
participant Mut as useDeleteInquiry
participant Query as React Query
participant Server as Backend
Admin->>Detail: Click "Delete" on reply
Detail->>Actions: deleteReply()
Actions->>Mut: mutate(inquiryId)
Mut->>Server: DELETE /inquiries/{id}/reply
alt Success
Server-->>Mut: 204 No Content
Mut-->>Actions: onSuccess
Actions->>Query: invalidate ["inquiry"]
Query->>Server: Refetch affected lists
else Error
Server-->>Mut: 4xx/5xx
Mut-->>Actions: onError(error)
Actions-->>Detail: propagate error
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
📝 추가 및 변경된 파일총 4개 파일 변경 |
✅ Mosu Admin 빌드가 성공적으로 완료되었습니다!📋 빌드 결과
🚀 다음 단계코드 리뷰 후 머지하면 프로덕션에 배포됩니다. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
mosu-admin/src/api/inquiry/getInquiryList.ts (1)
75-77: Don’t swallow the original error; rethrow it with contextRewrapping the error as a new Error() loses the HTTP status/message. Preserve it and optionally add context.
Apply:
- } catch (error) { - console.log(error); - throw new Error(); + } catch (error) { + console.error("getInquiryList failed:", error); + throw (error instanceof Error ? error : new Error("getInquiryList failed")); }mosu-admin/src/pages/InquiryDetailPage.tsx (1)
21-32: Deletion state not reflected; on delete success, detail remains stale
- isLoading only tracks create/update and ignores deletion. The Delete button may be clickable during an in-flight delete.
- The delete mutation (in the hook) doesn’t invalidate the detail query or call onSuccess; the page won’t refresh to hide the deleted answer and won’t trigger your success toast.
Apply:
- const { createReply, updateReply, deleteReply, isLoading } = useInquiryReplyActions({ + const { createReply, updateReply, deleteReply, isLoading, isDeleting } = useInquiryReplyActions({ inquiryId, onSuccess: () => { setMode("view"); console.log("답변이 성공적으로 처리되었습니다."); // TODO: 성공 토스트 표시 },And update the Delete button disabled state (see below).
Additionally, update the hook to:
- invalidate ["inquiry", inquiryId, "detail"] on delete success,
- call onSuccess() on delete success,
- expose isDeleting and include it in isLoading.
See hook comments for the diff.mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx (2)
55-66: Invalidate list queries after create to reflect status changes immediatelyAfter creating an answer, the inquiry’s status likely flips to "완료". Invalidate list queries too so list pages reflect the change without a hard refresh.
Apply:
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["inquiry", inquiryId, "detail"], }); + queryClient.invalidateQueries({ + queryKey: ["inquiry"], + }); onSuccess?.(); },
117-124: Expose isDeleting and include it in isLoading for consistent button statesMake deletion state available to UIs and ensure isLoading covers all three operations.
Apply:
return { createReply, updateReply, - deleteReply, - isCreating: createAnswerMutation.isPending, - isUpdating: updateAnswerMutation.isPending, - isLoading: createAnswerMutation.isPending || updateAnswerMutation.isPending, + deleteReply, + isCreating: createAnswerMutation.isPending, + isUpdating: updateAnswerMutation.isPending, + isDeleting: deleteAnswerMutation.isPending, + isLoading: + createAnswerMutation.isPending || + updateAnswerMutation.isPending || + deleteAnswerMutation.isPending, };
🧹 Nitpick comments (7)
mosu-admin/src/api/inquiry/getInquiryList.ts (3)
42-48: Tighten param typing for sort/status to prevent backend mismatchesGood addition. To catch typos at compile time, consider narrowing types:
- status is already a union; keep it.
- sort can be a union of allowed server fields (e.g., "id" | "title" | "createdAt" if supported). This prevents accidental invalid values from callers.
Apply this diff to narrow sort safely while keeping extensibility:
-export interface InquiryListParams { +export interface InquiryListParams { page?: number; size?: number; status?: "PENDING" | "COMPLETED"; - sort?: string; + sort?: "id" | "title" | "createdAt"; asc?: boolean; }
58-67: Param assembly is fine; optionally omit undefined and defaults for cleaner URLsCurrent approach works. If you want cleaner query strings, you can drop undefined and default-valued entries before sending.
Example (optional):
- const params: Record<string, unknown> = { - page, - size, - sort, - asc, - }; - - if (status) { - params.status = status; - } + const params = Object.fromEntries( + Object.entries({ page, size, sort, asc, status }).filter( + ([, v]) => v !== undefined, + ), + );
80-84: Add query ergonomics: staleTime and keepPreviousData for smoother paginationSince this query backs pagination, enable result reuse between pages and reduce refetch churn.
Apply:
export const useInquiryList = (params: InquiryListParams = {}) => { return useSuspenseQuery({ queryKey: ["inquiry", params], - queryFn: () => getInquiryList(params), + queryFn: () => getInquiryList(params), + staleTime: 10_000, + gcTime: 5 * 60_000, + placeholderData: (prev) => prev, // keepPreviousData equivalent with Suspense }); };Add the following import at the top if you opt for keepPreviousData utility instead:
import { useSuspenseQuery /*, keepPreviousData*/ } from "@tanstack/react-query";mosu-admin/src/components/inquiry/InquiryTabContent.tsx (2)
48-52: Suspense query + manual loading UI mismatchuseInquiryList wraps useSuspenseQuery. With Suspense enabled, the component will suspend on load, so the isLoading branch likely won’t execute. Either:
- keep Suspense and remove local isLoading UI here (use a Suspense boundary/Route fallback), or
- switch to useQuery in the hook if you prefer in-component spinners.
92-92: Table rendering is fine; consider passing total count if pagination component needs itIf CommonPagination or the table needs total elements, you may pass data.totalElements too.
mosu-admin/src/pages/InquiryDetailPage.tsx (1)
75-81: Prefer real author metadata over hard-coded '관리자'If the API provides the answer author, display it for auditability.
Apply:
- <PostContent - author="관리자" + <PostContent + author={inquiryAnswer.author ?? "관리자"} createdAt={inquiryAnswer.createdAt} content={inquiryAnswer.content} title={inquiryAnswer.title} attachments={inquiryAnswer.attachments} />mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx (1)
86-97: Same for updates: invalidate list as well as detailIf the update affects content surfaced in list rows (e.g., title), refresh the list caches too.
Apply:
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["inquiry", inquiryId, "detail"], }); + queryClient.invalidateQueries({ + queryKey: ["inquiry"], + }); onSuccess?.(); },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
mosu-admin/src/api/inquiry/getInquiryList.ts(2 hunks)mosu-admin/src/components/inquiry/InquiryTabContent.tsx(2 hunks)mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx(3 hunks)mosu-admin/src/pages/InquiryDetailPage.tsx(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx (1)
mosu-admin/src/api/inquiry/deleteInquiry.ts (1)
useDeleteInquiry(16-20)
mosu-admin/src/pages/InquiryDetailPage.tsx (2)
mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx (1)
useInquiryReplyActions(13-125)mosu-admin/src/components/PostContent.tsx (1)
PostContent(13-73)
mosu-admin/src/components/inquiry/InquiryTabContent.tsx (1)
mosu-admin/src/api/inquiry/getInquiryList.ts (1)
useInquiryList(80-85)
🔇 Additional comments (2)
mosu-admin/src/components/inquiry/InquiryTabContent.tsx (2)
39-46: LGTM on server-side param constructionReturning a single, memoized apiParams object ensures stable query keys and minimal re-renders.
24-37: Confirm backend sort field names for recencyI didn’t find any references to how the
sortparameter is consumed by our API in the client or service layer, so it’s unclear whether"id"will actually sort by creation date. Please verify the backend contract—e.g. check the request payload insrc/services/inquiryService.tsor our Swagger/OpenAPI spec—to confirm whether recency should use"id"or"createdAt".• File:
mosu-admin/src/components/inquiry/InquiryTabContent.tsx
• Lines 24–37: mapping"최신순"/"오래된순"tosort = "id"
• If the API expectscreatedAtfor date‐based sorting, update those cases:- case "최신순": - sort = "id"; + case "최신순": + sort = "createdAt"; asc = false; break; - case "오래된순": - sort = "id"; + case "오래된순": + sort = "createdAt"; asc = true; break;
| export const getInquiryList = async ({ | ||
| page = 0, | ||
| size = 10, | ||
| status, | ||
| sort = "id", | ||
| asc = false, | ||
| }: InquiryListParams = {}): Promise<InquiryListResponse> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Signature refactor looks good; ensure all callers switched to object params
Moving to a single params object is the right call for server-driven sorting/filtering.
Run to confirm no legacy calls with positional args remain:
🏁 Script executed:
#!/bin/bash
# Find any lingering positional-arg calls like useInquiryList(x, y) or getInquiryList(x, y)
rg -nP '\buseInquiryList\s*\([^)]*,\s*[^)]*\)' -C2
rg -nP '\bgetInquiryList\s*\([^)]*,\s*[^)]*\)' -C2Length of output: 454
Update lingering positional calls to use object parameters
We’ve detected a call site still using positional arguments for getInquiryList, which will break the new signature.
• mosu-app/src/entities/posts/services/getInquiryList.ts:
- Line 78: change
- queryFn: () => getInquiryList(page, size), + queryFn: () => getInquiryList({ page, size }),
This ensures all callers align with the refactored function signature.
📝 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.
| export const getInquiryList = async ({ | |
| page = 0, | |
| size = 10, | |
| status, | |
| sort = "id", | |
| asc = false, | |
| }: InquiryListParams = {}): Promise<InquiryListResponse> => { | |
| // In mosu-app/src/entities/posts/services/getInquiryList.ts, around line 78 | |
| // …other config… | |
| queryFn: () => getInquiryList({ page, size }), | |
| // …other config… |
🤖 Prompt for AI Agents
In mosu-app/src/entities/posts/services/getInquiryList.ts around line 78, there
is a call still using positional arguments for getInquiryList which now expects
a single object parameter; update the call to pass an object with named
properties (e.g. { page, size, status, sort, asc }) instead of positional args,
making sure to include only the fields you intend to override (omit undefined to
allow defaults) so the new signature is honored.
| const apiParams = useMemo(() => { | ||
| let status: "PENDING" | "COMPLETED" | undefined = undefined; | ||
| if (!selectedFilters.includes("전체")) { | ||
| filtered = inquiryData.filter((item) => selectedFilters.includes(item.status)); | ||
| if (selectedFilters.includes("미응답")) status = "PENDING"; | ||
| else if (selectedFilters.includes("완료")) status = "COMPLETED"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: selecting both '미응답' and '완료' forces PENDING; should show all (no status filter)
With multiselect filters, if both are chosen (and "전체" is not), the current logic picks "PENDING" due to the first includes() check. This yields incorrect server filtering.
Apply:
- const apiParams = useMemo(() => {
- let status: "PENDING" | "COMPLETED" | undefined = undefined;
- if (!selectedFilters.includes("전체")) {
- if (selectedFilters.includes("미응답")) status = "PENDING";
- else if (selectedFilters.includes("완료")) status = "COMPLETED";
- }
+ const apiParams = useMemo(() => {
+ let status: "PENDING" | "COMPLETED" | undefined = undefined;
+ if (!selectedFilters.includes("전체")) {
+ const picked = selectedFilters.filter((f) => f === "미응답" || f === "완료");
+ if (picked.length === 1) {
+ status = picked[0] === "미응답" ? "PENDING" : "COMPLETED";
+ }
+ // picked.length === 0 or 2 -> no status filter (show all)
+ }📝 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.
| const apiParams = useMemo(() => { | |
| let status: "PENDING" | "COMPLETED" | undefined = undefined; | |
| if (!selectedFilters.includes("전체")) { | |
| filtered = inquiryData.filter((item) => selectedFilters.includes(item.status)); | |
| if (selectedFilters.includes("미응답")) status = "PENDING"; | |
| else if (selectedFilters.includes("완료")) status = "COMPLETED"; | |
| } | |
| const apiParams = useMemo(() => { | |
| let status: "PENDING" | "COMPLETED" | undefined = undefined; | |
| if (!selectedFilters.includes("전체")) { | |
| const picked = selectedFilters.filter((f) => f === "미응답" || f === "완료"); | |
| if (picked.length === 1) { | |
| status = picked[0] === "미응답" ? "PENDING" : "COMPLETED"; | |
| } | |
| // picked.length === 0 or 2 -> no status filter (show all) | |
| } | |
| // ...rest of useMemo body unchanged | |
| }, [selectedFilters]); |
🤖 Prompt for AI Agents
In mosu-admin/src/components/inquiry/InquiryTabContent.tsx around lines 14 to
19, the status calculation currently checks includes("미응답") first and forces
status = "PENDING" when both "미응답" and "완료" are selected; change the logic so
that when both status options are selected (and "전체" is not), status remains
undefined (no server-side status filter). Concretely, detect the combination: if
selectedFilters includes both "미응답" and "완료" then leave status undefined;
otherwise set status to "PENDING" or "COMPLETED" depending on which single
option is present (keeping existing "전체" bypass).
| import { usePostInquiryAnswer } from "@/api/inquiry/postInquiryAnswer"; | ||
| import { useUpdateInquiryAnswer } from "@/api/inquiry/updateInquiryAnswer"; | ||
| import { uploadFile } from "@/api/file/uploadFile"; | ||
| import { useDeleteInquiry } from "@/api/inquiry/deleteInquiry"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Confirm API: useDeleteInquiry deletes an answer vs the entire inquiry
Naming suggests it deletes the inquiry, but here it is used to delete the admin’s answer. Ensure the endpoint and function name are aligned to prevent destructive operations.
If it deletes the whole inquiry, replace with a dedicated delete-answer endpoint/hook (e.g., useDeleteInquiryAnswer) or adjust the implementation.
🏁 Script executed:
#!/bin/bash
# Inspect deleteInquiry API to ensure it targets the answer resource
rg -n "deleteInquiry" -C3 mosu-admin/src/api/inquiry
cat mosu-admin/src/api/inquiry/deleteInquiry.tsLength of output: 1622
Rename API hook to reflect deleting an inquiry answer, not the inquiry itself
The useDeleteInquiry hook currently calls DELETE on /admin/inquiry/{postId}/answer, so it removes only the administrator’s answer, not the entire inquiry. To avoid confusion (and potential destructive mistakes), please refactor:
• mosu-admin/src/api/inquiry/deleteInquiry.ts
– Rename file to deleteInquiryAnswer.ts
– Change exported function from deleteInquiry → deleteInquiryAnswer
– Change hook export from useDeleteInquiry → useDeleteInquiryAnswer
• mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx
– Update import to:
diff - import { useDeleteInquiry } from "@/api/inquiry/deleteInquiry"; + import { useDeleteInquiryAnswer } from "@/api/inquiry/deleteInquiryAnswer";
– Update any call sites from useDeleteInquiry(...) → useDeleteInquiryAnswer(...)
This alignment will make it crystal-clear that you’re deleting an answer, not the inquiry itself.
📝 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.
| import { useDeleteInquiry } from "@/api/inquiry/deleteInquiry"; | |
| import { useDeleteInquiryAnswer } from "@/api/inquiry/deleteInquiryAnswer"; |
🤖 Prompt for AI Agents
In mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx around line 5, the
imported hook useDeleteInquiry is misnamed because the API actually DELETEs the
inquiry answer; rename the API file mosu-admin/src/api/inquiry/deleteInquiry.ts
→ mosu-admin/src/api/inquiry/deleteInquiryAnswer.ts, rename the exported
function deleteInquiry → deleteInquiryAnswer and the hook export
useDeleteInquiry → useDeleteInquiryAnswer, then update this file to import {
useDeleteInquiryAnswer } from "@/api/inquiry/deleteInquiryAnswer" and update all
call sites in this hook (and any other usages) from useDeleteInquiry(...) →
useDeleteInquiryAnswer(...).
| const deleteReply = async () => { | ||
| deleteAnswerMutation.mutate(inquiryId, { | ||
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ | ||
| queryKey: ["inquiry"], | ||
| }); | ||
| }, | ||
| onError: (error) => { | ||
| console.error("답변 삭제 실패:", error); | ||
| onError?.(error); | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deletion flow: invalidate detail, propagate success, and handle optimistic UX
Currently only ["inquiry"] is invalidated and onSuccess isn’t invoked. This leaves the detail page stale.
Apply:
- const deleteReply = async () => {
- deleteAnswerMutation.mutate(inquiryId, {
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: ["inquiry"],
- });
- },
- onError: (error) => {
- console.error("답변 삭제 실패:", error);
- onError?.(error);
- },
- });
- };
+ const deleteReply = async () => {
+ deleteAnswerMutation.mutate(inquiryId, {
+ onSuccess: () => {
+ // Refresh detail view and any lists
+ queryClient.invalidateQueries({ queryKey: ["inquiry", inquiryId, "detail"] });
+ queryClient.invalidateQueries({ queryKey: ["inquiry"] });
+ onSuccess?.();
+ },
+ onError: (error) => {
+ console.error("답변 삭제 실패:", error);
+ onError?.(error as Error);
+ },
+ });
+ };📝 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.
| const deleteReply = async () => { | |
| deleteAnswerMutation.mutate(inquiryId, { | |
| onSuccess: () => { | |
| queryClient.invalidateQueries({ | |
| queryKey: ["inquiry"], | |
| }); | |
| }, | |
| onError: (error) => { | |
| console.error("답변 삭제 실패:", error); | |
| onError?.(error); | |
| }, | |
| }); | |
| }; | |
| const deleteReply = async () => { | |
| deleteAnswerMutation.mutate(inquiryId, { | |
| onSuccess: () => { | |
| // Refresh detail view and any lists | |
| queryClient.invalidateQueries({ queryKey: ["inquiry", inquiryId, "detail"] }); | |
| queryClient.invalidateQueries({ queryKey: ["inquiry"] }); | |
| onSuccess?.(); | |
| }, | |
| onError: (error) => { | |
| console.error("답변 삭제 실패:", error); | |
| onError?.(error as Error); | |
| }, | |
| }); | |
| }; |
🤖 Prompt for AI Agents
mosu-admin/src/hooks/inquiry/useInquiryReplyAction.tsx lines 103-115: The
deleteReply onSuccess only invalidates ["inquiry"] and never calls the provided
onSuccess, leaving the detail page stale and preventing caller UX updates;
update the onSuccess to invalidate both the inquiry list and the specific
inquiry detail (e.g. ["inquiry", inquiryId] or however detail key is shaped) and
call onSuccess() so the caller can react, and for better UX implement an
optimistic update: cancel relevant queries, snapshot and setQueryData to remove
the reply immediately, then invalidate both queries on success and restore
snapshot on error while calling onError.
| <Button | ||
| onClick={() => deleteReply()} | ||
| variant="outline" | ||
| disabled={isLoading} | ||
| > | ||
| 답변 삭제하기 | ||
| </Button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add a lightweight confirm and reflect deleting state in the button
Prevent accidental deletions and disable the button while deleting.
Apply:
- <Button
- onClick={() => deleteReply()}
+ <Button
+ onClick={() => {
+ if (confirm("정말 삭제하시겠습니까?")) deleteReply();
+ }}
variant="outline"
- disabled={isLoading}
+ disabled={isLoading || isDeleting}
>
답변 삭제하기
</Button>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In mosu-admin/src/pages/InquiryDetailPage.tsx around lines 82-88, the "답변 삭제하기"
button lacks a confirmation prompt and does not reflect a deleting state; wrap
the delete flow in an onClick handler that first shows a lightweight confirm
(e.g., window.confirm) and returns early if cancelled, add a local isDeleting
state (boolean) to set true before calling deleteReply and false in finally, set
the Button's disabled prop to isLoading || isDeleting and optionally change its
label to indicate progress (e.g., "삭제 중...") so the UI prevents accidental
deletions and shows the deleting state.
✅ Linked Issue
🔍 What I did
Summary by CodeRabbit
New Features
Refactor
Bug Fixes