Skip to content

Commit

Permalink
[FE] 검색 기능 구현(#730) (#741)
Browse files Browse the repository at this point in the history
  • Loading branch information
pp449 authored Nov 8, 2024
2 parents 1ad66f0 + eea33b4 commit 0bf3b0f
Show file tree
Hide file tree
Showing 14 changed files with 219 additions and 20 deletions.
3 changes: 2 additions & 1 deletion frontend/src/@types/icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type IconKind =
| "check"
| "menu"
| "close"
| "githubLogo";
| "githubLogo"
| "search";

export default IconKind;
1 change: 1 addition & 0 deletions frontend/src/@types/roomInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type Classification = "ALL" | "FRONTEND" | "BACKEND" | "ANDROID";

export type RoomStatus = "OPEN" | "CLOSE" | "PROGRESS" | "FAIL";
export type RoomStatusCategory = "participated" | "progress" | "opened" | "closed";

export type ParticipationStatus =
| "NOT_PARTICIPATED"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/apis/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const API_ENDPOINTS = {
PROGRESS_ROOMS: "/rooms/progress",
OPENED_ROOMS: "/rooms/opened",
CLOSED_ROOMS: "/rooms/closed",
SEARCH_ROOMS: "/rooms/search",
PARTICIPATE_IN: (roomId: number) => `/participate/${roomId}`,
ROOMS: "/rooms",
PARTICIPANT_LIST: (roomId: number) => `/rooms/${roomId}/participants`,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/apis/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const QUERY_KEYS = {
// rooms
PARTICIPATED_ROOM_LIST: "participatedRoomList",
SEARCH_ROOM_LIST: "searchRoomList",
PROGRESS_ROOM_LIST: "progressRoomList",
OPENED_ROOM_LIST: "openedRoomList",
CLOSED_ROOM_LIST: "closedRoomList",
Expand Down
22 changes: 21 additions & 1 deletion frontend/src/apis/rooms.api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import apiClient from "./apiClient";
import { API_ENDPOINTS } from "./endpoints";
import { ParticipantListInfo } from "@/@types/participantList";
import { Role, RoomInfo, RoomListInfo, SubmitRoomInfo } from "@/@types/roomInfo";
import {
Classification,
Role,
RoomInfo,
RoomListInfo,
RoomStatus,
SubmitRoomInfo,
} from "@/@types/roomInfo";
import MESSAGES from "@/constants/message";

export const getParticipatedRoomList = async (
Expand Down Expand Up @@ -51,6 +58,19 @@ export const getClosedRoomList = async (
return res;
};

export const getSearchRoomList = async (
status: RoomStatus,
classification: Classification,
keywordTitle: string,
): Promise<Pick<RoomListInfo, "rooms">> => {
const res = await apiClient.get({
endpoint: `${API_ENDPOINTS.SEARCH_ROOMS}?status=${status}&classification=${classification.toUpperCase()}&keywordTitle=${keywordTitle}`,
errorMessage: MESSAGES.ERROR.GET_SEARCH_ROOM_LIST,
});

return res;
};

export const getRoomDetailInfo = async (id: number): Promise<RoomInfo> => {
const res = await apiClient.get({
endpoint: `${API_ENDPOINTS.ROOMS}/${id}`,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/common/icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
MdOutlineArrowDropUp,
MdOutlineCreate,
MdOutlinePeopleAlt,
MdOutlineSearch,
MdOutlineStar,
MdOutlineThumbDown,
MdOutlineThumbUp,
Expand Down Expand Up @@ -57,6 +58,7 @@ const ICON: { [key in IconKind]: IconType } = {
menu: MdMenu,
close: MdClear,
githubLogo: IoLogoGithub,
search: MdOutlineSearch,
};

interface IconProps {
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/components/common/searchBar/SearchBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import SearchBar from "./SearchBar";
import type { Meta, StoryObj } from "@storybook/react";

const meta = {
title: "Common/SearchBar",
component: SearchBar,
argTypes: {
placeholder: {
control: {
type: "text",
},
description: "placeholder",
defaultValue: "방 제목을 검색해주세요",
},
defaultValue: {
control: {
type: "text",
},
description: "textarea의 value",
},
},
} satisfies Meta<typeof SearchBar>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
value: "기본 입력창",
handleValue: () => {},
handleSearch: () => {},
},
};
16 changes: 16 additions & 0 deletions frontend/src/components/common/searchBar/SearchBar.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import styled from "styled-components";

export const SearchBarContainer = styled.div`
position: relative;
width: 100%;
height: 40px;
`;

export const SearchIconWrapper = styled.div`
cursor: pointer;
position: absolute;
top: 50%;
right: 3px;
transform: translate(-50%, -50%);
`;
34 changes: 34 additions & 0 deletions frontend/src/components/common/searchBar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as S from "./SearchBar.style";
import { InputHTMLAttributes } from "react";
import Icon from "@/components/common/icon/Icon";
import { Input } from "@/components/common/input/Input";

interface SearchBarProps extends InputHTMLAttributes<HTMLInputElement> {
value: string;
handleValue: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSearch: () => void;
}

const SearchBar = ({ value, handleValue, handleSearch, ...props }: SearchBarProps) => {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
handleSearch();
}
};

return (
<S.SearchBarContainer>
<Input
value={value}
onChange={handleValue}
onKeyDown={handleKeyDown}
style={{ paddingRight: "3rem", height: "40px" }}
/>
<S.SearchIconWrapper onClick={handleSearch}>
<Icon kind="search" />
</S.SearchIconWrapper>
</S.SearchBarContainer>
);
};

export default SearchBar;
8 changes: 6 additions & 2 deletions frontend/src/components/main/room/RoomList.style.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import styled from "styled-components";

export const DropdownWrapper = styled.div`
export const SearchBarWrapper = styled.div`
width: 180px;
`;

export const FilterWrapper = styled.div`
display: flex;
justify-content: flex-end;
justify-content: space-between;
margin-bottom: 1rem;
`;
68 changes: 57 additions & 11 deletions frontend/src/components/main/room/RoomListWithDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ChangeEvent, useEffect, useState } from "react";
import useToast from "@/hooks/common/useToast";
import { useFetchSearchRoomList } from "@/hooks/queries/useFetchRooms";
import ContentSection from "@/components/common/contentSection/ContentSection";
import Dropdown from "@/components/common/dropdown/Dropdown";
import SearchBar from "@/components/common/searchBar/SearchBar";
import * as S from "@/components/main/room/RoomList.style";
import RoomList from "@/components/shared/roomList/RoomList";
import { RoomInfo } from "@/@types/roomInfo";
import { Classification, RoomInfo, RoomStatusCategory } from "@/@types/roomInfo";
import { dropdownItems } from "@/constants/roomDropdownItems";

interface RoomListWithDropdownProps {
Expand All @@ -12,7 +16,7 @@ interface RoomListWithDropdownProps {
hasNextPage: boolean;
onLoadMore: () => void;
isFetchingNextPage: boolean;
roomType: "participated" | "progress" | "opened" | "closed";
roomType: RoomStatusCategory;
}

const RoomListWithDropdown = ({
Expand All @@ -24,23 +28,65 @@ const RoomListWithDropdown = ({
isFetchingNextPage,
roomType,
}: RoomListWithDropdownProps) => {
const [searchInput, setSearchInput] = useState("");
const [searchedRooms, setSearchedRooms] = useState<RoomInfo[]>([]);
const { openToast } = useToast();

const { refetch: fetchSearch, isLoading } = useFetchSearchRoomList(
roomType,
selectedCategory as Classification,
searchInput,
false,
);

const handleSearchInput = (e: ChangeEvent<HTMLInputElement>) => {
setSearchInput(e.target.value);
};

const handleSearch = async () => {
const { data } = await fetchSearch();
if (!data || data.rooms.length === 0) {
openToast("검색한 방이 없습니다.");
return;
}

setSearchedRooms(data.rooms);
};

useEffect(() => {
setSearchInput("");
setSearchedRooms([]);
}, [selectedCategory, roomType]);

return (
<ContentSection title="">
<S.DropdownWrapper>
<S.FilterWrapper>
<Dropdown
name="포지션 분류"
dropdownItems={dropdownItems}
selectedCategory={selectedCategory}
onSelectCategory={handleSelectedCategory}
/>
</S.DropdownWrapper>
<RoomList
roomList={roomList}
hasNextPage={hasNextPage}
onLoadMore={onLoadMore}
isFetchingNextPage={isFetchingNextPage}
roomType={roomType}
/>
<S.SearchBarWrapper>
<SearchBar
value={searchInput}
handleValue={handleSearchInput}
handleSearch={handleSearch}
placeholder="제목을 입력해주세요"
/>
</S.SearchBarWrapper>
</S.FilterWrapper>
{searchedRooms.length === 0 ? (
<RoomList
roomList={roomList}
hasNextPage={hasNextPage}
onLoadMore={onLoadMore}
isFetchingNextPage={isFetchingNextPage}
roomType={roomType}
/>
) : (
<RoomList roomList={searchedRooms} isFetchingNextPage={isLoading} roomType={roomType} />
)}
</ContentSection>
);
};
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/shared/roomList/RoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import PlusButton from "@/components/common/plusButton/PlusButton";
import RoomCard from "@/components/shared/roomCard/RoomCard";
import * as RoomCardSkeleton from "@/components/shared/roomCard/RoomCard.skeleton";
import * as S from "@/components/shared/roomList/RoomList.style";
import { RoomInfo } from "@/@types/roomInfo";
import { RoomInfo, RoomStatusCategory } from "@/@types/roomInfo";
import { defaultCharacter } from "@/assets";

interface RoomListProps {
roomList: RoomInfo[];
isFetchingNextPage: boolean;
hasNextPage?: boolean;
onLoadMore?: () => void;
roomType: "participated" | "progress" | "opened" | "closed";
roomType: RoomStatusCategory;
}

const RoomEmptyText = {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/constants/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const ERROR_MESSAGES = {
GET_PROGRESS_ROOM_LIST: "진행 중인 방 목록을 불러오는 도중 에러가 발생하였습니다.",
GET_OPENED_ROOM_LIST: "모집 중인 방 목록을 불러오는 도중 에러가 발생하였습니다.",
GET_CLOSED_ROOM_LIST: "모집 완료 방 목록을 불러오는 도중 에러가 발생하였습니다.",
GET_SEARCH_ROOM_LIST: "방 검색 결과를 불러오는 도중 에러가 발생하였습니다.",
GET_ROOM_DETAIL_INFO: "방 상세정보를 불러오는 도중 에러가 발생하였습니다.",
POST_CREATE_ROOM: "방 생성하기를 실패했습니다.",
PUT_EDIT_ROOM: "방 정보 수정하기를 실패했습니다.",
Expand Down
44 changes: 41 additions & 3 deletions frontend/src/hooks/queries/useFetchRooms.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useSuspenseInfiniteQuery, useSuspenseQuery } from "@tanstack/react-query";
import { RoomListInfo } from "@/@types/roomInfo";
import { useQuery, useSuspenseInfiniteQuery, useSuspenseQuery } from "@tanstack/react-query";
import { Classification, RoomListInfo, RoomStatus, RoomStatusCategory } from "@/@types/roomInfo";
import QUERY_KEYS from "@/apis/queryKeys";
import { getParticipantList, getParticipatedRoomList, getRoomDetailInfo } from "@/apis/rooms.api";
import {
getParticipantList,
getParticipatedRoomList,
getRoomDetailInfo,
getSearchRoomList,
} from "@/apis/rooms.api";

interface RoomListQueryProps {
queryKey: string[];
Expand All @@ -27,6 +32,39 @@ export const useInfiniteFetchRoomList = ({
});
};

export const useFetchSearchRoomList = (
status: RoomStatusCategory,
classification: Classification,
keyword: string,
enabled: boolean,
) => {
let roomStatus: RoomStatus;

switch (status) {
case "opened": {
roomStatus = "OPEN";
break;
}
case "closed": {
roomStatus = "CLOSE";
break;
}
case "progress": {
roomStatus = "PROGRESS";
break;
}
default: {
roomStatus = "OPEN";
}
}

return useQuery({
queryKey: [QUERY_KEYS.SEARCH_ROOM_LIST, status, classification, keyword],
queryFn: () => getSearchRoomList(roomStatus, classification, keyword),
enabled,
});
};

export const useFetchDetailRoomInfo = (roomId: number) => {
return useSuspenseQuery({
queryKey: [QUERY_KEYS.ROOM_DETAIL_INFO, roomId],
Expand Down

0 comments on commit 0bf3b0f

Please sign in to comment.