From ec2bfdc78453e7cca17e2ef2a0b95f24d27667ce Mon Sep 17 00:00:00 2001 From: TaeSeung Yoo <59465914+gudusol@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:43:31 +0900 Subject: [PATCH] =?UTF-8?q?[Feat/#71]=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20UI=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 관리자 사이드바 -> 헤더로 변경 * feat: 관리자페이지 비고란 추가 * design: 태블릿 화면 스타일 수정 (스크롤, 높이 설정) * feat: 관리자/태블릿 주문시간 추가 * design: 태블릿 화면 css 수정 (반응형 디자인 추가) * feat: validation alert -> modal 변경 * feat: 관리자 주문 리스트 useInfiniteQuery로 수정 * feat: Intersection Observer 를 이용한 무한스크롤 구현 * feat: 관리자 페이지 주문 비고 기능 추가 --- package.json | 2 + src/App.tsx | 4 + src/apis/domains/admin/useFetchOrders.ts | 26 ++-- src/apis/domains/admin/useFetchProductAll.ts | 1 + src/apis/domains/admin/usePatchOrderNote.ts | 36 ++++++ .../Edit/EditReceiver/EditReceiver.style.ts | 20 +++ .../Edit/EditReceiver/EditReceiver.tsx | 47 ++++++- .../common/steps/Receiver2/Receiver2.style.ts | 27 ++++ .../common/steps/Receiver2/Receiver2.tsx | 115 +++++++++++------- .../Admin/components/Filter/Filter.style.ts | 2 +- .../components/OrderTable/OrderTable.style.ts | 31 ++++- .../components/OrderTable/OrderTable.tsx | 49 +++++++- .../Admin/page/AdminPage/AdminPage.style.ts | 84 ++++++++----- src/pages/Admin/page/AdminPage/AdminPage.tsx | 82 +++++++------ .../AdminPage/OrderCheck/OrderCheck.style.ts | 8 ++ .../page/AdminPage/OrderCheck/OrderCheck.tsx | 57 ++++++++- .../components/DialButton/DialButton.style.ts | 7 +- .../OrderInfoSection.style.ts | 16 ++- .../OrderInfoSection/OrderInfoSection.tsx | 25 ++-- .../OrderNumberSearchSection.style.ts | 14 ++- .../components/PayButton/PayButton.style.ts | 6 +- .../orderCheck/page/OrderCheckPage.style.ts | 6 +- src/styles/global.ts | 1 + src/types/orderInfoWithOrderNumber.ts | 1 + src/types/orderType.ts | 5 +- yarn.lock | 17 +++ 26 files changed, 526 insertions(+), 163 deletions(-) create mode 100644 src/apis/domains/admin/usePatchOrderNote.ts diff --git a/package.json b/package.json index 64e0445..d3be8c6 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "@emotion/react": "^11.13.3", "@tanstack/react-query": "^5.56.2", + "@tanstack/react-query-devtools": "^5.62.16", "axios": "^1.7.7", "dayjs": "^1.11.13", "jotai": "^2.9.3", @@ -23,6 +24,7 @@ "react-dom": "^18.3.1", "react-router-dom": "^6.26.1", "react-select": "^5.8.1", + "react-spinners": "^0.15.0", "react-switch": "^7.0.0", "xlsx": "^0.18.5" }, diff --git a/src/App.tsx b/src/App.tsx index 83b7cbb..fe516e8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import theme from "@styles/theme"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import PrivateRoute from "./routes/PrivateRoute/PrivateRoute"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; const allRoutes = [ ...homeRoutes, @@ -36,6 +37,9 @@ function App() { +
+ +
); } diff --git a/src/apis/domains/admin/useFetchOrders.ts b/src/apis/domains/admin/useFetchOrders.ts index 8c592dc..45dcee1 100644 --- a/src/apis/domains/admin/useFetchOrders.ts +++ b/src/apis/domains/admin/useFetchOrders.ts @@ -1,7 +1,7 @@ import { get } from "@apis/api"; import { QUERY_KEY } from "@apis/queryKeys/queryKeys"; -import { useQuery } from "@tanstack/react-query"; -import { ApiResponseType, ErrorResponse, Order, OrderData } from "@types"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { ApiResponseType, ErrorResponse, OrderData } from "@types"; interface queryType { orderReceivedDate: string; @@ -22,12 +22,17 @@ const buildQuery = (query: queryType): string => { return queryString ? `${queryString}` : ""; }; -const getOrders = async (query: queryType): Promise => { +const getOrders = async ( + query: queryType, + pageParam?: string +): Promise => { try { + const extendedQuery = + pageParam === "-1" ? query : { ...query, cursorOrderId: pageParam }; const response = await get>( - `api/v1/order?${buildQuery(query)}` + `api/v1/orders?${buildQuery(extendedQuery)}` ); - return response.data.data.orderList; + return response.data.data; } catch (error) { const errorResponse = error as ErrorResponse; const errorData = errorResponse.response.data; @@ -36,8 +41,13 @@ const getOrders = async (query: queryType): Promise => { }; export const useFetchOrders = (query: queryType) => { - return useQuery({ - queryKey: [QUERY_KEY.ORDER_LIST, query], - queryFn: () => getOrders(query), + return useInfiniteQuery({ + queryKey: [QUERY_KEY.ORDER_LIST], + queryFn: ({ pageParam = null }) => getOrders(query, pageParam?.toString()), + getNextPageParam: (lastPage) => { + return lastPage?.nextCursor === null ? undefined : lastPage?.nextCursor; + }, + initialPageParam: -1, // 초기 페이지 파라미터 설정 + select: (data) => (data.pages ?? []).flatMap((page) => page?.orders), }); }; diff --git a/src/apis/domains/admin/useFetchProductAll.ts b/src/apis/domains/admin/useFetchProductAll.ts index 64f7551..9469a91 100644 --- a/src/apis/domains/admin/useFetchProductAll.ts +++ b/src/apis/domains/admin/useFetchProductAll.ts @@ -19,5 +19,6 @@ export const useFetchProductAll = () => { return useQuery({ queryKey: [QUERY_KEY.PRODUCT_LIST_ALL], queryFn: () => getProductList(), + staleTime: 1000 * 60 * 60, // 1시간 }); }; diff --git a/src/apis/domains/admin/usePatchOrderNote.ts b/src/apis/domains/admin/usePatchOrderNote.ts new file mode 100644 index 0000000..9ce6941 --- /dev/null +++ b/src/apis/domains/admin/usePatchOrderNote.ts @@ -0,0 +1,36 @@ +import { adminPatch } from "@apis/api"; +import { QUERY_KEY } from "@apis/queryKeys/queryKeys"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ErrorResponse, MutateResponseType } from "@types"; + +interface PatchOrderNote { + orderId: number; + note: string; +} + +const patchOrderNote = async ({ + orderId, + note, +}: PatchOrderNote): Promise => { + try { + const response = await adminPatch(`api/v1/order/note`, { + orderId, + note, + }); + return response.data; + } catch (error) { + const errorResponse = error as ErrorResponse; + const errorData = errorResponse.response.data; + throw errorData; + } +}; + +export const usePatchOrderNote = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (date: PatchOrderNote) => patchOrderNote(date), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.ORDER_LIST] }); + }, + }); +}; diff --git a/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.style.ts b/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.style.ts index 51e3111..5978bfb 100644 --- a/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.style.ts +++ b/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.style.ts @@ -72,3 +72,23 @@ export const radioWrapper = css` margin-top: 1rem; margin-bottom: 1.2rem; `; + +export const alertModal = css` + width: 30rem; + ${flexGenerator("column")}; + padding: 3rem; +`; + +export const alertModalText = (theme: Theme) => + css` + ${theme.font["head06-b-16"]} + color: ${theme.color.black}; + text-align: center; + `; + +export const buttonWrapper = css` + width: 100%; + margin-top: 2rem; + ${flexGenerator("column")}; + gap: 1rem; +`; diff --git a/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.tsx b/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.tsx index b07f781..55cb430 100644 --- a/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.tsx +++ b/src/components/common/steps/CheckInfo/Edit/EditReceiver/EditReceiver.tsx @@ -3,10 +3,14 @@ import { CountProduct, CustomCalendar, Input, + Modal, RadioInput, } from "@components"; import { addressFormWrapper, + alertModal, + alertModalText, + buttonWrapper, deliveryDateContainer, editReceiverLayout, mainSectionStyle, @@ -52,6 +56,13 @@ const EditReceiver = ({ receiverIndex }: EditReceiverProps) => { const navigate = useNavigate(); const [category] = useAtom(categoryAtom); + const [alertText, setAlertText] = useState(""); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalClose = () => { + setIsModalOpen(false); + }; + const [form, setForm] = useState({ address: receiver?.recipientAddress || "", addressDetail: receiver?.recipientAddressDetail || "", @@ -84,12 +95,26 @@ const EditReceiver = ({ receiverIndex }: EditReceiverProps) => { })); }; - const handleButtonClick = () => { - if (!form.address || !form.addressDetail || !form.zonecode) { - alert("주소와 상세주소를 모두 입력해주세요."); - return; + const checkValidation = () => { + if ( + !orderPostDataState.recipientInfo[receiverIndex]?.recipientName || + !orderPostDataState.recipientInfo[receiverIndex]?.recipientPhone + ) { + setAlertText("받는 분의 이름과 휴대폰 번호를 모두 입력해주세요."); + return false; + } else if (!form.address || !form.addressDetail || !form.zonecode) { + setAlertText("주소와 상세주소를 모두 입력해주세요."); + return false; } else if (selectedOption !== "regular" && selectedDate.length < 1) { - alert("희망 배송일자를 선택해주세요"); + setAlertText("희망 배송일자를 선택해주세요"); + return false; + } + return true; + }; + + const handleButtonClick = () => { + if (!checkValidation()) { + setIsModalOpen(true); return; } @@ -272,6 +297,18 @@ const EditReceiver = ({ receiverIndex }: EditReceiverProps) => { 수정 완료 + {isModalOpen && ( + +
+

{alertText}

+
+ +
+
+
+ )} ); }; diff --git a/src/components/common/steps/Receiver2/Receiver2.style.ts b/src/components/common/steps/Receiver2/Receiver2.style.ts index d074063..28b1adc 100644 --- a/src/components/common/steps/Receiver2/Receiver2.style.ts +++ b/src/components/common/steps/Receiver2/Receiver2.style.ts @@ -1,5 +1,6 @@ import { css, Theme } from "@emotion/react"; import { flexGenerator } from "@styles/generator"; +import { CategoryType } from "@types"; export const mainSectionStyle = (theme: Theme) => css` ${flexGenerator("column")} @@ -24,3 +25,29 @@ export const zonecodeWrapper = css` border-radius: 10px; } `; + +export const alertModal = css` + width: 30rem; + ${flexGenerator("column")}; + padding: 3rem; +`; + +export const alertModalText = (category: CategoryType) => (theme: Theme) => + css` + ${theme.font["head06-b-16"]} + color: ${theme.color.black}; + text-align: center; + + & > strong { + color: ${category === "experience" + ? theme.color.green + : theme.color.orange}; + } + `; + +export const buttonWrapper = css` + width: 100%; + margin-top: 2rem; + ${flexGenerator("column")}; + gap: 1rem; +`; diff --git a/src/components/common/steps/Receiver2/Receiver2.tsx b/src/components/common/steps/Receiver2/Receiver2.tsx index f640380..308e6b4 100644 --- a/src/components/common/steps/Receiver2/Receiver2.tsx +++ b/src/components/common/steps/Receiver2/Receiver2.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Button, Input } from "@components"; +import { Button, Input, Modal } from "@components"; import { buttonSectionStyle, layoutStyle, @@ -8,7 +8,13 @@ import { textStyle, } from "@pages/orderInfo/styles"; import { StepProps } from "@types"; -import { mainSectionStyle, zonecodeWrapper } from "./Receiver2.style"; +import { + alertModal, + alertModalText, + buttonWrapper, + mainSectionStyle, + zonecodeWrapper, +} from "./Receiver2.style"; import { useDaumPostcodePopup } from "react-daum-postcode"; import { useOrderPostDataChange } from "src/hooks/useOrderPostDataChange"; import { useAtom } from "jotai"; @@ -41,6 +47,12 @@ const Receiver2 = ({ onNext }: StepProps) => { zonecode: "", }); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleModalClose = () => { + setIsModalOpen(false); + }; + const open = useDaumPostcodePopup(scriptUrl); const handleComplete = (data: DaumPostcodeData) => { @@ -66,8 +78,8 @@ const Receiver2 = ({ onNext }: StepProps) => { }; const handleNextClick = () => { - if (!form.address || !form.addressDetail || !form.zonecode) { - alert("주소와 상세주소를 모두 입력해주세요."); + if (!form.address || !form.addressDetail.trim() || !form.zonecode) { + setIsModalOpen(true); return; } @@ -110,50 +122,69 @@ const Receiver2 = ({ onNext }: StepProps) => { }, [receiver]); return ( -
-
-
- 받는 분의 -
- 주소를 입력해주세요 -
-
-
-
+ <> +
+
+
+ 받는 분의 +
+ 주소를 입력해주세요 +
+
+
+
+ + +
-
+
+ -
- - setForm({ ...form, addressDetail: e.target.value })} - name="addressDetail" - type="text" - placeholder="상세주소 (예시: 101동 1201호 / 단독주택)" - /> -
-
- -
-
+ + + {isModalOpen && ( + +
+

+ 주소상세주소를 모두 + 입력해주세요. +

+
+ +
+
+
+ )} + ); }; diff --git a/src/pages/Admin/components/Filter/Filter.style.ts b/src/pages/Admin/components/Filter/Filter.style.ts index a75f4c6..e32f0ea 100644 --- a/src/pages/Admin/components/Filter/Filter.style.ts +++ b/src/pages/Admin/components/Filter/Filter.style.ts @@ -6,7 +6,7 @@ export const filterContainer = css` ${flexGenerator("column")}; gap: 2rem; width: 100%; - margin-bottom: 7.5rem; + margin-bottom: 5rem; `; export const filterTable = (theme: Theme) => css` diff --git a/src/pages/Admin/components/OrderTable/OrderTable.style.ts b/src/pages/Admin/components/OrderTable/OrderTable.style.ts index a02396d..b3e9b1d 100644 --- a/src/pages/Admin/components/OrderTable/OrderTable.style.ts +++ b/src/pages/Admin/components/OrderTable/OrderTable.style.ts @@ -39,7 +39,6 @@ export const tableStyle = (theme: Theme) => css` td { border: 1px solid ${theme.color.black}; text-align: center; - height: 4rem; ${theme.font["subhead-b-14"]}; line-height: 4rem; background-color: ${theme.color.background}; @@ -50,6 +49,7 @@ export const tableStyle = (theme: Theme) => css` ${theme.font["subhead-m-14"]}; background-color: ${theme.color.white}; padding: 0.8rem 0.5rem; + vertical-align: middle; } th:first-of-type, @@ -66,7 +66,21 @@ export const tableStyle = (theme: Theme) => css` /* 상품명 */ th:nth-of-type(4), td:nth-of-type(4) { - min-width: 20rem; + width: 20rem; + } + + /* 접수 날짜 */ + th:nth-of-type(2), + td:nth-of-type(2), + /* 전화번호 */ + th:nth-of-type(6), + td:nth-of-type(6), + th:nth-of-type(8), + td:nth-of-type(8), + /* 비고 */ + th:nth-of-type(12), + td:nth-of-type(12) { + width: 15rem; } `; @@ -114,3 +128,16 @@ export const productText = (theme: Theme) => css` export const modalNotice = (theme: Theme) => css` ${theme.font["head06-b-16"]}; `; + +export const noteTdBox = (theme: Theme) => css` + transition: 0.3s; + + &:hover { + background-color: ${theme.color.lightgray1}; + } +`; + +export const notesInput = css` + width: 100%; + height: 100%; +`; diff --git a/src/pages/Admin/components/OrderTable/OrderTable.tsx b/src/pages/Admin/components/OrderTable/OrderTable.tsx index 8f32c82..2843ba6 100644 --- a/src/pages/Admin/components/OrderTable/OrderTable.tsx +++ b/src/pages/Admin/components/OrderTable/OrderTable.tsx @@ -7,6 +7,8 @@ import { iconStyle, modalNotice, modalTitle, + notesInput, + noteTdBox, numberText, productText, sectionStyle, @@ -21,12 +23,18 @@ import { Button, Modal, Toast } from "@components"; import { IcCheckedTrue, IcCopy, IcDownload } from "@svg"; import * as XLSX from "xlsx"; import useToast from "src/hooks/useToast"; +import { usePatchOrderNote } from "@apis/domains/admin/usePatchOrderNote"; interface OrderTableProps { orders: Order[]; } const OrderTable = ({ orders }: OrderTableProps) => { + const [notesInputValue, setNotesInputValue] = useState(""); + const [writingNotesOrderId, setWritingNotesOrderId] = useState( + null + ); + const [selectedOrders, setSelectedOrders] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); @@ -36,6 +44,7 @@ const OrderTable = ({ orders }: OrderTableProps) => { const [productCount, setProductCount] = useState>({}); + const { mutate: patchOrderNote } = usePatchOrderNote(); const { mutate } = usePatchDeliveryShipped(); const handleShippedClick = () => { @@ -226,10 +235,11 @@ ${order.productList.join(", ")}`; 받는 분 주소 출발 날짜 결제 내역 + 비고 - {orders.map((order) => ( + {orders.map((order, index) => ( {order.productList.map((product) => { - return
{product}
; + return
{product}
; })} {order.senderName} @@ -268,6 +278,41 @@ ${order.productList.join(", ")}`; {`${order.recipientAddress} ${order.recipientAddressDetail}`} {order.deliveryDate} {order.deliveryStatus} + { + setWritingNotesOrderId(order.orderId); + setNotesInputValue(order.note); + }} + > + {writingNotesOrderId === order.orderId ? ( + setNotesInputValue(e.target.value)} + autoFocus + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }} + onBlur={() => { + if (notesInputValue !== order.note) { + patchOrderNote({ + orderId: order.orderId, + note: notesInputValue, + }); + } + setWritingNotesOrderId(null); + setNotesInputValue(""); + }} + /> + ) : ( + order.note + )} + ))} diff --git a/src/pages/Admin/page/AdminPage/AdminPage.style.ts b/src/pages/Admin/page/AdminPage/AdminPage.style.ts index 8597cce..81a7ec0 100644 --- a/src/pages/Admin/page/AdminPage/AdminPage.style.ts +++ b/src/pages/Admin/page/AdminPage/AdminPage.style.ts @@ -3,7 +3,7 @@ import { css } from "@emotion/react"; import { flexGenerator } from "@styles/generator"; export const AdminLayout = (theme: Theme) => css` - ${flexGenerator("row", "flex-start", "flex-start")}; + ${flexGenerator("column", "flex-start", "flex-start")}; position: absolute; top: 0; left: 0; @@ -12,54 +12,74 @@ export const AdminLayout = (theme: Theme) => css` background-color: ${theme.color.white}; `; -export const tapLayoutStyle = (theme: Theme) => css` - ${flexGenerator("column", "flex-start", "flex-start")}; - min-width: 20rem; - height: 100%; - background-color: ${theme.color.background2}; +export const adminHeader = (theme: Theme) => css` + ${flexGenerator("row", "space-between")}; + width: 100%; + padding: 2rem; + position: fixed; + top: 0; + background-color: ${theme.color.white}; + z-index: 3; +`; + +export const headerSection = css` + display: flex; + align-items: center; +`; + +export const headerLogo = css` + width: 15%; + gap: 1rem; +`; + +export const headerMenus = css` + width: 70%; + justify-content: center; +`; + +export const headerRight = css` + width: 15%; + justify-content: flex-end; `; export const tabTextStyle = (theme: Theme) => css` - ${theme.font["subhead-m-18"]} - padding: 2rem; + ${theme.font["head02-b-20"]}; `; -export const tabButtonStyle = (theme: Theme) => css` - width: 100%; - height: 5rem; - color: ${theme.color.black}; - background-color: ${theme.color.background2}; - border: 1px solid ${theme.color.lightgray2}; +export const tabMenuStyle = (theme: Theme) => css` + ${theme.font["head02-b-20"]}; + font-weight: 400; + color: ${theme.color.midgray1}; + padding: 1rem 2rem; + cursor: pointer; - :active { - background-color: ${theme.color.orange}; - color: ${theme.color.white}; + &:hover { + color: ${theme.color.orange}; } `; -export const activeTabButtonStyle = (theme: Theme) => css` - background-color: ${theme.color.orange}; - color: ${theme.color.white}; +export const activeTabMenuStyle = (theme: Theme) => css` + font-weight: 700; + color: ${theme.color.orange}; `; export const logoutButton = (theme: Theme) => css` - width: 100%; - ${flexGenerator()}; - margin-top: auto; - margin-bottom: 2rem; + width: 10rem; + height: 4rem; + background-color: ${theme.color.white}; + border: 1px solid ${theme.color.orange}; + border-radius: 0.4rem; + color: ${theme.color.orange}; ${theme.font["subhead-m-18"]} - cursor: pointer; - &:hover { - color: ${theme.color.orange}; - path { - fill: ${theme.color.orange}; - } + background-color: ${theme.color.orange}; + color: ${theme.color.white}; } `; export const pageLayout = () => css` - width: calc(100vw - 20rem); - padding: 5rem 10rem; + width: 100%; + margin-top: 8rem; + padding: 2rem 10rem 5rem; `; diff --git a/src/pages/Admin/page/AdminPage/AdminPage.tsx b/src/pages/Admin/page/AdminPage/AdminPage.tsx index 8709827..2647822 100644 --- a/src/pages/Admin/page/AdminPage/AdminPage.tsx +++ b/src/pages/Admin/page/AdminPage/AdminPage.tsx @@ -1,26 +1,24 @@ import { - activeTabButtonStyle, + activeTabMenuStyle, + adminHeader, AdminLayout, + headerLogo, + headerMenus, + headerRight, + headerSection, logoutButton, pageLayout, - tabButtonStyle, + tabMenuStyle, tabTextStyle, - tapLayoutStyle, } from "./AdminPage.style"; import { useNavigate, useParams } from "react-router-dom"; import { OrderCheck, ProductCheck, DeliveryCheck } from ".."; -import { IcLogout } from "@svg"; +import { IcMainCharacter } from "@svg"; const Admin = () => { const { tab = "order" } = useParams<{ tab: string }>(); const navigate = useNavigate(); - const handleButtonClick = ( - event: React.MouseEvent - ): void => { - navigate(`/admin/${event.currentTarget.name}`); - }; - const handleLogoutClick = (): void => { localStorage.removeItem("accessToken"); navigate("/admin/login"); @@ -28,35 +26,43 @@ const Admin = () => { return (
-
-

Course

- - - - -
- - 로그아웃 +
+
+ +

나무와 열매

-
+ +
+ +
+
{tab === "order" && } diff --git a/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.style.ts b/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.style.ts index 5d0cd9e..a8079ff 100644 --- a/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.style.ts +++ b/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.style.ts @@ -15,3 +15,11 @@ export const sectionTitle = (theme: Theme) => css` ${theme.font["head02-b-20"]} margin-bottom: 1.2rem; `; + +export const observerRefDiv = css` + height: 1px; +`; + +export const orderDataSpinner = css` + margin: 2rem auto 0; +`; diff --git a/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.tsx b/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.tsx index 9f6d145..21a9cfc 100644 --- a/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.tsx +++ b/src/pages/Admin/page/AdminPage/OrderCheck/OrderCheck.tsx @@ -1,8 +1,15 @@ import { Filter, OrderTable } from "@pages/Admin/components"; -import { pageLayout, sectionStyle, sectionTitle } from "./OrderCheck.style"; -import { useRef, useState } from "react"; +import { + observerRefDiv, + orderDataSpinner, + pageLayout, + sectionStyle, + sectionTitle, +} from "./OrderCheck.style"; +import { useRef, useState, useEffect, useCallback } from "react"; import { Dayjs } from "dayjs"; import { useFetchOrders } from "@apis/domains/admin/useFetchOrders"; +import { ClipLoader } from "react-spinners"; interface Option { value: string; @@ -20,9 +27,40 @@ const OrderCheck = () => { deliveryDate: "", productName: "", deliveryStatus: "", + nextCursor: "", }); - const { data: orderData } = useFetchOrders(query); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + useFetchOrders(query); + + const orders = data ?? []; + + const observerRef = useRef(null); + + const onIntersect = useCallback( + (entries: IntersectionObserverEntry[]) => { + if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, + [fetchNextPage, hasNextPage, isFetchingNextPage] + ); + + useEffect(() => { + if (!observerRef.current) return; + + const observer = new IntersectionObserver(onIntersect, { + root: null, + rootMargin: "0px", + threshold: 0, + }); + + observer.observe(observerRef.current); + + return () => { + observer.disconnect(); + }; + }, [onIntersect]); const handleSearchClick = () => { const newQuery = { @@ -31,6 +69,7 @@ const OrderCheck = () => { deliveryDate: deliveryDateRef.current?.format("YYYY-MM-DD") || "", productName: productRef.current?.value || "", deliveryStatus: statusRef.current?.value || "", + nextCursor: "", }; setQuery(newQuery); }; @@ -46,6 +85,7 @@ const OrderCheck = () => { deliveryDate: "", productName: "", deliveryStatus: "", + nextCursor: "", }); }; @@ -62,7 +102,16 @@ const OrderCheck = () => { handleResetClick={handleResetClick} /> - + +
+ {hasNextPage && !isFetchingNextPage && ( + + )}
); }; diff --git a/src/pages/orderCheck/components/DialButton/DialButton.style.ts b/src/pages/orderCheck/components/DialButton/DialButton.style.ts index 369750a..98a02c0 100644 --- a/src/pages/orderCheck/components/DialButton/DialButton.style.ts +++ b/src/pages/orderCheck/components/DialButton/DialButton.style.ts @@ -4,9 +4,8 @@ import { flexGenerator } from "@styles/generator"; export const buttonStyle = (index: number) => (theme: Theme) => css` ${flexGenerator()}; - padding: 1rem; - width: 15rem; - height: 12.8rem; + width: 100%; + height: 100%; border: 1px solid ${theme.color.lightgray3}; background-color: ${index === 9 || index === 11 ? theme.color.lightgray2 @@ -15,5 +14,5 @@ export const buttonStyle = (index: number) => (theme: Theme) => export const buttonSpan = (theme: Theme) => css` color: ${theme.color.black}; - ${theme.font["dialNumber-56"]} + ${theme.font["dialNumber-56"]}; `; diff --git a/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.style.ts b/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.style.ts index aa85a78..f914667 100644 --- a/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.style.ts +++ b/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.style.ts @@ -3,26 +3,31 @@ import { flexGenerator } from "@styles/generator"; export const section3Container = (theme: Theme) => css` ${flexGenerator("column", "start", "start")}; - width: 38rem; - min-height: 100%; + width: 35%; + height: 100%; + overflow-y: auto; background-color: ${theme.color.white}; `; + export const section3InfoWrapper = css` ${flexGenerator("column", "start", "start")}; - gap: 1rem; + gap: 0.8rem; + margin-bottom: 1rem; `; export const section3Div = css` ${flexGenerator("column", "start", "start")}; gap: 0.5rem; `; + export const graySpan = (theme: Theme) => css` color: ${theme.color.lightgray4}; - ${theme.font["head01-b-24"]} + ${theme.font["head02-b-20"]}; `; + export const blackSpan = (theme: Theme) => css` color: ${theme.color.black}; - ${theme.font["orderCheck-32"]} + ${theme.font["head01-b-24"]}; `; export const statusStyle = (statusStyle: string) => (theme: Theme) => @@ -38,6 +43,7 @@ export const statusStyle = (statusStyle: string) => (theme: Theme) => export const buttonWrapper = css` ${flexGenerator("row", "space-between", "center")}; + gap: 1rem; width: 100%; margin-top: auto; `; diff --git a/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.tsx b/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.tsx index 04ad392..94ba67b 100644 --- a/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.tsx +++ b/src/pages/orderCheck/components/OrderInfoSection/OrderInfoSection.tsx @@ -36,6 +36,11 @@ const OrderInfoSection = () => { [] ); + const productCount = (mergedOrders || []).reduce( + (acc, current) => acc + current.productCount, + 0 + ); + const { mutate: mutatePayComplete } = usePatchPayComplete(); const { mutate: mutatePayCancel } = usePatchPayCancel(); @@ -49,21 +54,27 @@ const OrderInfoSection = () => {
- 주문번호 - {previousOrderNumber} + 접수일시 + {orderInfo?.orderList[0].orderTimeInfo}
- 주문상태 - - {orderStatus} - + 주문번호 / 주문상태 +
+ {`${previousOrderNumber} / `} + + + {orderStatus} + +
이름 {orderInfo?.senderName}
- {`상품 (총 ${orderCount}개)`} + {`상품 (주문 ${orderCount}개 / 총 상품 ${productCount}개)`} {(mergedOrders || []).map((order, i) => ( css` `; export const section2Container = css` - ${flexGenerator("column")}; - width: 45rem; + ${flexGenerator("column", "flex-start")}; + flex: 1; height: 100%; `; export const orderNumberStyle = (theme: Theme) => css` - ${flexGenerator()} + ${flexGenerator()}; width: 100%; - height: 12.6rem; - padding: 2rem 0; + height: 100%; + max-height: 12.6rem; + padding: 1rem 0; border: 1px solid ${theme.color.lightgray3}; background-color: ${theme.color.white}; `; export const orderNumberSpan = (theme: Theme) => css` color: ${theme.color.black}; - ${theme.font["dialNumber-72"]} + ${theme.font["dialNumber-72"]}; `; export const dialButtonWrapper = css` width: 100%; + height: 100%; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(4, 1fr); diff --git a/src/pages/orderCheck/components/PayButton/PayButton.style.ts b/src/pages/orderCheck/components/PayButton/PayButton.style.ts index dbe3cae..eb798a2 100644 --- a/src/pages/orderCheck/components/PayButton/PayButton.style.ts +++ b/src/pages/orderCheck/components/PayButton/PayButton.style.ts @@ -3,12 +3,12 @@ import { flexGenerator } from "@styles/generator"; export const buttonStyle = (theme: Theme) => css` ${flexGenerator()}; - width: 18rem; - height: 8.8rem; + width: 100%; + height: 7rem; padding: 1rem; border: 1px solid ${theme.color.black}; border-radius: 20px; - ${theme.font["orderCheck-36"]} + ${theme.font["orderCheck-32"]}; `; export const buttonVariant = { diff --git a/src/pages/orderCheck/page/OrderCheckPage.style.ts b/src/pages/orderCheck/page/OrderCheckPage.style.ts index 558d71d..4053265 100644 --- a/src/pages/orderCheck/page/OrderCheckPage.style.ts +++ b/src/pages/orderCheck/page/OrderCheckPage.style.ts @@ -2,11 +2,11 @@ import { css, Theme } from "@emotion/react"; import { flexGenerator } from "@styles/generator"; export const orderCheckLayout = (theme: Theme) => css` - ${flexGenerator()}; + ${flexGenerator("row", "space-between", "flex-start")}; position: absolute; top: 0; left: 0; - padding: 9.3rem 6.1rem 6.7rem; + padding: 5rem; gap: 3rem; width: 100vw; height: 100%; @@ -15,7 +15,7 @@ export const orderCheckLayout = (theme: Theme) => css` export const refreshButton = (theme: Theme) => css` position: absolute; - top: 50px; + top: 30px; right: 50px; ${flexGenerator()}; width: 8rem; diff --git a/src/styles/global.ts b/src/styles/global.ts index d6bf1bf..0f9f7b2 100644 --- a/src/styles/global.ts +++ b/src/styles/global.ts @@ -33,6 +33,7 @@ const GlobalStyle = css` a { text-decoration: none; + color: inherit; } select { diff --git a/src/types/orderInfoWithOrderNumber.ts b/src/types/orderInfoWithOrderNumber.ts index 34a9325..b555735 100644 --- a/src/types/orderInfoWithOrderNumber.ts +++ b/src/types/orderInfoWithOrderNumber.ts @@ -3,6 +3,7 @@ export interface OrderInfo { productCount: number; deliveryStatus: string; price: number; + orderTimeInfo: string; } export interface OrderInfoData { diff --git a/src/types/orderType.ts b/src/types/orderType.ts index f5ecb22..1247f4f 100644 --- a/src/types/orderType.ts +++ b/src/types/orderType.ts @@ -1,4 +1,5 @@ export interface Order { + orderId: number; deliveryId: number; orderNumber: number; senderName: string; @@ -13,9 +14,11 @@ export interface Order { deliveryStatus: string; orderReceivedDate: string; // 날짜를 문자열로 표현 deliveryDate: string; // 날짜를 문자열로 표현 + note: string; } // 전체 데이터 구조를 위한 타입 정의 export interface OrderData { - orderList: Order[]; + nextCursor: number | null; + orders: Order[]; } diff --git a/yarn.lock b/yarn.lock index c707fbf..6ffabf0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -780,6 +780,18 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.56.2.tgz#2def2fb0290cd2836bbb08afb0c175595bb8109b" integrity sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q== +"@tanstack/query-devtools@5.62.16": + version "5.62.16" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz#a4b71c6b5bbf7575861437ef9a9f232333569255" + integrity sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q== + +"@tanstack/react-query-devtools@^5.62.16": + version "5.62.16" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.62.16.tgz#be7ef75a72002d6e0792359dd1b5b305faa34bff" + integrity sha512-EjF0tgHnWYcqhk8KxGKnmGlYcnldhWjW3bbH2WZqxo7t41ytzkIQtZ/UyLph//YMmZZE/RVTmSo3rGq/EG9iCA== + dependencies: + "@tanstack/query-devtools" "5.62.16" + "@tanstack/react-query@^5.56.2": version "5.56.2" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.56.2.tgz#3a0241b9d010910905382f5e99160997b8795f91" @@ -1987,6 +1999,11 @@ react-select@^5.8.1: react-transition-group "^4.3.0" use-isomorphic-layout-effect "^1.1.2" +react-spinners@^0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/react-spinners/-/react-spinners-0.15.0.tgz#bb9536a3839ab4e1513bb98847d79cc1fc930b93" + integrity sha512-ZO3/fNB9Qc+kgpG3SfdlMnvTX6LtLmTnOogb3W6sXIaU/kZ1ydEViPfZ06kSOaEsor58C/tzXw2wROGQu3X2pA== + react-switch@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/react-switch/-/react-switch-7.0.0.tgz#400990bb9822864938e343ed24f13276a617bdc0"