Skip to content

Comments

[Feat] 회의실 관리 페이지#162

Merged
bokeeeey merged 59 commits intodevelopfrom
119-fe-feat-회의실-관리-페이지-데이터-로딩
Jan 13, 2025

Hidden character warning

The head ref may contain hidden characters: "119-fe-feat-\ud68c\uc758\uc2e4-\uad00\ub9ac-\ud398\uc774\uc9c0-\ub370\uc774\ud130-\ub85c\ub529"
Merged

[Feat] 회의실 관리 페이지#162
bokeeeey merged 59 commits intodevelopfrom
119-fe-feat-회의실-관리-페이지-데이터-로딩

Conversation

@AdamSeungheonShin
Copy link
Collaborator

@AdamSeungheonShin AdamSeungheonShin commented Dec 2, 2024

🚀 작업 내용

  • 회의실 관리 페이지
  • UI
  • 데이터 로딩
  • api 요청

📝 참고 사항

  • 카테고리 삭제 부분 db설정 이슈로 동작이 안되는데 확인 해보고 백엔드 코드 수정 할게요

🖼️ 스크린샷

🚨 관련 이슈 (이슈 번호)

✅ 체크리스트

  • Code Review 요청
  • Label 설정
  • PR 제목 규칙에 맞는지 확인

Summary by CodeRabbit

릴리즈 노트

  • 새로운 기능

    • 회의실 및 카테고리 관리 기능 추가
    • 회의실 및 카테고리 생성, 수정, 삭제 지원
    • 회의실 목록 및 카테고리 목록 조회 기능 구현
  • 개선 사항

    • 사용자 인터페이스 개선
    • 폼 유효성 검사 및 오류 처리 강화
    • 드롭다운 및 모달 컴포넌트 접근성 향상
  • 기타 변경 사항

    • Next.js 라우팅 설정 업데이트
    • API 엔드포인트 동적 생성 지원
    • 상태 관리 최적화

@github-actions
Copy link

github-actions bot commented Jan 13, 2025

🚀 Preview URL

Branch: 119-fe-feat-회의실-관리-페이지-데이터-로딩
Commit: 78ffa0e

Preview URL: https://codeit.click?pr=162

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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: 타입 안전성 개선이 필요합니다.

handleAddItemhandleEditItem의 반환 타입이 유니온 타입(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로 고정된 높이를 사용하고 있어, 레이아웃 시프트가 발생할 수 있습니다.

AnimatePresenceauto 높이를 사용하여 개선하는 것을 제안합니다:

+ 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7230f8b and b478133.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is 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 suggestion

useEffect 대신 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

타입 검증 및 불필요한 헤더 제거 필요

  1. axios는 기본적으로 "Content-Type: application/json"을 설정하므로 명시적으로 지정할 필요가 없습니다.
  2. 응답 데이터에 대한 타입 검증이 필요합니다.
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: 에러 처리가 잘 구현되어 있습니다!

handleAddItemhandleEditItem 함수에서:

  • 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

Comment on lines +66 to +79
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;
};
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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;
};

Comment on lines 107 to 109
<Input {...register("name", { required: true })} placeholder="회의실 이름" type="text" />
<Input {...register("description")} placeholder="설명" type="text" />
<div className="mb-24">
Copy link

Choose a reason for hiding this comment

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

🛠️ 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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 네이밍 단순화

이전 피드백에 따르면, 다른 클릭 동작이 없는 경우 onClickEditonClick으로 단순화할 수 있습니다.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b478133 and 402c5a3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is 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 되어 있습니다.

Comment on lines +122 to +135
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);
}
}}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

React Input 컴포넌트에서 defaultValue 대신 value를 사용해야 합니다.

현재 input 컴포넌트에서 defaultValueonChange 핸들러를 함께 사용하고 있습니다. 이는 비제어 컴포넌트와 제어 컴포넌트를 혼용하는 것이므로, 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.

Suggested change
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);
}
}}
/>

Comment on lines +38 to +41
if (isModifyingCategoryName) {
setIsModifyingCategoryName(false);
}
});
Copy link

Choose a reason for hiding this comment

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

🛠️ 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" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
<PlusIcon width={20} fill="true" />
<PlusIcon width={20} fill />

Comment on lines +21 to +24
<ErrorBoundary fallback={<div>오류가 발생했습니다.</div>}>
{(panelState === "add" || panelState === "edit") && <EditItemForm />}
{panelState === "category" && <AddCategoryForm />}
</ErrorBoundary>
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +20 to +29
<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>
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
<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>

Comment on lines +24 to +26
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>;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

반환 타입 구체화 필요

handleAddItemhandleEditItem의 반환 타입이 너무 광범위합니다.

다음과 같이 개선하는 것을 제안합니다:

- 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.

Comment on lines +53 to +71
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 || "알 수 없는 오류가 발생했습니다. 다시 시도해주세요.");
},
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리와 메시지 국제화가 필요합니다.

  1. 에러 처리가 너무 단순화되어 있습니다. 구체적인 에러 케이스별 처리가 필요합니다.
  2. 성공/실패 메시지가 하드코딩되어 있어 국제화가 어렵습니다.

다음과 같이 개선해보세요:

// 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);
  }
}

Comment on lines +73 to +81
const handleFormSubmit = handleSubmit((data) => {
const payload = {
...data,
category: selectedCategory?._id ?? String(currentCategory?._id),
capacity: String(data.capacity),
};

EditItem(payload);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

폼 제출 전 데이터 검증이 필요합니다.

  1. capacity 필드의 타입 변환이 안전하지 않습니다.
  2. 제출 전 데이터 유효성 검증이 누락되어 있습니다.

다음과 같이 개선해보세요:

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);
});

Comment on lines +20 to +29
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,
},
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

폼 유효성 검사와 타입 일관성 개선이 필요합니다.

  1. capacity 필드가 초기값은 number로 설정되지만 나중에 string으로 처리되고 있습니다.
  2. 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.

Comment on lines +91 to +136
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>
);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성과 에러 상태 처리를 개선해주세요.

  1. 폼 요소들에 접근성 속성이 누락되어 있습니다.
  2. 입력 필드의 에러 상태가 사용자에게 표시되지 않습니다.

다음과 같이 개선해보세요:

-<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.

@bokeeeey bokeeeey merged commit dadcbc8 into develop Jan 13, 2025
2 checks passed
@bokeeeey bokeeeey deleted the 119-fe-feat-회의실-관리-페이지-데이터-로딩 branch January 13, 2025 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👾 FE 프론트엔드 관련 작업 ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FE-Feat] 회의실 관리 페이지 데이터 로딩

5 participants