Skip to content

Commit dadcbc8

Browse files
[Feat] 회의실 관리 페이지 (#162)
* FE-Refactor: 예약정보 호출 시 아이템정보 populate 명령어 수정 * FE-Feat: dropdown, modal 컴포넌트로 분리 * FE-Feat: 회의실 하위 아이템 컴포넌트 UI 구현 * FE-Feat: 아이템 생성 버튼 , 아코디언 펼치기용 caret 버튼 추가 * FE-Feat: 회의실 추가 아이템 등록 드로워 추가 * FE-Feat: 회의실 추가 폼 제작 * FE-Feat: 사이드패널 listItem 뎁스로 이동, 추가/수정 시 동일한 폼 재사용할 수 있도록 버튼 click시 동작 수정 * FE-Feat: 사이드패널, 모달 안내 텍스트 조건부 렌더링 추가 * FE-Feat: 아이템 등록/수정 폼 react-hook-form 적용 * FE-Feat: 아이템 정보 입력 폼 submit 함수 및 핸들러 추가 * FE-Refactor: 리스트 아이템 펼치기 애니메이션 개선 * FE-Feat: 카테고리 별 아이템 데이터 페칭 부분 작성 * FE-Feat: room 아이템 생성 api함수 작성 * FE-Feat: 카테고리 추가 side panel 기본 ui * FE-Feat: submit 핸들러 버튼 추가 * FE-Feat: form input 기본값 설정 useEffect 추가 * FE-Feat: 사이드패널 버튼 하단으로 위치 수정 * FE-Refactor: zustand 적용, 불필요한 상태, 사이드이펙트 제거, sidePanel 한개로 관리 * FE-Refactor: item 등록/수정 form 초기화 로직 추가 * FE-Refactor: Room 수정/추가 form 오류 해결 밑 zustand활용 초기값 설정 * FE-Refactor: 함수명 중복, 함수명 수정 * FE-Refactor: categories, rooms 데이터 로딩부분에 react-query 적용 * FE-Refactor: editItemForm의 post요청 mutate로 데이터 업데이트 구현 * FE-Feat: 회의실 삭제 구현 * FE-Feat: 카테고리 수정/삭제 구현 * FE-Refactor: form 초기화 안되던 부분 reset, useEffect로 수정 * FE-Refactor: 불필요한 컴포넌트 삭제 * FE-Refactor: 변경된 toast 트리거 적용 * FE-Refactor: 회의실 관리자 페이지 헤더 컴포넌트명 변경 * FE-Refactor: 사이드패널 조건부 렌더링 최적화 * FE-Refactor: 카테고리 수정 팝오버 prop naming 수정 * FE-Refactor: 카테고리 추가 폼 에러처리 개선 * FE-Refactor: 회의실 카테고리 목록 쿼리 에러 얼리리턴, 로딩상태 및 예외처리 개선 * FE-Refactor: 카테고리별 회의실 필터링 중복로직 삭제 * FE-Refactor: 회의실 수정 사이드바 동작 개선, 불필요 동작 삭제 및 내부 데이터만 변경되도록 변경 * FE-Fix: patchCategory 함수 반환 타입 수정, API와 불일치 해소 * FE-Refactor: EditItemForm과 Store의 api 요청 핸들러 함수 에러 처리 방식 개선, response의 에러메세지 활용하도록 변경 * FE-Refactor: MeetingsStore의 handleDeleteItem핸들러에도 로딩상테 추가 * FE-Refactor: 카테고리 수정 input 값 상태 초기화 수정, 카테고리명 수정 시 깜빡임 개선 * FE-Refactor: 카테고리명 수정 input 빈값 또는 공백 입력하는 경우 예외처처리 * FE-Refactor: AddCategoryForm 에러처리 서버에서 받는 에러메세지로 수정 * FE-Refactor: AddCategoryForm 불필요한 비동기처리 삭제, 쿼리 키 상수화 * FE-Refactor: 카테고리 추가 mutation 구조분해 할당으로 함수명 addCategory로 수정 * FE-Refactor: 에러 메세지 String으로 변환 * FE-Refactor: 카테고리 수정, 삭제 mutation 함수명 구조분해할당으로 작성 * FE-Refactor: CategoryListItem의 카테고리 수정/삭제 뮤테이션 함수 불필요한 비동기처리 삭제 * FE-Refactor: EditItemForm 불필요한 에러 Throw 삭제 * FE-Refactor: 카테고리 선택 핸들러로 분리 * FE-Refactor: Form 컴포넌트에 불필요한 name 속성 삭제 * FE-Refactor: 엔드포인트 상수값에 누락된 파라미터 추가 * FE-Refactor: 불필요한 비동기 삭제, mutation 함수명 구조분해 할당으로 작성 * FE-Fix: items, meetings 폴더 admin/rooms로 경로 수정
1 parent f2388d5 commit dadcbc8

20 files changed

Lines changed: 2555 additions & 1705 deletions

apps/api/src/controllers/reservationControllers.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,7 @@ export const getUserReservations = async (
7373
})
7474
.populate("user", "name email")
7575
.populate("attendees", "name email")
76-
.populate({
77-
path: "item",
78-
select: "name itemType",
79-
})
76+
.populate("item", "name itemType")
8077
.sort({ itemType: 1, startAt: 1 });
8178

8279
if (userReservations.length === 0) {

apps/web/api/items.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const createItem = async (params: CreateItemParams): Promise<TBaseItem> =
5252
const { data } = await axiosRequester<TBaseItem>({
5353
options: {
5454
method: "POST",
55-
url: API_ENDPOINTS.ITEMS.CREATE_ITEM,
55+
url: API_ENDPOINTS.ITEMS.CREATE_ITEM(itemType),
5656
data: {
5757
itemType,
5858
...itemData,

apps/web/api/meetings.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { API_ENDPOINTS } from "@repo/constants";
2+
import { type IRoom, type ICategory, type TItemType, type IEquipment } from "@repo/types";
3+
import { axiosRequester } from "@/lib/axios";
4+
5+
export const getAllCategories = async (): Promise<ICategory[]> => {
6+
const { data } = await axiosRequester<ICategory[]>({
7+
options: {
8+
method: "GET",
9+
url: API_ENDPOINTS.CATEGORIES.GET_ALL,
10+
},
11+
});
12+
return data;
13+
};
14+
15+
export const getAllRooms = async (): Promise<IRoom[]> => {
16+
const { data } = await axiosRequester<IRoom[]>({
17+
options: {
18+
method: "GET",
19+
url: API_ENDPOINTS.ITEMS.GET_ALL("room"),
20+
},
21+
});
22+
return data;
23+
};
24+
25+
export const postNewRoom = async (itemType: TItemType, body: Record<string, string>): Promise<IRoom | IEquipment> => {
26+
const { data } = await axiosRequester<IRoom | IEquipment>({
27+
options: {
28+
method: "POST",
29+
url: API_ENDPOINTS.ITEMS.CREATE_ITEM(itemType),
30+
headers: {
31+
"Content-Type": "application/json",
32+
},
33+
data: body,
34+
},
35+
});
36+
37+
return data;
38+
};
39+
40+
export const patchRoom = async (itemId: string, body: Record<string, string>): Promise<IRoom | IEquipment> => {
41+
const { data } = await axiosRequester<IRoom | IEquipment>({
42+
options: {
43+
method: "PATCH",
44+
url: API_ENDPOINTS.ITEMS.UPDATE_ITEM(itemId),
45+
headers: {
46+
"Content-Type": "application/json",
47+
},
48+
data: body,
49+
},
50+
});
51+
52+
return data;
53+
};
54+
55+
export const deleteRoom = async (itemId: string): Promise<string> => {
56+
const { data } = await axiosRequester<string>({
57+
options: {
58+
method: "DELETE",
59+
url: API_ENDPOINTS.ITEMS.DELETE_ITEM(itemId),
60+
},
61+
});
62+
63+
return data;
64+
};
65+
66+
export const postNewCategory = async (body: Record<string, string>): Promise<ICategory> => {
67+
const { data } = await axiosRequester<ICategory>({
68+
options: {
69+
method: "POST",
70+
url: API_ENDPOINTS.CATEGORIES.CREATE_CATEGORY,
71+
headers: {
72+
"Content-Type": "application/json",
73+
},
74+
data: body,
75+
},
76+
});
77+
78+
return data;
79+
};
80+
81+
export const patchCategory = async (categoryId: string, body: Record<string, string>): Promise<ICategory> => {
82+
const { data } = await axiosRequester<ICategory>({
83+
options: {
84+
method: "PATCH",
85+
url: API_ENDPOINTS.CATEGORIES.UPDATE_CATEGORY(categoryId),
86+
headers: {
87+
"Content-Type": "application/json",
88+
},
89+
data: body,
90+
},
91+
});
92+
93+
return data;
94+
};
95+
96+
export const deleteCategory = async (categoryId: string): Promise<string> => {
97+
const { data } = await axiosRequester<string>({
98+
options: {
99+
method: "DELETE",
100+
url: API_ENDPOINTS.CATEGORIES.DELETE_CATEGORY(categoryId),
101+
},
102+
});
103+
104+
return data;
105+
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use client";
2+
3+
import { useMutation, useQueryClient } from "@tanstack/react-query";
4+
import { Button, Input } from "@ui/index";
5+
import { useForm } from "react-hook-form";
6+
import { AxiosError } from "axios";
7+
import { postNewCategory } from "@/api/meetings";
8+
import { notify } from "@/app/store/useToastStore";
9+
import { QUERY_KEYS } from "@/lib/queryKey";
10+
11+
export default function AddCategoryForm(): JSX.Element {
12+
const {
13+
register,
14+
handleSubmit,
15+
reset,
16+
formState: { errors, isSubmitting },
17+
} = useForm({
18+
defaultValues: {
19+
name: "",
20+
},
21+
});
22+
const queryClient = useQueryClient();
23+
24+
const { mutate: addCategory } = useMutation({
25+
mutationFn: async (payload: Record<string, string>) => {
26+
return await postNewCategory(payload);
27+
},
28+
onSuccess: () => {
29+
notify("success", "카테고리가 추가되었습니다!");
30+
void queryClient.invalidateQueries({ queryKey: QUERY_KEYS.CATEGORIES });
31+
reset();
32+
},
33+
onError: (error) => {
34+
if (error instanceof AxiosError && error.response) {
35+
notify("error", String(error.response.data.message));
36+
} else {
37+
notify("error", "알 수 없는 오류가 발생했습니다. 다시 시도해주세요.");
38+
}
39+
},
40+
});
41+
42+
const handleSubmitForm = handleSubmit((data) => {
43+
const payload = {
44+
...data,
45+
itemType: "room",
46+
};
47+
48+
addCategory(payload);
49+
});
50+
51+
return (
52+
<form onSubmit={handleSubmitForm} className="flex h-full flex-col justify-between">
53+
<div>
54+
<h1 className="my-24">카테고리 추가</h1>
55+
<Input
56+
placeholder="카테고리명"
57+
{...register("name", { required: true })}
58+
error={errors.name}
59+
disabled={isSubmitting}
60+
/>
61+
</div>
62+
<Button variant="Action" type="submit" disabled={isSubmitting} isPending={isSubmitting}>
63+
카테고리 추가
64+
</Button>
65+
</form>
66+
);
67+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use client";
2+
3+
import { PlusIcon } from "@ui/public";
4+
5+
interface AddItemButtonProps {
6+
onClick: () => void;
7+
}
8+
9+
export default function AddItemButton({ onClick }: AddItemButtonProps): JSX.Element {
10+
return (
11+
<button
12+
className="hover:bg-custom-black/5 flex size-32 cursor-pointer justify-center rounded-full transition-colors duration-300 ease-in-out"
13+
type="button"
14+
onClick={onClick}
15+
>
16+
<PlusIcon width={20} fill="true" />
17+
</button>
18+
);
19+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Modal } from "@ui/index";
2+
import Dropdown from "@ui/src/components/common/Dropdown";
3+
4+
interface CategoryEditDropdownProps {
5+
isEditing?: boolean;
6+
onClickEdit: () => void;
7+
}
8+
9+
export default function CategoryEditDropdown({ isEditing, onClickEdit }: CategoryEditDropdownProps): JSX.Element {
10+
return (
11+
<Dropdown
12+
selectedValue={isEditing}
13+
onSelect={(value: string | boolean) => {
14+
if (value === "수정") {
15+
onClickEdit();
16+
}
17+
}}
18+
size="sm"
19+
>
20+
<Dropdown.Toggle iconType="kebab" />
21+
<Dropdown.Wrapper className="-left-30 top-56">
22+
<Dropdown.Item hoverStyle="purple" value="수정">
23+
수정
24+
</Dropdown.Item>
25+
<Modal.Trigger>
26+
<Dropdown.Item hoverStyle="purple" value="삭제">
27+
삭제
28+
</Dropdown.Item>
29+
</Modal.Trigger>
30+
</Dropdown.Wrapper>
31+
</Dropdown>
32+
);
33+
}
Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,65 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
import { useQuery } from "@tanstack/react-query";
5+
import EmptyState from "@ui/src/components/common/EmptyState";
6+
import { getAllCategories, getAllRooms } from "@/api/meetings";
7+
import LoadingBar from "@/components/common/Skeleton/LoadingBar";
8+
import { notify } from "@/app/store/useToastStore";
9+
import useMeetingsStore from "../_store/useMeetingsStore";
10+
import SidePanel from "./SidePanel";
111
import CategoryListItem from "./CategoryListItem";
212

313
export default function CategoryList(): JSX.Element {
14+
const { categories, setCategories, rooms, setRooms } = useMeetingsStore();
15+
16+
const {
17+
data: fetchedCategories,
18+
isLoading: isCategoriesLoading,
19+
error: categoriesError,
20+
} = useQuery({
21+
queryKey: ["categories"],
22+
queryFn: getAllCategories,
23+
});
24+
const {
25+
data: fetchedRooms,
26+
isLoading: isRoomsLoading,
27+
error: roomsError,
28+
} = useQuery({ queryKey: ["rooms"], queryFn: getAllRooms });
29+
30+
useEffect(() => {
31+
if (fetchedCategories) {
32+
const roomCategories = fetchedCategories.filter((category) => category.itemType === "room");
33+
setCategories(roomCategories);
34+
}
35+
}, [fetchedCategories, setCategories]);
36+
37+
useEffect(() => {
38+
if (fetchedRooms) {
39+
setRooms(fetchedRooms);
40+
}
41+
}, [fetchedRooms, setRooms]);
42+
43+
if (categoriesError ?? roomsError) {
44+
notify("error", "데이터를 불러오는데 실패했습니다.");
45+
return <div>데이터를 불러오는데 실패했습니다.</div>;
46+
}
47+
48+
const categoriesWithRooms = categories.map((category) => ({
49+
...category,
50+
rooms: rooms.filter((room) => room.category._id === category._id),
51+
}));
52+
453
return (
5-
<div>
6-
<CategoryListItem />
7-
</div>
54+
<>
55+
{isCategoriesLoading || (isRoomsLoading && <LoadingBar classNames="w-full h-72" />)}
56+
{!isCategoriesLoading && categories.length === 0 && (
57+
<EmptyState message={{ title: "", description: "등록된 카테고리가 없습니다." }} />
58+
)}
59+
{categoriesWithRooms.map((category) => {
60+
return <CategoryListItem key={category._id} category={category} rooms={category.rooms} />;
61+
})}
62+
<SidePanel />
63+
</>
864
);
965
}

0 commit comments

Comments
 (0)