diff --git a/src/app/(header-nav)/hakgwan/page.tsx b/src/app/(header-nav)/hakgwan/page.tsx index 84ecae1..b8d2e8b 100644 --- a/src/app/(header-nav)/hakgwan/page.tsx +++ b/src/app/(header-nav)/hakgwan/page.tsx @@ -1,7 +1,8 @@ import AdBannerCarousel from "@/features/banner/components/AdBannerCarousel"; import styles from "./page.module.css"; -import { getHakgwanMenus, HakgwanMenuData } from "@/features/menu/services/hakgwanMenuService"; +import { getHakgwanMenus } from "@/features/menu/services/hakgwanMenuService.server"; import MenuPageContainer from "@/features/menu/components/MenuPageContainer"; +import { HakgwanMenuData } from "@/features/menu/types/hakgwanType"; export default async function HakgwanMainPage() { diff --git a/src/app/(header-nav)/my/page.tsx b/src/app/(header-nav)/my/page.tsx index 8de7145..0b862f9 100644 --- a/src/app/(header-nav)/my/page.tsx +++ b/src/app/(header-nav)/my/page.tsx @@ -1,7 +1,7 @@ "use client" // TODO: 추후 로그아웃 정상 구현 후 SSR 변경 import { useRouter } from "next/navigation"; -import { logout } from "@/features/auth/api/logoutApi"; +import { logout } from "@/features/auth/api/logoutApi.client"; export default function MyMainPage() { const router = useRouter(); diff --git a/src/app/(header-only)/cart/page.tsx b/src/app/(header-only)/cart/page.tsx index 297ca8c..29ed71e 100644 --- a/src/app/(header-only)/cart/page.tsx +++ b/src/app/(header-only)/cart/page.tsx @@ -1,19 +1,11 @@ import EmptyCartView from "@/features/cart/components/view/EmptyCartView"; -import CartMenuCard from "@/features/cart/components/card/CartMenuCard"; +import CartView from "@/features/cart/components/view/CartView"; +import { getCartInfo } from "@/features/cart/services/cartService.server"; -export default function CartPage() { - return ( - // <> - // - // - // - - ); +export default async function CartPage() { + + const cartInfo = await getCartInfo(); + const empty = cartInfo.totalQuantity === 0; + + return empty ? : } \ No newline at end of file diff --git a/src/app/(header-only)/order/page.tsx b/src/app/(header-only)/order/page.tsx new file mode 100644 index 0000000..735424a --- /dev/null +++ b/src/app/(header-only)/order/page.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { useRouter } from "next/navigation"; + +export default function OrderPage() { + const router = useRouter(); + + const onClick = () => { + router.replace("/"); + } + return ( +
+ 주문 페이지 입니다 + +
+ ); +}; \ No newline at end of file diff --git a/src/features/auth/api/loginApi.ts b/src/features/auth/api/loginApi.client.ts similarity index 95% rename from src/features/auth/api/loginApi.ts rename to src/features/auth/api/loginApi.client.ts index 39257ea..6886bc0 100644 --- a/src/features/auth/api/loginApi.ts +++ b/src/features/auth/api/loginApi.client.ts @@ -1,3 +1,4 @@ +"use client"; import { apiClient } from "@/shared/lib/api/apiClient"; import { LoginRequest, LoginResponse } from "@/features/auth/types/loginTypes"; diff --git a/src/features/auth/api/logoutApi.ts b/src/features/auth/api/logoutApi.client.ts similarity index 91% rename from src/features/auth/api/logoutApi.ts rename to src/features/auth/api/logoutApi.client.ts index 254b866..b538e6b 100644 --- a/src/features/auth/api/logoutApi.ts +++ b/src/features/auth/api/logoutApi.client.ts @@ -1,3 +1,4 @@ +"use client"; import { apiClient } from "@/shared/lib/api/apiClient"; export async function logout(): Promise { diff --git a/src/features/auth/components/form/LoginForm.tsx b/src/features/auth/components/form/LoginForm.tsx index 6947138..a204c8e 100644 --- a/src/features/auth/components/form/LoginForm.tsx +++ b/src/features/auth/components/form/LoginForm.tsx @@ -11,7 +11,7 @@ import { useRouter } from "next/navigation"; import { CustomError } from "@/shared/lib/errors/customError"; import { ERROR_MESSAGE } from "@/shared/lib/errors/errorCodes"; import { LoginRequest } from "@/features/auth/types/loginTypes"; -import { login } from "@/features/auth/api/loginApi"; +import { login } from "@/features/auth/api/loginApi.client"; export default function LoginForm() { const router = useRouter(); diff --git a/src/features/cart/api/cartApi.ts b/src/features/cart/api/cartApi.client.ts similarity index 97% rename from src/features/cart/api/cartApi.ts rename to src/features/cart/api/cartApi.client.ts index e1693e4..717bc5b 100644 --- a/src/features/cart/api/cartApi.ts +++ b/src/features/cart/api/cartApi.client.ts @@ -1,3 +1,4 @@ +"use client"; import { CartApiResponse, CartRequest } from "@/features/cart/types/cartType"; import { apiClient } from "@/shared/lib/api/apiClient"; diff --git a/src/features/cart/api/cartApi.server.ts b/src/features/cart/api/cartApi.server.ts new file mode 100644 index 0000000..b73e327 --- /dev/null +++ b/src/features/cart/api/cartApi.server.ts @@ -0,0 +1,11 @@ +import "server-only"; +import { CartApiResponse } from "@/features/cart/types/cartType"; +import { apiServer } from "@/shared/lib/api/apiServer"; + +/** + * 장바구니 조회 (SSR) + * GET /api/cart + */ +export async function getCart(): Promise { + return await apiServer.get("/api/cart"); +} \ No newline at end of file diff --git a/src/features/cart/components/bar/PaymentBar.module.css b/src/features/cart/components/bar/PaymentBar.module.css new file mode 100644 index 0000000..34a71b9 --- /dev/null +++ b/src/features/cart/components/bar/PaymentBar.module.css @@ -0,0 +1,43 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + + border-radius: 1.25rem 1.5rem 0 0; + background-color: var(--color-white); + + box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.20); + + width: 100%; + height: var(--payment-bar-height); + + padding-inline: 1.5rem; + padding-top: 1rem; + padding-bottom: 1.25rem; +} + +.button { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + + border-radius: 1rem; + background-color: var(--color-main); + + padding-block: 1.25rem; +} + +.label { + color: var(--color-white); + text-align: center; + + font-size: var(--text-base); + font-weight: var(--font-bold); + line-height: var(--line-height-single); + letter-spacing: var(--letter-spacing); +} + +@theme { + --payment-bar-height: 5.75rem; +} \ No newline at end of file diff --git a/src/features/cart/components/bar/PaymentBar.tsx b/src/features/cart/components/bar/PaymentBar.tsx new file mode 100644 index 0000000..357915a --- /dev/null +++ b/src/features/cart/components/bar/PaymentBar.tsx @@ -0,0 +1,26 @@ +import styles from "./PaymentBar.module.css"; +import { formatNumberWithComma } from "@/shared/utils/number/utils"; + +interface PaymentBarProps { + totalPrice: number; + onClick: () => void; +} + +export default function PaymentBar({ + totalPrice, + onClick +}: PaymentBarProps) { + + const formattedPrice = formatNumberWithComma(totalPrice); + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/src/features/cart/components/card/CartMenuCard.module.css b/src/features/cart/components/card/CartMenuCard.module.css index 10e7196..592f010 100644 --- a/src/features/cart/components/card/CartMenuCard.module.css +++ b/src/features/cart/components/card/CartMenuCard.module.css @@ -3,8 +3,6 @@ position: relative; gap: 1rem; - border: 1px solid black; - width: 100%; background-color: var(--color-white); diff --git a/src/features/cart/components/list/CartMenuList.module.css b/src/features/cart/components/list/CartMenuList.module.css new file mode 100644 index 0000000..85e0dd0 --- /dev/null +++ b/src/features/cart/components/list/CartMenuList.module.css @@ -0,0 +1,20 @@ +.container { + display: flex; + flex-direction: column; + width: 100%; + + border-radius: 1rem; + border: 1px solid var(--color-gray-100); + background-color: var(--color-white); + + padding-block: 1.25rem; +} + +.cardWrapper { + padding-inline: 1.25rem; +} + +.separatorWrapper { + padding-inline: 1rem; + padding-block: 1rem; +} \ No newline at end of file diff --git a/src/features/cart/components/list/CartMenuList.tsx b/src/features/cart/components/list/CartMenuList.tsx new file mode 100644 index 0000000..1c025a9 --- /dev/null +++ b/src/features/cart/components/list/CartMenuList.tsx @@ -0,0 +1,36 @@ +import { CartInfo } from "@/features/cart/types/cartType"; +import CartMenuCard from "@/features/cart/components/card/CartMenuCard"; +import SeparationBar from "@/shared/components/bar/SeparationBar"; +import React from "react"; +import styles from "./CartMenuList.module.css"; + +interface CartMenuListProps { + cartInfo: CartInfo +} + +export default function CartMenuList({ + cartInfo, +}: CartMenuListProps) { + return ( +
+ {cartInfo.cartItems.map((item, index) => ( + +
+ +
+ {index < cartInfo.cartItems.length - 1 && ( +
+ +
+ )} +
+ ))} +
+ ); +} \ No newline at end of file diff --git a/src/features/cart/components/view/CartView.module.css b/src/features/cart/components/view/CartView.module.css new file mode 100644 index 0000000..ec95b7b --- /dev/null +++ b/src/features/cart/components/view/CartView.module.css @@ -0,0 +1,20 @@ +.container { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + + padding-inline: 1.25rem; + padding-top: 0.75rem; + padding-bottom: var(--payment-bar-height); +} + +.paymentBarWrapper { + position: fixed; + bottom: 0; + left: 50%; + transform: translateX(-50%); + + width: 100%; + max-width: var(--container-3xl); +} \ No newline at end of file diff --git a/src/features/cart/components/view/CartView.tsx b/src/features/cart/components/view/CartView.tsx new file mode 100644 index 0000000..1ea585e --- /dev/null +++ b/src/features/cart/components/view/CartView.tsx @@ -0,0 +1,40 @@ +"use client"; + +import CartMenuList from "@/features/cart/components/list/CartMenuList"; +import styles from "./CartView.module.css"; +import PaymentBar from "@/features/cart/components/bar/PaymentBar"; +import { useRouter } from "next/navigation"; +import { useCart } from "@/features/cart/hooks/useCart"; +import EmptyCartView from "@/features/cart/components/view/EmptyCartView"; + +export default function CartView() { + + const router = useRouter(); + const { cartInfo } = useCart(); + + const handleClick = () => { + router.replace("/order"); + } + + if (!cartInfo) { + return null; + } + + // 장바구니가 비어있으면 EmptyCartView 표시 + if (cartInfo.totalQuantity === 0) { + return ; + } + + return ( +
+ + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/features/cart/hooks/useCart.ts b/src/features/cart/hooks/useCart.ts index 6d31f7d..e12255f 100644 --- a/src/features/cart/hooks/useCart.ts +++ b/src/features/cart/hooks/useCart.ts @@ -2,8 +2,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CartInfo, CartRequest } from "@/features/cart/types/cartType"; -import { getCartInfo, upsertCartInfo } from "@/features/cart/services/cartService"; -import { useCallback, useRef } from "react"; +import { useCallback, useMemo, useRef } from "react"; +import { getCartInfo, upsertCartInfo } from "@/features/cart/services/cartService.client"; interface UseCartReturn { cartInfo: CartInfo | undefined, @@ -37,6 +37,18 @@ export function useCart(): UseCartReturn { retry: 1, }); + // menuId 기준으로 순서 정렬 + const sortedCartInfo = useMemo(() => { + if (!cartInfo) { + return undefined; + } + + return { + ...cartInfo, + cartItems: [...cartInfo.cartItems].sort((a, b) => a.menuId - b.menuId) + }; + }, [cartInfo]); + const upsertCartMutation = useMutation({ mutationFn: (request: CartRequest) => upsertCartInfo(request), onSuccess: (data: CartInfo) => { @@ -51,9 +63,9 @@ export function useCart(): UseCartReturn { * 특정 메뉴의 현재 수량 조회 */ const getMenuQuantity = useCallback((menuId: number): number => { - const cartItem = cartInfo?.cartItems.find((item) => item.menuId === menuId); + const cartItem = sortedCartInfo?.cartItems.find((item) => item.menuId === menuId); return cartItem?.quantity ?? 0; - }, [cartInfo]); + }, [sortedCartInfo]); /** * 메뉴 1개 추가 (기존 수량 + 1) @@ -151,7 +163,7 @@ export function useCart(): UseCartReturn { }, [getMenuQuantity, upsertCartMutation]); return { - cartInfo, + cartInfo: sortedCartInfo, isLoading, error, getMenuQuantity, diff --git a/src/features/cart/services/cartService.ts b/src/features/cart/services/cartService.client.ts similarity index 95% rename from src/features/cart/services/cartService.ts rename to src/features/cart/services/cartService.client.ts index c97258e..45bf09b 100644 --- a/src/features/cart/services/cartService.ts +++ b/src/features/cart/services/cartService.client.ts @@ -1,6 +1,7 @@ +"use client"; import { CartApiResponse, CartInfo, CartRequest } from "@/features/cart/types/cartType"; -import { getCart, upsertCart } from "@/features/cart/api/cartApi"; import { toCartInfo } from "@/features/cart/utils/cartMapper"; +import { getCart, upsertCart } from "@/features/cart/api/cartApi.client"; export async function getCartInfo(): Promise { const cartApiResponse: CartApiResponse = await getCart(); diff --git a/src/features/cart/services/cartService.server.ts b/src/features/cart/services/cartService.server.ts new file mode 100644 index 0000000..9ef00c0 --- /dev/null +++ b/src/features/cart/services/cartService.server.ts @@ -0,0 +1,9 @@ +import "server-only"; +import { CartApiResponse, CartInfo } from "@/features/cart/types/cartType"; +import { getCart } from "@/features/cart/api/cartApi.server"; +import { toCartInfo } from "@/features/cart/utils/cartMapper"; + +export async function getCartInfo():Promise { + const cartApiResponse: CartApiResponse = await getCart(); + return toCartInfo(cartApiResponse); +} \ No newline at end of file diff --git a/src/features/menu/api/cafeteriaApi.ts b/src/features/menu/api/cafeteriaApi.server.ts similarity index 92% rename from src/features/menu/api/cafeteriaApi.ts rename to src/features/menu/api/cafeteriaApi.server.ts index effe435..9f1149b 100644 --- a/src/features/menu/api/cafeteriaApi.ts +++ b/src/features/menu/api/cafeteriaApi.server.ts @@ -1,3 +1,4 @@ +import "server-only"; import { apiServer } from "@/shared/lib/api/apiServer"; import { CafeteriaApiResponse } from "@/features/menu/types/cafeteriaType"; diff --git a/src/features/menu/api/categoryApi.ts b/src/features/menu/api/categoryApi.server.ts similarity index 93% rename from src/features/menu/api/categoryApi.ts rename to src/features/menu/api/categoryApi.server.ts index 60cd861..6f8d6c7 100644 --- a/src/features/menu/api/categoryApi.ts +++ b/src/features/menu/api/categoryApi.server.ts @@ -1,3 +1,4 @@ +import "server-only"; import { apiServer } from "@/shared/lib/api/apiServer"; import { CategoryApiResponse } from "@/features/menu/types/categoryType"; diff --git a/src/features/menu/api/menuApi.ts b/src/features/menu/api/menuApi.server.ts similarity index 93% rename from src/features/menu/api/menuApi.ts rename to src/features/menu/api/menuApi.server.ts index 96a8827..d6d9cb7 100644 --- a/src/features/menu/api/menuApi.ts +++ b/src/features/menu/api/menuApi.server.ts @@ -1,3 +1,4 @@ +import "server-only"; import { apiServer } from "@/shared/lib/api/apiServer"; import { MenuApiResponse } from "@/features/menu/types/menuType"; diff --git a/src/features/menu/components/MenuPageContainer.tsx b/src/features/menu/components/MenuPageContainer.tsx index 17fda47..fb82539 100644 --- a/src/features/menu/components/MenuPageContainer.tsx +++ b/src/features/menu/components/MenuPageContainer.tsx @@ -1,6 +1,5 @@ "use client"; -import { HakgwanMenuData } from "@/features/menu/services/hakgwanMenuService"; import CategoryBar from "@/features/menu/components/category/CategoryBar"; import MenuList from "@/features/menu/components/list/MenuList"; import { useMemo, useState } from "react"; @@ -10,6 +9,7 @@ import { useCart } from "@/features/cart/hooks/useCart"; import OrderSummaryBar from "@/features/menu/components/bar/OrderSummaryBar"; import CartToast from "@/features/menu/components/toast/CartToast"; import { useToast } from "@/shared/hooks/useToast"; +import { HakgwanMenuData } from "@/features/menu/types/hakgwanType"; interface MenuPageContainerProps { hakgwanMenuData: HakgwanMenuData; diff --git a/src/features/menu/components/bar/OrderSummaryBar.tsx b/src/features/menu/components/bar/OrderSummaryBar.tsx index f7c24f3..d28f7e9 100644 --- a/src/features/menu/components/bar/OrderSummaryBar.tsx +++ b/src/features/menu/components/bar/OrderSummaryBar.tsx @@ -19,7 +19,7 @@ export default function OrderSummaryBar({ const router = useRouter(); const handleCartButtonClick: MouseEventHandler = () => { - router.replace("/cart"); + router.push("/cart"); } return ( diff --git a/src/features/menu/services/hakgwanMenuService.ts b/src/features/menu/services/hakgwanMenuService.server.ts similarity index 92% rename from src/features/menu/services/hakgwanMenuService.ts rename to src/features/menu/services/hakgwanMenuService.server.ts index 7118a04..1827057 100644 --- a/src/features/menu/services/hakgwanMenuService.ts +++ b/src/features/menu/services/hakgwanMenuService.server.ts @@ -1,16 +1,12 @@ -import { getAllCafeteria } from "@/features/menu/api/cafeteriaApi"; -import { getAllMenuByCafeteriaId } from "@/features/menu/api/menuApi"; -import { getCategoriesByCafeteriaId } from "@/features/menu/api/categoryApi"; +import { getAllCafeteria } from "@/features/menu/api/cafeteriaApi.server"; +import { getAllMenuByCafeteriaId } from "@/features/menu/api/menuApi.server"; +import { getCategoriesByCafeteriaId } from "@/features/menu/api/categoryApi.server"; import { CategoryApiResponse, CategoryItem } from "@/features/menu/types/categoryType"; import { MenuApiResponse, MenuItem } from "@/features/menu/types/menuType"; import { CafeteriaApiResponse } from "@/features/menu/types/cafeteriaType"; import { toCategoryItem } from "@/features/menu/utils/categoryMapper"; import { toMenuItem } from "@/features/menu/utils/menuMapper"; - -export interface HakgwanMenuData { - categoryItems: CategoryItem[]; - menuItems: MenuItem[]; -} +import { HakgwanMenuData } from "@/features/menu/types/hakgwanType"; export async function getHakgwanMenus(): Promise { const cafeterias: CafeteriaApiResponse[] = await getAllCafeteria(); diff --git a/src/features/menu/types/hakgwanType.ts b/src/features/menu/types/hakgwanType.ts new file mode 100644 index 0000000..b62b334 --- /dev/null +++ b/src/features/menu/types/hakgwanType.ts @@ -0,0 +1,7 @@ +import { MenuItem } from "@/features/menu/types/menuType"; +import { CategoryItem } from "@/features/menu/types/categoryType"; + +export interface HakgwanMenuData { + categoryItems: CategoryItem[]; + menuItems: MenuItem[]; +} \ No newline at end of file