diff --git a/src/apis/banner.ts b/src/apis/banner.ts new file mode 100644 index 0000000..fadc8c0 --- /dev/null +++ b/src/apis/banner.ts @@ -0,0 +1,16 @@ +import { API_SERVICE_URL } from '../constants'; +import { fetchApi } from './fetchApi'; + +export interface Banner { + id: number; + image: string; + link: string; +} + +export const getBanners = async () => { + const response = await fetchApi(`${API_SERVICE_URL}/banners`, { + method: 'GET', + }); + const data: Banner[] = await response.json(); + return data; +}; diff --git a/src/constants/index.ts b/src/constants/index.ts index 776f2a8..7ecd198 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,14 +1,22 @@ -export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; +export const API_BASE_URL = import.meta.env.PROD + ? import.meta.env.VITE_API_BASE_URL + : '/api/admin'; + +export const API_SERVICE_URL = import.meta.env.PROD + ? import.meta.env.VITE_API_SERVICE_URL + : '/api'; export const ROUTE = { HOME: '/', PRODUCT: '/products', REVIEW: '/reviews', + BANNER: '/banners', }; export const ROUTES = [ { path: ROUTE.PRODUCT, name: '상품' }, { path: ROUTE.REVIEW, name: '리뷰' }, + { path: ROUTE.BANNER, name: '배너' }, ]; export interface Column { @@ -43,3 +51,12 @@ export const PRODUCT_SEARCH_COLUMNS: Column[] = [ { id: 1, name: '아이디', align: 'right' }, { id: 2, name: '상품명' }, ]; + +export const BANNER_COLUMNS_WIDTH = [10, 40, 40, 10]; + +export const BANNER_COLUMNS: Column[] = [ + { id: 1, name: '아이디', align: 'right' }, + { id: 2, name: '이미지', align: 'center' }, + { id: 3, name: '링크' }, + { id: 4, name: '', align: 'center' }, +]; diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts index d708098..e14b723 100644 --- a/src/hooks/queries/index.ts +++ b/src/hooks/queries/index.ts @@ -2,6 +2,7 @@ export * from './useProductQuery'; export * from './useCategoryQuery'; export * from './useReviewQuery'; export * from './useLoginQuery'; +export * from './useBannerQuery'; export * from './useLoginMutation'; export * from './useProductMutation'; diff --git a/src/hooks/queries/useBannerQuery.ts b/src/hooks/queries/useBannerQuery.ts new file mode 100644 index 0000000..720621d --- /dev/null +++ b/src/hooks/queries/useBannerQuery.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getBanners } from '../../apis/banner'; + +export const useBannerQuery = () => { + return useQuery({ + queryKey: ['banners'], + queryFn: () => getBanners(), + }); +}; diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts index 2a3d53f..ca6e970 100644 --- a/src/mocks/browser.ts +++ b/src/mocks/browser.ts @@ -5,11 +5,13 @@ import { productHandlers, reviewHandlers, loginHandlers, + bannerHandlers, } from './handlers'; export const worker = setupWorker( ...categoryHandlers, ...productHandlers, ...reviewHandlers, - ...loginHandlers + ...loginHandlers, + ...bannerHandlers ); diff --git a/src/mocks/data/banners.json b/src/mocks/data/banners.json new file mode 100644 index 0000000..346810d --- /dev/null +++ b/src/mocks/data/banners.json @@ -0,0 +1,17 @@ +[ + { + "id": 3, + "image": "https://image.funeat.site/prod/banner.png", + "link": "https://funeat.site" + }, + { + "id": 2, + "image": "https://image.funeat.site/prod/banner.png", + "link": "https://funeat.site/products/food" + }, + { + "id": 1, + "image": "https://image.funeat.site/prod/banner.png", + "link": "https://funeat.site/recipes" + } +] diff --git a/src/mocks/handlers/bannerHandlers.ts b/src/mocks/handlers/bannerHandlers.ts new file mode 100644 index 0000000..c742bd6 --- /dev/null +++ b/src/mocks/handlers/bannerHandlers.ts @@ -0,0 +1,9 @@ +import { rest } from 'msw'; + +import banners from '../data/banners.json'; + +export const bannerHandlers = [ + rest.get('/api/banners', (_, res, ctx) => { + return res(ctx.status(200), ctx.json(banners)); + }), +]; diff --git a/src/mocks/handlers/index.ts b/src/mocks/handlers/index.ts index a8ab793..6f05565 100644 --- a/src/mocks/handlers/index.ts +++ b/src/mocks/handlers/index.ts @@ -2,3 +2,4 @@ export * from './categoryHandlers'; export * from './productHandlers'; export * from './reviewHandlers'; export * from './loginHandlers'; +export * from './bannerHandlers'; diff --git a/src/pages/Banners/Banners.tsx b/src/pages/Banners/Banners.tsx new file mode 100644 index 0000000..d0d08e1 --- /dev/null +++ b/src/pages/Banners/Banners.tsx @@ -0,0 +1,57 @@ +import BannerRow from './components/BannerRow'; + +import { BANNER_COLUMNS, BANNER_COLUMNS_WIDTH } from '../../constants'; +import { + Colgroup, + Table, + TableBody, + TableHeader, +} from '../../components/Table'; +import { useDisclosure } from '../../hooks'; +import { useBannerQuery } from '../../hooks/queries'; + +import { + addButton, + tableTitle, + tableWrapper, + title, + titleWrapper, +} from './banners.css'; + +const Banners = () => { + const { data: banners } = useBannerQuery(); + const { isOpen, onOpen, onClose } = useDisclosure(); + + // TODO 배너 추가 모달 + + if (!banners) { + return null; + } + + return ( + <> +
+

배너

+ +
+
+

+ 총 {banners.length.toLocaleString('ko-KR')}개의 상품이 검색되었습니다. +

+ + + + + {banners.map((banner) => ( + + ))} + +
+
+ + ); +}; + +export default Banners; diff --git a/src/pages/Banners/banners.css.ts b/src/pages/Banners/banners.css.ts new file mode 100644 index 0000000..81f6f72 --- /dev/null +++ b/src/pages/Banners/banners.css.ts @@ -0,0 +1,57 @@ +import { style } from '@vanilla-extract/css'; + +export const container = style({ + maxWidth: 1200, + margin: '0 auto', +}); + +export const titleWrapper = style([ + container, + { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + height: 60, + padding: '0 20px', + }, +]); + +export const title = style({ + height: 60, + lineHeight: '60px', + fontSize: 28, + fontWeight: 700, +}); + +export const searchSection = style([ + container, + { + padding: '0 20px', + marginTop: 20, + }, +]); + +export const addButton = style({ + width: 120, + height: 45, + lineHeight: '45px', + fontSize: 16, + border: '1px solid #ccc', + borderRadius: 8, +}); + +export const tableWrapper = style([ + container, + { + width: '100%', + padding: '0 20px', + marginTop: 20, + }, +]); + +export const tableTitle = style({ + height: 60, + lineHeight: '60px', + fontSize: 18, + fontWeight: 500, +}); diff --git a/src/pages/Banners/components/BannerRow/BannerRow.tsx b/src/pages/Banners/components/BannerRow/BannerRow.tsx new file mode 100644 index 0000000..2a79eaf --- /dev/null +++ b/src/pages/Banners/components/BannerRow/BannerRow.tsx @@ -0,0 +1,35 @@ +import { Banner } from '../../../../apis/banner'; +import { td } from '../../../../components/Table/table.css'; + +import { bannerImage, bannerLink, deleteButton } from './bannerRow.css'; + +interface BannerRowProps { + banner: Banner; +} + +const BannerRow = ({ banner }: BannerRowProps) => { + const { id, image, link } = banner; + + // TODO 배너 삭제 클릭 이벤트 + + return ( + + {id} + + {`배너 + + + + {link} + + + + + + + ); +}; + +export default BannerRow; diff --git a/src/pages/Banners/components/BannerRow/bannerRow.css.ts b/src/pages/Banners/components/BannerRow/bannerRow.css.ts new file mode 100644 index 0000000..fa4fabc --- /dev/null +++ b/src/pages/Banners/components/BannerRow/bannerRow.css.ts @@ -0,0 +1,19 @@ +import { style } from '@vanilla-extract/css'; + +export const bannerImage = style({ + width: '100%', + height: 'auto', +}); + +export const bannerLink = style({ + textDecoration: 'underline', +}); + +export const deleteButton = style({ + width: 60, + height: 45, + lineHeight: '45px', + fontSize: 16, + border: '1px solid #ccc', + borderRadius: 8, +}); diff --git a/src/pages/Banners/components/BannerRow/index.ts b/src/pages/Banners/components/BannerRow/index.ts new file mode 100644 index 0000000..042be33 --- /dev/null +++ b/src/pages/Banners/components/BannerRow/index.ts @@ -0,0 +1,3 @@ +import BannerRow from './BannerRow'; + +export default BannerRow; diff --git a/src/pages/Banners/index.ts b/src/pages/Banners/index.ts new file mode 100644 index 0000000..91c8892 --- /dev/null +++ b/src/pages/Banners/index.ts @@ -0,0 +1,3 @@ +import Banners from './Banners'; + +export default Banners; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 06761ec..2418390 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,15 +1,16 @@ import { createBrowserRouter } from 'react-router-dom'; import Layout from './Layout'; +import AuthLayout from './Layout/AuthLayout'; import Home from './Home'; import Products from './Products'; import Reviews from './Reviews'; +import Banners from './Banners'; import { ROUTE } from '../constants'; import PageProvider from '../contexts/PageContext'; import ProductSearchQueryProvider from './Products/contexts/ProductSearchQueryContext'; import ReviewSearchQueryProvider from './Reviews/contexts/ReviewSearchQueryContext'; -import AuthLayout from './Layout/AuthLayout'; const router = createBrowserRouter([ { @@ -46,6 +47,7 @@ const router = createBrowserRouter([ ), }, + { path: ROUTE.BANNER, element: }, ], }, ]); diff --git a/vite.config.ts b/vite.config.ts index f725ec7..3742650 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,19 +1,16 @@ -import { defineConfig, loadEnv } from 'vite'; +import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'; // https://vitejs.dev/config/ -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ''); - return { - plugins: [react(), vanillaExtractPlugin()], - server: { - proxy: { - '/api': { - target: env.VITE_API_URL, - changeOrigin: true, - }, +export default defineConfig({ + plugins: [react(), vanillaExtractPlugin()], + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, }, }, - }; + }, });