Conversation
🚀 Preview URLBranch: 119-fe-feat-회의실-관리-페이지-데이터-로딩 Preview URL: https://codeit.click?pr=162 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (9)
apps/web/lib/queryKey.ts (1)
12-13: 쿼리 키 구조 개선 제안회의실과 카테고리에 대한 쿼리 키가 잘 추가되었습니다. 하지만 향후 확장성을 고려하여 다음과 같은 구조로 개선하는 것을 제안드립니다:
- CATEGORIES: ["categories"], - ROOMS: ["rooms"], + CATEGORIES: { + ALL: ["categories"], + list: (params?: CategoryParams) => [...QUERY_KEYS.CATEGORIES.ALL, params], + detail: (id: number) => [...QUERY_KEYS.CATEGORIES.ALL, id], + }, + ROOMS: { + ALL: ["rooms"], + list: (params?: RoomParams) => [...QUERY_KEYS.ROOMS.ALL, params], + detail: (id: number) => [...QUERY_KEYS.ROOMS.ALL, id], + },이렇게 구조화하면 다음과 같은 이점이 있습니다:
- 페이지네이션, 필터링 등의 매개변수를 쉽게 추가할 수 있습니다
- 상세 조회 시 캐시 무효화를 더 세밀하게 제어할 수 있습니다
MEMBERS쿼리 키와 일관된 구조를 유지할 수 있습니다apps/web/app/admin/(items)/_components/ItemsAdminHeader.tsx (1)
11-16: 사이드바가 이미 열려있을 때의 동작 개선 필요현재
openPanel함수는 사이드바가 닫혀있을 때만 동작합니다. 사이드바가 이미 열려있을 때도 패널 상태를 업데이트하도록 로직을 개선하면 좋을 것 같습니다.const openPanel = (): void => { + setPanelState("category"); if (!isSidebarOpen) { - setPanelState("category"); openSidebar(); } };apps/web/app/admin/(items)/_components/CategoryListSubItem.tsx (1)
31-42: 에러 처리 개선 필요mutation의 에러 처리가 일반적인 메시지만 표시하고 있습니다. 구체적인 에러 정보를 사용자에게 제공하면 좋을 것 같습니다.
const mutation = useMutation({ mutationFn: async (itemId: string) => { return await deleteRoom(itemId); }, onSuccess: async () => { notify("success", "회의실이 삭제되었습니다."); await queryClient.invalidateQueries({ queryKey: ["rooms"] }); }, - onError: () => { + onError: (error: Error) => { - notify("error", "회의실 삭제에 실패했습니다. 다시 시도해주세요"); + notify("error", `회의실 삭제 실패: ${error.message}`); }, });apps/web/app/admin/(items)/_components/CategoryList.tsx (1)
43-46: 에러 상태 UI 개선 필요현재 에러 상태에서는 간단한 메시지만 표시됩니다. 사용자에게 더 자세한 정보와 재시도 옵션을 제공하면 좋을 것 같습니다.
- if (categoriesError ?? roomsError) { - notify("error", "데이터를 불러오는데 실패했습니다."); - return <div>데이터를 불러오는데 실패했습니다.</div>; + if (categoriesError || roomsError) { + const error = categoriesError || roomsError; + notify("error", `데이터 로딩 실패: ${error.message}`); + return ( + <div className="flex flex-col items-center gap-4"> + <p>데이터를 불러오는데 실패했습니다.</p> + <Button onClick={() => queryClient.invalidateQueries()}> + 다시 시도 + </Button> + </div> + ); }apps/web/app/admin/(items)/_store/useMeetingsStore.tsx (2)
7-27: 타입 안전성 개선이 필요합니다.
handleAddItem과handleEditItem의 반환 타입이 유니온 타입(IRoom | IEquipment | string)으로 되어 있어 타입 안전성이 떨어집니다.다음과 같이 개선하는 것을 제안합니다:
- handleAddItem: (data: Record<string, string>) => Promise<IRoom | IEquipment | string>; - handleEditItem: (data: Record<string, string>, itemId: string) => Promise<IRoom | IEquipment | string>; + handleAddItem: (data: Record<string, string>) => Promise<IRoom>; + handleEditItem: (data: Record<string, string>, itemId: string) => Promise<IRoom>;
90-100: 에러 처리 일관성 개선이 필요합니다.
handleDeleteItem함수의 에러 처리가 다른 함수들과 일관성이 없습니다.다음과 같이 개선하는 것을 제안합니다:
} catch (error) { set({ isLoading: false }); - notify("error", "삭제 실패"); + if (error instanceof AxiosError && error.response) { + notify("error", String(error.response.data.message)); + } else { + notify("error", "알 수 없는 오류가 발생했습니다. 다시 시도해주세요."); + } }apps/web/app/admin/(items)/_components/EditItemForm.tsx (1)
73-81: capacity 필드의 타입 변환이 불필요합니다.
handleFormSubmit함수에서 capacity를 문자열로 변환하는 것은 불필요하며, 타입의 일관성을 해칠 수 있습니다.다음과 같이 수정하는 것을 제안합니다:
const payload = { ...data, category: selectedCategory?._id ?? String(currentCategory?._id), - capacity: String(data.capacity), + capacity: Number(data.capacity), };apps/web/app/admin/(items)/_components/CategoryListItem.tsx (2)
129-135: 키보드 이벤트 처리를 개선해야 합니다.현재 Enter 키만 처리되고 있으며, Escape 키를 통한 편집 취소 기능이 없습니다.
다음과 같이 개선하는 것을 제안합니다:
- onKeyDown={(e) => { - if (e.key === "Enter") { - handleUpdateCategory(); - setIsModifyingCategoryName(false); - } - }} + onKeyDown={(e) => { + switch (e.key) { + case "Enter": + handleUpdateCategory(); + setIsModifyingCategoryName(false); + break; + case "Escape": + setInputValue(category.name); + setIsModifyingCategoryName(false); + break; + } + }}
171-177: 애니메이션 구현 개선이 필요합니다.현재 구현은 rooms.length * 75로 고정된 높이를 사용하고 있어, 레이아웃 시프트가 발생할 수 있습니다.
AnimatePresence와auto높이를 사용하여 개선하는 것을 제안합니다:+ import { AnimatePresence, motion } from "framer-motion"; - <motion.div - initial={{ opacity: 0, height: 0 }} - animate={{ opacity: 1, height: rooms.length * 75 }} - exit={{ opacity: 0, height: 0 }} + <AnimatePresence> + <motion.div + initial={{ opacity: 0, height: 0 }} + animate={{ opacity: 1, height: "auto" }} + exit={{ opacity: 0, height: 0 }}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
apps/web/api/items.ts(1 hunks)apps/web/api/meetings.ts(1 hunks)apps/web/app/admin/(items)/_components/AddCategoryForm.tsx(1 hunks)apps/web/app/admin/(items)/_components/CategoryEditDropdown.tsx(1 hunks)apps/web/app/admin/(items)/_components/CategoryList.tsx(1 hunks)apps/web/app/admin/(items)/_components/CategoryListItem.tsx(1 hunks)apps/web/app/admin/(items)/_components/CategoryListSubItem.tsx(1 hunks)apps/web/app/admin/(items)/_components/ConfirmationModal.tsx(1 hunks)apps/web/app/admin/(items)/_components/EditItemForm.tsx(1 hunks)apps/web/app/admin/(items)/_components/ItemsAdminHeader.tsx(1 hunks)apps/web/app/admin/(items)/_components/SidePanel.tsx(1 hunks)apps/web/app/admin/(items)/_store/useMeetingsStore.tsx(1 hunks)apps/web/app/admin/(items)/meetings/page.tsx(1 hunks)apps/web/lib/queryKey.ts(1 hunks)apps/web/next.config.mjs(1 hunks)packages/constants/index.ts(2 hunks)packages/ui/src/components/common/Dropdown/index.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/web/app/admin/(items)/_components/SidePanel.tsx
- apps/web/app/admin/(items)/_components/CategoryEditDropdown.tsx
- apps/web/app/admin/(items)/meetings/page.tsx
- apps/web/app/admin/(items)/_components/AddCategoryForm.tsx
- apps/web/app/admin/(items)/_components/ConfirmationModal.tsx
- packages/ui/src/components/common/Dropdown/index.tsx
- packages/constants/index.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: preview
🔇 Additional comments (7)
apps/web/api/items.ts (1)
55-55: 동적 엔드포인트 구현이 잘 되었습니다!itemType 파라미터를 사용하여 URL을 동적으로 생성하는 방식으로 변경한 것이 타입 안정성을 높이고 API 구조를 더 유연하게 만들었습니다.
apps/web/next.config.mjs (1)
5-5: trailingSlash 설정의 영향 범위를 확인해주세요.이 설정은 유효하지만, 다음 사항들을 확인하시기 바랍니다:
- 내부 링크와 라우팅이 trailing slash와 함께 올바르게 작동하는지
- API 엔드포인트 URL이 이 변경으로 인해 영향을 받지 않는지
- SEO에 미치는 영향을 고려했는지
다음 스크립트로 프로젝트 내의 URL 패턴을 확인할 수 있습니다:
✅ Verification successful
trailing slash 설정이 안전하게 적용될 수 있습니다.
Next.js가 자동으로 모든 내부 라우팅에 trailing slash를 처리하므로 코드 수정이 필요하지 않습니다. Link 컴포넌트나 API 엔드포인트에도 영향을 주지 않습니다.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: 프로젝트 내의 URL 패턴 검사 # URL 문자열이 포함된 파일 검색 echo "URL 패턴 검사 중..." rg -g '!*.svg' -g '!*.lock' -g '!dist/*' -g '!.next/*' "https?://[^\s'\"]+" # Link 컴포넌트 사용 검사 echo "Next.js Link 컴포넌트 사용 검사 중..." rg -g '*.tsx' -g '*.jsx' "<Link\s+[^>]*href="Length of output: 8638
apps/web/app/admin/(items)/_components/CategoryList.tsx (1)
30-41: 🛠️ Refactor suggestionuseEffect 대신 React Query의 상태 활용 권장
현재 useEffect를 사용하여 상태를 업데이트하고 있지만, React Query의
isSuccess또는isFetched상태를 활용하면 더 효율적으로 관리할 수 있습니다.- useEffect(() => { - if (fetchedCategories) { - const roomCategories = fetchedCategories.filter((category) => category.itemType === "room"); - setCategories(roomCategories); - } - }, [fetchedCategories, setCategories]); - useEffect(() => { - if (fetchedRooms) { - setRooms(fetchedRooms); - } - }, [fetchedRooms, setRooms]); + if (isSuccess) { + const roomCategories = fetchedCategories.filter((category) => category.itemType === "room"); + setCategories(roomCategories); + setRooms(fetchedRooms); + }Likely invalid or redundant comment.
apps/web/api/meetings.ts (1)
25-38: 🛠️ Refactor suggestion타입 검증 및 불필요한 헤더 제거 필요
- axios는 기본적으로 "Content-Type: application/json"을 설정하므로 명시적으로 지정할 필요가 없습니다.
- 응답 데이터에 대한 타입 검증이 필요합니다.
export const postNewRoom = async (itemType: TItemType, body: Record<string, string>): Promise<IRoom | IEquipment> => { + const isRoom = (data: unknown): data is IRoom => { + return typeof data === 'object' && data !== null && 'name' in data && 'category' in data; + }; const { data } = await axiosRequester<IRoom | IEquipment>({ options: { method: "POST", url: API_ENDPOINTS.ITEMS.CREATE_ITEM(itemType), - headers: { - "Content-Type": "application/json", - }, data: body, }, }); + if (itemType === 'room' && !isRoom(data)) { + throw new Error('서버 응답이 올바른 회의실 형식이 아닙니다'); + } return data; };Likely invalid or redundant comment.
apps/web/app/admin/(items)/_store/useMeetingsStore.tsx (1)
60-74: 에러 처리가 잘 구현되어 있습니다!
handleAddItem과handleEditItem함수에서:
- AxiosError 타입 체크
- 로딩 상태 관리
- 지역화된 에러 메시지
- isLoading 상태 정리
위 사항들이 모두 적절하게 구현되어 있습니다.
Also applies to: 75-89
apps/web/app/admin/(items)/_components/EditItemForm.tsx (1)
91-134: 🛠️ Refactor suggestion접근성과 로딩 상태 처리를 개선해야 합니다.
폼에 다음과 같은 접근성 개선이 필요합니다:
- aria-label 속성 추가
- 로딩 중 상태 표시
다음과 같이 수정하는 것을 제안합니다:
- <form onSubmit={handleFormSubmit} className="flex h-full flex-col justify-between"> + <form + onSubmit={handleFormSubmit} + className="flex h-full flex-col justify-between" + aria-label="회의실 관리 폼" + > {/* ... */} <Button type="submit" variant="Action" + disabled={isLoading} > - 회의실 {panelState === "add" ? "추가" : "수정"} + {isLoading ? "처리중..." : `회의실 ${panelState === "add" ? "추가" : "수정"}`} </Button>Likely invalid or redundant comment.
apps/web/app/admin/(items)/_components/CategoryListItem.tsx (1)
62-77: 뮤테이션 에러 처리가 잘 구현되어 있습니다!카테고리 삭제와 수정 뮤테이션에서:
- AxiosError 타입 체크
- 지역화된 에러 메시지
- 적절한 쿼리 무효화
위 사항들이 모두 적절하게 구현되어 있습니다.
Also applies to: 83-98
| export const postNewCategory = async (body: Record<string, string>): Promise<ICategory> => { | ||
| const { data } = await axiosRequester<ICategory>({ | ||
| options: { | ||
| method: "POST", | ||
| url: API_ENDPOINTS.CATEGORIES.CREATE_CATEGORY, | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| data: body, | ||
| }, | ||
| }); | ||
|
|
||
| return data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
에러 처리 및 입력값 검증 추가 필요
카테고리 생성 시 입력값 검증과 구체적인 에러 처리가 필요합니다.
export const postNewCategory = async (body: Record<string, string>): Promise<ICategory> => {
+ if (!body.name) {
+ throw new Error('카테고리 이름은 필수입니다');
+ }
const { data } = await axiosRequester<ICategory>({
options: {
method: "POST",
url: API_ENDPOINTS.CATEGORIES.CREATE_CATEGORY,
- headers: {
- "Content-Type": "application/json",
- },
data: body,
},
});
+ if (!data._id || !data.name) {
+ throw new Error('서버 응답이 올바른 카테고리 형식이 아닙니다');
+ }
return data;
};📝 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 postNewCategory = async (body: Record<string, string>): Promise<ICategory> => { | |
| const { data } = await axiosRequester<ICategory>({ | |
| options: { | |
| method: "POST", | |
| url: API_ENDPOINTS.CATEGORIES.CREATE_CATEGORY, | |
| headers: { | |
| "Content-Type": "application/json", | |
| }, | |
| data: body, | |
| }, | |
| }); | |
| return data; | |
| }; | |
| export const postNewCategory = async (body: Record<string, string>): Promise<ICategory> => { | |
| if (!body.name) { | |
| throw new Error('카테고리 이름은 필수입니다'); | |
| } | |
| const { data } = await axiosRequester<ICategory>({ | |
| options: { | |
| method: "POST", | |
| url: API_ENDPOINTS.CATEGORIES.CREATE_CATEGORY, | |
| data: body, | |
| }, | |
| }); | |
| if (!data._id || !data.name) { | |
| throw new Error('서버 응답이 올바른 카테고리 형식이 아닙니다'); | |
| } | |
| return data; | |
| }; |
| <Input {...register("name", { required: true })} placeholder="회의실 이름" type="text" /> | ||
| <Input {...register("description")} placeholder="설명" type="text" /> | ||
| <div className="mb-24"> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
폼 유효성 검사를 강화해야 합니다.
capacity와 location 필드에 대한 유효성 검사가 누락되어 있습니다.
다음과 같이 개선하는 것을 제안합니다:
- <Input {...register("capacity")} placeholder="수용인원" type="text" />
+ <Input {...register("capacity", {
+ required: "수용인원은 필수입니다",
+ min: { value: 1, message: "수용인원은 1명 이상이어야 합니다" },
+ valueAsNumber: true
+ })} placeholder="수용인원" type="number" />
- <Input {...register("location")} placeholder="위치" type="text" />
+ <Input {...register("location", {
+ required: "위치는 필수입니다"
+ })} placeholder="위치" type="text" />Also applies to: 128-129
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (10)
apps/web/app/(admin)/rooms/_components/CategoryListItem.tsx (1)
171-182: Framer Motion에서 높이 애니메이션 처리 개선 제안현재
height: rooms.length * 75로 높이를 계산하여 애니메이션을 적용하고 있습니다. 요소의 높이가 변경되거나 반응형 디자인이 필요한 경우 문제가 발생할 수 있습니다. Framer Motion에서height: 'auto'를 사용하거나AnimatePresence를 활용하여 자동으로 높이를 조절하는 것이 더 효율적입니다.예를 들어, 다음과 같이 수정할 수 있습니다:
- animate={{ opacity: 1, height: rooms.length * 75 }} + animate={{ opacity: 1, height: 'auto' }}apps/web/app/(admin)/rooms/_components/ItemsAdminHeader.tsx (2)
7-16: 상태 관리 로직 개선 제안
openPanel함수의 로직을useMeetingsStore훅으로 이동하는 것이 좋을 것 같습니다. 이렇게 하면 상태 관리 로직이 한 곳에서 관리되어 유지보수가 더 쉬워질 것입니다.// useMeetingsStore.ts에 추가 + const openCategoryPanel = () => { + const { isSidebarOpen, openSidebar } = useSidebarStore(); + if (!isSidebarOpen) { + setPanelState("category"); + openSidebar(); + } + }; // ItemsAdminHeader.tsx - const openPanel = (): void => { - if (!isSidebarOpen) { - setPanelState("category"); - openSidebar(); - } - }; + const { openCategoryPanel } = useMeetingsStore();
19-24: 접근성 개선 필요헤더에 적절한 시맨틱 마크업과 ARIA 레이블이 누락되어 있습니다. 스크린 리더 사용자를 위해 다음과 같은 개선이 필요합니다.
- <div className="mt-80 flex justify-between"> + <header className="mt-80 flex justify-between" role="banner" aria-label="회의실 관리"> - <h1>회의실 관리</h1> + <h1 className="text-2xl font-bold">회의실 관리</h1> <Button variant="Secondary" onClick={openPanel}> 분류 추가 </Button> - </div> + </header>apps/web/app/(admin)/rooms/_components/CategoryEditDropdown.tsx (1)
4-7: Props 네이밍 단순화이전 피드백에 따르면, 다른 클릭 동작이 없는 경우
onClickEdit을onClick으로 단순화할 수 있습니다.interface CategoryEditDropdownProps { isEditing?: boolean; - onClickEdit: () => void; + onClick: () => void; }apps/web/app/(admin)/rooms/_components/ConfirmationModal.tsx (1)
6-10: 타입 정의 개선 필요
type속성의 타입을 더 명시적으로 정의하면 좋을 것 같습니다.+ type ModalType = "item" | "category"; interface ConfirmationModalProps extends PropsWithChildren { title: string; - type: "item" | "category"; + type: ModalType; onConfirm: () => void; }apps/web/app/(admin)/rooms/_components/CategoryListSubItem.tsx (1)
44-46: 삭제 작업 중 로딩 상태 표시 필요삭제 작업 중에 사용자에게 피드백이 없습니다.
다음과 같이 개선하는 것을 제안합니다:
const handleDeleteRoom = (itemId: string): void => { + if (mutation.isPending) return; mutation.mutate(itemId); };apps/web/app/(admin)/rooms/_components/CategoryList.tsx (1)
43-46: 에러 처리 개선 필요에러 발생 시 사용자에게 충분한 정보를 제공하지 않고 있습니다.
다음과 같이 개선하는 것을 제안합니다:
if (categoriesError ?? roomsError) { - notify("error", "데이터를 불러오는데 실패했습니다."); - return <div>데이터를 불러오는데 실패했습니다.</div>; + const errorMessage = categoriesError + ? "카테고리 목록을 불러오는데 실패했습니다." + : "회의실 목록을 불러오는데 실패했습니다."; + notify("error", errorMessage); + return ( + <EmptyState + message={{ + title: "오류 발생", + description: `${errorMessage}\n새로고침 후 다시 시도해주세요.` + }} + /> + ); }apps/web/app/(admin)/rooms/_store/useMeetingsStore.tsx (1)
60-74: 에러 처리 중복 코드 제거 필요
handleAddItem,handleEditItem,handleDeleteItem메서드에서 에러 처리 로직이 중복되고 있습니다.공통 에러 처리 유틸리티 함수를 만들어 사용하는 것을 제안합니다:
+ const handleApiError = (error: unknown): never => { + if (error instanceof AxiosError && error.response) { + throw new Error(String(error.response.data.message)); + } + throw new Error("알 수 없는 오류가 발생했습니다. 다시 시도해주세요."); + }; handleAddItem: async (data): Promise<IRoom> => { set({ isLoading: true, error: null }); try { const res = await postNewRoom("room", data); set({ isLoading: false }); return res; } catch (error) { set({ isLoading: false }); - if (error instanceof AxiosError && error.response) { - throw new Error(String(error.response.data.message)); - } else { - throw new Error("알 수 없는 오류가 발생했습니다. 다시 시도해주세요."); - } + handleApiError(error); } },apps/web/app/(admin)/rooms/_components/EditItemForm.tsx (2)
31-51: 폼 초기화 로직의 중복을 제거해주세요.defaultValues와 동일한 값들이 reset 함수에서 중복되어 있습니다. 이를 재사용 가능한 상수로 분리하면 유지보수가 더 쉬워질 것 같습니다.
다음과 같이 개선해보세요:
const DEFAULT_VALUES = { name: "", description: "", capacity: "1", location: "", status: "available" as const, category: "", }; // useForm에서 사용 const { register, handleSubmit, setValue, reset } = useForm({ defaultValues: currentItem ? { name: currentItem.name, description: currentItem.description, capacity: currentItem.capacity.toString(), location: currentItem.location, status: currentItem.status, category: currentItem.category._id, } : DEFAULT_VALUES, }); // useEffect에서 사용 useEffect(() => { if (panelState === "add") { reset({ ...DEFAULT_VALUES, category: currentCategory?._id, }); } else if (panelState === "edit" && currentItem) { reset({ name: currentItem.name, description: currentItem.description, capacity: currentItem.capacity.toString(), location: currentItem.location, status: currentItem.status, category: currentItem.category._id, }); } }, [panelState, currentItem, currentCategory, reset]);
83-89: 카테고리 선택 처리의 안전성 개선이 필요합니다.카테고리를 찾지 못했을 때의 에러 처리가 누락되어 있습니다.
다음과 같이 개선해보세요:
const handleSelectCategory = (value: string | boolean): void => { if (typeof value !== 'string') { notify("error", "올바르지 않은 카테고리입니다."); return; } const selectedValue = categories.find((category) => category._id === value); if (!selectedValue) { notify("error", "존재하지 않는 카테고리입니다."); return; } setSelectedCategory(selectedValue); setValue("category", value); };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
apps/web/app/(admin)/rooms/_components/AddCategoryForm.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/AddItemButton.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/CategoryEditDropdown.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/CategoryList.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/CategoryListItem.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/CategoryListSubItem.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/ConfirmationModal.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/EditItemForm.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/ItemsAdminHeader.tsx(1 hunks)apps/web/app/(admin)/rooms/_components/SidePanel.tsx(1 hunks)apps/web/app/(admin)/rooms/_store/useMeetingsStore.tsx(1 hunks)apps/web/app/(admin)/rooms/page.tsx(1 hunks)apps/web/lib/queryKey.ts(1 hunks)packages/constants/index.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/lib/queryKey.ts
- packages/constants/index.ts
🧰 Additional context used
📓 Learnings (1)
apps/web/app/(admin)/rooms/_components/CategoryEditDropdown.tsx (1)
Learnt from: bokeeeey
PR: codeit-internship-group-b/codeit-resources#162
File: apps/web/app/admin/(items)/_components/CategoryEditDropdown.tsx:4-7
Timestamp: 2024-12-03T00:47:58.526Z
Learning: `CategoryEditDropdown` 컴포넌트에서 다른 클릭 동작이 없다면 `onClickEdit`을 `onClick`으로 받아도 괜찮습니다.
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: preview
🔇 Additional comments (2)
apps/web/app/(admin)/rooms/page.tsx (1)
2-7: 변경 내용 확인 완료
ItemsAdminHeader컴포넌트를 사용하여 코드가 간결해졌으며, UI 구성 요소가 명확하게 분리되었습니다. 기능적으로 문제가 없으며 코드가 잘 정리되어 있습니다.apps/web/app/(admin)/rooms/_components/EditItemForm.tsx (1)
1-13: 코드 구조가 잘 정리되어 있습니다!필요한 의존성들이 잘 정리되어 있고, TypeScript 타입도 적절하게 import 되어 있습니다.
| defaultValue={inputValue} | ||
| ref={inputRef} | ||
| placeholder="카테고리명" | ||
| className="placeholder:text-custom-black/50 bg-gray-60 w-full placeholder:underline placeholder:underline-offset-4 focus:outline-none" | ||
| onChange={(e) => { | ||
| setInputValue(e.target.value); | ||
| }} | ||
| onKeyDown={(e) => { | ||
| if (e.key === "Enter") { | ||
| handleUpdateCategory(); | ||
| setIsModifyingCategoryName(false); | ||
| } | ||
| }} | ||
| /> |
There was a problem hiding this comment.
React Input 컴포넌트에서 defaultValue 대신 value를 사용해야 합니다.
현재 input 컴포넌트에서 defaultValue와 onChange 핸들러를 함께 사용하고 있습니다. 이는 비제어 컴포넌트와 제어 컴포넌트를 혼용하는 것이므로, value 속성을 사용하여 제어 컴포넌트로 만드는 것이 좋습니다.
다음과 같이 수정하십시오:
- defaultValue={inputValue}
+ value={inputValue}📝 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.
| defaultValue={inputValue} | |
| ref={inputRef} | |
| placeholder="카테고리명" | |
| className="placeholder:text-custom-black/50 bg-gray-60 w-full placeholder:underline placeholder:underline-offset-4 focus:outline-none" | |
| onChange={(e) => { | |
| setInputValue(e.target.value); | |
| }} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter") { | |
| handleUpdateCategory(); | |
| setIsModifyingCategoryName(false); | |
| } | |
| }} | |
| /> | |
| value={inputValue} | |
| ref={inputRef} | |
| placeholder="카테고리명" | |
| className="placeholder:text-custom-black/50 bg-gray-60 w-full placeholder:underline placeholder:underline-offset-4 focus:outline-none" | |
| onChange={(e) => { | |
| setInputValue(e.target.value); | |
| }} | |
| onKeyDown={(e) => { | |
| if (e.key === "Enter") { | |
| handleUpdateCategory(); | |
| setIsModifyingCategoryName(false); | |
| } | |
| }} | |
| /> |
| if (isModifyingCategoryName) { | ||
| setIsModifyingCategoryName(false); | ||
| } | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
카테고리 이름 편집 중 포커스 아웃 시 변경 사항 저장 여부 확인 필요
현재 카테고리 이름을 수정하다가 입력 필드 밖을 클릭하면 변경 사항이 저장되지 않고 편집 모드가 종료됩니다. 사용자가 의도치 않게 변경 사항을 잃을 수 있으므로, 포커스 아웃 시 변경 사항을 저장하거나 저장 여부를 확인하는 알림을 제공하는 것이 좋습니다.
사용자가 입력을 완료하지 않고 포커스를 이동할 경우를 대비하여, 다음과 같이 수정할 수 있습니다:
useOnClickOutside(inputRef, () => {
if (isModifyingCategoryName) {
+ handleUpdateCategory();
setIsModifyingCategoryName(false);
}
});Committable suggestion skipped: line range outside the PR's diff.
| type="button" | ||
| onClick={onClick} | ||
| > | ||
| <PlusIcon width={20} fill="true" /> |
There was a problem hiding this comment.
PlusIcon 컴포넌트의 fill 속성 전달 방법 확인 필요
fill 속성에 문자열 "true"를 전달하고 있습니다. fill 속성이 불리언 값을 기대한다면, fill={true} 또는 fill만 전달하는 것이 좋습니다.
다음과 같이 수정하십시오:
- <PlusIcon width={20} fill="true" />
+ <PlusIcon width={20} fill />📝 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.
| <PlusIcon width={20} fill="true" /> | |
| <PlusIcon width={20} fill /> |
| <ErrorBoundary fallback={<div>오류가 발생했습니다.</div>}> | ||
| {(panelState === "add" || panelState === "edit") && <EditItemForm />} | ||
| {panelState === "category" && <AddCategoryForm />} | ||
| </ErrorBoundary> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
에러 처리 및 로딩 상태 개선 필요
ErrorBoundary의 fallback UI가 너무 단순합니다. 사용자에게 더 자세한 정보와 복구 옵션을 제공하면 좋을 것 같습니다. 또한 로딩 상태 처리가 누락되어 있습니다.
+ import { Spinner } from "@ui/index";
+ import { useIsFetching } from "@tanstack/react-query";
export default function SidePanel(): JSX.Element {
const { isSidebarOpen, closeSidebar } = useSidebarStore();
const { panelState } = useMeetingsStore();
+ const isFetching = useIsFetching();
+ const ErrorFallback = ({ error, resetErrorBoundary }) => (
+ <div className="p-4 text-center">
+ <h2 className="text-lg font-bold mb-2">오류가 발생했습니다</h2>
+ <p className="text-sm text-gray-600 mb-4">{error.message}</p>
+ <Button onClick={resetErrorBoundary}>다시 시도</Button>
+ </div>
+ );
return (
<Sidebar isOpen={isSidebarOpen} onClose={closeSidebar}>
- <ErrorBoundary fallback={<div>오류가 발생했습니다.</div>}>
+ <ErrorBoundary FallbackComponent={ErrorFallback}>
+ {isFetching ? (
+ <div className="flex justify-center items-center h-full">
+ <Spinner size="lg" />
+ </div>
+ ) : (
{(panelState === "add" || panelState === "edit") && <EditItemForm />}
{panelState === "category" && <AddCategoryForm />}
+ )}
</ErrorBoundary>
</Sidebar>
);
}Committable suggestion skipped: line range outside the PR's diff.
| <Dropdown.Toggle iconType="kebab" /> | ||
| <Dropdown.Wrapper className="-left-30 top-56"> | ||
| <Dropdown.Item hoverStyle="purple" value="수정"> | ||
| 수정 | ||
| </Dropdown.Item> | ||
| <Modal.Trigger> | ||
| <Dropdown.Item hoverStyle="purple" value="삭제"> | ||
| 삭제 | ||
| </Dropdown.Item> | ||
| </Modal.Trigger> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
접근성 및 키보드 네비게이션 개선 필요
드롭다운 메뉴의 접근성이 부족합니다. 키보드 네비게이션과 ARIA 속성을 추가하면 좋을 것 같습니다.
- <Dropdown.Toggle iconType="kebab" />
+ <Dropdown.Toggle
+ iconType="kebab"
+ aria-label="카테고리 수정 메뉴"
+ aria-haspopup="true"
+ />
<Dropdown.Wrapper className="-left-30 top-56">
- <Dropdown.Item hoverStyle="purple" value="수정">
+ <Dropdown.Item
+ hoverStyle="purple"
+ value="수정"
+ role="menuitem"
+ tabIndex={0}
+ >
수정
</Dropdown.Item>
<Modal.Trigger>
- <Dropdown.Item hoverStyle="purple" value="삭제">
+ <Dropdown.Item
+ hoverStyle="purple"
+ value="삭제"
+ role="menuitem"
+ tabIndex={0}
+ >
삭제
</Dropdown.Item>
</Modal.Trigger>
</Dropdown.Wrapper>📝 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.
| <Dropdown.Toggle iconType="kebab" /> | |
| <Dropdown.Wrapper className="-left-30 top-56"> | |
| <Dropdown.Item hoverStyle="purple" value="수정"> | |
| 수정 | |
| </Dropdown.Item> | |
| <Modal.Trigger> | |
| <Dropdown.Item hoverStyle="purple" value="삭제"> | |
| 삭제 | |
| </Dropdown.Item> | |
| </Modal.Trigger> | |
| <Dropdown.Toggle | |
| iconType="kebab" | |
| aria-label="카테고리 수정 메뉴" | |
| aria-haspopup="true" | |
| /> | |
| <Dropdown.Wrapper className="-left-30 top-56"> | |
| <Dropdown.Item | |
| hoverStyle="purple" | |
| value="수정" | |
| role="menuitem" | |
| tabIndex={0} | |
| > | |
| 수정 | |
| </Dropdown.Item> | |
| <Modal.Trigger> | |
| <Dropdown.Item | |
| hoverStyle="purple" | |
| value="삭제" | |
| role="menuitem" | |
| tabIndex={0} | |
| > | |
| 삭제 | |
| </Dropdown.Item> | |
| </Modal.Trigger> | |
| </Dropdown.Wrapper> |
| handleAddItem: (data: Record<string, string>) => Promise<IRoom | IEquipment | string>; | ||
| handleEditItem: (data: Record<string, string>, itemId: string) => Promise<IRoom | IEquipment | string>; | ||
| handleDeleteItem: (itemId: string) => Promise<void>; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
반환 타입 구체화 필요
handleAddItem과 handleEditItem의 반환 타입이 너무 광범위합니다.
다음과 같이 개선하는 것을 제안합니다:
- handleAddItem: (data: Record<string, string>) => Promise<IRoom | IEquipment | string>;
- handleEditItem: (data: Record<string, string>, itemId: string) => Promise<IRoom | IEquipment | string>;
+ handleAddItem: (data: Record<string, string>) => Promise<IRoom>;
+ handleEditItem: (data: Record<string, string>, itemId: string) => Promise<IRoom>;Committable suggestion skipped: line range outside the PR's diff.
| const { mutate: EditItem } = useMutation({ | ||
| mutationFn: async (payload: Record<string, string>) => { | ||
| if (panelState === "add") { | ||
| return await handleAddItem(payload); | ||
| } | ||
|
|
||
| if (panelState === "edit" && currentItem) { | ||
| return await handleEditItem(payload, currentItem._id); | ||
| } | ||
| }, | ||
| onSuccess: () => { | ||
| notify("success", panelState === "add" ? "등록완료!" : "수정완료!"); | ||
| closeSidebar(); | ||
| void queryClient.invalidateQueries({ queryKey: QUERY_KEYS.ROOMS }); | ||
| }, | ||
| onError: (error) => { | ||
| notify("error", error.message || "알 수 없는 오류가 발생했습니다. 다시 시도해주세요."); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
에러 처리와 메시지 국제화가 필요합니다.
- 에러 처리가 너무 단순화되어 있습니다. 구체적인 에러 케이스별 처리가 필요합니다.
- 성공/실패 메시지가 하드코딩되어 있어 국제화가 어렵습니다.
다음과 같이 개선해보세요:
// constants/messages.ts 파일 생성
export const MESSAGES = {
success: {
add: "등록완료!",
edit: "수정완료!"
},
error: {
unknown: "알 수 없는 오류가 발생했습니다. 다시 시도해주세요.",
network: "네트워크 오류가 발생했습니다.",
validation: "입력값을 확인해주세요."
}
} as const;
// EditItemForm.tsx
onError: (error) => {
if (error instanceof NetworkError) {
notify("error", MESSAGES.error.network);
} else if (error instanceof ValidationError) {
notify("error", MESSAGES.error.validation);
} else {
notify("error", MESSAGES.error.unknown);
}
}| const handleFormSubmit = handleSubmit((data) => { | ||
| const payload = { | ||
| ...data, | ||
| category: selectedCategory?._id ?? String(currentCategory?._id), | ||
| capacity: String(data.capacity), | ||
| }; | ||
|
|
||
| EditItem(payload); | ||
| }); |
There was a problem hiding this comment.
폼 제출 전 데이터 검증이 필요합니다.
- capacity 필드의 타입 변환이 안전하지 않습니다.
- 제출 전 데이터 유효성 검증이 누락되어 있습니다.
다음과 같이 개선해보세요:
const handleFormSubmit = handleSubmit((data) => {
// capacity 값 검증
const capacityNum = parseInt(data.capacity, 10);
if (isNaN(capacityNum) || capacityNum <= 0) {
notify("error", "올바른 수용인원을 입력해주세요.");
return;
}
// 카테고리 선택 검증
if (!selectedCategory?._id && !currentCategory?._id) {
notify("error", "카테고리를 선택해주세요.");
return;
}
const payload = {
...data,
category: selectedCategory?._id ?? String(currentCategory?._id),
capacity: String(capacityNum),
};
EditItem(payload);
});| const { register, handleSubmit, setValue, reset } = useForm({ | ||
| defaultValues: { | ||
| name: currentItem?.name ?? "", | ||
| description: currentItem?.description ?? "", | ||
| capacity: currentItem?.capacity ?? 1, | ||
| location: currentItem?.location ?? "", | ||
| status: currentItem?.status ?? "available", | ||
| category: currentCategory?._id, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
폼 유효성 검사와 타입 일관성 개선이 필요합니다.
- capacity 필드가 초기값은 number로 설정되지만 나중에 string으로 처리되고 있습니다.
- name 필드 외에 다른 필요한 필드들의 유효성 검사가 누락되어 있습니다.
다음과 같이 개선해보세요:
const { register, handleSubmit, setValue, reset } = useForm({
defaultValues: {
name: currentItem?.name ?? "",
description: currentItem?.description ?? "",
- capacity: currentItem?.capacity ?? 1,
+ capacity: currentItem?.capacity?.toString() ?? "1",
location: currentItem?.location ?? "",
status: currentItem?.status ?? "available",
category: currentCategory?._id,
},
});그리고 필수 필드들에 대한 유효성 검사를 추가하세요:
-<Input {...register("capacity")} placeholder="수용인원" type="text" />
+<Input {...register("capacity", {
+ required: true,
+ pattern: {
+ value: /^[0-9]+$/,
+ message: "숫자만 입력 가능합니다"
+ }
+})} placeholder="수용인원" type="text" />Committable suggestion skipped: line range outside the PR's diff.
| return ( | ||
| <form onSubmit={handleFormSubmit} className="flex h-full flex-col justify-between"> | ||
| <div> | ||
| <h1>회의실 {panelState === "add" ? "추가" : "수정"}</h1> | ||
| <div className="my-20"> | ||
| <Radio.Group | ||
| value={currentItem?.status} | ||
| defaultValue={currentItem?.status ?? "available"} | ||
| onChange={(value) => { | ||
| setValue("status", value as TItemStatus); | ||
| }} | ||
| > | ||
| <Radio.Option value="available">사용 가능</Radio.Option> | ||
| <Radio.Option value="unavailable">사용 불가</Radio.Option> | ||
| </Radio.Group> | ||
| </div> | ||
| <Input {...register("name", { required: true })} placeholder="회의실 이름" type="text" /> | ||
| <Input {...register("description")} placeholder="설명" type="text" /> | ||
| <div className="mb-24"> | ||
| <Dropdown | ||
| selectedValue={selectedCategory?.name ?? currentCategory?.name} | ||
| onSelect={handleSelectCategory} | ||
| isError={false} | ||
| errorMessage="Error" | ||
| > | ||
| <Dropdown.Toggle title="카테고리">{currentItem ? currentItem.category.name : ""}</Dropdown.Toggle> | ||
| <Dropdown.Wrapper> | ||
| {categories.map((category) => { | ||
| return ( | ||
| <Dropdown.Item key={category._id} value={category._id}> | ||
| {category.name} | ||
| </Dropdown.Item> | ||
| ); | ||
| })} | ||
| </Dropdown.Wrapper> | ||
| </Dropdown> | ||
| </div> | ||
| <Input {...register("capacity")} placeholder="수용인원" type="text" /> | ||
| <Input {...register("location")} placeholder="위치" type="text" /> | ||
| </div> | ||
| <Button type="submit" variant="Action"> | ||
| 회의실 {panelState === "add" ? "추가" : "수정"} | ||
| </Button> | ||
| </form> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
접근성과 에러 상태 처리를 개선해주세요.
- 폼 요소들에 접근성 속성이 누락되어 있습니다.
- 입력 필드의 에러 상태가 사용자에게 표시되지 않습니다.
다음과 같이 개선해보세요:
-<Input {...register("name", { required: true })} placeholder="회의실 이름" type="text" />
+<Input
+ {...register("name", { required: "회의실 이름은 필수입니다" })}
+ placeholder="회의실 이름"
+ type="text"
+ aria-label="회의실 이름"
+ aria-required="true"
+ aria-invalid={errors.name ? "true" : "false"}
+/>
+{errors.name && <span role="alert" className="text-red-500">{errors.name.message}</span>}그리고 Dropdown 컴포넌트의 에러 처리도 개선이 필요합니다:
<Dropdown
selectedValue={selectedCategory?.name ?? currentCategory?.name}
onSelect={handleSelectCategory}
- isError={false}
- errorMessage="Error"
+ isError={!selectedCategory && !currentCategory}
+ errorMessage="카테고리를 선택해주세요"
+ aria-label="카테고리 선택"
>Committable suggestion skipped: line range outside the PR's diff.
🚀 작업 내용
📝 참고 사항
🖼️ 스크린샷
🚨 관련 이슈 (이슈 번호)
✅ 체크리스트
Summary by CodeRabbit
릴리즈 노트
새로운 기능
개선 사항
기타 변경 사항