diff --git a/src/common/constants/index.js b/src/common/constants/index.js index a97d352d8615228dc861babdb2c529c55cd542ab..aa92f57a45ba48ffdc1f7ee704cebfffa56169e6 100644 --- a/src/common/constants/index.js +++ b/src/common/constants/index.js @@ -10,5 +10,9 @@ const ALERT = Object.freeze({ REQ_WRONG: '잘못된 접근입니다' }); +const STATUS = Object.freeze({ + PENDING: 'Pending', + COMPLETED: 'Completed' +}); -export { CONFIRM, ALERT }; +export { CONFIRM, ALERT, STATUS }; diff --git a/src/common/css/common.css b/src/common/css/common.css index d09633c5fc6707a0b37e5657f019f6756bdf2a9a..61369a8a5a5a8b12a626ce3291f6e4624ff038b2 100644 --- a/src/common/css/common.css +++ b/src/common/css/common.css @@ -36,6 +36,5 @@ .mr-4 { margin-right: 16px; } -.bold { - font-weight: bold; -} +.bold { font-weight: bold; } +.del { text-decoration: line-through; } diff --git a/src/common/instances/AdminSocket.ts b/src/common/instances/AdminSocket.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ba1136363a432fad6903d7bc43e29acbf584a87 --- /dev/null +++ b/src/common/instances/AdminSocket.ts @@ -0,0 +1,28 @@ +import { io } from "socket.io-client"; + +import { SOCKET_URL } from "../utils/api"; + + +class AdminSocket { + private ioInstance = io(`${SOCKET_URL}/admin`, { withCredentials: true }); + + constructor() { + this.ioInstance.on('connect', () => { + console.log('WS: admin connected'); + }); + + this.ioInstance.on('auth', (e) => { + console.log('WS: admin auth', e); + }); + } + + onOrder(listener: (a: any) => void) { + return this.ioInstance.on('order', listener); + } + + disconnect() { + this.ioInstance.disconnect(); + } +} + +export default AdminSocket; diff --git a/src/common/instances/Connector.ts b/src/common/instances/Connector.ts index f39ccb09a506625cbb4d8247819258cc62259760..b30d08601630413d07f4c778eb130ea42e3cb3b1 100644 --- a/src/common/instances/Connector.ts +++ b/src/common/instances/Connector.ts @@ -21,6 +21,13 @@ class Connector { return this.isAdmin; } + private checkSession(status: number) { + if (status === RESPONSE_STATUS.UNAUTH) { + this.setLoginInstance(false); + window.location.href = '/login'; + } + } + async login<T>(payload?: any): Promise<T> { const postRequest = await fetchData<T>('/user/login', FETCH_METHOD.POST, payload); if (postRequest.status === RESPONSE_STATUS.OK) { @@ -36,21 +43,21 @@ class Connector { async get<T>(url: string, payload?: any): Promise<T> { const getRequest = await fetchData<T>(url, FETCH_METHOD.GET, payload); - if (getRequest.status === RESPONSE_STATUS.UNAUTH) { - this.setLoginInstance(false); - window.location.href = '/login'; - } + this.checkSession(getRequest.status); return getRequest.response; } async post<T>(url: string, payload?: any): Promise<T> { const postRequest = await fetchData<T>(url, FETCH_METHOD.POST, payload); - if (postRequest.status === RESPONSE_STATUS.UNAUTH) { - this.setLoginInstance(false); - window.location.href = '/login'; - } + this.checkSession(postRequest.status); return postRequest.response; } + + async patch<T>(url: string, payload?: any): Promise<T> { + const patchRequest = await fetchData<T>(url, FETCH_METHOD.PATCH, payload); + this.checkSession(patchRequest.status); + return patchRequest.response; + } } export default Connector; diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index a71012bda1da0cbfca6dbb5742e4247bbc369234..f5cdaaa1719607c3179838a281ad8781180d9d70 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -93,6 +93,7 @@ const Header: GFCWithProp<Props> = ({ headerName, connector }) => { if (_loc === 'menu') setReturnEl(<MenuHeader headerName={headerName} connector={connector} />); if (_loc === 'cart') setReturnEl(<CartHeader />); if (_loc === 'history') setReturnEl(<HistoryHeader />); + if (_loc === 'admin') setReturnEl(<HomeHeader connector={connector} />); }, [location, headerName]); return ( diff --git a/src/pages/admin-page/AdminPage.module.css b/src/pages/admin-page/AdminPage.module.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..862805a0c10b0c0f001ccd641f2354194c70244d 100644 --- a/src/pages/admin-page/AdminPage.module.css +++ b/src/pages/admin-page/AdminPage.module.css @@ -0,0 +1,8 @@ +.container { + display: flex; + flex-direction: column; + gap: 24px; + + padding: 48px; + line-height: 1.2; +} diff --git a/src/pages/admin-page/AdminPage.tsx b/src/pages/admin-page/AdminPage.tsx index 08c8f1960acb7f5499fe0fa2f0a6abe5aa0994f0..52ec257bcf3e318bcf47333263f4cc4462411e1b 100644 --- a/src/pages/admin-page/AdminPage.tsx +++ b/src/pages/admin-page/AdminPage.tsx @@ -1,14 +1,23 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import APP_ROUTE from "../../_app/config/route"; -import { ALERT } from "../../common/constants"; +import { ALERT, STATUS } from "../../common/constants"; +import AdminSocket from "../../common/instances/AdminSocket"; +import S from './AdminPage.module.css'; +import AdminOrderBox from "./modules/admin-order-box/AdminOrderBox"; + +import type { OrderedItemModel } from "./config/type"; import type { GFC } from "../../common/types/fc"; + const AdminPage: GFC = ({ connector }) => { const navigator = useNavigate(); + let adminSocket: AdminSocket|null = null; + const [orderList, setOrderList] = useState<Array<OrderedItemModel>>([]); + useEffect(() => { if (!connector.current?.getIsAdmin()) { @@ -16,10 +25,55 @@ const AdminPage: GFC = ({ connector }) => { navigator(APP_ROUTE.LOGIN); return; } + + adminSocket = new AdminSocket(); + + adminSocket.onOrder((e) => { + setOrderList((prev) => { + let sortedArray = [...prev]; + sortedArray.push(e); + + const idStatusMap = new Map(); + sortedArray.forEach(item => { + if (item.status === STATUS.COMPLETED) { + idStatusMap.set(item._id, true); + } else if (!idStatusMap.has(item._id)) { + idStatusMap.set(item._id, false); + } + }); + + sortedArray = sortedArray.filter(item => { + return item.status === STATUS.COMPLETED || !idStatusMap.get(item._id); + }); + + sortedArray.sort((a, b) => { + if (a.status === STATUS.COMPLETED && b.status !== STATUS.COMPLETED) { + return 1; + } else if (b.status === STATUS.COMPLETED && a.status !== STATUS.COMPLETED) { + return -1; + } + return 0; + }); + + return sortedArray; + }); + }); }, [connector]); return ( - <div>admin</div> + <div className={S['container']}> + { + orderList.length && orderList.map((d, i) => { + return ( + <AdminOrderBox connector={connector} items={d.items} + waitingCount={d.waitingCount} takeout={d.takeout} + createdTime={d.createdTime} status={d.status} + _id={d._id} key={`aob-c1-${i}`} + /> + ); + }) + } + </div> ); }; diff --git a/src/pages/admin-page/config/dummy.ts b/src/pages/admin-page/config/dummy.ts new file mode 100644 index 0000000000000000000000000000000000000000..71fd58aed6d3931f0f08ca626363701ad467beb0 --- /dev/null +++ b/src/pages/admin-page/config/dummy.ts @@ -0,0 +1,25 @@ +import type { OrderedItemModel } from "./type"; + +const ADMIN_DUMMY: OrderedItemModel = { + "userId": "656b8ec725f8b6df24ab910c", + "shopId": "3", + "items": [ + { + "menuId": "6562292377dd1a645a7e960a", + "quantity": 2 + }, + { + "menuId": "6562292377dd1a645a7e9609", + "quantity": 1 + } + ], + "paymentMethod": "MONEY", + "waitingCount": 39, + "takeout": true, + "totalPrice": 22000, + "createdTime": "2023-12-02T23:07:47.000Z", + "status": "Pending", + "_id": "123" +}; + +export { ADMIN_DUMMY }; diff --git a/src/pages/admin-page/config/type.ts b/src/pages/admin-page/config/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..defcccbaecc0f7c610fa1a1c2d5fca02e070b891 --- /dev/null +++ b/src/pages/admin-page/config/type.ts @@ -0,0 +1,8 @@ +import type { CartItemPostModel } from "../../cart-page/config/type"; + +type OrderedItemModel = CartItemPostModel & { + status: string; + _id: string; +} + +export type { OrderedItemModel }; diff --git a/src/pages/admin-page/modules/admin-order-box/AdminOrderBox.module.css b/src/pages/admin-page/modules/admin-order-box/AdminOrderBox.module.css new file mode 100644 index 0000000000000000000000000000000000000000..8bec466a8cfb79ce92f223c8b7cde260de5a0c92 --- /dev/null +++ b/src/pages/admin-page/modules/admin-order-box/AdminOrderBox.module.css @@ -0,0 +1,8 @@ +.container { + width: 100%; + min-height: 48px; + padding: 16px; + + border: 1px solid var(--gray-2); + border-radius: 16px; +} diff --git a/src/pages/admin-page/modules/admin-order-box/AdminOrderBox.tsx b/src/pages/admin-page/modules/admin-order-box/AdminOrderBox.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c5e29716a92fcb8428b7a2c6d3a487e9b69362c3 --- /dev/null +++ b/src/pages/admin-page/modules/admin-order-box/AdminOrderBox.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; + +import { ALERT, STATUS } from "../../../../common/constants"; + +import S from './AdminOrderBox.module.css'; + +import type Connector from "../../../../common/instances/Connector"; +import type { MenuPageData } from "../../../menu-page/config/type"; +import type { OrderedItemModel } from "../../config/type"; +import type { FC, MutableRefObject } from "react"; + + +interface Props { + connector: MutableRefObject<Connector|null>; + items: OrderedItemModel['items']; + waitingCount: OrderedItemModel['waitingCount']; + takeout: OrderedItemModel['takeout']; + createdTime: OrderedItemModel['createdTime']; + status: OrderedItemModel['status']; + _id: OrderedItemModel['_id']; +} + +const AdminOrderBox: FC<Props> = ({ connector, items, waitingCount, takeout, createdTime, status, _id }) => { + const [itemName, setItemName] = useState<string[]>([]); + + const handleClickButton = (_id: string) => { + connector.current!.patch('/admin/order', { + orderId: _id, + newStatus: 'Completed' + }); + }; + + useEffect(() => { + void (async () => { + try { + const response = await Promise.allSettled( + items.map((d) => + connector.current!.get<MenuPageData>(`/menu/${d.menuId}`) + ) + ); + const itemNames = response.map(r => { + if (r.status === 'fulfilled' && r.value.name) { + return r.value.name; + } + return null; + }).filter(name => name !== null) as string[]; + + setItemName(itemNames); + } catch (e) { + alert(ALERT.REQ_FAIL); + } + })(); + }, [items]); + + return ( + <div className={[S['container'], status === STATUS.COMPLETED ? 'del' : ''].join(' ')}> + <h1>대기 번호: {waitingCount}번</h1> + <div> + { + items.map((d, i) => { + return ( + <div key={`aob-c2-${i}`}> + <p>{itemName[i]} {d.quantity}개</p> + </div> + ); + }) + } + </div> + <h2>주문 시각: {createdTime}</h2> + <h2>포장 여부: {takeout ? '포장' : '매장'}</h2> + { + status !== STATUS.COMPLETED && <button onClick={() => handleClickButton(_id)}>조리 완료</button> + } + </div> + ); +}; + +export default AdminOrderBox; diff --git a/src/pages/cart-page/config/type.ts b/src/pages/cart-page/config/type.ts index ff29ac0d29fc2795715b2d6c9ce9eb88e5dfe7ce..5f900bafac53d6406456d2786c739aa7923d0ea2 100644 --- a/src/pages/cart-page/config/type.ts +++ b/src/pages/cart-page/config/type.ts @@ -17,7 +17,7 @@ interface CartItemPostModel { takeout: boolean; waitingCount: number; totalPrice: number; - createdTime: Date; + createdTime: string; }