Skip to content

[feature] 와인 정보 검색 API 구현 #19

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

Merged
merged 7 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
60 changes: 60 additions & 0 deletions src/api/getWineSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { customAxios } from "./customAxios";
import { Response } from "../types/Response";

export interface PageResult<T> {
content: T[];
pageNumber: number;
totalPages: number;
}

export interface WineDtoDataTypes {
wineId: number;
name: string;
sort: string;
variety: string;
country: string;
region: string;
createdAt: string;
}

export interface WineSearchParams {
searchName?: string | null;
wineSort?: string | null;
wineVariety?: string | null;
wineCountry?: string | null;
page?: number; // page, size, sort는 기본값 채워서 nullable 안 해도 됨..
size?: number;
sort?: string;
}

export async function getWineSearch(params: WineSearchParams) {
const {
searchName = null,
wineSort = null,
wineVariety = null,
wineCountry = null,
page = 0, // 기본 0페이지
size = 7, // 기본 7개 가져오기
sort = "name,ASC", // 기본 name 오름차순pre
} = params;
// 입력된 것만 params 객체로 만들기
const queryParams = Object.fromEntries(
Object.entries({
searchName,
wineSort,
wineVariety,
wineCountry,
page,
size,
sort,
}).filter(([, v]) => v !== null && v !== undefined)
);

const { data } = await customAxios.get<
Response<PageResult<WineDtoDataTypes>>
>(`/admin/wine`, {
params: queryParams,
});

return data;
}
60 changes: 60 additions & 0 deletions src/components/common/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import styled from "styled-components";
import { BtnWrapper } from "../../styles/GlobalStyle";

interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}

export default function Pagination({
currentPage,
totalPages,
onPageChange,
}: PaginationProps) {
const pages = [];
const maxPage = Math.min(totalPages, 5);

for (let i = 1; i <= maxPage; i++) {
pages.push(i);
}

return (
<Container>
{pages.map((page) => (
<BtnWrapper key={page} onClick={() => onPageChange(page)}>
<Number $isActive={page === currentPage}>{page}</Number>
</BtnWrapper>
))}
{currentPage < totalPages && (
<BtnWrapper onClick={() => onPageChange(currentPage + 1)}>
<Number>{">"}</Number>
</BtnWrapper>
)}
</Container>
);
}

const Container = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0%;
padding-bottom: 5.25rem;
width: 8.9375rem;
`;

const Number = styled.p<{ $isActive?: boolean }>`
${({ theme }) => theme.fonts.Body_4};
color: ${({ theme, $isActive }) =>
$isActive ? theme.colors.purple_100 : theme.colors.black};

${({ $isActive }) =>
$isActive &&
`
text-decoration-line: underline;
`}
`;
15 changes: 12 additions & 3 deletions src/components/common/SearchBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,29 @@ import { BtnWrapper } from "../../styles/GlobalStyle";

interface SearchBoxProps {
titles: string[];
inputValues: string[];
onInputChange: (index: number, value: string) => void;
onSearchClick: () => void;
}

export default function SearchBox({ titles }: SearchBoxProps) {
export default function SearchBox(props: SearchBoxProps) {
const { titles, inputValues, onInputChange, onSearchClick } = props;

return (
<Container>
<SearchContainer>
{titles.map((title, index) => (
<SearchWrapper key={index}>
<Text>{title}</Text>
<SearchInput type="text" />
<SearchInput
type="text"
value={inputValues[index] || ""}
onChange={(e) => onInputChange(index, e.target.value)}
/>
</SearchWrapper>
))}
</SearchContainer>
<BtnWrapper type="button">
<BtnWrapper type="button" onClick={onSearchClick}>
<BtnText>검색</BtnText>
</BtnWrapper>
</Container>
Expand Down
22 changes: 10 additions & 12 deletions src/components/common/Table.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import styled from "styled-components";
import { WineRow } from "../../types/CommonTypes";
import { renderCell } from "../../utils/renderCell";

interface TableProps<T> {
interface TableProps {
columns: string[];
data: T[];
data: WineRow[];
onRowClick?: (id: string) => void;
}

export default function Table<T extends { id: string }>({
columns,
data,
onRowClick,
}: TableProps<T>) {
export default function Table({ columns, data, onRowClick }: TableProps) {
return (
<Container>
<TableRow>
Expand All @@ -20,11 +18,11 @@ export default function Table<T extends { id: string }>({
</TableRow>
<TitleLine />
<TableBody>
{data.map((row, rowIndex) => (
<DataWrapper key={rowIndex}>
<TableRow onClick={() => onRowClick && onRowClick(row.id)}>
{Object.values(row).map((cell, cellIndex) => (
<TableCell key={cellIndex}>{cell}</TableCell>
{data.map((row) => (
<DataWrapper key={row.id}>
<TableRow onClick={() => onRowClick?.(row.id)}>
{columns.map((column, index) => (
<TableCell key={index}>{renderCell(row, column)}</TableCell>
))}
</TableRow>
<Line />
Expand Down
3 changes: 1 addition & 2 deletions src/constants/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ export const wineColumns = [
"지역",
"생산지(국가)",
"등록일",
"-",
"-",
];

export const userColumns = [
"회원 번호",
"회원명",
Expand Down
38 changes: 38 additions & 0 deletions src/hooks/useGetWineSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useQuery } from "react-query";
import { getWineSearch, WineSearchParams } from "../api/getWineSearch";

interface UseGetWineSearchProps extends WineSearchParams {
trigger: number; // trigger 추가
}

export function useGetWineSearch({
searchName,
wineSort,
wineVariety,
wineCountry,
page,
size,
sort,
trigger,
}: UseGetWineSearchProps) {
const { data } = useQuery(
["getMonthHoney", trigger],
() =>
getWineSearch({
searchName,
wineSort,
wineVariety,
wineCountry,
page,
size,
sort,
}),
{
onError: (error) => {
console.log("에러 발생", error);
},
}
);

return { data };
}
1 change: 1 addition & 0 deletions src/pages/login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const Input = styled.input`
const StyledBtnWrapper = styled(BtnWrapper)`
width: 11.4rem;
height: 11.8rem;
margin-bottom: 1.8rem;

border: 1px solid #fff;
background-color: transparent;
Expand Down
90 changes: 64 additions & 26 deletions src/pages/wine/WinePage.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,59 @@
import { useState } from "react";
import styled from "styled-components";
import DetailHeader from "../../components/common/Header/DetailHeader";
import SideBar from "../../components/common/SideBar";
import SearchBox from "../../components/common/SearchBox";
import Table from "../../components/common/Table";
import { TitledWineDataTypes } from "../../types/CommonTypes";
import { useNavigate } from "react-router-dom";
import { wineColumns } from "../../constants/constants";
import AdminHeader from "../../components/common/Header/AdminHeader";
import { useGetWineSearch } from "../../hooks/useGetWineSearch";
import { formatDate } from "../../utils/formatDate";
import Pagination from "../../components/common/Pagination";

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

const titleData: TitledWineDataTypes[] = [
//추후 api로 개별 와인 조회 예정이라 정적 데이터
{
id: "102391",
name: "루이 로드레 크리스탈 2014",
sort: "스파클링",
region: "상파뉴",
country: "프랑스",
date: "2024-09-03",
action1: "-",
action2: "-",
},
{
id: "102391",
name: "루이 로드레 크리스탈 2014",
sort: "스파클링",
region: "상파뉴",
country: "프랑스",
date: "2024-09-03",
action1: "-",
action2: "-",
},
];
const fieldNames = ["searchName", "wineSort", "wineVariety", "wineCountry"];
const [searchTrigger, setSearchTrigger] = useState(0);
const [page, setPage] = useState(0);

// state를 필드 이름 기반으로 관리
const [searchParams, setSearchParams] = useState({
searchName: "",
wineSort: "",
wineVariety: "",
wineCountry: "",
});

const { data: WineData } = useGetWineSearch({
...searchParams,
page: page,
size: 7,
sort: "name,ASC",
trigger: searchTrigger,
});

const handleInputChange = (index: number, value: string) => {
const field = fieldNames[index];
setSearchParams((prev) => ({
...prev,
[field]: value,
}));
};

const handleSearchClick = () => {
setSearchTrigger((prev) => prev + 1);
};

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

if (!WineData) {
return <></>;
}

return (
<Container>
<AdminHeader />
Expand All @@ -52,12 +67,35 @@ export default function WinePage() {
]}
/>
<InnerContainer>
<SearchBox titles={["와인명 :", "종류 :", "품종 :", "생산지 :"]} />
<SearchBox
titles={["와인명 :", "종류 :", "품종 :", "생산지 :"]}
inputValues={fieldNames.map(
(name) => searchParams[name as keyof typeof searchParams]
)}
onInputChange={handleInputChange}
onSearchClick={handleSearchClick}
/>
<Table
columns={wineColumns}
data={titleData}
data={(WineData?.result.content ?? []).map((wine) => ({
id: wine.wineId.toString(), // 클릭용
wineId: wine.wineId.toString(), // 출력용
name: wine.name,
sort: wine.sort,
variety: wine.variety,
country: wine.country,
createdAt: formatDate(wine.createdAt),
}))}
onRowClick={handleRowClick}
/>
<Pagination
currentPage={page + 1} // 서버는 0부터니까 사용자에게는 1부터 보여주기
totalPages={WineData?.result.totalPages || 1}
onPageChange={(newPage) => {
setPage(newPage - 1); // 사용자는 1페이지부터 누르지만 서버는 0부터니까 -1
setSearchTrigger((prev) => prev + 1); // API 다시 요청
}}
/>
</InnerContainer>
</ContentContainer>
</Container>
Expand Down
9 changes: 9 additions & 0 deletions src/styles/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ const fonts = {
line-height: 140%; /* 21px */
letter-spacing: -0.0375rem;
`,

// 페이지 숫자
Body_4: css`
font-size: 1.2rem;
font-style: normal;
font-weight: 700;
line-height: 140%; /* 1.05rem */
letter-spacing: -0.01875rem;
`,
};

const theme = {
Expand Down
Loading