diff --git a/client/package-lock.json b/client/package-lock.json index 4016b32..e34a485 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,6 +14,7 @@ "@mui/material": "^5.14.15", "@tanstack/react-query": "^5.8.1", "@tanstack/react-query-devtools": "^5.8.1", + "@tanstack/react-virtual": "^3.0.1", "axios": "^1.6.0", "dayjs": "^1.11.10", "framer-motion": "^10.16.4", @@ -24,6 +25,7 @@ "react-dom": "^18", "react-intersection-observer": "^9.5.3", "react-quill": "^2.0.0", + "react-virtualized-auto-sizer": "^1.0.20", "sharp": "^0.32.6", "zustand": "^4.4.6" }, @@ -44,6 +46,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-window-infinite-loader": "^1.0.9", "autoprefixer": "^10", "eslint": "^8", "eslint-config-next": "14.0.0", @@ -7221,6 +7224,31 @@ "react-dom": "^18.0.0" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.1.tgz", + "integrity": "sha512-IFOFuRUTaiM/yibty9qQ9BfycQnYXIDHGP2+cU+0LrFFGNhVxCXSQnaY6wkX8uJVteFEBjUondX0Hmpp7TNcag==", + "dependencies": { + "@tanstack/virtual-core": "3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz", + "integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.3", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", @@ -7755,6 +7783,25 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-window-infinite-loader": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/react-window-infinite-loader/-/react-window-infinite-loader-1.0.9.tgz", + "integrity": "sha512-gEInTjQwURCnDOFyIEK2+fWB5gTjqwx30O62QfxA9stE5aiB6EWkGj4UMhc0axq7/FV++Gs/TGW8FtgEx0S6Tw==", + "dev": true, + "dependencies": { + "@types/react": "*", + "@types/react-window": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -19249,6 +19296,15 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz", + "integrity": "sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/client/package.json b/client/package.json index 1a2aab9..40d9b75 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "@mui/material": "^5.14.15", "@tanstack/react-query": "^5.8.1", "@tanstack/react-query-devtools": "^5.8.1", + "@tanstack/react-virtual": "^3.0.1", "axios": "^1.6.0", "dayjs": "^1.11.10", "framer-motion": "^10.16.4", @@ -29,6 +30,7 @@ "react-dom": "^18", "react-intersection-observer": "^9.5.3", "react-quill": "^2.0.0", + "react-virtualized-auto-sizer": "^1.0.20", "sharp": "^0.32.6", "zustand": "^4.4.6" }, @@ -49,6 +51,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-window-infinite-loader": "^1.0.9", "autoprefixer": "^10", "eslint": "^8", "eslint-config-next": "14.0.0", diff --git a/client/public/mockServiceWorker.js b/client/public/mockServiceWorker.js new file mode 100644 index 0000000..78933b6 --- /dev/null +++ b/client/public/mockServiceWorker.js @@ -0,0 +1,292 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (2.0.4). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '0877fcdc026242810f5bfde0d7178db4' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + // When performing original requests, response body will + // always be a ReadableStream, even for 204 responses. + // But when creating a new Response instance on the client, + // the body for a 204 response must be null. + const responseBody = response.status === 204 ? null : responseClone.body + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseBody, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseBody], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + const mswIntention = request.headers.get('x-msw-intention') + if (['bypass', 'passthrough'].includes(mswIntention)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/client/src/app/wiki/(wiki-main)/page.tsx b/client/src/app/wiki/(wiki-main)/page.tsx index c567bb9..b7b24dc 100644 --- a/client/src/app/wiki/(wiki-main)/page.tsx +++ b/client/src/app/wiki/(wiki-main)/page.tsx @@ -1,16 +1,28 @@ +"use client"; +import { useState } from "react"; import AlcoholPagination from "@/components/wiki/AlcoholPagination"; import WikiAlcoholSelector from "@/components/wiki/WikiAlcoholSelector"; - import SectionHeading from "@/components/SectionHeading"; - +import { Stack } from "@mui/material"; const WikiPage = () => { + const [currentAlcoholNo, setCurrentAlcoholNo] = useState( + undefined + ); + return ( - <> - - - - + + + { + setCurrentAlcoholNo(alcoholNo); + }} + /> + + ); }; diff --git a/client/src/app/wiki/[alcoholNo]/page.tsx b/client/src/app/wiki/[alcoholNo]/page.tsx index 58ad3f3..abf2f75 100644 --- a/client/src/app/wiki/[alcoholNo]/page.tsx +++ b/client/src/app/wiki/[alcoholNo]/page.tsx @@ -30,9 +30,9 @@ export async function generateMetadata({ params }: Props): Promise { nameOfApp, alcoholType, `${alcoholType}추천`, - taste.Aroma.map((aroma) => `${aroma}향 ${alcoholType}`).join(", "), - taste.Finish.map((finish) => `${finish}피니시 ${alcoholType}`).join(", "), - taste.Taste.map((taste) => `${taste}맛 ${alcoholType}`).join(", "), + taste.Aroma?.map((aroma) => `${aroma}향 ${alcoholType}`).join(", "), + taste.Finish?.map((finish) => `${finish}피니시 ${alcoholType}`).join(", "), + taste.Taste?.map((taste) => `${taste}맛 ${alcoholType}`).join(", "), ], openGraph: { title: `${nameOfApp} | ${alcoholType} ${alcoholName}`, diff --git a/client/src/assets/icons/Alcohol/BeerIcon.svg b/client/src/assets/icons/Alcohol/BeerIcon.svg new file mode 100644 index 0000000..1bfe9a5 --- /dev/null +++ b/client/src/assets/icons/Alcohol/BeerIcon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/client/src/assets/icons/Alcohol/BrandyIcon.svg b/client/src/assets/icons/Alcohol/BrandyIcon.svg new file mode 100644 index 0000000..bdb2685 --- /dev/null +++ b/client/src/assets/icons/Alcohol/BrandyIcon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/client/src/assets/icons/Alcohol/LiquorIcon.svg b/client/src/assets/icons/Alcohol/LiquorIcon.svg new file mode 100644 index 0000000..6cb8f28 --- /dev/null +++ b/client/src/assets/icons/Alcohol/LiquorIcon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/client/src/assets/icons/Alcohol/RumIcon.svg b/client/src/assets/icons/Alcohol/RumIcon.svg new file mode 100644 index 0000000..1e41542 --- /dev/null +++ b/client/src/assets/icons/Alcohol/RumIcon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/client/src/assets/icons/Alcohol/SpiritsIcon.svg b/client/src/assets/icons/Alcohol/SpiritsIcon.svg deleted file mode 100644 index 7855116..0000000 --- a/client/src/assets/icons/Alcohol/SpiritsIcon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/client/src/components/layout/CustomContainer.tsx b/client/src/components/layout/CustomContainer.tsx index 8c18ee3..fc0a888 100644 --- a/client/src/components/layout/CustomContainer.tsx +++ b/client/src/components/layout/CustomContainer.tsx @@ -19,6 +19,7 @@ const CustomContainer = ({ sx, mt, children }: CustomContainerInterface) => { gap: 2, p: 2, minHeight: `calc(100vh - ${appbarHeight} - ${navbarHeight})`, + height:'100%' }} > {children} diff --git a/client/src/components/post/PostCardSkeleton.tsx b/client/src/components/post/PostCardSkeleton.tsx index f6f031a..79420e2 100644 --- a/client/src/components/post/PostCardSkeleton.tsx +++ b/client/src/components/post/PostCardSkeleton.tsx @@ -1,23 +1,28 @@ -import { Box, Card, Skeleton, Stack } from "@mui/material"; +import { Box, Card, CardProps, Skeleton, Stack } from "@mui/material"; +import { Ref, forwardRef } from "react"; -const PostCardSkeleton = () => { +const PostCardSkeleton = ( + cardProps?: Omit, + ref?: Ref +) => { return ( - + - + - - + + @@ -32,4 +37,4 @@ const PostCardSkeleton = () => { ); }; -export default PostCardSkeleton; +export default forwardRef(PostCardSkeleton); diff --git a/client/src/components/post/VirtualizedPostCardList.tsx b/client/src/components/post/VirtualizedPostCardList.tsx new file mode 100644 index 0000000..4667a96 --- /dev/null +++ b/client/src/components/post/VirtualizedPostCardList.tsx @@ -0,0 +1,127 @@ +"use client"; + +import PostCard from "@/components/post/PostCard"; +import useGetPostListInfiniteQuery, { + UseGetPostListQueryInterface, +} from "@/queries/post/useGetPostListInfiniteQuery"; + +import { memo, useEffect, useRef } from "react"; +import { useMemo } from "react"; +import getTokenFromLocalStorage from "@/utils/getTokenFromLocalStorage"; +import { postcardContext } from "@/store/post/PostCardContext"; +import PostCardSkeleton from "./PostCardSkeleton"; + +import { useVirtualizer } from "@tanstack/react-virtual"; + +function VirtualizedPostCardList({ + height, + width, + searchAlcoholNos, + searchKeyword, + searchUserNos, + sort, + ...props +}: UseGetPostListQueryInterface & { width: number; height: number }) { + const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isSuccess } = + useGetPostListInfiniteQuery({ + searchAlcoholNos: + searchKeyword === "" && searchAlcoholNos ? searchAlcoholNos : undefined, + sort, + searchUserNos, + searchKeyword: searchKeyword !== "" ? searchKeyword : undefined, + ...props, + headers: { Authorization: getTokenFromLocalStorage() }, + }); + + const allRows = data ? data.pages.flatMap(({ content }) => content) : []; + + const parentRef = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: allRows.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 550, + initialRect: { width, height }, + overscan: 5, + }); + + const hasResult = useMemo( + () => data && data.pages[0].content.length > 0, + [data] + ); + + const loadMoreItems = () => { + const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse(); + + if ( + lastItem && + lastItem.index >= allRows.length - 1 && + hasNextPage && + !isFetchingNextPage + ) { + fetchNextPage(); + } + }; + + useEffect(loadMoreItems, [ + hasNextPage, + fetchNextPage, + allRows.length, + isFetchingNextPage, + rowVirtualizer.getVirtualItems(), + ]); + + return ( + +
+ {hasResult && isSuccess && ( +
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const post = allRows[virtualRow.index]; + const isLoaderRow = virtualRow.index > allRows.length - 1; + + return ( +
+ {isLoaderRow ? ( + hasNextPage && + ) : ( + + )} +
+ ); + })} +
+
+ )} +
+
+ ); +} + +export default memo(VirtualizedPostCardList); diff --git a/client/src/components/wiki/AlcoholPagination.tsx b/client/src/components/wiki/AlcoholPagination.tsx index 1ae2eb8..53cabb6 100644 --- a/client/src/components/wiki/AlcoholPagination.tsx +++ b/client/src/components/wiki/AlcoholPagination.tsx @@ -9,25 +9,26 @@ import usePushToWikiDetail from "@/hooks/wiki/usePushToWikiDetail"; import { useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; -const AlcoholPagenation = () => { +const AlcoholPagenation = ({ alcoholTypeNo }: { alcoholTypeNo?: number }) => { const [currentPage, setCurrentPage] = useState(1); const size = 10; const { data: alcohols } = useGetAlcoholListQuery({ searchKeyword: "", page: currentPage - 1, + alcoholType: alcoholTypeNo, size, }); - const [totalCount, setTotalCount] = useState(alcohols?.totalCount); + // const [totalCount, setTotalCount] = useState(alcohols?.totalPages); - useEffect(() => { - const isSameWithPreviousValue = - totalCount === alcohols?.totalCount || alcohols?.totalCount === undefined; + // useEffect(() => { + // const isSameWithPreviousValue = + // totalCount === alcohols?.totalPages || alcohols?.totalPages === undefined; - if (isSameWithPreviousValue) return; - setTotalCount(alcohols?.totalCount); - }, [alcohols]); + // if (isSameWithPreviousValue) return; + // setTotalCount(alcohols?.totalPages); + // }, [alcohols]); const queryClient = useQueryClient(); @@ -35,6 +36,7 @@ const AlcoholPagenation = () => { const handler = async () => { const nextPageParams = { searchKeyword: "", + alcoholType: alcoholTypeNo, size, page: currentPage + 1, }; @@ -61,7 +63,7 @@ const AlcoholPagenation = () => { onClickElementHandler({ alcoholName, alcoholNo }) @@ -69,7 +71,7 @@ const AlcoholPagenation = () => { /> setCurrentPage(page)} diff --git a/client/src/components/wiki/WikiAlcoholSelector.tsx b/client/src/components/wiki/WikiAlcoholSelector.tsx index dfff919..ea1c67c 100644 --- a/client/src/components/wiki/WikiAlcoholSelector.tsx +++ b/client/src/components/wiki/WikiAlcoholSelector.tsx @@ -1,43 +1,77 @@ "use client"; -import { useState, useCallback, useMemo } from "react"; -import { Stack } from "@mui/material"; +import { useState, useCallback, useMemo, useEffect } from "react"; +import { Stack, StackProps } from "@mui/material"; import WikiAlcoholSelectorBtn from "./WikiAlcoholSelectorBtn"; import WineIcon from "@/assets/icons/Alcohol/WineIcon.svg"; +import BrandyIcon from "@/assets/icons/Alcohol/BrandyIcon.svg"; import AllAlcoholIcon from "@/assets/icons/Alcohol/AllAlcoholIcon.svg"; import WiskyIcon from "@/assets/icons/Alcohol/WiskyIcon.svg"; -import SpiritsIcon from "@/assets/icons/Alcohol/SpiritsIcon.svg"; +import LiquorIcon from "@/assets/icons/Alcohol/LiquorIcon.svg"; +import BeerIcon from "@/assets/icons/Alcohol/BeerIcon.svg"; +import RumIcon from "@/assets/icons/Alcohol/RumIcon.svg"; + import TraditionalAlcoholIcon from "@/assets/icons/Alcohol/TraditionalAlcoholIcon.svg"; import SakeIcon from "@/assets/icons/Alcohol/SakeIcon.svg"; -const WikiAlcoholSelector = () => { +interface WikiAlcoholSelectorInterface extends Omit { + onChange?: (selectedAlcoholNo: number | undefined) => void; +} + +const WikiAlcoholSelector = ({ onChange }: WikiAlcoholSelectorInterface) => { const btnList = useMemo( () => [ - { title: "전체", iconComponent: }, - { title: "포도주", iconComponent: }, - { title: "위스키", iconComponent: }, - { title: "증류주", iconComponent: }, - { title: "우리술", iconComponent: }, - { title: "사케", iconComponent: }, + { + title: "전체", + iconComponent: , + alcoholTypeNo: undefined, + }, + { title: "포도주", iconComponent: , alcoholTypeNo: 167 }, + { title: "브랜디", iconComponent: , alcoholTypeNo: 69 }, + { title: "위스키", iconComponent: , alcoholTypeNo: 111 }, + { title: "리큐르", iconComponent: , alcoholTypeNo: 33 }, + { title: "맥주", iconComponent: , alcoholTypeNo: 2 }, + + { + title: "우리술", + iconComponent: , + alcoholTypeNo: 109, + }, + { title: "사케", iconComponent: , alcoholTypeNo: 78 }, + { title: "럼", iconComponent: , alcoholTypeNo: 28 }, + { + title: "미분류", + iconComponent: , + alcoholTypeNo: 149, + }, ], [] ); - const [selectedAlcohol, setSelectedAlcohol] = useState(btnList[0].title); + const [selectedAlcohol, setSelectedAlcohol] = useState( + btnList[0].alcoholTypeNo + ); + + useEffect(() => { + onChange?.(selectedAlcohol); + }, [selectedAlcohol]); - const clickHandler = useCallback((title: string) => { - setSelectedAlcohol(title); + const clickHandler = useCallback((alcoholTypeNo: number | undefined) => { + setSelectedAlcohol(alcoholTypeNo); }, []); return ( - - {btnList.map((btnInfo) => ( - clickHandler(btnInfo.title)} - {...btnInfo} - /> - ))} + + + {btnList.map(({ alcoholTypeNo, title, iconComponent }) => ( + clickHandler(alcoholTypeNo)} + iconComponent={iconComponent} + /> + ))} + ); }; diff --git a/client/src/components/wiki/detail/AlcoholDetailPage.tsx b/client/src/components/wiki/detail/AlcoholDetailPage.tsx index f38e121..400d4c4 100644 --- a/client/src/components/wiki/detail/AlcoholDetailPage.tsx +++ b/client/src/components/wiki/detail/AlcoholDetailPage.tsx @@ -38,19 +38,19 @@ const AlcoholDetailPage = ({ alcoholNo, initialData, children }: Props) => { Aroma - {alcoholInfo.taste.Aroma.join(", ")} + {alcoholInfo.taste.Aroma?.join(", ")} Taste - {alcoholInfo.taste.Taste.join(", ")} + {alcoholInfo.taste.Taste?.join(", ")} Finish - {alcoholInfo.taste.Finish.join(", ")} + {alcoholInfo.taste.Finish?.join(", ")} diff --git a/client/src/const/serverPath.ts b/client/src/const/serverPath.ts index 0703103..56edf9e 100644 --- a/client/src/const/serverPath.ts +++ b/client/src/const/serverPath.ts @@ -91,13 +91,13 @@ export const REMOVE_FILE = (attachNo: string) => `/attach/${attachNo}` as const; /** * 알콜리스트를 받아오는 URL */ -export const GET_ALCOHOL_LIST = "/alcohols" as const; +export const GET_ALCOHOL_LIST = "/alcohols/v2" as const; /** * 알콜 디테일을 받아오는 URL */ export const GET_ALCOHOL_DETAIL = (id: string) => - `${GET_ALCOHOL_LIST}/${id}` as const; + `/alcohols/${id}` as const; /** * 포스트의 PK를 입력받아 해당 PK의 게시글의 좋아요 취소를 요청 diff --git a/client/src/queries/alcohol/useGetAlcoholListQuery.tsx b/client/src/queries/alcohol/useGetAlcoholListQuery.tsx index f6a4d25..b02dc06 100644 --- a/client/src/queries/alcohol/useGetAlcoholListQuery.tsx +++ b/client/src/queries/alcohol/useGetAlcoholListQuery.tsx @@ -1,5 +1,6 @@ import { GET_ALCOHOL_LIST } from "@/const/serverPath"; import axios from "@/libs/axios"; +import PageNated from "@/types/Pagenated"; import { AlcoholDetailInterface } from "@/types/alcohol/AlcoholInterface"; import { useQuery } from "@tanstack/react-query"; @@ -7,17 +8,24 @@ export interface useGetAlcoholListQueryInterface { page?: number; size?: number; searchKeyword?: string; + alcoholType?: number; } const useGetAlcoholListQuery = ({ searchKeyword = "", size = 5, page = 0, + alcoholType, }: useGetAlcoholListQueryInterface) => { return useQuery({ - queryKey: AlcohilListQueryKey.byKeyword({ page, size, searchKeyword }), + queryKey: AlcohilListQueryKey.byKeyword({ + page, + size, + searchKeyword, + alcoholType, + }), queryFn: async () => - await getAlcoholListByKeyword({ searchKeyword, size, page }), + await getAlcoholListByKeyword({ searchKeyword, size, page, alcoholType }), enabled: searchKeyword != undefined, }); }; @@ -26,24 +34,31 @@ export const getAlcoholListByKeyword = async ({ searchKeyword, size, page, + alcoholType, }: useGetAlcoholListQueryInterface) => { - const { data } = await axios.get<{ - list: AlcoholDetailInterface[]; - totalCount: number; - }>(GET_ALCOHOL_LIST, { - params: { - page, - size, - searchKeyword, - }, - }); + const { data } = await axios.get>( + GET_ALCOHOL_LIST, + { + params: { + page, + size, + searchKeyword, + alcoholType, + }, + } + ); return data; }; export const AlcohilListQueryKey = { all: ["alcohol"] as const, - byKeyword: ({ searchKeyword, size, page }: useGetAlcoholListQueryInterface) => - ["alcohol", { searchKeyword, size, page }] as const, + byKeyword: ({ + searchKeyword, + size, + page, + alcoholType, + }: useGetAlcoholListQueryInterface) => + ["alcohol", { searchKeyword, size, page, alcoholType }] as const, }; export default useGetAlcoholListQuery;