Conversation
- 관리자 라우트, 타입, API 함수 생성 - 탭 네비게이션 레이아웃 구현 - 대시보드: 통계 요약 카드, 미처리 신고 테이블, 신고 처리 모달 - shadcn table, badge 컴포넌트 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 프롬프트 목록 테이블 (제목, 작성자, 카테고리, 상태, 작성일) - 제목 검색 및 상태 필터 기능 - 체크박스 다중 선택 및 일괄 삭제/비공개 전환 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 유저 목록 테이블 (이름, 역할, 게시글 수, 댓글 수, 활동 제한) - 닉네임 검색 기능 - 체크박스 다중 선택 및 활동 제한/해제 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 카테고리 트리 구조 표시 (상위/하위 2단계) - 카테고리 추가, 인라인 수정, 삭제 기능 - 순서 변경 (위/아래 이동) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 공지사항 목록 테이블 (제목, 내용 미리보기, 공지일) - 체크박스 다중 선택 및 일괄 삭제 - 공지 작성 폼 (제목 50자, 내용 5000자 제한) - 작성 완료 토스트 알림 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- (admin) 라우트 그룹은 URL에 반영되지 않아 /admin 접근 시 404 발생 - app/(afterLogin)/(admin)/ → app/(afterLogin)/admin/ 으로 이동 - import 경로 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Header(GNB) 추가 및 max-width 확장 - "총 가입자" → "오늘 가입자" 라벨 수정 - 신고 테이블 헤더 배경색 추가 및 텍스트 중앙 정렬 - 통계 카드 에러 상태 UI 추가 (오류 발생 표시) - 증감/수치 텍스트 오른쪽 하단 정렬 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 탭 버튼 크기 통일 (W197, H48, gap 20px) - 게시글 관리: 카테고리 + 공개여부 드롭다운 2개로 분리 - 테이블 헤더 배경색 frog-100 + 중앙 정렬 (전체 탭 통일) - 신고 API 필드명 변경 반영 (reason, targetTitle, reasonContent, pageInfo 통일) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 대시보드: 증감 텍스트 검은색 통일, 신고 관련글 클릭 시 이동 - 신고 처리 모달: 텍스트 강조, 처리 방식 4가지, 버튼 비율 조정 - 게시글 관리: 카테고리+공개여부 필터 분리, 인라인 드롭다운, 일괄 액션 3종, 카테고리 필터 연동 - 유저 관리: 10명 표시, 프로필 아바타, 인라인 활동 제한 드롭다운 - 공지사항: 상세 조회, 버튼 매핑 수정, 공지일 컬럼 축소 - 카테고리: 소분류 수정/삭제 버튼, filled 아이콘, DnD 보류(TODO) - 전체 탭 테이블 헤더 bg-frog-100 통일 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthrough이 PR은 관리자 전용 대시보드 기능을 새로 추가합니다. 관리자용 타입 정의, API 헬퍼(query functions), React Query 기반 데이터 페칭/뮤테이션, 탭 네비게이션(대시보드/프롬프트/유저/카테고리/공지), 각 탭의 CRUD UI(목록, 상세, 작성, 처리 모달 등), 공통 UI 컴포넌트(Badge, Table)와 유틸(formatDate 등), 라우트 상수 및 패키지(radix-ui) 의존성 추가를 포함합니다. 일부 레이아웃/컴포넌트 파일이 새로 생성되거나 재배치되었습니다. Sequence Diagram(s)sequenceDiagram
participant Browser as "Admin UI (Browser)"
participant ReactQuery as "React Query (client hooks)"
participant Fetcher as "fetcher / queries/api/admin.ts"
participant Server as "Next.js API / Backend"
participant DB as "Database"
Browser->>ReactQuery: 사용자 액션 (예: 탭선택/삭제/처리)
ReactQuery->>Fetcher: query/mutation 호출 (GET/POST/PUT/DELETE)
Fetcher->>Server: HTTP 요청 /admin/...
Server->>DB: DB 조회/수정
DB-->>Server: 결과 반환
Server-->>Fetcher: JSON 응답
Fetcher-->>ReactQuery: 응답 파싱
ReactQuery-->>Browser: 업데이트된 데이터 렌더링 / invalidateQueries
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 리뷰 포인트1. 상태 관리·쿼리 무결성
짧은 칭찬: queryKey 규칙( ["admin", ... ]) 잘 잡아놓은 점 굿. 2. 입력 검증과 UX
3. 모달/뷰 상태 관리 단순화
4. 에러·재시도 UX
5. 접근성(ARIA / 키보드)
6. 타입·매핑 일관성
짧은 칭찬: 타입 파일이 잘 정리되어 있어 리팩터링/테스트하기 좋습니다. 7. 카테고리 트리 렌더링 안전성
8. 선택·페이지네이션 일관성
9. 문서화와 코드 주석
마무리: 전반적으로 구조와 패턴이 잘 잡혀 있습니다. 작은 상태 단순화, 명확한 쿼리 키 전략, 접근성 보강에 집중하면 안전성과 유지보수성이 크게 향상됩니다. 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🧹 Nitpick comments (15)
app/(afterLogin)/admin/prompts/prompt-filters.tsx (1)
17-24: 카테고리 옵션 상수는 공용으로 분리하는 게 좋습니다.동일한 옵션 집합이 형제 파일에도 중복돼 있어(라벨만 일부 상이), 이후 수정 시 화면 간 불일치가 생기기 쉽습니다.
admin/constants로 단일화해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/prompts/prompt-filters.tsx around lines 17 - 24, Extract the CATEGORY_OPTIONS constant from prompt-filters.tsx into a shared module (e.g., export it from admin/constants) and replace the local declaration with an import; update prompt-filters.tsx to import { CATEGORY_OPTIONS } from "admin/constants" (or the chosen module name) and remove the local const, then find the sibling files that duplicate the same options and change them to import the same exported CATEGORY_OPTIONS so all usages share a single source of truth.app/(afterLogin)/admin/constant.ts (1)
1-22:PROMPT_STATUS_LABEL타입을PromptStatus로 좁혀주세요.
Record<string, string>은 키 오타/누락을 잡지 못합니다. 이미PromptStatus가 있으니 exhaustiveness를 살리는 게 유지보수에 유리합니다.
참고 개념: TypeScript의 literal union 기반 매핑 타입.♻️ 제안 수정안
-import { ReportReason, UserStatus } from "./types"; +import { PromptStatus, ReportReason, UserStatus } from "./types"; @@ -export const PROMPT_STATUS_LABEL: Record<string, string> = { +export const PROMPT_STATUS_LABEL: Record<PromptStatus, string> = { PUBLIC: "공개", PRIVATE: "비공개", DELETED: "삭제", };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/constant.ts around lines 1 - 22, PROMPT_STATUS_LABEL is typed too broadly as Record<string,string>; narrow it to use the PromptStatus union so TypeScript will catch missing/typoed keys (change the declaration to a mapping keyed by PromptStatus), ensure the object includes every PromptStatus variant, and import PromptStatus from "./types" if not already imported so the compiler enforces exhaustiveness for PROMPT_STATUS_LABEL.app/(afterLogin)/admin/dashboard/report-process-modal.tsx (1)
111-111: 확인 버튼 기본 문구는처리하기가 더 정확합니다.현재는 처리 전에도 “처리 완료”로 보여 사용자 기대와 어긋납니다. idle/processing 상태를 분리해 명확히 표현해 주세요.
🎯 제안 수정안
- {isProcessing ? "처리 중..." : "처리 완료"} + {isProcessing ? "처리 중..." : "처리하기"}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/dashboard/report-process-modal.tsx at line 111, The button text currently uses {isProcessing ? "처리 중..." : "처리 완료"} which shows "처리 완료" before any action; change the idle label to "처리하기" so it reads {isProcessing ? "처리 중..." : "처리하기"} in report-process-modal.tsx (update the JSX where isProcessing is referenced), and ensure any related accessible labels or disabled logic tied to isProcessing remain consistent with the new text.app/(afterLogin)/admin/prompts/prompts-tab.tsx (3)
316-328: 인라인 상태 드롭다운과 일괄 변경 옵션 불일치인라인 상태 드롭다운에는
DELETED옵션이 있지만, 일괄 상태 변경(STATUS_OPTIONS, Line 41-45)에는PUBLIC과PRIVATE만 있습니다.의도된 설계일 수 있으나, 일관성을 위해 동일한 옵션을 제공하거나, 삭제 상태가 일괄 변경에서 제외된 이유를 주석으로 남겨두면 좋겠습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/prompts/prompts-tab.tsx around lines 316 - 328, Inline status dropdown includes "DELETED" while the batch/status list (STATUS_OPTIONS) only has "PUBLIC" and "PRIVATE"; make them consistent by either adding "DELETED" to STATUS_OPTIONS or intentionally excluding it with a comment. Update the STATUS_OPTIONS constant to include a { value: "DELETED", label: "삭제" } entry if deletion should be available in batch changes, or add a concise comment next to STATUS_OPTIONS explaining why "DELETED" is omitted; ensure any UI text/labels match the select options and that handleInlineStatus and any batch-change handler accept the "DELETED" value if added.
24-39: 하드코딩된 카테고리 매핑에 대한 유지보수 우려
CATEGORY_OPTIONS와CATEGORY_NAME_TO_ID가 하드코딩되어 있습니다. 서버에서 카테고리가 추가/수정/삭제되면 이 코드도 함께 수정해야 합니다.카테고리 목록을 API에서 가져오거나, 최소한 공유 상수 파일로 분리하여
categories-tab.tsx와 동기화하는 것을 권장합니다.💡 개선 방향
// 방법 1: API에서 카테고리 목록 가져오기 const { data: categories } = useQuery({ queryKey: ["admin", "categories"], queryFn: getAdminCategories, }); // 방법 2: 공유 상수로 분리 // constants/categories.ts export const CATEGORY_OPTIONS = [...];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/prompts/prompts-tab.tsx around lines 24 - 39, Replace the hardcoded CATEGORY_OPTIONS and CATEGORY_NAME_TO_ID in prompts-tab.tsx with a dynamic source: either fetch categories from the server using your existing query hook (e.g., useQuery with getAdminCategories) and derive options and a name->id map from the response, or import shared constants from a new constants file (e.g., constants/categories.ts) used by both prompts-tab and categories-tab; update any usages of CATEGORY_OPTIONS and CATEGORY_NAME_TO_ID to read from the fetched data or imported constants and handle loading/error states accordingly.
347-359: 페이지 수가 많을 경우 페이지네이션 UI 개선 필요현재
Array.from({ length: totalPages })로 모든 페이지 버튼을 렌더링합니다. 데이터가 많아져totalPages가 100 이상이 되면 UI가 깨지고 성능에도 영향을 줍니다.💡 축약된 페이지네이션 예시
// 예: 1 ... 4 5 6 ... 100 const getPageNumbers = (current: number, total: number) => { const delta = 2; const range: (number | "...")[] = []; for (let i = 1; i <= total; i++) { if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) { range.push(i); } else if (range[range.length - 1] !== "...") { range.push("..."); } } return range; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/prompts/prompts-tab.tsx around lines 347 - 359, The pagination currently renders all pages via Array.from({ length: totalPages }) which breaks UI and performance when totalPages is large; implement a truncated pagination helper (e.g., getPageNumbers(current, total) that returns numbers and "..." placeholders) and replace the full mapping with mapping over getPageNumbers(page, totalPages), rendering normal buttons for numbers (calling setPage(n)) and non-interactive/static elements for "..." placeholders, preserving the existing className logic to highlight the active page (use symbols: getPageNumbers, page, totalPages, setPage) and ensure keys are stable and unique when mapping.queries/api/admin.ts (2)
66-68:categoryIdfalsy 체크에 대한 고려사항
if (params.categoryId)는 falsy 체크로,categoryId가0인 경우 조건을 통과하지 못합니다. 일반적으로 카테고리 ID는 1부터 시작하므로 문제가 없을 가능성이 높지만, 만약0이 유효한 ID라면 문제가 될 수 있습니다.🔧 더 명시적인 체크를 원하시면
- if (params.categoryId) searchParams["categoryId"] = params.categoryId; + if (params.categoryId !== undefined) searchParams["categoryId"] = params.categoryId;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queries/api/admin.ts` around lines 66 - 68, The current falsy check "if (params.categoryId)" will skip valid zero values; update the guard for categoryId to explicitly check for undefined/null (e.g., "if (params.categoryId !== undefined && params.categoryId !== null)") and then set searchParams["categoryId"] = params.categoryId so zero is preserved; locate the conditional using params.categoryId and the assignment to searchParams["categoryId"] and replace the falsy check with the explicit null/undefined check.
116-123: Role 타입 재사용으로 일관성과 유지보수성 개선
types.ts에 이미 정의된Role타입(export type Role = "USER" | "ADMIN")을 활용하지 않고 있습니다. 함수 파라미터에서 동일한 union 타입을 인라인으로 정의하면 타입 정의가 분산되어 나중에 Role이 변경될 때 여러 곳을 수정해야 합니다.import type { Role } from "./types"; export const bulkUpdateUserRole = async (data: { userIds: number[]; role: Role; }) => { return await fetcher .patch("admin/users/bulk-role", { json: data }) .json<ApiResponse<{ updatedCount: number; message: string }>>(); };이 패턴은 같은 파일의
bulkUpdateUserStatus에서도 적용할 수 있으니 확인해 보세요. 타입을 중앙에서 관리하면 일관성도 높아지고 유지보수도 훨씬 쉬워집니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@queries/api/admin.ts` around lines 116 - 123, Replace the inline union type for the role parameter with the shared Role type from types.ts: import the Role type and change the function signature of bulkUpdateUserRole to use role: Role instead of role: "USER" | "ADMIN"; also update the same pattern in bulkUpdateUserStatus to use Role so both functions (bulkUpdateUserRole, bulkUpdateUserStatus) reference the centralized Role type for consistency and easier maintenance.app/(afterLogin)/admin/notices/notice-detail.tsx (1)
13-16:formatDate함수 중복이
formatDate함수는notices-tab.tsx,prompts-tab.tsx등 여러 파일에서 유사하게 정의되어 있습니다. 공통 유틸리티 함수로 추출하면 일관성을 유지하고 중복을 줄일 수 있습니다.💡 유틸리티 함수로 추출
// lib/utils/date.ts export function formatDateShort(dateStr: string) { const d = new Date(dateStr); const year = String(d.getFullYear()).slice(2); const month = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${year}.${month}.${day}`; } export function formatDateKorean(dateStr: string) { const d = new Date(dateStr); return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${d.getDate()}일`; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/notices/notice-detail.tsx around lines 13 - 16, The formatDate function in notice-detail.tsx is duplicated across multiple files (e.g., notices-tab.tsx, prompts-tab.tsx); extract it into a shared utility (e.g., create lib/utils/date.ts) and export named functions (suggest formatDateKorean and formatDateShort as shown in the review) then replace the local formatDate implementation in notice-detail.tsx, notices-tab.tsx, prompts-tab.tsx, etc. with an import from the new utility and update usages to call formatDateKorean (or formatDateShort where appropriate) so all files use the centralized, tested helper.app/(afterLogin)/admin/users/users-tab.tsx (1)
169-181: 페이지네이션 개선 필요 (prompts-tab과 동일)
prompts-tab.tsx와 마찬가지로 모든 페이지 버튼을 렌더링하는 패턴입니다. 공통Pagination컴포넌트로 추출하여 재사용하면 일관성과 유지보수성이 향상됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/users/users-tab.tsx around lines 169 - 181, Extract the repeated page-button mapping into a reusable Pagination component and replace the inline mapping in users-tab.tsx (the JSX that maps Array.from({ length: totalPages }) to buttons) with that component, mirroring prompts-tab.tsx usage; the Pagination component should accept props: totalPages, currentPage (page), onChange (setPage) and className/customization for active/inactive styles, and ensure users-tab.tsx imports and uses <Pagination totalPages={totalPages} currentPage={page} onChange={setPage} /> instead of rendering buttons directly.app/(afterLogin)/admin/notices/notices-tab.tsx (1)
169-171: 공지사항 내용 미리보기 고려현재 "공지사항 내용..."이라는 고정 텍스트가 표시됩니다. API 응답(
AdminNoticeInfo)에content필드가 없어 미리보기를 표시할 수 없는 것으로 보입니다.사용자 경험 개선을 위해:
- API에서
contentPreview필드를 추가하거나- 해당 컬럼을 제거하거나
- "클릭하여 내용 보기" 같은 안내 문구로 변경하는 것을 고려해 주세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/notices/notices-tab.tsx around lines 169 - 171, The TableCell in notices-tab.tsx currently shows the hardcoded placeholder "공지사항 내용..." instead of actual data; update the UI to either read a preview field from your API model (add contentPreview to AdminNoticeInfo and render item.contentPreview in the TableCell with truncation/fallback), or remove this column entirely, or replace the placeholder with a clear CTA like "클릭하여 내용 보기" so it’s not misleading; locate the TableCell in notices-tab.tsx and modify the rendering logic to use AdminNoticeInfo.contentPreview (with a safe fallback) or implement one of the alternate options above.app/(afterLogin)/admin/categories/categories-tab.tsx (2)
123-126: 드래그 아이콘이 비활성 기능을 암시함
GripVertical아이콘과cursor-grab스타일이 적용되어 있지만 실제 드래그 앤 드롭 기능은 구현되어 있지 않습니다(TODO 주석 확인). 사용자가 드래그를 시도할 때 혼란을 줄 수 있습니다.DnD 구현 전까지 아이콘을 숨기거나 비활성 스타일(
cursor-not-allowed,opacity-50)을 적용하는 것을 고려해 주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/categories/categories-tab.tsx around lines 123 - 126, The GripVertical drag handle currently shows an active grab cursor (className "cursor-grab") while drag-and-drop isn't implemented; update the element rendering GripVertical inside the div with className "flex items-center gap-3" to indicate disabled state until DnD is added: replace "cursor-grab" with a disabled style like "cursor-not-allowed opacity-50" (and consider adding aria-hidden or visually-hidden text) so the icon no longer misleads users; locate the GripVertical usage to make this change.
132-140:onBlur자동 저장 동작에 대한 UX 고려현재
onBlur가handleUpdate를 호출하여 포커스를 잃으면 자동으로 저장됩니다. 사용자가 취소 의도로 다른 곳을 클릭해도 저장될 수 있어 혼란을 줄 수 있습니다.또한
editName.trim()이 빈 문자열일 경우handleUpdate가 early return하지만editingId는 리셋되지 않아 편집 모드가 유지됩니다.🔧 개선 제안
const handleUpdate = (id: number) => { - if (!editName.trim()) return; + if (!editName.trim()) { + setEditingId(null); + return; + } updateMutation.mutate({ id, name: editName.trim() }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/categories/categories-tab.tsx around lines 132 - 140, The onBlur currently calls handleUpdate causing unintended auto-save; change the onBlur handler to avoid blind saves by: check editName.trim() and compare to the original name (parent.name) — if non-empty and changed, call handleUpdate(parent.categoryId) then setEditingId(null); otherwise reset editName to the original name and setEditingId(null) to exit edit mode (do not leave editingId set when editName.trim() is empty). Update the onBlur logic (the input's onBlur, handleUpdate, editingId, editName, and parent.categoryId usage) so blur either commits a valid change then clears editingId or cancels/reverts the edit and clears editingId.app/(afterLogin)/admin/layout.tsx (1)
17-19: 탭 영역도 fallback 없이 suspend되면 제목 아래가 빈칸이 됩니다.
AdminTabs가 suspend되는 동안 현재는 탭 바가 아예 사라져서 레이아웃 점프가 생깁니다. 탭 높이를 유지하는 placeholder 하나만 있어도 첫 진입 경험이 훨씬 안정적입니다.🔧 제안 수정안
- <Suspense> + <Suspense + fallback={ + <div className="h-12 animate-pulse rounded-md bg-gray-100" /> + } + > <AdminTabs /> </Suspense>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/layout.tsx around lines 17 - 19, AdminTabs가 Suspense로 로드될 때 탭 바가 사라져 레이아웃 점프가 발생하니 layout.tsx에서 현재 <Suspense><AdminTabs/></Suspense>를 fallback을 갖는 형태로 바꿔 AdminTabs가 로딩되는 동안 탭 바 높이를 유지하는 플레이스홀더를 렌더하세요; 즉 Suspense의 fallback에 탭과 동일한 높이/스타일을 가진 정적 div 또는 빈 탭 바 컴포넌트(예: className으로 고정 높이와 skeleton 스타일 적용)를 넣어 초기 진입 시 레이아웃 안정성을 보장하도록 수정하세요.app/(afterLogin)/admin/page.tsx (1)
7-9: 본문 Suspense에 fallback을 넣어주세요.
AdminContent가 suspend되면 현재 경계는null을 렌더링해서 본문이 통째로 비어 보입니다. 특히app/(afterLogin)/admin/layout.tsx의 탭 영역도 같은 패턴이라 첫 진입 시 제목만 남는 프레임이 생길 수 있습니다. 관리자 화면은 로딩 중에도 레이아웃이 유지되는 편이 훨씬 덜 불안하니, 최소한 스켈레톤이나 로딩 문구는 넣어두는 게 좋습니다.🔧 제안 수정안
export default function AdminPage() { return ( - <Suspense> + <Suspense + fallback={ + <div className="rounded-lg bg-white p-6 text-sm text-gray-500"> + 관리자 화면을 불러오는 중... + </div> + } + > <AdminContent /> </Suspense> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/page.tsx around lines 7 - 9, The Suspense wrapper around AdminContent renders nothing while suspended; add a fallback so the admin layout and tabs keep visible during load — update the Suspense in this file to include a fallback prop (e.g., fallback={<AdminSkeleton/>} or fallback={<div>Loading admin…</div>}) and create/import a simple AdminSkeleton or loading component if not present; ensure you add the import and use the same pattern for any similar Suspense usages in app/(afterLogin)/admin/layout.tsx to preserve layout during loading.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(afterLogin)/admin/admin-tabs.tsx:
- Around line 7-24: The currentTab assignment in AdminTabs is only a type
assertion and needs real validation: replace the casted expression that reads
searchParams.get("tab") with logic that checks the value against the allowed
keys in TABS (e.g., derive a Set of allowed keys from TABS) and if the value is
missing or invalid fall back to "dashboard"; update any active-tab detection to
use this validated value and consider centralizing the tab/key definitions with
ROUTES.admin or the tab-rendering source to avoid drift between AdminTabs,
AdminContent and handleTabClick.
In `@app/`(afterLogin)/admin/categories/categories-tab.tsx:
- Around line 79-81: The delete handler currently calls deleteMutation.mutate
immediately in handleDelete (using deleteMutation.mutate(id)), so add a
confirmation step before invoking the mutation: show the project's modal/dialog
component (or a browser confirm as a temporary fallback), and only call
deleteMutation.mutate(id) if the user confirms; update the handler
(handleDelete) and the delete button click path to open the dialog, pass the
target id to the dialog, and call deleteMutation.mutate from the dialog’s
confirm callback to ensure deletions are user-confirmed.
In `@app/`(afterLogin)/admin/dashboard/pending-reports-table.tsx:
- Around line 58-61: The onSuccess handler currently just invalidates the
["admin","reports"] query and clears selected report, which can leave the UI on
an out-of-range page when the last item of the current page is removed; update
the onSuccess in the pending-reports-table component to refetch the reports
query (or await queryClient.refetchQueries for ["admin","reports"]) and then
clamp or adjust the current page state (the `page` used to fetch reports)
against the returned pageInfo.totalPages (e.g., set page = Math.max(1,
Math.min(page, pageInfo.totalPages)) or decrement page if the current page >
totalPages) before clearing selection with setSelectedReport(null) so the table
never shows a fake empty-last-page state.
- Around line 132-138: The PendingReport type is missing the target article ID
and the UI is incorrectly using report.reportId for navigation; update the
PendingReport interface to include a targetId (or articleId) numeric field,
update the table link in pending-reports-table.tsx to use
ROUTES.community.DETAIL(report.targetId) (replace
href={`/community/${report.reportId}`} with the ROUTES helper), and ensure any
API mapping that constructs PendingReport populates this new targetId from the
backend response (verify the backend returns the target article ID and map it
into the PendingReport used by the TableCell/Link rendering).
- Around line 42-46: The current useQuery call only checks isLoading so when the
request fails data is undefined and the UI falls back to an empty reports array;
update the query usage to also destructure isError, error, and refetch (e.g.
const { data, isLoading, isError, error, refetch } = useQuery(...)) and change
the render logic to handle the error case separately from the empty-list case:
when isError is true render a clear error message (use error?.message or a
friendly fallback) and a retry control that calls refetch; only fall back to
reports = data?.content ?? [] when not isError so failures aren’t hidden as “no
pending reports.”
In `@app/`(afterLogin)/admin/dashboard/report-process-modal.tsx:
- Around line 46-47: 선택한 액션이 "SUSPEND"일 때 잘못되어 onProcess("PRIVATE")로 치환되는 로직을
수정하세요: report-process-modal 컴포넌트에서 selectedAction === "SUSPEND" 분기(및 관련 문자열
"SUSPEND", "PRIVATE")를 찾아 해당 분기를 제거하거나 실제 제한 API가 없으면 onProcess 호출을 하지 않는 비활성/임시
숨김 처리로 변경하고, UI에서 "SUSPEND" 옵션을 완전히 제거하거나 비활성(disabled) 표시하도록 처리해 라벨과 서버 액션의 1:1
정합성을 유지하세요.
In `@app/`(afterLogin)/admin/notices/notice-write-form.tsx:
- Around line 35-41: The back button's label is incorrect—inside the button
using onBack and ArrowLeft change the visible text from "공지 작성하기" to "목록으로 돌아가기"
to match notice-detail.tsx; update the JSX string in the button element so the
text is consistent with the list-return wording and keep the existing onClick
handler and classes unchanged.
- Around line 20-26: 현재 useMutation 호출 (const mutation = useMutation({
mutationFn: createNotice, onSuccess: ... }))에 onError 핸들러가 빠져 있어 실패 시 사용자 피드백이
없습니다; useMutation 호출에 onError 콜백을 추가하여 실패 시 toasts.error로 실패 메시지(예: "공지 등록에
실패했습니다.")와 가능하면 에러 메시지를 표시하도록 구현하고, 필요하면 onError 내에서 추가적인 실패 처리(리로드 차단, 폼 상태 리셋
등)를 호출하도록 수정하세요.
In `@app/`(afterLogin)/admin/notices/notices-tab.tsx:
- Around line 85-91: The delete button currently calls
deleteMutation.mutate(selectedIds) immediately; add a confirmation step before
invoking the mutation (e.g., show a confirm dialog/modal) so accidental
mass-deletes are prevented. Modify the Button onClick handler in notices-tab
(where selectedIds and deleteMutation are used) to check selectedIds, open a
confirmation UI (or use window.confirm), and only call
deleteMutation.mutate(selectedIds) inside the confirmation callback/handler;
keep the disabled logic (selectedIds.length === 0 || deleteMutation.isPending)
and ensure the confirmation flow also respects deleteMutation.isPending to avoid
double submissions.
In `@app/`(afterLogin)/admin/prompts/prompt-filters.tsx:
- Around line 34-40: The local input state (inputValue) initialized via
useState(keyword) isn't synced when the parent updates keyword; add a useEffect
that watches the keyword prop and calls setInputValue(keyword) to keep the
controlled input in sync, and ensure the input's value remains bound to
inputValue and onSearch is called in handleKeyDown as it is now.
In `@app/`(afterLogin)/admin/prompts/prompts-tab.tsx:
- Around line 139-142: The bulk-delete handler (handleBulkDelete) currently
calls deleteMutation.mutate(selectedIds) immediately; add a confirmation step to
prevent accidental deletes by prompting the user (e.g., window.confirm or your
modal/confirmDialog hook) when selectedIds.length > 0 and only proceed with
deleteMutation.mutate(selectedIds) if the user confirms; ensure the prompt
provides context (number of items or a warning) and keep selectedIds and
deleteMutation references intact so rollback/refresh logic still runs after a
confirmed mutation.
In `@app/`(afterLogin)/admin/types.ts:
- Around line 59-62: The ReportProcessRequest type currently makes action
optional which allows invalid requests to compile; change the interface so
action is required (replace action?: ReportAction with action: ReportAction in
ReportProcessRequest) and then update any call sites that construct
ReportProcessRequest (e.g., forms, API callers, functions that build request
payloads) to always provide a valid ReportAction value or handle missing action
before creating the payload; ensure related handlers or validators that rely on
ReportProcessRequest reflect the required field.
In `@app/`(afterLogin)/admin/users/users-tab.tsx:
- Around line 52-54: handleInlineStatusChange currently calls
statusMutation.mutate directly and needs a confirmation step when changing a
user to SUSPENDED to prevent accidental suspension; modify
handleInlineStatusChange to detect when status === UserStatus.SUSPENDED and then
show a confirmation dialog/modal (e.g., browser confirm or existing
ConfirmModal) and only call statusMutation.mutate({ userIds: [userId], status })
if the user confirms, otherwise abort the mutation. Ensure the confirmation UI
provides clear context (user id/name and consequences) and keep the existing
behavior for non-SUSPENDED statuses.
In `@CLAUDE.md`:
- Around line 28-42: The two fenced code blocks in CLAUDE.md (the block starting
with "app/ # Next.js App Router pages & layouts" and the
other block containing the commit list beginning with "feat: 로그인 화면 UI 구성 및 API
연동") lack a language tag and trigger markdownlint MD040; fix by adding an
explicit language tag (e.g., ```text) to each opening fence so both code blocks
specify a language.
---
Nitpick comments:
In `@app/`(afterLogin)/admin/categories/categories-tab.tsx:
- Around line 123-126: The GripVertical drag handle currently shows an active
grab cursor (className "cursor-grab") while drag-and-drop isn't implemented;
update the element rendering GripVertical inside the div with className "flex
items-center gap-3" to indicate disabled state until DnD is added: replace
"cursor-grab" with a disabled style like "cursor-not-allowed opacity-50" (and
consider adding aria-hidden or visually-hidden text) so the icon no longer
misleads users; locate the GripVertical usage to make this change.
- Around line 132-140: The onBlur currently calls handleUpdate causing
unintended auto-save; change the onBlur handler to avoid blind saves by: check
editName.trim() and compare to the original name (parent.name) — if non-empty
and changed, call handleUpdate(parent.categoryId) then setEditingId(null);
otherwise reset editName to the original name and setEditingId(null) to exit
edit mode (do not leave editingId set when editName.trim() is empty). Update the
onBlur logic (the input's onBlur, handleUpdate, editingId, editName, and
parent.categoryId usage) so blur either commits a valid change then clears
editingId or cancels/reverts the edit and clears editingId.
In `@app/`(afterLogin)/admin/constant.ts:
- Around line 1-22: PROMPT_STATUS_LABEL is typed too broadly as
Record<string,string>; narrow it to use the PromptStatus union so TypeScript
will catch missing/typoed keys (change the declaration to a mapping keyed by
PromptStatus), ensure the object includes every PromptStatus variant, and import
PromptStatus from "./types" if not already imported so the compiler enforces
exhaustiveness for PROMPT_STATUS_LABEL.
In `@app/`(afterLogin)/admin/dashboard/report-process-modal.tsx:
- Line 111: The button text currently uses {isProcessing ? "처리 중..." : "처리 완료"}
which shows "처리 완료" before any action; change the idle label to "처리하기" so it
reads {isProcessing ? "처리 중..." : "처리하기"} in report-process-modal.tsx (update
the JSX where isProcessing is referenced), and ensure any related accessible
labels or disabled logic tied to isProcessing remain consistent with the new
text.
In `@app/`(afterLogin)/admin/layout.tsx:
- Around line 17-19: AdminTabs가 Suspense로 로드될 때 탭 바가 사라져 레이아웃 점프가 발생하니
layout.tsx에서 현재 <Suspense><AdminTabs/></Suspense>를 fallback을 갖는 형태로 바꿔
AdminTabs가 로딩되는 동안 탭 바 높이를 유지하는 플레이스홀더를 렌더하세요; 즉 Suspense의 fallback에 탭과 동일한
높이/스타일을 가진 정적 div 또는 빈 탭 바 컴포넌트(예: className으로 고정 높이와 skeleton 스타일 적용)를 넣어 초기 진입
시 레이아웃 안정성을 보장하도록 수정하세요.
In `@app/`(afterLogin)/admin/notices/notice-detail.tsx:
- Around line 13-16: The formatDate function in notice-detail.tsx is duplicated
across multiple files (e.g., notices-tab.tsx, prompts-tab.tsx); extract it into
a shared utility (e.g., create lib/utils/date.ts) and export named functions
(suggest formatDateKorean and formatDateShort as shown in the review) then
replace the local formatDate implementation in notice-detail.tsx,
notices-tab.tsx, prompts-tab.tsx, etc. with an import from the new utility and
update usages to call formatDateKorean (or formatDateShort where appropriate) so
all files use the centralized, tested helper.
In `@app/`(afterLogin)/admin/notices/notices-tab.tsx:
- Around line 169-171: The TableCell in notices-tab.tsx currently shows the
hardcoded placeholder "공지사항 내용..." instead of actual data; update the UI to
either read a preview field from your API model (add contentPreview to
AdminNoticeInfo and render item.contentPreview in the TableCell with
truncation/fallback), or remove this column entirely, or replace the placeholder
with a clear CTA like "클릭하여 내용 보기" so it’s not misleading; locate the TableCell
in notices-tab.tsx and modify the rendering logic to use
AdminNoticeInfo.contentPreview (with a safe fallback) or implement one of the
alternate options above.
In `@app/`(afterLogin)/admin/page.tsx:
- Around line 7-9: The Suspense wrapper around AdminContent renders nothing
while suspended; add a fallback so the admin layout and tabs keep visible during
load — update the Suspense in this file to include a fallback prop (e.g.,
fallback={<AdminSkeleton/>} or fallback={<div>Loading admin…</div>}) and
create/import a simple AdminSkeleton or loading component if not present; ensure
you add the import and use the same pattern for any similar Suspense usages in
app/(afterLogin)/admin/layout.tsx to preserve layout during loading.
In `@app/`(afterLogin)/admin/prompts/prompt-filters.tsx:
- Around line 17-24: Extract the CATEGORY_OPTIONS constant from
prompt-filters.tsx into a shared module (e.g., export it from admin/constants)
and replace the local declaration with an import; update prompt-filters.tsx to
import { CATEGORY_OPTIONS } from "admin/constants" (or the chosen module name)
and remove the local const, then find the sibling files that duplicate the same
options and change them to import the same exported CATEGORY_OPTIONS so all
usages share a single source of truth.
In `@app/`(afterLogin)/admin/prompts/prompts-tab.tsx:
- Around line 316-328: Inline status dropdown includes "DELETED" while the
batch/status list (STATUS_OPTIONS) only has "PUBLIC" and "PRIVATE"; make them
consistent by either adding "DELETED" to STATUS_OPTIONS or intentionally
excluding it with a comment. Update the STATUS_OPTIONS constant to include a {
value: "DELETED", label: "삭제" } entry if deletion should be available in batch
changes, or add a concise comment next to STATUS_OPTIONS explaining why
"DELETED" is omitted; ensure any UI text/labels match the select options and
that handleInlineStatus and any batch-change handler accept the "DELETED" value
if added.
- Around line 24-39: Replace the hardcoded CATEGORY_OPTIONS and
CATEGORY_NAME_TO_ID in prompts-tab.tsx with a dynamic source: either fetch
categories from the server using your existing query hook (e.g., useQuery with
getAdminCategories) and derive options and a name->id map from the response, or
import shared constants from a new constants file (e.g.,
constants/categories.ts) used by both prompts-tab and categories-tab; update any
usages of CATEGORY_OPTIONS and CATEGORY_NAME_TO_ID to read from the fetched data
or imported constants and handle loading/error states accordingly.
- Around line 347-359: The pagination currently renders all pages via
Array.from({ length: totalPages }) which breaks UI and performance when
totalPages is large; implement a truncated pagination helper (e.g.,
getPageNumbers(current, total) that returns numbers and "..." placeholders) and
replace the full mapping with mapping over getPageNumbers(page, totalPages),
rendering normal buttons for numbers (calling setPage(n)) and
non-interactive/static elements for "..." placeholders, preserving the existing
className logic to highlight the active page (use symbols: getPageNumbers, page,
totalPages, setPage) and ensure keys are stable and unique when mapping.
In `@app/`(afterLogin)/admin/users/users-tab.tsx:
- Around line 169-181: Extract the repeated page-button mapping into a reusable
Pagination component and replace the inline mapping in users-tab.tsx (the JSX
that maps Array.from({ length: totalPages }) to buttons) with that component,
mirroring prompts-tab.tsx usage; the Pagination component should accept props:
totalPages, currentPage (page), onChange (setPage) and className/customization
for active/inactive styles, and ensure users-tab.tsx imports and uses
<Pagination totalPages={totalPages} currentPage={page} onChange={setPage} />
instead of rendering buttons directly.
In `@queries/api/admin.ts`:
- Around line 66-68: The current falsy check "if (params.categoryId)" will skip
valid zero values; update the guard for categoryId to explicitly check for
undefined/null (e.g., "if (params.categoryId !== undefined && params.categoryId
!== null)") and then set searchParams["categoryId"] = params.categoryId so zero
is preserved; locate the conditional using params.categoryId and the assignment
to searchParams["categoryId"] and replace the falsy check with the explicit
null/undefined check.
- Around line 116-123: Replace the inline union type for the role parameter with
the shared Role type from types.ts: import the Role type and change the function
signature of bulkUpdateUserRole to use role: Role instead of role: "USER" |
"ADMIN"; also update the same pattern in bulkUpdateUserStatus to use Role so
both functions (bulkUpdateUserRole, bulkUpdateUserStatus) reference the
centralized Role type for consistency and easier maintenance.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8b12a00d-a08b-4ad6-b55b-77e20a4dcf2d
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (25)
.gitignoreCLAUDE.mdapp/(afterLogin)/(admin)/layout.tsxapp/(afterLogin)/admin/admin-content.tsxapp/(afterLogin)/admin/admin-tabs.tsxapp/(afterLogin)/admin/categories/categories-tab.tsxapp/(afterLogin)/admin/constant.tsapp/(afterLogin)/admin/dashboard/dashboard-tab.tsxapp/(afterLogin)/admin/dashboard/pending-reports-table.tsxapp/(afterLogin)/admin/dashboard/report-process-modal.tsxapp/(afterLogin)/admin/dashboard/stats-cards.tsxapp/(afterLogin)/admin/layout.tsxapp/(afterLogin)/admin/notices/notice-detail.tsxapp/(afterLogin)/admin/notices/notice-write-form.tsxapp/(afterLogin)/admin/notices/notices-tab.tsxapp/(afterLogin)/admin/page.tsxapp/(afterLogin)/admin/prompts/prompt-filters.tsxapp/(afterLogin)/admin/prompts/prompts-tab.tsxapp/(afterLogin)/admin/types.tsapp/(afterLogin)/admin/users/users-tab.tsxcomponents/ui/badge.tsxcomponents/ui/table.tsxlib/routes.tspackage.jsonqueries/api/admin.ts
💤 Files with no reviewable changes (1)
- app/(afterLogin)/(admin)/layout.tsx
| const TABS = [ | ||
| { key: "dashboard", label: "대시보드" }, | ||
| { key: "prompts", label: "게시글 관리" }, | ||
| { key: "users", label: "유저 관리" }, | ||
| { key: "notices", label: "공지사항" }, | ||
| { key: "categories", label: "카테고리 관리" }, | ||
| ] as const; | ||
|
|
||
| export type AdminTabKey = (typeof TABS)[number]["key"]; | ||
|
|
||
| export function AdminTabs() { | ||
| const router = useRouter(); | ||
| const searchParams = useSearchParams(); | ||
| const currentTab = (searchParams.get("tab") as AdminTabKey) || "dashboard"; | ||
|
|
||
| const handleTabClick = (key: AdminTabKey) => { | ||
| router.push(`/admin?tab=${key}`); | ||
| }; |
There was a problem hiding this comment.
tab 값을 캐스팅만 하지 말고 검증해주세요.
Line 20은 타입 단언만 하고 있어서 ?tab=foo 같은 잘못된 URL에서 app/(afterLogin)/admin/admin-content.tsx는 대시보드로 fallback되지만, 여기서는 어떤 탭도 active가 아니게 됩니다. TABS 기준으로 실제 값을 검증한 뒤 기본 탭으로 치환하고, 가능하면 이 정의를 ROUTES.admin/탭 렌더링과 같은 소스로 묶어 드리프트도 같이 막아주세요.
🔧 제안 수정안
const TABS = [
{ key: "dashboard", label: "대시보드" },
{ key: "prompts", label: "게시글 관리" },
{ key: "users", label: "유저 관리" },
{ key: "notices", label: "공지사항" },
{ key: "categories", label: "카테고리 관리" },
] as const;
export type AdminTabKey = (typeof TABS)[number]["key"];
+
+const DEFAULT_TAB: AdminTabKey = "dashboard";
+
+function isAdminTabKey(value: string | null): value is AdminTabKey {
+ return TABS.some((tab) => tab.key === value);
+}
export function AdminTabs() {
const router = useRouter();
const searchParams = useSearchParams();
- const currentTab = (searchParams.get("tab") as AdminTabKey) || "dashboard";
+ const rawTab = searchParams.get("tab");
+ const currentTab = isAdminTabKey(rawTab) ? rawTab : DEFAULT_TAB;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/admin-tabs.tsx around lines 7 - 24, The currentTab
assignment in AdminTabs is only a type assertion and needs real validation:
replace the casted expression that reads searchParams.get("tab") with logic that
checks the value against the allowed keys in TABS (e.g., derive a Set of allowed
keys from TABS) and if the value is missing or invalid fall back to "dashboard";
update any active-tab detection to use this validated value and consider
centralizing the tab/key definitions with ROUTES.admin or the tab-rendering
source to avoid drift between AdminTabs, AdminContent and handleTabClick.
| const handleDelete = (id: number) => { | ||
| deleteMutation.mutate(id); | ||
| }; |
There was a problem hiding this comment.
카테고리 삭제 전 확인 다이얼로그 필요
현재 삭제 버튼 클릭 시 즉시 삭제가 실행됩니다. 카테고리는 하위 카테고리를 포함할 수 있으므로, 실수로 삭제하면 복구가 어렵습니다.
🛡️ 확인 다이얼로그 추가 제안
const handleDelete = (id: number) => {
+ if (!window.confirm("정말 이 카테고리를 삭제하시겠습니까?")) return;
deleteMutation.mutate(id);
};또는 더 나은 UX를 위해 프로젝트의 모달/다이얼로그 컴포넌트를 활용하세요.
📝 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 handleDelete = (id: number) => { | |
| deleteMutation.mutate(id); | |
| }; | |
| const handleDelete = (id: number) => { | |
| if (!window.confirm("정말 이 카테고리를 삭제하시겠습니까?")) return; | |
| deleteMutation.mutate(id); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/categories/categories-tab.tsx around lines 79 - 81,
The delete handler currently calls deleteMutation.mutate immediately in
handleDelete (using deleteMutation.mutate(id)), so add a confirmation step
before invoking the mutation: show the project's modal/dialog component (or a
browser confirm as a temporary fallback), and only call
deleteMutation.mutate(id) if the user confirms; update the handler
(handleDelete) and the delete button click path to open the dialog, pass the
target id to the dialog, and call deleteMutation.mutate from the dialog’s
confirm callback to ensure deletions are user-confirmed.
| const { data, isLoading } = useQuery({ | ||
| queryKey: ["admin", "reports", "pending", page], | ||
| queryFn: () => getPendingReports(page, 5), | ||
| select: (res) => res.data, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "pending-reports-table.tsx" | head -20Repository: Inflearn-Prog/Prog-fe
Length of output: 125
🏁 Script executed:
git ls-files | grep -i "pending-reports-table"Repository: Inflearn-Prog/Prog-fe
Length of output: 123
🏁 Script executed:
wc -l app/\(afterLogin\)/admin/dashboard/pending-reports-table.tsxRepository: Inflearn-Prog/Prog-fe
Length of output: 127
🏁 Script executed:
cat -n "app/(afterLogin)/admin/dashboard/pending-reports-table.tsx" | head -150Repository: Inflearn-Prog/Prog-fe
Length of output: 6057
🏁 Script executed:
cat -n "app/(afterLogin)/admin/dashboard/pending-reports-table.tsx" | tail -100Repository: Inflearn-Prog/Prog-fe
Length of output: 4301
쿼리 실패가 빈 목록으로 숨겨집니다
useQuery에서 isLoading만 체크하고 있어서, 요청 실패 시 data가 undefined가 되면 결과적으로 reports = data?.content ?? []에 의해 빈 배열이 되고, "미처리 신고가 없습니다."가 표시됩니다. 관리자 대시보드에서는 장애를 정상 상태로 오인하게 만드는 위험한 상황이므로, isError 상태를 분리해서 명확한 에러 메시지와 재시도 UI를 제공해야 합니다.
🔧 제안
- const { data, isLoading } = useQuery({
+ const { data, isLoading, isError } = useQuery({
queryKey: ["admin", "reports", "pending", page],
queryFn: () => getPendingReports(page, 5),
select: (res) => res.data,
});
...
- ) : reports.length === 0 ? (
+ ) : isError ? (
+ <TableRow>
+ <TableCell
+ colSpan={5}
+ className="py-12 text-center text-red-500"
+ >
+ 신고 목록을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.
+ </TableCell>
+ </TableRow>
+ ) : reports.length === 0 ? (React Query 문서: Error Handling
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/dashboard/pending-reports-table.tsx around lines 42 -
46, The current useQuery call only checks isLoading so when the request fails
data is undefined and the UI falls back to an empty reports array; update the
query usage to also destructure isError, error, and refetch (e.g. const { data,
isLoading, isError, error, refetch } = useQuery(...)) and change the render
logic to handle the error case separately from the empty-list case: when isError
is true render a clear error message (use error?.message or a friendly fallback)
and a retry control that calls refetch; only fall back to reports =
data?.content ?? [] when not isError so failures aren’t hidden as “no pending
reports.”
| onSuccess: () => { | ||
| queryClient.invalidateQueries({ queryKey: ["admin", "reports"] }); | ||
| setSelectedReport(null); | ||
| }, |
There was a problem hiding this comment.
마지막 페이지의 마지막 건 처리 후 가짜 빈 상태가 뜰 수 있습니다.
성공 후 현재 page를 그대로 둔 채 목록만 무효화하고 있어서, 현재 페이지에 1건만 남아 있던 상태라면 다음 조회가 out-of-range 페이지를 보게 됩니다. 실제로는 이전 페이지에 데이터가 남아 있어도 “미처리 신고가 없습니다.”가 보여 운영자가 오인할 수 있습니다. 성공 시 페이지를 한 칸 당기거나, refetch 후 pageInfo.totalPages로 clamp해주세요.
🔧 제안 수정안
onSuccess: () => {
+ setPage((current) =>
+ current > 0 && reports.length === 1 ? current - 1 : current
+ );
queryClient.invalidateQueries({ queryKey: ["admin", "reports"] });
setSelectedReport(null);
},
});
...
onSuccess: () => {
+ setPage((current) =>
+ current > 0 && reports.length === 1 ? current - 1 : current
+ );
queryClient.invalidateQueries({ queryKey: ["admin", "reports"] });
setSelectedReport(null);
},
});Also applies to: 72-75
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/dashboard/pending-reports-table.tsx around lines 58 -
61, The onSuccess handler currently just invalidates the ["admin","reports"]
query and clears selected report, which can leave the UI on an out-of-range page
when the last item of the current page is removed; update the onSuccess in the
pending-reports-table component to refetch the reports query (or await
queryClient.refetchQueries for ["admin","reports"]) and then clamp or adjust the
current page state (the `page` used to fetch reports) against the returned
pageInfo.totalPages (e.g., set page = Math.max(1, Math.min(page,
pageInfo.totalPages)) or decrement page if the current page > totalPages) before
clearing selection with setSelectedReport(null) so the table never shows a fake
empty-last-page state.
| <TableCell className="max-w-[300px] truncate text-center text-sm"> | ||
| <Link | ||
| href={`/community/${report.reportId}`} | ||
| className="hover:text-frog-600 hover:underline" | ||
| > | ||
| {report.targetTitle} | ||
| </Link> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
# PendingReport 타입과 신고 목록 API가 어떤 식별자를 내려주는지 확인합니다.
rg -n -C4 'export (type|interface) PendingReport|reportId|target[A-Za-z]*Id|articleId|postId|targetTitle' app queries
rg -n -C4 'getPendingReports|PendingReportListData' app queriesRepository: Inflearn-Prog/Prog-fe
Length of output: 17328
🏁 Script executed:
# Check community route structure and what parameter it expects
rg -n -C3 'community.*\[id\]|community.*detail|DETAIL' app/\\(afterLogin\\) --type ts --type tsx | head -30
# Look for ROUTES constant definition
rg -n 'export.*ROUTES|const ROUTES' --type ts --type tsx | head -20
# Search for targetId usage in other files
rg -n 'targetId' app --type ts --type tsx
# Check if there's a community detail page to see what parameter it expects
fd -type f -name '\\[id\\].tsx' app/\\(afterLogin\\)/communityRepository: Inflearn-Prog/Prog-fe
Length of output: 259
🏁 Script executed:
# Search for community route structure
find app -type f -name "*.tsx" | xargs rg -l "community" | head -10
# Search for ROUTES constant
rg -n "export.*ROUTES" --type ts --type tsx
# Search for targetId in the codebase
rg -n "targetId" app --type ts --type tsx | head -20
# Look for community detail directory structure
find app -type d -name "community"Repository: Inflearn-Prog/Prog-fe
Length of output: 388
🏁 Script executed:
# Look at community directory structure
ls -la app/\(afterLogin\)/\(dashboard\)/\(community-category\)/community/
# Search for ROUTES constant with simpler pattern
rg -n "ROUTES\s*=" app queries lib
# Search for targetId usage
rg -n "targetId" app
# Look for detail page in community
find app -type f -name "*detail*" -o -name "*\[id\]*"Repository: Inflearn-Prog/Prog-fe
Length of output: 1298
🏁 Script executed:
# Read ROUTES definition
cat lib/routes.ts
# Read community detail page
cat app/\(afterLogin\)/\(dashboard\)/\(community-category\)/community/\[id\]/page.tsx
# Check if there's any targetId in admin types or API
rg -n "target" app/\(afterLogin\)/admin/types.ts queries/api/admin.tsRepository: Inflearn-Prog/Prog-fe
Length of output: 1970
🏁 Script executed:
# Check if there are any mock data or test files that show API response structure
find . -type f \( -name "*.mock.ts" -o -name "*.test.ts" -o -name "*.spec.ts" \) | xargs rg -l "PendingReport" 2>/dev/null
# Check if targetId appears anywhere in admin-related files
rg -n "target" queries/api/admin.ts
# Look for any comment or documentation about report structure
rg -n -B2 -A2 "신고|report" app/\(afterLogin\)/admin/types.ts | head -40Repository: Inflearn-Prog/Prog-fe
Length of output: 540
PendingReport 타입에서 대상 글 ID가 누락되어 있어 잘못된 페이지로 이동하고 있습니다.
현재 코드는 report.reportId(신고 레코드의 ID)를 경로에 사용하고 있지만, 실제로는 신고 대상 글의 ID가 필요합니다.
확인된 문제점:
PendingReport인터페이스에targetId,articleId등 대상 글 식별자가 없음 (타입 정의 누락)- 현재 코드:
href={/community/${report.reportId}}→ 신고 ID 사용 (틀림) - 예상되는 올바른 구조:
href={ROUTES.community.DETAIL(report.targetId)}
개선 방향:
PendingReport타입에 대상 글 ID 필드 추가 (targetId: number또는 유사)- 하드코딩된 경로 대신
ROUTES.community.DETAIL()헬퍼 함수 사용 (일관성 유지) - API 응답에서 실제로 대상 글 ID를 전달하는지 백엔드 확인 필요
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/dashboard/pending-reports-table.tsx around lines 132
- 138, The PendingReport type is missing the target article ID and the UI is
incorrectly using report.reportId for navigation; update the PendingReport
interface to include a targetId (or articleId) numeric field, update the table
link in pending-reports-table.tsx to use
ROUTES.community.DETAIL(report.targetId) (replace
href={`/community/${report.reportId}`} with the ROUTES helper), and ensure any
API mapping that constructs PendingReport populates this new targetId from the
backend response (verify the backend returns the target article ID and map it
into the PendingReport used by the TableCell/Link rendering).
| const [inputValue, setInputValue] = useState(keyword); | ||
|
|
||
| const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (e.key === "Enter") { | ||
| onSearch(inputValue); | ||
| } | ||
| }; |
There was a problem hiding this comment.
검색어 상태가 prop 변경과 동기화되지 않습니다.
useState(keyword) 초기화만 하고 있어 부모에서 keyword를 갱신해도 입력창이 이전 값을 유지할 수 있습니다. 파생 상태 동기화가 필요합니다.
참고: React 공식 문서의 “state synchronization with props/effects” 패턴.
🧩 제안 수정안
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
const [inputValue, setInputValue] = useState(keyword);
+
+ useEffect(() => {
+ setInputValue(keyword);
+ }, [keyword]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/prompts/prompt-filters.tsx around lines 34 - 40, The
local input state (inputValue) initialized via useState(keyword) isn't synced
when the parent updates keyword; add a useEffect that watches the keyword prop
and calls setInputValue(keyword) to keep the controlled input in sync, and
ensure the input's value remains bound to inputValue and onSearch is called in
handleKeyDown as it is now.
| const handleBulkDelete = () => { | ||
| if (selectedIds.length === 0) return; | ||
| deleteMutation.mutate(selectedIds); | ||
| }; |
There was a problem hiding this comment.
일괄 삭제 전 확인 다이얼로그 필요
여러 게시글을 한 번에 삭제하는 중요한 작업이므로, 실수 방지를 위해 확인 다이얼로그가 필요합니다.
🛡️ 확인 다이얼로그 추가
const handleBulkDelete = () => {
if (selectedIds.length === 0) return;
+ if (!window.confirm(`선택한 ${selectedIds.length}개의 게시글을 삭제하시겠습니까?`)) return;
deleteMutation.mutate(selectedIds);
};📝 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 handleBulkDelete = () => { | |
| if (selectedIds.length === 0) return; | |
| deleteMutation.mutate(selectedIds); | |
| }; | |
| const handleBulkDelete = () => { | |
| if (selectedIds.length === 0) return; | |
| if (!window.confirm(`선택한 ${selectedIds.length}개의 게시글을 삭제하시겠습니까?`)) return; | |
| deleteMutation.mutate(selectedIds); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/prompts/prompts-tab.tsx around lines 139 - 142, The
bulk-delete handler (handleBulkDelete) currently calls
deleteMutation.mutate(selectedIds) immediately; add a confirmation step to
prevent accidental deletes by prompting the user (e.g., window.confirm or your
modal/confirmDialog hook) when selectedIds.length > 0 and only proceed with
deleteMutation.mutate(selectedIds) if the user confirms; ensure the prompt
provides context (number of items or a warning) and keep selectedIds and
deleteMutation references intact so rollback/refresh logic still runs after a
confirmed mutation.
| export interface ReportProcessRequest { | ||
| adminRemark?: string; | ||
| action?: ReportAction; | ||
| } |
There was a problem hiding this comment.
ReportProcessRequest.action은 필수로 고정하는 편이 안전합니다.
처리 API 요청에서 action이 빠져도 타입이 통과되어, 런타임 400류 오류를 숨길 수 있습니다. 계약 위반을 컴파일 타임에 막아주세요.
🛠️ 제안 수정안
export interface ReportProcessRequest {
adminRemark?: string;
- action?: ReportAction;
+ action: ReportAction;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/types.ts around lines 59 - 62, The
ReportProcessRequest type currently makes action optional which allows invalid
requests to compile; change the interface so action is required (replace
action?: ReportAction with action: ReportAction in ReportProcessRequest) and
then update any call sites that construct ReportProcessRequest (e.g., forms, API
callers, functions that build request payloads) to always provide a valid
ReportAction value or handle missing action before creating the payload; ensure
related handlers or validators that rely on ReportProcessRequest reflect the
required field.
| const handleInlineStatusChange = (userId: number, status: UserStatus) => { | ||
| statusMutation.mutate({ userIds: [userId], status }); | ||
| }; |
There was a problem hiding this comment.
사용자 활동 제한 시 확인 필요
사용자 상태를 SUSPENDED로 변경하는 것은 해당 사용자의 서비스 이용에 큰 영향을 미칩니다. 실수로 변경하는 것을 방지하기 위해 확인 단계가 필요합니다.
🛡️ 확인 로직 추가
const handleInlineStatusChange = (userId: number, status: UserStatus) => {
+ if (status === "SUSPENDED") {
+ if (!window.confirm("이 사용자의 활동을 제한하시겠습니까?")) return;
+ }
statusMutation.mutate({ userIds: [userId], 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.
| const handleInlineStatusChange = (userId: number, status: UserStatus) => { | |
| statusMutation.mutate({ userIds: [userId], status }); | |
| }; | |
| const handleInlineStatusChange = (userId: number, status: UserStatus) => { | |
| if (status === "SUSPENDED") { | |
| if (!window.confirm("이 사용자의 활동을 제한하시겠습니까?")) return; | |
| } | |
| statusMutation.mutate({ userIds: [userId], status }); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/users/users-tab.tsx around lines 52 - 54,
handleInlineStatusChange currently calls statusMutation.mutate directly and
needs a confirmation step when changing a user to SUSPENDED to prevent
accidental suspension; modify handleInlineStatusChange to detect when status ===
UserStatus.SUSPENDED and then show a confirmation dialog/modal (e.g., browser
confirm or existing ConfirmModal) and only call statusMutation.mutate({ userIds:
[userId], status }) if the user confirms, otherwise abort the mutation. Ensure
the confirmation UI provides clear context (user id/name and consequences) and
keep the existing behavior for non-SUSPENDED statuses.
| ``` | ||
| app/ # Next.js App Router pages & layouts | ||
| (beforeLogin)/ # Route group: landing, signin, signup | ||
| (afterLogin)/ # Route group: dashboard, mypage, community, rank, admin | ||
| providers/ # AuthProvider, QueryProvider, MSWProvider | ||
| store/ # Zustand stores (signUpStore, mypageStore) | ||
| styles/ # globals.css (design tokens in OKLCH) | ||
| types/ # Shared TypeScript types | ||
| components/ # Feature components + Shadcn UI primitives (components/ui/) | ||
| hooks/ # Custom hooks (use-*.ts) | ||
| lib/ # fetcher.ts (Ky HTTP client), routes.ts, utils.ts, auth config | ||
| queries/ # TanStack Query - API functions (queries/api/) and query options | ||
| mocks/ # MSW handlers organized by domain | ||
| stories/ # Storybook stories | ||
| ``` |
There was a problem hiding this comment.
Fenced code block에 언어 태그를 명시해주세요.
현재 코드블록 2곳이 언어 미지정이라 markdownlint(MD040) 경고가 납니다. 문서 품질/CI 안정성을 위해 언어를 붙여주세요.
참고: markdownlint MD040 규칙 문서.
✍️ 제안 수정안
-```
+```text
app/ # Next.js App Router pages & layouts
(beforeLogin)/ # Route group: landing, signin, signup
(afterLogin)/ # Route group: dashboard, mypage, community, rank, admin
providers/ # AuthProvider, QueryProvider, MSWProvider
store/ # Zustand stores (signUpStore, mypageStore)
styles/ # globals.css (design tokens in OKLCH)
types/ # Shared TypeScript types
components/ # Feature components + Shadcn UI primitives (components/ui/)
hooks/ # Custom hooks (use-*.ts)
lib/ # fetcher.ts (Ky HTTP client), routes.ts, utils.ts, auth config
queries/ # TanStack Query - API functions (queries/api/) and query options
mocks/ # MSW handlers organized by domain
stories/ # Storybook stories@@
- +text
feat: 로그인 화면 UI 구성 및 API 연동
fix: 헤더 왼쪽 섹션에서 md:visible을 md:block으로 수정
design: PC UI 글씨 크기 css 수정
refactor: 회원가입 플로우 반응형 추가
chore: 마이페이지 API 타입, 모킹 및 상태관리 설정
style: 헤더에 하단 테두리 추가
Also applies to: 80-87
🧰 Tools
🪛 markdownlint-cli2 (0.22.0)
[warning] 28-28: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@CLAUDE.md` around lines 28 - 42, The two fenced code blocks in CLAUDE.md (the
block starting with "app/ # Next.js App Router pages &
layouts" and the other block containing the commit list beginning with "feat:
로그인 화면 UI 구성 및 API 연동") lack a language tag and trigger markdownlint MD040; fix
by adding an explicit language tag (e.g., ```text) to each opening fence so both
code blocks specify a language.
- 신고 관련글 Link 제거 (targetId 필드 미제공으로 인한 잘못된 라우팅 방지) - 신고 처리 모달 SUSPEND 옵션 제거, PRIVATE 라벨 명확화 - categoryId falsy 체크 버그 수정 (0 값 스킵 방지) - formatDate 유틸 함수 utils.ts로 추출 (4곳 중복 제거) - 공지사항 내용 placeholder 컬럼 제거 - 카테고리 DnD 미구현 cursor-grab → cursor-default Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
app/(afterLogin)/admin/categories/categories-tab.tsx (1)
79-80:⚠️ Potential issue | 🟠 Major삭제는 한 번 더 물어보는 편이 안전합니다.
현재는 클릭 즉시 삭제가 나가서 계층형 카테고리를 오삭제하기 쉽습니다. 최소 confirm/dialog 한 단계는 두는 게 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/categories/categories-tab.tsx around lines 79 - 80, The deletion is immediate and risky; modify the handleDelete function to prompt the user before performing the mutation (e.g., show a confirmation dialog or use window.confirm) and only call deleteMutation.mutate(id) if the user confirms. Locate handleDelete and replace the direct call to deleteMutation.mutate(id) with a confirmation step (or open your existing Modal/Confirm component and await/handle its result) so accidental clicks won't delete categories.app/(afterLogin)/admin/notices/notices-tab.tsx (1)
78-84:⚠️ Potential issue | 🟠 Major일괄 삭제에도 확인 단계가 필요합니다.
현재는 선택된 공지를 바로 삭제해서 실수 한 번이면 복구가 어렵습니다. 최소 confirm/dialog 한 번은 넣어 두는 편이 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/`(afterLogin)/admin/notices/notices-tab.tsx around lines 78 - 84, Add a confirmation step before calling deleteMutation: when the destructive <Button> is clicked and selectedIds is non-empty, open a confirmation dialog (or use window.confirm) and only call deleteMutation.mutate(selectedIds) if the user confirms; keep the existing disabled condition (selectedIds.length === 0 || deleteMutation.isPending), ensure the dialog is modal/awaited and canceling it does nothing, and clean up any dialog state (e.g., showConfirm boolean or ConfirmDialog component) so the mutation is invoked only after explicit user confirmation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/`(afterLogin)/admin/categories/categories-tab.tsx:
- Around line 132-137: The Escape key currently calls setEditingId(null) but the
subsequent blur still triggers handleUpdate due to browser keydown→blur
ordering; update the input handlers (onKeyDown, onBlur) and related state
(editingId) to prevent saving on blur when Escape was used—e.g., add a transient
cancel flag or a ref like wasCancelled (toggled in the Escape branch of
onKeyDown) and check that flag inside onBlur before calling
handleUpdate(parent.categoryId); alternatively, change onBlur to only save on
explicit confirmation (Enter or explicit Save) and ensure handleUpdate,
setEditingId, and the cancel/wasCancelled state are coordinated; apply the same
change to the other occurrence around lines 180–185.
In `@app/`(afterLogin)/admin/dashboard/report-process-modal.tsx:
- Around line 28-29: The modal currently ignores the adminRemark parameter even
though the props onProcess and onReject accept it; add a local state (e.g.,
adminRemark via useState) in ReportProcessModal, render a textarea bound to that
state, and ensure both submit handlers (the function that calls
props.onProcess(action, ...) and the reject handler that calls
props.onReject(...)) pass the current adminRemark value when invoking those
callbacks so the remark is forwarded to the parent.
In `@app/`(afterLogin)/admin/notices/notices-tab.tsx:
- Around line 23-24: selectedIds persists across pagination causing selections
from other pages to be acted on; update the pagination change handler (the code
that calls setPage) to clear selections by calling setSelectedIds([]) or add a
useEffect that resets setSelectedIds([]) when page changes, and also change any
allSelected / selection checks (references: selectedIds, setSelectedIds, page,
allSelected, toggle/select-all logic and delete handlers) to compute selection
only against the current page's item IDs (e.g., intersect selectedIds with
currentPageNotices.map(n => n.id)) so selection state and “select all” behave
per-page.
---
Duplicate comments:
In `@app/`(afterLogin)/admin/categories/categories-tab.tsx:
- Around line 79-80: The deletion is immediate and risky; modify the
handleDelete function to prompt the user before performing the mutation (e.g.,
show a confirmation dialog or use window.confirm) and only call
deleteMutation.mutate(id) if the user confirms. Locate handleDelete and replace
the direct call to deleteMutation.mutate(id) with a confirmation step (or open
your existing Modal/Confirm component and await/handle its result) so accidental
clicks won't delete categories.
In `@app/`(afterLogin)/admin/notices/notices-tab.tsx:
- Around line 78-84: Add a confirmation step before calling deleteMutation: when
the destructive <Button> is clicked and selectedIds is non-empty, open a
confirmation dialog (or use window.confirm) and only call
deleteMutation.mutate(selectedIds) if the user confirms; keep the existing
disabled condition (selectedIds.length === 0 || deleteMutation.isPending),
ensure the dialog is modal/awaited and canceling it does nothing, and clean up
any dialog state (e.g., showConfirm boolean or ConfirmDialog component) so the
mutation is invoked only after explicit user confirmation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a878720e-0475-491a-8780-2da1f968ad3b
📒 Files selected for processing (8)
app/(afterLogin)/admin/categories/categories-tab.tsxapp/(afterLogin)/admin/dashboard/pending-reports-table.tsxapp/(afterLogin)/admin/dashboard/report-process-modal.tsxapp/(afterLogin)/admin/notices/notice-detail.tsxapp/(afterLogin)/admin/notices/notices-tab.tsxapp/(afterLogin)/admin/prompts/prompts-tab.tsxapp/(afterLogin)/admin/utils.tsqueries/api/admin.ts
✅ Files skipped from review due to trivial changes (2)
- app/(afterLogin)/admin/utils.ts
- app/(afterLogin)/admin/notices/notice-detail.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- app/(afterLogin)/admin/dashboard/pending-reports-table.tsx
- app/(afterLogin)/admin/prompts/prompts-tab.tsx
| onKeyDown={(e) => { | ||
| if (e.key === "Enter") | ||
| handleUpdate(parent.categoryId); | ||
| if (e.key === "Escape") setEditingId(null); | ||
| }} | ||
| onBlur={() => handleUpdate(parent.categoryId)} |
There was a problem hiding this comment.
Escape가 취소가 아니라 저장으로 이어집니다.
지금은 Escape에서 setEditingId(null)만 호출해도 바로 이어지는 blur에서 handleUpdate가 실행됩니다. 브라우저의 keydown → blur 순서 때문에 실제로는 저장되니, 취소 플래그를 두거나 onBlur 저장을 Enter/명시적 확인으로 분리해 주세요.
Also applies to: 180-185
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/categories/categories-tab.tsx around lines 132 - 137,
The Escape key currently calls setEditingId(null) but the subsequent blur still
triggers handleUpdate due to browser keydown→blur ordering; update the input
handlers (onKeyDown, onBlur) and related state (editingId) to prevent saving on
blur when Escape was used—e.g., add a transient cancel flag or a ref like
wasCancelled (toggled in the Escape branch of onKeyDown) and check that flag
inside onBlur before calling handleUpdate(parent.categoryId); alternatively,
change onBlur to only save on explicit confirmation (Enter or explicit Save) and
ensure handleUpdate, setEditingId, and the cancel/wasCancelled state are
coordinated; apply the same change to the other occurrence around lines 180–185.
| onProcess: (action: ReportAction, adminRemark?: string) => void; | ||
| onReject: (adminRemark?: string) => void; |
There was a problem hiding this comment.
관리자 메모가 항상 버려집니다.
onProcess/onReject 시그니처는 adminRemark를 받도록 열려 있는데, 이 모달은 입력 상태도 없고 제출 시에도 값을 넘기지 않습니다. app/(afterLogin)/admin/dashboard/pending-reports-table.tsx:38-47,54-65도 remark 전송을 이미 준비하고 있으니, textarea를 추가하고 handleSubmit에서 함께 전달해 주세요. 신고 처리 이력은 나중에 감사 로그나 CS 대응에서 꽤 중요합니다.
Also applies to: 42-47
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/dashboard/report-process-modal.tsx around lines 28 -
29, The modal currently ignores the adminRemark parameter even though the props
onProcess and onReject accept it; add a local state (e.g., adminRemark via
useState) in ReportProcessModal, render a textarea bound to that state, and
ensure both submit handlers (the function that calls props.onProcess(action,
...) and the reject handler that calls props.onReject(...)) pass the current
adminRemark value when invoking those callbacks so the remark is forwarded to
the parent.
| const [page, setPage] = useState(0); | ||
| const [selectedIds, setSelectedIds] = useState<number[]>([]); |
There was a problem hiding this comment.
페이지를 바꿔도 이전 선택이 남아 있어 보이지 않는 공지가 삭제될 수 있습니다.
selectedIds가 페이지 전환 시 초기화되지 않아 1페이지에서 고른 항목이 2페이지에서도 삭제 대상에 남습니다. 지금 allSelected도 길이만 비교해서 다른 페이지 ID가 섞여 있어도 오판할 수 있으니, 페이지 변경 시 선택을 비우거나 현재 페이지 ID 집합 기준으로만 선택 상태를 계산해 주세요.
간단한 수정 방향
+ const changePage = (nextPage: number) => {
+ setPage(nextPage);
+ setSelectedIds([]);
+ };
...
- onClick={() => setPage((p) => Math.max(0, p - 1))}
+ onClick={() => changePage(Math.max(0, page - 1))}
...
- onClick={() => setPage(i)}
+ onClick={() => changePage(i)}
...
- onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
+ onClick={() => changePage(Math.min(totalPages - 1, page + 1))}Also applies to: 45-46, 98-102, 141-146, 168-197
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/`(afterLogin)/admin/notices/notices-tab.tsx around lines 23 - 24,
selectedIds persists across pagination causing selections from other pages to be
acted on; update the pagination change handler (the code that calls setPage) to
clear selections by calling setSelectedIds([]) or add a useEffect that resets
setSelectedIds([]) when page changes, and also change any allSelected /
selection checks (references: selectedIds, setSelectedIds, page, allSelected,
toggle/select-all logic and delete handlers) to compute selection only against
the current page's item IDs (e.g., intersect selectedIds with
currentPageNotices.map(n => n.id)) so selection state and “select all” behave
per-page.
🛠️ 변경 사항
✅ 체크리스트
스크린샷
(디자인 시안과 다소 다른 부분 있으나, 우선은 기능 연결을 우선시 하였음)
🔗 연결된 이슈
카테고리 관리화면에서, 카테고리 순서 편집과 관련해서는 관련 라이브러리를 추가해야 할 것 같습니다.현재 해당 부분은 TODO 처리된 상태이며, 순서 이동 기능은 서버측 API만 구현된 상태입니다.
추가
Summary by CodeRabbit