Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 검색 기능 구현(#730) #741

Merged
merged 7 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: () => {},
},
};
28 changes: 28 additions & 0 deletions frontend/src/components/common/searchBar/SearchBar.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import styled from "styled-components";

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

export const SearchBar = styled.input`
width: 100%;
height: 100%;
padding: 0.6rem 3rem 0.6rem 0.6rem;

font: ${({ theme }) => theme.TEXT.small};

border: 1px solid ${({ theme }) => theme.COLOR.grey1};
border-radius: 6px;
`;
Copy link
Contributor

Choose a reason for hiding this comment

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

이 부분이 padding과 height 말고는 Input 컴포넌트와 굉장히 유사하더라고요! 사실 height는 없어도 되는 속성이라 padding-right: 3rem만 추가면 Input 컴포넌트를 그대로 써도 괜찮을 것 같다고 생각합니다!

outline-color: ${({ theme }) => theme.COLOR.black};를 추가하면 focus 시 outline이 검정색으로 나온답니다😁


export const SearchIconWrapper = styled.div`
cursor: pointer;

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

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>
<S.SearchBar value={value} onChange={handleValue} onKeyDown={handleKeyDown} {...props} />
<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} />
)}
Comment on lines +79 to +89
Copy link
Contributor

Choose a reason for hiding this comment

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

검색 결과가 없을 때도 처리를 해주었네요! 저도 원래 데이터를 그대로 보여주는 게 좋다고 생각합니다👍

</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
Loading