Skip to content

Commit b4e1ed0

Browse files
authored
Merge pull request #19 from Drink-Easy/feature/18
[feature] 와인 정보 검색 API 구현
2 parents 5bdeeb7 + 265ea2c commit b4e1ed0

File tree

12 files changed

+298
-43
lines changed

12 files changed

+298
-43
lines changed

src/api/getWineSearch.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { customAxios } from "./customAxios";
2+
import { Response } from "../types/Response";
3+
4+
export interface PageResult<T> {
5+
content: T[];
6+
pageNumber: number;
7+
totalPages: number;
8+
}
9+
10+
export interface WineDtoDataTypes {
11+
wineId: number;
12+
name: string;
13+
sort: string;
14+
variety: string;
15+
country: string;
16+
region: string;
17+
createdAt: string;
18+
}
19+
20+
export interface WineSearchParams {
21+
searchName?: string | null;
22+
wineSort?: string | null;
23+
wineVariety?: string | null;
24+
wineCountry?: string | null;
25+
page?: number; // page, size, sort는 기본값 채워서 nullable 안 해도 됨..
26+
size?: number;
27+
sort?: string;
28+
}
29+
30+
export async function getWineSearch(params: WineSearchParams) {
31+
const {
32+
searchName = null,
33+
wineSort = null,
34+
wineVariety = null,
35+
wineCountry = null,
36+
page = 0, // 기본 0페이지
37+
size = 7, // 기본 7개 가져오기
38+
sort = "name,ASC", // 기본 name 오름차순pre
39+
} = params;
40+
// 입력된 것만 params 객체로 만들기
41+
const queryParams = Object.fromEntries(
42+
Object.entries({
43+
searchName,
44+
wineSort,
45+
wineVariety,
46+
wineCountry,
47+
page,
48+
size,
49+
sort,
50+
}).filter(([, v]) => v !== null && v !== undefined)
51+
);
52+
53+
const { data } = await customAxios.get<
54+
Response<PageResult<WineDtoDataTypes>>
55+
>(`/admin/wine`, {
56+
params: queryParams,
57+
});
58+
59+
return data;
60+
}

src/components/common/Pagination.tsx

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import styled from "styled-components";
2+
import { BtnWrapper } from "../../styles/GlobalStyle";
3+
4+
interface PaginationProps {
5+
currentPage: number;
6+
totalPages: number;
7+
onPageChange: (page: number) => void;
8+
}
9+
10+
export default function Pagination({
11+
currentPage,
12+
totalPages,
13+
onPageChange,
14+
}: PaginationProps) {
15+
const pages = [];
16+
const maxPage = Math.min(totalPages, 5);
17+
18+
for (let i = 1; i <= maxPage; i++) {
19+
pages.push(i);
20+
}
21+
22+
return (
23+
<Container>
24+
{pages.map((page) => (
25+
<BtnWrapper key={page} onClick={() => onPageChange(page)}>
26+
<Number $isActive={page === currentPage}>{page}</Number>
27+
</BtnWrapper>
28+
))}
29+
{currentPage < totalPages && (
30+
<BtnWrapper onClick={() => onPageChange(currentPage + 1)}>
31+
<Number>{">"}</Number>
32+
</BtnWrapper>
33+
)}
34+
</Container>
35+
);
36+
}
37+
38+
const Container = styled.div`
39+
display: flex;
40+
justify-content: space-between;
41+
align-items: center;
42+
position: absolute;
43+
left: 50%;
44+
transform: translateX(-50%);
45+
bottom: 0%;
46+
padding-bottom: 5.25rem;
47+
width: 8.9375rem;
48+
`;
49+
50+
const Number = styled.p<{ $isActive?: boolean }>`
51+
${({ theme }) => theme.fonts.Body_4};
52+
color: ${({ theme, $isActive }) =>
53+
$isActive ? theme.colors.purple_100 : theme.colors.black};
54+
55+
${({ $isActive }) =>
56+
$isActive &&
57+
`
58+
text-decoration-line: underline;
59+
`}
60+
`;

src/components/common/SearchBox.tsx

+12-3
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,29 @@ import { BtnWrapper } from "../../styles/GlobalStyle";
33

44
interface SearchBoxProps {
55
titles: string[];
6+
inputValues: string[];
7+
onInputChange: (index: number, value: string) => void;
8+
onSearchClick: () => void;
69
}
710

8-
export default function SearchBox({ titles }: SearchBoxProps) {
11+
export default function SearchBox(props: SearchBoxProps) {
12+
const { titles, inputValues, onInputChange, onSearchClick } = props;
13+
914
return (
1015
<Container>
1116
<SearchContainer>
1217
{titles.map((title, index) => (
1318
<SearchWrapper key={index}>
1419
<Text>{title}</Text>
15-
<SearchInput type="text" />
20+
<SearchInput
21+
type="text"
22+
value={inputValues[index] || ""}
23+
onChange={(e) => onInputChange(index, e.target.value)}
24+
/>
1625
</SearchWrapper>
1726
))}
1827
</SearchContainer>
19-
<BtnWrapper type="button">
28+
<BtnWrapper type="button" onClick={onSearchClick}>
2029
<BtnText>검색</BtnText>
2130
</BtnWrapper>
2231
</Container>

src/components/common/Table.tsx

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import styled from "styled-components";
2+
import { WineRow } from "../../types/CommonTypes";
3+
import { renderCell } from "../../utils/renderCell";
24

3-
interface TableProps<T> {
5+
interface TableProps {
46
columns: string[];
5-
data: T[];
7+
data: WineRow[];
68
onRowClick?: (id: string) => void;
79
}
810

9-
export default function Table<T extends { id: string }>({
10-
columns,
11-
data,
12-
onRowClick,
13-
}: TableProps<T>) {
11+
export default function Table({ columns, data, onRowClick }: TableProps) {
1412
return (
1513
<Container>
1614
<TableRow>
@@ -20,11 +18,11 @@ export default function Table<T extends { id: string }>({
2018
</TableRow>
2119
<TitleLine />
2220
<TableBody>
23-
{data.map((row, rowIndex) => (
24-
<DataWrapper key={rowIndex}>
25-
<TableRow onClick={() => onRowClick && onRowClick(row.id)}>
26-
{Object.values(row).map((cell, cellIndex) => (
27-
<TableCell key={cellIndex}>{cell}</TableCell>
21+
{data.map((row) => (
22+
<DataWrapper key={row.id}>
23+
<TableRow onClick={() => onRowClick?.(row.id)}>
24+
{columns.map((column, index) => (
25+
<TableCell key={index}>{renderCell(row, column)}</TableCell>
2826
))}
2927
</TableRow>
3028
<Line />

src/constants/constants.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ export const wineColumns = [
55
"지역",
66
"생산지(국가)",
77
"등록일",
8-
"-",
9-
"-",
108
];
9+
1110
export const userColumns = [
1211
"회원 번호",
1312
"회원명",

src/hooks/useGetWineSearch.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useQuery } from "react-query";
2+
import { getWineSearch, WineSearchParams } from "../api/getWineSearch";
3+
4+
interface UseGetWineSearchProps extends WineSearchParams {
5+
trigger: number; // trigger 추가
6+
}
7+
8+
export function useGetWineSearch({
9+
searchName,
10+
wineSort,
11+
wineVariety,
12+
wineCountry,
13+
page,
14+
size,
15+
sort,
16+
trigger,
17+
}: UseGetWineSearchProps) {
18+
const { data } = useQuery(
19+
["getMonthHoney", trigger],
20+
() =>
21+
getWineSearch({
22+
searchName,
23+
wineSort,
24+
wineVariety,
25+
wineCountry,
26+
page,
27+
size,
28+
sort,
29+
}),
30+
{
31+
onError: (error) => {
32+
console.log("에러 발생", error);
33+
},
34+
}
35+
);
36+
37+
return { data };
38+
}

src/pages/login/LoginPage.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ const Input = styled.input`
113113
const StyledBtnWrapper = styled(BtnWrapper)`
114114
width: 11.4rem;
115115
height: 11.8rem;
116+
margin-bottom: 1.8rem;
116117
117118
border: 1px solid #fff;
118119
background-color: transparent;

src/pages/wine/WinePage.tsx

+64-26
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,59 @@
1+
import { useState } from "react";
12
import styled from "styled-components";
23
import DetailHeader from "../../components/common/Header/DetailHeader";
34
import SideBar from "../../components/common/SideBar";
45
import SearchBox from "../../components/common/SearchBox";
56
import Table from "../../components/common/Table";
6-
import { TitledWineDataTypes } from "../../types/CommonTypes";
77
import { useNavigate } from "react-router-dom";
88
import { wineColumns } from "../../constants/constants";
99
import AdminHeader from "../../components/common/Header/AdminHeader";
10+
import { useGetWineSearch } from "../../hooks/useGetWineSearch";
11+
import { formatDate } from "../../utils/formatDate";
12+
import Pagination from "../../components/common/Pagination";
1013

1114
export default function WinePage() {
1215
const navigate = useNavigate();
1316

14-
const titleData: TitledWineDataTypes[] = [
15-
//추후 api로 개별 와인 조회 예정이라 정적 데이터
16-
{
17-
id: "102391",
18-
name: "루이 로드레 크리스탈 2014",
19-
sort: "스파클링",
20-
region: "상파뉴",
21-
country: "프랑스",
22-
date: "2024-09-03",
23-
action1: "-",
24-
action2: "-",
25-
},
26-
{
27-
id: "102391",
28-
name: "루이 로드레 크리스탈 2014",
29-
sort: "스파클링",
30-
region: "상파뉴",
31-
country: "프랑스",
32-
date: "2024-09-03",
33-
action1: "-",
34-
action2: "-",
35-
},
36-
];
17+
const fieldNames = ["searchName", "wineSort", "wineVariety", "wineCountry"];
18+
const [searchTrigger, setSearchTrigger] = useState(0);
19+
const [page, setPage] = useState(0);
20+
21+
// state를 필드 이름 기반으로 관리
22+
const [searchParams, setSearchParams] = useState({
23+
searchName: "",
24+
wineSort: "",
25+
wineVariety: "",
26+
wineCountry: "",
27+
});
28+
29+
const { data: WineData } = useGetWineSearch({
30+
...searchParams,
31+
page: page,
32+
size: 7,
33+
sort: "name,ASC",
34+
trigger: searchTrigger,
35+
});
36+
37+
const handleInputChange = (index: number, value: string) => {
38+
const field = fieldNames[index];
39+
setSearchParams((prev) => ({
40+
...prev,
41+
[field]: value,
42+
}));
43+
};
44+
45+
const handleSearchClick = () => {
46+
setSearchTrigger((prev) => prev + 1);
47+
};
3748

3849
const handleRowClick = (id: string) => {
3950
navigate(`/wine/${id}`);
4051
};
4152

53+
if (!WineData) {
54+
return <></>;
55+
}
56+
4257
return (
4358
<Container>
4459
<AdminHeader />
@@ -52,12 +67,35 @@ export default function WinePage() {
5267
]}
5368
/>
5469
<InnerContainer>
55-
<SearchBox titles={["와인명 :", "종류 :", "품종 :", "생산지 :"]} />
70+
<SearchBox
71+
titles={["와인명 :", "종류 :", "품종 :", "생산지 :"]}
72+
inputValues={fieldNames.map(
73+
(name) => searchParams[name as keyof typeof searchParams]
74+
)}
75+
onInputChange={handleInputChange}
76+
onSearchClick={handleSearchClick}
77+
/>
5678
<Table
5779
columns={wineColumns}
58-
data={titleData}
80+
data={(WineData?.result.content ?? []).map((wine) => ({
81+
id: wine.wineId.toString(), // 클릭용
82+
wineId: wine.wineId.toString(), // 출력용
83+
name: wine.name,
84+
sort: wine.sort,
85+
variety: wine.variety,
86+
country: wine.country,
87+
createdAt: formatDate(wine.createdAt),
88+
}))}
5989
onRowClick={handleRowClick}
6090
/>
91+
<Pagination
92+
currentPage={page + 1} // 서버는 0부터니까 사용자에게는 1부터 보여주기
93+
totalPages={WineData?.result.totalPages || 1}
94+
onPageChange={(newPage) => {
95+
setPage(newPage - 1); // 사용자는 1페이지부터 누르지만 서버는 0부터니까 -1
96+
setSearchTrigger((prev) => prev + 1); // API 다시 요청
97+
}}
98+
/>
6199
</InnerContainer>
62100
</ContentContainer>
63101
</Container>

src/styles/theme.ts

+9
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ const fonts = {
7575
line-height: 140%; /* 21px */
7676
letter-spacing: -0.0375rem;
7777
`,
78+
79+
// 페이지 숫자
80+
Body_4: css`
81+
font-size: 1.2rem;
82+
font-style: normal;
83+
font-weight: 700;
84+
line-height: 140%; /* 1.05rem */
85+
letter-spacing: -0.01875rem;
86+
`,
7887
};
7988

8089
const theme = {

0 commit comments

Comments
 (0)