├── packages ├── server │ ├── .gitignore │ ├── src │ │ ├── auth │ │ │ ├── entities │ │ │ │ ├── auth.entity.ts │ │ │ │ └── token.entity.ts │ │ │ ├── dto │ │ │ │ ├── naver-singIn.dto.ts │ │ │ │ └── signup.dto.ts │ │ │ ├── interfaces │ │ │ │ └── jwtPayload.ts │ │ │ ├── decorators │ │ │ │ ├── public.decorator.ts │ │ │ │ └── roles.decorator.ts │ │ │ ├── guard │ │ │ │ ├── jwt.guard.ts │ │ │ │ └── role.guard.ts │ │ │ ├── strategy │ │ │ │ └── jwt.strategy.ts │ │ │ ├── auth.module.ts │ │ │ ├── naverOAuth.service.ts │ │ │ └── auth.controller.ts │ │ ├── cafe │ │ │ ├── enum │ │ │ │ ├── menuType.enum.ts │ │ │ │ └── menuSize.enum.ts │ │ │ ├── mock │ │ │ │ ├── menuOptionRelation.mock.ts │ │ │ │ ├── option.entity.mock.ts │ │ │ │ ├── menu.entity.mock.ts │ │ │ │ └── mockDataGenerator.ts │ │ │ ├── entities │ │ │ │ ├── option.entity.ts │ │ │ │ ├── cafeMenu.entity.ts │ │ │ │ ├── menuOption.entity.ts │ │ │ │ ├── menu.entity.ts │ │ │ │ └── cafe.entity.ts │ │ │ ├── cafe.module.ts │ │ │ ├── dto │ │ │ │ ├── CafeMenuRes.dto.ts │ │ │ │ ├── CafeRes.dto.ts │ │ │ │ ├── OptionRes.dto.ts │ │ │ │ ├── MenuRes.dto.ts │ │ │ │ └── MenuDetailRes.dto.ts │ │ │ ├── cafe.controller.ts │ │ │ └── cafe.service.ts │ │ ├── user │ │ │ ├── dto │ │ │ │ └── update-user.dto.ts │ │ │ ├── enum │ │ │ │ └── userRole.enum.ts │ │ │ ├── user.module.ts │ │ │ ├── user.controller.ts │ │ │ ├── user.service.ts │ │ │ └── entities │ │ │ │ └── user.entity.ts │ │ ├── order │ │ │ ├── enum │ │ │ │ └── orderStatus.enum.ts │ │ │ ├── dto │ │ │ │ ├── requested-order.dto.ts │ │ │ │ ├── update-order.dto.ts │ │ │ │ ├── oldRequestedOrdersDto.ts │ │ │ │ ├── OrderStatusRes.dto.ts │ │ │ │ ├── updateOrderReq.dto.ts │ │ │ │ ├── create-order.dto.ts │ │ │ │ ├── orderMenu.dto.ts │ │ │ │ ├── orderRes.dto.ts │ │ │ │ └── ordersRes.dto.ts │ │ │ ├── order.v1.module.ts │ │ │ ├── order.v2.module.ts │ │ │ ├── order.v3.module.ts │ │ │ └── entities │ │ │ │ └── orderMenu.entity.ts │ │ ├── common │ │ │ └── entities │ │ │ │ └── common.entity.ts │ │ ├── utils │ │ │ ├── dateTime.util.ts │ │ │ └── getMySQLTestTypeOrmModule.ts │ │ ├── redisCache │ │ │ ├── redisCache.controller.ts │ │ │ └── redisCache.module.ts │ │ ├── main.ts │ │ ├── setEnvVar.ts │ │ ├── middleware │ │ │ ├── logger.http.ts │ │ │ └── exception.filter.ts │ │ ├── setNestApp.ts │ │ ├── config │ │ │ └── route.ts │ │ └── app.module.ts │ ├── tsconfig.build.json │ ├── pm2.prod.yml │ ├── nest-cli.json │ ├── test │ │ └── mock │ │ │ ├── create-cafe.json │ │ │ └── create-order.json │ ├── Dockerfile.dev │ ├── jest-e2e.json │ ├── Dockerfile.prod │ ├── tsconfig.json │ ├── scripts │ │ ├── prod.deploy.sh │ │ └── dev.deploy.sh │ └── README.md ├── nginx │ ├── configs │ │ ├── modules │ │ ├── conf.d │ │ │ └── default.conf │ │ ├── scgi_params │ │ ├── uwsgi_params │ │ ├── nginx.conf │ │ └── fastcgi_params │ └── dockerfile └── client │ ├── src │ ├── react-app-env.d.ts │ ├── pages │ │ ├── manager │ │ │ └── AcceptList │ │ │ │ ├── styled.ts │ │ │ │ ├── index.tsx │ │ │ │ └── index.test.tsx │ │ ├── customer │ │ │ ├── MenuDetail │ │ │ │ ├── styled.ts │ │ │ │ └── components │ │ │ │ │ ├── Amount │ │ │ │ │ ├── styled.ts │ │ │ │ │ ├── index.test.tsx │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── OrderButton │ │ │ │ │ ├── styled.ts │ │ │ │ │ └── index.test.tsx │ │ │ │ │ ├── MenuInformation │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── styled.ts │ │ │ │ │ └── index.test.tsx │ │ │ │ │ ├── TemperatureSelector │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── index.test.tsx.snap │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── styled.ts │ │ │ │ │ └── index.test.tsx │ │ │ │ │ ├── OptionSelector │ │ │ │ │ ├── index.test.tsx │ │ │ │ │ └── styled.ts │ │ │ │ │ └── SizeSelector │ │ │ │ │ ├── styled.ts │ │ │ │ │ ├── index.test.tsx │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.test.tsx.snap │ │ │ │ │ └── index.tsx │ │ │ ├── Cart │ │ │ │ ├── components │ │ │ │ │ ├── EmptyCart │ │ │ │ │ │ ├── styled.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── CartFooter │ │ │ │ │ │ ├── styled.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── CartItem │ │ │ │ │ │ ├── styled.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── styled.ts │ │ │ │ ├── index.spec.tsx │ │ │ │ └── index.tsx │ │ │ ├── MenuList │ │ │ │ ├── components │ │ │ │ │ ├── MenuItem │ │ │ │ │ │ ├── styled.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── SnackBar │ │ │ │ │ │ ├── styled.ts │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── styled.ts │ │ │ │ ├── index.spec.tsx │ │ │ │ └── index.tsx │ │ │ └── OrderStatus │ │ │ │ ├── index.spec.tsx │ │ │ │ └── index.tsx │ │ ├── Home │ │ │ ├── styled.ts │ │ │ └── index.tsx │ │ ├── MyPage │ │ │ ├── index.spec.tsx │ │ │ ├── styled.ts │ │ │ └── index.tsx │ │ ├── Signin │ │ │ ├── styled.ts │ │ │ └── __snapshots__ │ │ │ │ └── index.spec.tsx.snap │ │ └── Signup │ │ │ └── styled.ts │ ├── mocks │ │ ├── server.ts │ │ ├── handlers │ │ │ ├── index.ts │ │ │ ├── menu.ts │ │ │ ├── auth.ts │ │ │ └── order.ts │ │ └── data │ │ │ ├── cart.ts │ │ │ └── menu.ts │ ├── index.tsx │ ├── setupTests.ts │ ├── components │ │ ├── Header │ │ │ ├── index.tsx │ │ │ ├── styled.ts │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.tsx.snap │ │ │ └── index.test.tsx │ │ ├── Toast │ │ │ ├── styled.ts │ │ │ └── index.tsx │ │ ├── Button │ │ │ ├── index.tsx │ │ │ └── styled.ts │ │ ├── Footer │ │ │ ├── NavigateItem.tsx │ │ │ ├── index.spec.tsx │ │ │ ├── styled.ts │ │ │ └── index.tsx │ │ ├── OrderDateList │ │ │ ├── styled.ts │ │ │ └── index.tsx │ │ ├── LeftArrow │ │ │ ├── styled.ts │ │ │ └── index.tsx │ │ ├── CountSelector │ │ │ ├── styled.ts │ │ │ ├── index.tsx │ │ │ └── index.test.tsx │ │ ├── OrderDetailList │ │ │ └── styled.ts │ │ └── OrderList │ │ │ └── styled.ts │ ├── assets │ │ └── icons │ │ │ ├── minus.svg │ │ │ ├── plus.svg │ │ │ ├── x_icon.svg │ │ │ ├── withdrawal.svg │ │ │ ├── change.svg │ │ │ ├── order.svg │ │ │ ├── signout.svg │ │ │ ├── home.svg │ │ │ ├── cart.svg │ │ │ ├── down_arrow.svg │ │ │ ├── left_arrow.svg │ │ │ ├── size.svg │ │ │ ├── edit_nickname.svg │ │ │ └── receipt.svg │ ├── stores │ │ ├── index.ts │ │ └── MenuDetail │ │ │ └── index.tsx │ ├── utils │ │ ├── fetch.ts │ │ ├── index.ts │ │ ├── index.test.ts │ │ ├── localStorage.ts │ │ └── testSetup.tsx │ ├── hooks │ │ ├── useFetchOrderList.tsx │ │ ├── useMenuListData.tsx │ │ ├── useCustomQuery.tsx │ │ ├── useOrderStatus.tsx │ │ ├── useFetch.tsx │ │ └── useOrderDates.tsx │ ├── App.tsx │ ├── UserRoleProvider.tsx │ ├── constants.ts │ ├── index.css │ ├── Router.tsx │ └── types │ │ └── index.ts │ ├── public │ ├── robots.txt │ ├── manifest.json │ └── index.html │ ├── tsconfig.json │ ├── craco.config.ts │ ├── package.json │ └── README.md ├── .prettierrc ├── .github ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── 기능-이슈.md │ ├── 리팩토링-이슈.md │ └── 버그-이슈.md └── workflows │ ├── fe-dev-ci.yml │ ├── be-dev-ci.yml │ └── fe-dev-cd.yml ├── .eslintrc.json └── package.json /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | *.env -------------------------------------------------------------------------------- /packages/nginx/configs/modules: -------------------------------------------------------------------------------- 1 | /usr/lib/nginx/modules -------------------------------------------------------------------------------- /packages/server/src/auth/entities/auth.entity.ts: -------------------------------------------------------------------------------- 1 | export class Auth {} 2 | -------------------------------------------------------------------------------- /packages/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/server/src/cafe/enum/menuType.enum.ts: -------------------------------------------------------------------------------- 1 | export enum MENU_TYPE { 2 | HOT = 'hot', 3 | ICED = 'iced', 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/src/user/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UpdateUserDto { 2 | readonly nickname: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/server/src/user/enum/userRole.enum.ts: -------------------------------------------------------------------------------- 1 | export enum USER_ROLE { 2 | CLIENT = 'CLIENT', 3 | MANAGER = 'MANAGER', 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/src/auth/dto/naver-singIn.dto.ts: -------------------------------------------------------------------------------- 1 | export class NaverSignInDto { 2 | readonly code: string; 3 | readonly state: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/pm2.prod.yml: -------------------------------------------------------------------------------- 1 | name: buddah-server 2 | script: dist/main.js 3 | instances: 4 4 | exec_mode: cluster 5 | env: 6 | NODE_ENV: production 7 | -------------------------------------------------------------------------------- /packages/client/src/pages/manager/AcceptList/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.div` 4 | width: 100%; 5 | `; 6 | -------------------------------------------------------------------------------- /packages/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuDetail/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.main` 4 | width: 100%; 5 | `; 6 | -------------------------------------------------------------------------------- /packages/server/src/cafe/mock/menuOptionRelation.mock.ts: -------------------------------------------------------------------------------- 1 | export const mockMenuOptionRelation = { 2 | 1: [1, 2], 3 | 2: [3], 4 | 3: [1, 3], 5 | 4: [2, 3], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/client/src/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers/index'; 3 | 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /packages/server/test/mock/create-cafe.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "부스트 다방", 3 | "description": "은계점", 4 | "latitude": 123, 5 | "longitude": 123, 6 | "address": "경기도 시흥시 은행로" 7 | } 8 | -------------------------------------------------------------------------------- /packages/server/src/auth/interfaces/jwtPayload.ts: -------------------------------------------------------------------------------- 1 | import { USER_ROLE } from 'src/user/enum/userRole.enum'; 2 | 3 | export interface JwtPayload { 4 | id: number; 5 | userRole: USER_ROLE; 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/src/order/enum/orderStatus.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ORDER_STATUS { 2 | REQUESTED = 'REQUESTED', 3 | ACCEPTED = 'ACCEPTED', 4 | REJECTED = 'REJECTED', 5 | COMPLETED = 'COMPLETED', 6 | } 7 | -------------------------------------------------------------------------------- /packages/server/src/auth/decorators/public.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const PUBLIC_KEY = 'PUBLIC_KEY'; 4 | export const Public = () => SetMetadata(PUBLIC_KEY, true); 5 | -------------------------------------------------------------------------------- /packages/server/src/order/dto/requested-order.dto.ts: -------------------------------------------------------------------------------- 1 | import { ArrayMinSize, IsArray } from 'class-validator'; 2 | 3 | export class RequestedOrderDto { 4 | @IsArray() 5 | @ArrayMinSize(1) 6 | newOrders: number[]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/server/src/order/dto/update-order.dto.ts: -------------------------------------------------------------------------------- 1 | import { PartialType } from '@nestjs/mapped-types'; 2 | import { CreateOrderDto } from './create-order.dto'; 3 | 4 | export class UpdateOrderDto extends PartialType(CreateOrderDto) {} 5 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuDetail/components/Amount/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | justify-content: space-between; 6 | margin: 1rem; 7 | `; 8 | -------------------------------------------------------------------------------- /packages/server/src/auth/dto/signup.dto.ts: -------------------------------------------------------------------------------- 1 | import { USER_ROLE } from '../../user/enum/userRole.enum'; 2 | 3 | export class SignUpDto { 4 | readonly userRole: USER_ROLE; 5 | readonly nickname: string; 6 | readonly corporate: string; 7 | } 8 | -------------------------------------------------------------------------------- /packages/client/src/pages/Home/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.main` 4 | width: 100%; 5 | `; 6 | 7 | export const ListContainer = styled.section` 8 | margin: 0 0 3rem 0; 9 | `; 10 | -------------------------------------------------------------------------------- /packages/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Buddha", 3 | "name": "Boost DDhaBang", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /packages/client/src/mocks/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { authHandlers } from './auth'; 2 | import { menuHandlers } from './menu'; 3 | import { orderHandlers } from './order'; 4 | 5 | export const handlers = [...authHandlers, ...menuHandlers, ...orderHandlers]; 6 | -------------------------------------------------------------------------------- /packages/server/src/cafe/enum/menuSize.enum.ts: -------------------------------------------------------------------------------- 1 | export enum MENU_SIZE { 2 | TALL = 'tall', 3 | GRANDE = 'grande', 4 | VENTI = 'venti', 5 | } 6 | 7 | export enum SIZE_PRICE { 8 | 'tall' = 0, 9 | 'grande' = 500, 10 | 'venti' = 1000, 11 | } 12 | -------------------------------------------------------------------------------- /packages/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | import App from './App'; 3 | import './index.css'; 4 | 5 | const root = ReactDOM.createRoot( 6 | document.getElementById('root') as HTMLElement 7 | ); 8 | 9 | root.render(); 10 | -------------------------------------------------------------------------------- /packages/server/src/auth/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { USER_ROLE } from '../../user/enum/userRole.enum'; 3 | 4 | export const ROLES_KEY = 'roles'; 5 | export const Roles = (...roles: USER_ROLE[]) => SetMetadata(ROLES_KEY, roles); 6 | -------------------------------------------------------------------------------- /packages/server/src/order/dto/oldRequestedOrdersDto.ts: -------------------------------------------------------------------------------- 1 | import { ArrayMinSize, IsArray, IsNumber } from 'class-validator'; 2 | export class OldRequestedOrdersDto { 3 | @IsArray() 4 | @ArrayMinSize(1) 5 | @IsNumber({}, { each: true }) 6 | oldRequestedOrderPks: Array; 7 | } 8 | -------------------------------------------------------------------------------- /packages/client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "consistent", 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "endOfLine": "lf" 12 | } 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # 제목 - 주요 기능/버그/수정 사항 2 | 3 | ## 📕 제목 4 | - #관련이슈 번호 5 | 6 | ## 📗 작업 내용 7 | 8 | > 구현 내용 및 작업 했던 내역 9 | 10 | - [x] 작업내용1 11 | - [x] 작업내용2 12 | - [x] 작업내용3 13 | 14 | ## 📘 PR 특이 사항 15 | 16 | > PR을 볼 때 주의깊게 봐야하거나 말하고 싶은 점 17 | 18 | - 특이사항1 19 | - 특이사항2 20 | -------------------------------------------------------------------------------- /packages/server/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # BUILD 2 | FROM node:18.12.1 AS build 3 | 4 | WORKDIR /app 5 | 6 | COPY *.json ./ 7 | 8 | COPY package*.json ./ 9 | 10 | RUN npm install 11 | 12 | # RUN 13 | FROM node:alpine 14 | 15 | WORKDIR /app 16 | 17 | COPY --from=build /app /app 18 | 19 | CMD ["npm", "run", "start:dev-remote"] -------------------------------------------------------------------------------- /packages/nginx/configs/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 3000; 3 | 4 | access_log /var/log/nginx/access.log main_log_format; 5 | 6 | location / { 7 | root /usr/share/buddah-client; 8 | index index.html index.htm; 9 | try_files $uri $uri/ /index.html; 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /packages/nginx/dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | # COPY ./nginx/nginx.conf /etc/nginx/nginx.conf 4 | # COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf 5 | 6 | WORKDIR /usr/share 7 | RUN mkdir buddah-client 8 | 9 | # COPY ./client/build/index.html /usr/share/buddah-client 10 | # COPY ./client/build/static /usr/share/buddah-client/static -------------------------------------------------------------------------------- /packages/client/src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { Container } from './styled'; 3 | 4 | interface Props { 5 | title: string; 6 | } 7 | 8 | function Header({ title }: Props) { 9 | return ( 10 | 11 |

{title}

12 |
13 | ); 14 | } 15 | 16 | export default memo(Header); 17 | -------------------------------------------------------------------------------- /packages/server/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "moduleDirectories": ["/", "node_modules", "mock"], 5 | "testEnvironment": "node", 6 | "testRegex": [".int.spec.ts$"], 7 | "transform": { 8 | "^.+\\.(t|j)s$": "ts-jest" 9 | }, 10 | "setupFiles": ["/src/setEnvVar.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/server/src/common/entities/common.entity.ts: -------------------------------------------------------------------------------- 1 | import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm'; 2 | 3 | export abstract class TimestampableEntity { 4 | @CreateDateColumn() 5 | public created_at: Date; 6 | 7 | @UpdateDateColumn() 8 | public updated_at: Date; 9 | 10 | @DeleteDateColumn() 11 | public deleted_at: Date; 12 | } 13 | -------------------------------------------------------------------------------- /packages/server/src/utils/dateTime.util.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | const DAYS = [ 4 | '일요일', 5 | '월요일', 6 | '화요일', 7 | '수요일', 8 | '목요일', 9 | '금요일', 10 | '토요일', 11 | ]; 12 | 13 | export class DateTimeUtil { 14 | static toString(date: Date): string { 15 | return moment(date).format('YYYY-MM-DD-HH:MM-') + DAYS[date.getDay()]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/기능-이슈.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 기능 이슈 3 | about: 기능 설명 4 | title: "[Feature] : 기능명" 5 | labels: "✨ FEAT" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 💁 설명 11 | - Github 이슈 설명 12 | 13 | ## 📑 체크리스트 14 | > 구현해야하는 이슈 체크리스트 15 | 16 | - [ ] 체크 사항 1 17 | - [ ] 체크 사항 2 18 | - [ ] 체크 사항 3 19 | - [ ] 체크 사항 4 20 | 21 | ## 🚧 주의 사항 22 | > 이슈를 구현할 때 유의깊게 살펴볼 사항 23 | 24 | - 주의 사항 1 25 | - 주의 사항 2 26 | -------------------------------------------------------------------------------- /packages/server/src/cafe/mock/option.entity.mock.ts: -------------------------------------------------------------------------------- 1 | export const mockOptions = { 2 | 1: { 3 | id: 1, 4 | name: '옵션1', 5 | price: 500, 6 | category: '옵션 카테고리 1', 7 | }, 8 | 2: { 9 | id: 2, 10 | name: '옵션2', 11 | price: 300, 12 | category: '옵션 카테고리 2', 13 | }, 14 | 3: { 15 | id: 3, 16 | name: '옵션3', 17 | price: 0, 18 | category: '옵션 카테고리 2', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/리팩토링-이슈.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 리팩토링 이슈 3 | about: 리팩토링 설명 4 | title: "[Refactor] : 기능명" 5 | labels: "\U0001F528 REFACTOR" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 💁 설명 11 | - Github 이슈 설명 12 | 13 | ## 📑 체크리스트 14 | > 구현해야하는 이슈 체크리스트 15 | 16 | - [ ] 체크 사항 1 17 | - [ ] 체크 사항 2 18 | - [ ] 체크 사항 3 19 | - [ ] 체크 사항 4 20 | 21 | ## 🚧 주의 사항 22 | > 이슈를 구현할 때 유의깊게 살펴볼 사항 23 | 24 | - 주의 사항 1 25 | - 주의 사항 2 26 | -------------------------------------------------------------------------------- /packages/server/src/auth/entities/token.entity.ts: -------------------------------------------------------------------------------- 1 | import { TimestampableEntity } from 'src/common/entities/common.entity'; 2 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | export class Token extends TimestampableEntity { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | accessToken: string; 11 | 12 | @Column() 13 | refreshToken: string; 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/버그-이슈.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 버그 이슈 3 | about: 버그 설명 4 | title: "[BUG] : 버그명" 5 | labels: "\U0001F41E BUG" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 💁 설명 11 | - 버그 제보 (가능하면 버그 시나리오를 상세히 적어주세요) 12 | 13 | ## 🎬 시나리오 14 | 1. 15 | 2. 16 | 3. 17 | 18 | ## 📢 예상 결과 19 | - 정상동작 했을 때 결과를 적어주세요 20 | 21 | ## 🃏 실제 결과 22 | - 버그가 발생한 현재 동작 결과를 적어주세요 23 | 24 | ## 📸 스크린샷 25 | - 가능하다면 오류메세지나 동작결과에 대해 스크린샷을 첨부해주세요 26 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/server/src/redisCache/redisCache.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { RedisCacheService } from './redisCache.service'; 3 | 4 | @Controller() 5 | export class RedisCacheController { 6 | constructor(private readonly redisCacheService: RedisCacheService) {} 7 | 8 | @Get('/reconnect') 9 | async getRequestedOrders() { 10 | return await this.redisCacheService.reconnect(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/client/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | import { CartMenu, UserRole } from '@/types'; 3 | 4 | export const cartState = atom({ 5 | key: 'cartState', 6 | default: [], 7 | }); 8 | 9 | export const userRoleState = atom({ 10 | key: 'userRoleState', 11 | default: 'UNAUTH', 12 | }); 13 | 14 | export const toastMessageState = atom({ 15 | key: 'toastMessageState', 16 | default: '', 17 | }); 18 | -------------------------------------------------------------------------------- /packages/server/src/cafe/entities/option.entity.ts: -------------------------------------------------------------------------------- 1 | import { TimestampableEntity } from 'src/common/entities/common.entity'; 2 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 3 | 4 | @Entity() 5 | export class Option extends TimestampableEntity { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | name: string; 11 | 12 | @Column() 13 | price: number; 14 | 15 | @Column() 16 | category: string; 17 | } 18 | -------------------------------------------------------------------------------- /packages/client/src/components/Header/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.header` 4 | width: 100%; 5 | margin-bottom: 20px; 6 | padding: 15px; 7 | font-size: ${(props) => props.theme.font.size.lg}; 8 | font-weight: ${(props) => props.theme.font.weight.bold700}; 9 | border-bottom: ${(props) => props.theme.border.default}; 10 | border-bottom-color: ${(props) => props.theme.colors.grey200}; 11 | `; 12 | -------------------------------------------------------------------------------- /packages/client/src/components/Header/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`헤더 컴포넌트 스냅샷 1`] = ` 4 | 5 |
8 |
11 |
14 |

15 | 주문 내역 16 |

17 |
18 |
19 |
20 |
21 | `; 22 | -------------------------------------------------------------------------------- /packages/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { setNestApp } from 'src/setNestApp'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | 5 | declare module 'express-session' { 6 | interface SessionData { 7 | name: string; 8 | email: string; 9 | } 10 | } 11 | 12 | async function bootstrap() { 13 | const app = await NestFactory.create(AppModule); 14 | 15 | setNestApp(app); 16 | await app.listen(8080); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuDetail/components/OrderButton/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | padding: 20px 0; 7 | position: fixed; 8 | bottom: 0; 9 | width: 100%; 10 | min-width: 320px; 11 | max-width: 480px; 12 | background-color: white; 13 | box-shadow: 0px 0px 4px rgba(204, 204, 204, 0.5), 14 | 0px 0px 4px rgba(0, 0, 0, 0.25); 15 | `; 16 | -------------------------------------------------------------------------------- /packages/server/test/mock/create-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "menus": [ 3 | { 4 | "id": 6, 5 | "name": "아메리카노", 6 | "price": 5000, 7 | "options": [], 8 | "size": "tall", 9 | "type": "hot", 10 | "count": 2 11 | }, 12 | { 13 | "id": 7, 14 | "name": "카페라떼", 15 | "price": 6700, 16 | "options": [1, 2], 17 | "size": "grande", 18 | "type": "iced", 19 | "count": 3 20 | } 21 | ], 22 | "cafeId": 1 23 | } 24 | -------------------------------------------------------------------------------- /packages/client/src/components/Toast/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.section` 4 | position: absolute; 5 | padding: 0.6rem 1.5rem; 6 | left: 50%; 7 | bottom: 4rem; 8 | transform: translate(-50%, 0); 9 | color: white; 10 | background-color: rgba(102, 102, 102, 0.9); 11 | border-radius: 50px; 12 | font-size: ${({ theme }) => theme.font.size.xs}; 13 | font-weight: ${({ theme }) => theme.font.weight.bold500}; 14 | z-index: 999; 15 | `; 16 | -------------------------------------------------------------------------------- /packages/client/src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject, APIMethod } from '@/types'; 2 | import axios from 'axios'; 3 | 4 | interface Params { 5 | url: string; 6 | method: APIMethod; 7 | data?: AnyObject; 8 | } 9 | 10 | export async function customFetch({ url, method, data }: Params) { 11 | const api = process.env.REACT_APP_API_SERVER_BASE_URL; 12 | 13 | return await axios({ 14 | method, 15 | url: `${api}${url}`, 16 | data: method !== 'get' && data, 17 | withCredentials: true, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /packages/server/src/cafe/cafe.module.ts: -------------------------------------------------------------------------------- 1 | import { Cafe } from './entities/cafe.entity'; 2 | import { Menu } from './entities/menu.entity'; 3 | import { Module } from '@nestjs/common'; 4 | import { CafeService } from './cafe.service'; 5 | import { CafeController } from './cafe.controller'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([Cafe, Menu])], 10 | controllers: [CafeController], 11 | providers: [CafeService], 12 | }) 13 | export class CafeModule {} 14 | -------------------------------------------------------------------------------- /packages/client/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler, ReactNode } from 'react'; 2 | import { CustomButton } from './styled'; 3 | 4 | interface ButtonProps { 5 | onClick?: MouseEventHandler; 6 | children?: ReactNode | ReactNode[]; 7 | className?: string; 8 | } 9 | 10 | function Button({ onClick, children, className }: ButtonProps) { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | 18 | export default Button; 19 | -------------------------------------------------------------------------------- /packages/client/src/components/Footer/NavigateItem.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler, ReactNode } from 'react'; 2 | import { NavItem } from './styled'; 3 | 4 | interface NavigateItemProps { 5 | onClick: MouseEventHandler; 6 | children: ReactNode[]; 7 | className: string; 8 | } 9 | 10 | function NavigateItem({ onClick, children, className }: NavigateItemProps) { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | } 17 | 18 | export default NavigateItem; 19 | -------------------------------------------------------------------------------- /packages/server/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | # BUILD 2 | FROM node:18.12.1 AS build 3 | 4 | WORKDIR /app 5 | 6 | COPY package*.json ./ 7 | 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | # RUN 15 | FROM node:alpine 16 | 17 | WORKDIR /app 18 | 19 | COPY --from=build /app/node_modules /app/node_modules 20 | COPY --from=build /app/package.json /app/package.json 21 | COPY --from=build /app/pm2.prod.yml /app/pm2.prod.yml 22 | COPY --from=build /app/dist /app/dist 23 | 24 | CMD ["npx", "pm2-runtime", "start", "pm2.prod.yml"] -------------------------------------------------------------------------------- /packages/server/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { UserController } from './user.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { User } from './entities/user.entity'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([User])], 9 | controllers: [UserController], 10 | providers: [UserService], 11 | exports: [UserService], // User repository 만든걸 다른 곳에서도 사용하고 싶은 경우 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /packages/server/src/cafe/entities/cafeMenu.entity.ts: -------------------------------------------------------------------------------- 1 | import { TimestampableEntity } from 'src/common/entities/common.entity'; 2 | import { Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 3 | import { Cafe } from './cafe.entity'; 4 | import { Menu } from './menu.entity'; 5 | 6 | @Entity() 7 | export class CafeMenu extends TimestampableEntity { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @ManyToOne(() => Cafe, (cafe) => cafe.cafeMenus) 12 | cafe: Cafe; 13 | 14 | @ManyToOne(() => Menu) 15 | menu: Menu; 16 | } 17 | -------------------------------------------------------------------------------- /packages/server/src/cafe/entities/menuOption.entity.ts: -------------------------------------------------------------------------------- 1 | import { TimestampableEntity } from 'src/common/entities/common.entity'; 2 | import { Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; 3 | import { Menu } from './menu.entity'; 4 | import { Option } from './option.entity'; 5 | 6 | @Entity() 7 | export class MenuOption extends TimestampableEntity { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @ManyToOne(() => Menu, (menu) => menu.menuOptions) 12 | menu: Menu; 13 | 14 | @ManyToOne(() => Option) 15 | option: Option; 16 | } 17 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/x_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/Cart/components/EmptyCart/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const EmptyCartWrapper = styled.div` 4 | width: 100%; 5 | padding: 1rem 1rem 1rem 1rem; 6 | 7 | p { 8 | padding: 0 0 0.7rem 0; 9 | } 10 | 11 | p.empty-title { 12 | font-weight: ${(props) => props.theme.font.weight.bold700}; 13 | } 14 | 15 | p.description { 16 | padding-right: 5rem; 17 | font-size: ${(props) => props.theme.font.size.sm}; 18 | color: ${(props) => props.theme.colors.grey600}; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /packages/server/src/setEnvVar.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'fs'; 2 | import path from 'path'; 3 | 4 | const TEST_ENV_NAME = '.test.env'; 5 | const envPath = path.join(process.env.PWD, TEST_ENV_NAME); 6 | if (!existsSync(envPath)) { 7 | throw new Error('환경변수 로드가 안됐습니다.'); 8 | } 9 | const env = readFileSync(path.join(process.env.PWD, TEST_ENV_NAME)).toString(); 10 | 11 | const lines = env.split('\n').filter((el) => el !== ''); 12 | 13 | lines.forEach((el) => { 14 | const [key, value] = el.split('='); 15 | 16 | process.env[key] = value; 17 | }); 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:import/recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "prettier" 13 | ], 14 | "overrides": [], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": "latest", 18 | "sourceType": "module" 19 | }, 20 | "plugins": ["@typescript-eslint"], 21 | "rules": { 22 | "import/no-unresolved": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/withdrawal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/change.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuDetail/components/MenuInformation/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | import { Img, MenuInfoContainer } from './styled'; 4 | import { MenuInfo } from '@/types'; 5 | 6 | interface Props { 7 | menu: MenuInfo; 8 | } 9 | 10 | function MenuInformation({ menu }: Props) { 11 | return ( 12 | <> 13 | 음료 14 | 15 |

{menu.name}

16 |

{menu.description}

17 |
18 | 19 | ); 20 | } 21 | 22 | export default memo(MenuInformation); 23 | -------------------------------------------------------------------------------- /packages/server/src/order/dto/OrderStatusRes.dto.ts: -------------------------------------------------------------------------------- 1 | import { ORDER_STATUS } from './../enum/orderStatus.enum'; 2 | import { Exclude, Expose } from 'class-transformer'; 3 | 4 | export class OrderStatusResDto { 5 | @Exclude() private readonly _id: number; 6 | @Exclude() private readonly _orderStatus: ORDER_STATUS; 7 | 8 | constructor(orderId: number, status: ORDER_STATUS) { 9 | this._id = orderId; 10 | this._orderStatus = status; 11 | } 12 | 13 | @Expose() 14 | get id(): number { 15 | return this._id; 16 | } 17 | 18 | @Expose() 19 | get orderStatus(): ORDER_STATUS { 20 | return this._orderStatus; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/server/src/utils/getMySQLTestTypeOrmModule.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModule } from '@nestjs/typeorm'; 2 | import * as path from 'path'; 3 | 4 | // test db 접속용 5 | export function getMySQLTestTypeOrmModule() { 6 | const entityPath = path.join(process.env.PWD, 'src/**/*.entity{.ts,.js}'); 7 | return TypeOrmModule.forRoot({ 8 | type: 'mysql', 9 | host: process.env.MYSQL_HOST, 10 | port: parseInt(process.env.MYSQL_PORT), 11 | username: process.env.MYSQL_USERNAME, 12 | password: process.env.MYSQL_PASSWORD, 13 | database: process.env.MYSQL_DATABASE, 14 | entities: [entityPath], 15 | synchronize: true, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/order.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuDetail/components/TemperatureSelector/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`음료 타입(핫, 아이스) 선택 컴포넌트 스냅샷 1`] = ` 4 | 5 |
8 |
11 |
14 | 17 | HOT 18 | 19 | 22 | ICED 23 | 24 |
25 |
26 |
27 |
28 | `; 29 | -------------------------------------------------------------------------------- /packages/server/src/order/dto/updateOrderReq.dto.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from 'class-transformer'; 2 | import { Order } from '../entities/order.entity'; 3 | import { ORDER_STATUS } from '../enum/orderStatus.enum'; 4 | 5 | export class UpdateOrderReqDto { 6 | @Exclude() private readonly _id: number; 7 | @Exclude() private readonly _newStatus: ORDER_STATUS; 8 | 9 | constructor(order: Order) { 10 | this._id = order.id; 11 | this._newStatus = order.status; 12 | } 13 | 14 | @Expose() 15 | get id(): number { 16 | return this._id; 17 | } 18 | 19 | @Expose() 20 | get newStatus(): ORDER_STATUS { 21 | return this._newStatus; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/client/src/mocks/handlers/menu.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | import { menuDetailData, menuListData } from '@/mocks/data/menu'; 3 | 4 | const api = process.env.REACT_APP_API_SERVER_BASE_URL; 5 | 6 | export const menuHandlers = [ 7 | rest.get(`${api}/cafe/1/menus`, (req, res, next) => { 8 | return res(next.json(menuListData)); 9 | }), 10 | // 메뉴 상세 정보 조회 11 | rest.get(`${api}/cafe/menu/:menuId`, (req, res, next) => { 12 | const { menuId } = req.params; 13 | 14 | switch (menuId) { 15 | case '1': 16 | return res(next.json(menuDetailData)); 17 | default: 18 | return res(next.status(400)); 19 | } 20 | }), 21 | ]; 22 | -------------------------------------------------------------------------------- /packages/client/src/components/Toast/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { useRecoilState } from 'recoil'; 3 | 4 | import { toastMessageState } from '@/stores'; 5 | import { Container } from './styled'; 6 | 7 | function Toast() { 8 | const [toastMessage, setToastMessage] = useRecoilState(toastMessageState); 9 | const timer = useRef(null); 10 | 11 | useEffect(() => { 12 | timer.current = setTimeout(() => setToastMessage(''), 2000); 13 | 14 | return () => { 15 | clearTimeout(timer.current); 16 | }; 17 | }); 18 | 19 | return <>{toastMessage !== '' && {toastMessage}}; 20 | } 21 | 22 | export default Toast; 23 | -------------------------------------------------------------------------------- /packages/client/src/components/Button/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const CustomButton = styled.button` 4 | width: 100%; 5 | padding: 0.3rem 1rem 0.3rem 1rem; 6 | background-color: ${(props) => props.theme.colors.primary}; 7 | color: white; 8 | font-size: ${(props) => props.theme.font.size.xs}; 9 | font-weight: ${(props) => props.theme.font.weight.bold700}; 10 | border: none; 11 | border-radius: 50px; 12 | cursor: pointer; 13 | 14 | &.wd-80 { 15 | width: 80%; 16 | } 17 | 18 | &.wd-fit { 19 | width: fit-content; 20 | } 21 | 22 | &.disabled { 23 | background-color: ${(props) => props.theme.colors.grey200}; 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuDetail/components/MenuInformation/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Img = styled.img` 4 | width: 100%; 5 | object-fit: cover; 6 | `; 7 | 8 | export const MenuInfoContainer = styled.section` 9 | margin: 1rem; 10 | 11 | & > .price { 12 | font-size: ${({theme}) => theme.font.size.lg}; 13 | } 14 | 15 | h2{ 16 | font-size: ${({theme}) => theme.font.size.xl}; 17 | font-weight: ${({theme}) => theme.font.weight.bold500}; 18 | } 19 | 20 | & > .description { 21 | margin: 1rem 0; 22 | color: ${({theme}) => theme.colors.grey600}; 23 | font-size: ${({theme}) => theme.font.size.sm}; 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /packages/server/src/middleware/logger.http.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware, Logger } from '@nestjs/common'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | @Injectable() 5 | export class LoggerMiddleware implements NestMiddleware { 6 | private logger = new Logger('HTTP'); 7 | 8 | use(req: Request, res: Response, next: NextFunction) { 9 | const { ip, method, originalUrl } = req; 10 | const userAgent = req.get('user-agent') || ''; 11 | res.on('finish', () => { 12 | const { statusCode } = res; 13 | this.logger.log( 14 | `${method} ${statusCode} - ${originalUrl} - ${ip} - ${userAgent}` 15 | ); 16 | }); 17 | next(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/signout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/client/src/components/Header/index.test.tsx: -------------------------------------------------------------------------------- 1 | import Layout from '@/Layout'; 2 | import { render, screen } from '@testing-library/react'; 3 | import Header from '.'; 4 | 5 | const setup = ({ title }: { title: string }) => { 6 | const { asFragment } = render( 7 | 8 |
9 | 10 | ); 11 | 12 | return { asFragment }; 13 | }; 14 | 15 | describe('헤더 컴포넌트', () => { 16 | const title = '주문 내역'; 17 | it('요소 존재 여부', () => { 18 | setup({ title }); 19 | 20 | screen.getByText(title); 21 | }); 22 | 23 | it('스냅샷', () => { 24 | const { asFragment } = setup({ title }); 25 | 26 | expect(asFragment()).toMatchSnapshot(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/client/src/hooks/useFetchOrderList.tsx: -------------------------------------------------------------------------------- 1 | import { QUERY_KEYS } from '@/constants'; 2 | import { UserRole } from '@/types'; 3 | import { customFetch } from '@/utils/fetch'; 4 | import { useQuery } from '@tanstack/react-query'; 5 | 6 | interface Params { 7 | userRole: UserRole; 8 | url: string; 9 | } 10 | 11 | function useFetchOrderList({ userRole, url }: Params) { 12 | const queryResponse = useQuery( 13 | [QUERY_KEYS.ORDER_LIST], 14 | async () => { 15 | const res = await customFetch({ url, method: 'GET' }); 16 | return res.data; 17 | }, 18 | { 19 | refetchInterval: 2000, 20 | } 21 | ); 22 | 23 | return queryResponse; 24 | } 25 | 26 | export default useFetchOrderList; 27 | -------------------------------------------------------------------------------- /packages/client/src/components/OrderDateList/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Container = styled.section<{ noBottomPadding?: boolean }>` 4 | padding: 0 0 5 | ${({ noBottomPadding }) => (noBottomPadding === true ? '0' : '3rem')} 0; 6 | font-size: ${({ theme }) => theme.font.size.sm}; 7 | 8 | .date-title { 9 | font-size: ${({ theme }) => theme.font.size.lg}; 10 | } 11 | `; 12 | 13 | export const ItemContainer = styled.div` 14 | padding: 1rem; 15 | width: 100%; 16 | `; 17 | 18 | export const NoOrderContainer = styled.div` 19 | margin: 0.5rem 1rem; 20 | padding: 1.5rem; 21 | border-radius: 10px; 22 | background-color: ${({ theme }) => theme.colors.fourth}; 23 | `; 24 | -------------------------------------------------------------------------------- /packages/nginx/configs/scgi_params: -------------------------------------------------------------------------------- 1 | 2 | scgi_param REQUEST_METHOD $request_method; 3 | scgi_param REQUEST_URI $request_uri; 4 | scgi_param QUERY_STRING $query_string; 5 | scgi_param CONTENT_TYPE $content_type; 6 | 7 | scgi_param DOCUMENT_URI $document_uri; 8 | scgi_param DOCUMENT_ROOT $document_root; 9 | scgi_param SCGI 1; 10 | scgi_param SERVER_PROTOCOL $server_protocol; 11 | scgi_param REQUEST_SCHEME $scheme; 12 | scgi_param HTTPS $https if_not_empty; 13 | 14 | scgi_param REMOTE_ADDR $remote_addr; 15 | scgi_param REMOTE_PORT $remote_port; 16 | scgi_param SERVER_PORT $server_port; 17 | scgi_param SERVER_NAME $server_name; 18 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "declaration": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "esModuleInterop": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuList/components/MenuItem/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const MenuWrapper = styled.li` 4 | display: flex; 5 | flex-direction: row; 6 | gap: 1rem; 7 | width: 100%; 8 | margin: 1rem 0 1rem 0; 9 | cursor: pointer; 10 | transition: transform; 11 | transition-duration: 0.2s; 12 | 13 | &:hover{ 14 | transform: scale(1.02); 15 | } 16 | `; 17 | 18 | export const MenuImg = styled.img` 19 | width: 5rem; 20 | border-radius: 50%; 21 | `; 22 | 23 | export const MenuInfoWrapper = styled.div` 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: center; 27 | gap: 0.5rem; 28 | font-size: ${(props) => props.theme.font.size.sm} 29 | ` 30 | -------------------------------------------------------------------------------- /packages/server/src/auth/guard/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | import { PUBLIC_KEY } from '../decorators/public.decorator'; 5 | 6 | @Injectable() 7 | export class JwtGuard extends AuthGuard('jwt') { 8 | constructor(private reflector: Reflector) { 9 | super(); 10 | } 11 | 12 | canActivate(context: ExecutionContext) { 13 | const isPublic = this.reflector.getAllAndOverride(PUBLIC_KEY, [ 14 | context.getHandler(), 15 | context.getClass(), 16 | ]); 17 | if (isPublic) { 18 | return true; 19 | } 20 | 21 | return super.canActivate(context); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/Cart/components/EmptyCart/index.tsx: -------------------------------------------------------------------------------- 1 | import Button from 'components/Button'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { EmptyCartWrapper } from './styled'; 4 | 5 | function EmptyCart() { 6 | const navigate = useNavigate(); 7 | 8 | const handleClickMenu = () => { 9 | navigate('/menu'); 10 | }; 11 | 12 | return ( 13 | 14 |

장바구니가 비어있습니다.

15 |

16 | 원하는 메뉴를 장바구니에 담고 한번에 주문해보세요. 17 |

18 | 21 |
22 | ); 23 | } 24 | 25 | export default EmptyCart; 26 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuDetail/components/Amount/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | 3 | import Layout from '@/Layout'; 4 | import MenuDetailContextProvider from '@/stores/MenuDetail'; 5 | import Amount from '.'; 6 | 7 | const setup = async () => { 8 | const { asFragment } = render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | return { asFragment }; 17 | }; 18 | 19 | describe('메뉴 가격, 수량 선택 컴포넌트', () => { 20 | it('요소 존재 여부', async () => { 21 | await setup(); 22 | 23 | screen.getByText('1'); 24 | screen.getByText('6,000원'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/server/src/order/order.v1.module.ts: -------------------------------------------------------------------------------- 1 | import { OrderMenu } from './entities/orderMenu.entity'; 2 | import { Module } from '@nestjs/common'; 3 | import { OrderService } from './order.service'; 4 | import { OrderController } from './order.v1.controller'; 5 | import { Order } from './entities/order.entity'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { MenuOption } from 'src/cafe/entities/menuOption.entity'; 8 | import { RedisCacheModule } from 'src/redisCache/redisCache.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([Order, MenuOption, OrderMenu]), 13 | RedisCacheModule, 14 | ], 15 | controllers: [OrderController], 16 | providers: [OrderService], 17 | }) 18 | export class OrderModuleV1 {} 19 | -------------------------------------------------------------------------------- /packages/server/src/order/order.v2.module.ts: -------------------------------------------------------------------------------- 1 | import { OrderMenu } from './entities/orderMenu.entity'; 2 | import { Module } from '@nestjs/common'; 3 | import { OrderService } from './order.service'; 4 | import { OrderController } from './order.v2.controller'; 5 | import { Order } from './entities/order.entity'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { MenuOption } from 'src/cafe/entities/menuOption.entity'; 8 | import { RedisCacheModule } from 'src/redisCache/redisCache.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([Order, MenuOption, OrderMenu]), 13 | RedisCacheModule, 14 | ], 15 | controllers: [OrderController], 16 | providers: [OrderService], 17 | }) 18 | export class OrderModuleV2 {} 19 | -------------------------------------------------------------------------------- /packages/server/src/order/order.v3.module.ts: -------------------------------------------------------------------------------- 1 | import { OrderMenu } from './entities/orderMenu.entity'; 2 | import { Module } from '@nestjs/common'; 3 | import { OrderService } from './order.service'; 4 | import { OrderController } from './order.v3.controller'; 5 | import { Order } from './entities/order.entity'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { MenuOption } from 'src/cafe/entities/menuOption.entity'; 8 | import { RedisCacheModule } from 'src/redisCache/redisCache.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | TypeOrmModule.forFeature([Order, MenuOption, OrderMenu]), 13 | RedisCacheModule, 14 | ], 15 | controllers: [OrderController], 16 | providers: [OrderService], 17 | }) 18 | export class OrderModuleV3 {} 19 | -------------------------------------------------------------------------------- /packages/nginx/configs/uwsgi_params: -------------------------------------------------------------------------------- 1 | 2 | uwsgi_param QUERY_STRING $query_string; 3 | uwsgi_param REQUEST_METHOD $request_method; 4 | uwsgi_param CONTENT_TYPE $content_type; 5 | uwsgi_param CONTENT_LENGTH $content_length; 6 | 7 | uwsgi_param REQUEST_URI $request_uri; 8 | uwsgi_param PATH_INFO $document_uri; 9 | uwsgi_param DOCUMENT_ROOT $document_root; 10 | uwsgi_param SERVER_PROTOCOL $server_protocol; 11 | uwsgi_param REQUEST_SCHEME $scheme; 12 | uwsgi_param HTTPS $https if_not_empty; 13 | 14 | uwsgi_param REMOTE_ADDR $remote_addr; 15 | uwsgi_param REMOTE_PORT $remote_port; 16 | uwsgi_param SERVER_PORT $server_port; 17 | uwsgi_param SERVER_NAME $server_name; 18 | -------------------------------------------------------------------------------- /packages/server/scripts/prod.deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 이미지 pull 4 | docker pull $1/$2:latest 5 | 6 | # 컨테이너 내리고, 띄우기 7 | if [[ $(docker ps -a --filter="name=$3" --filter "status=running" | grep -w $3) ]]; then 8 | docker stop $3 9 | fi 10 | if [[ $(docker ps -a --filter="name=$3" --filter "status=created" | grep -w $3) ]]; then 11 | docker rm $3 12 | fi 13 | if [[ $(docker ps -a --filter="name=$3" --filter "status=exited" | grep -w $3) ]]; then 14 | docker rm $3 15 | fi 16 | 17 | docker create -p 8080:8080 --name $3 $1/$2:latest 18 | 19 | docker cp ~/server/src/.prod.env $3:/app 20 | 21 | docker start $3 22 | 23 | if [[ $(docker ps -a --filter="name=$3" --filter "status=running" | grep -w $3) ]]; then 24 | echo 'prod-deploy success' 25 | fi -------------------------------------------------------------------------------- /packages/client/src/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/server/scripts/dev.deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 이미지 pull 4 | docker pull $1/$2:latest 5 | 6 | # 컨테이너 내리고, 띄우기 7 | if [[ $(docker ps -a --filter="name=$3" --filter "status=running" | grep -w $3) ]]; then 8 | docker stop $3 9 | fi 10 | if [[ $(docker ps -a --filter="name=$3" --filter "status=created" | grep -w $3) ]]; then 11 | docker rm $3 12 | fi 13 | if [[ $(docker ps -a --filter="name=$3" --filter "status=exited" | grep -w $3) ]]; then 14 | docker rm $3 15 | fi 16 | 17 | docker create -p 8080:8080 -v ~/server/src:/app/src --name $3 $1/$2:latest 18 | 19 | docker cp ~/server/src/.dev.env $3:/app 20 | 21 | docker start $3 22 | 23 | if [[ $(docker ps -a --filter="name=$3" --filter "status=running" | grep -w $3) ]]; then 24 | echo 'dev-deploy success' 25 | fi 26 | -------------------------------------------------------------------------------- /packages/server/src/order/dto/create-order.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { 3 | ArrayMinSize, 4 | IsArray, 5 | IsNumber, 6 | ValidateNested, 7 | } from 'class-validator'; 8 | import { OrderMenuDto } from './orderMenu.dto'; 9 | export class CreateOrderDto { 10 | @IsArray() 11 | @ValidateNested({ each: true }) 12 | @ArrayMinSize(1) 13 | @Type(() => OrderMenuDto) 14 | menus: OrderMenuDto[]; 15 | 16 | @IsNumber() 17 | cafeId: number; 18 | 19 | static of({ menus, cafeId }): CreateOrderDto { 20 | const createOrderDto = new CreateOrderDto(); 21 | createOrderDto.cafeId = cafeId; 22 | createOrderDto.menus = menus.map((menu) => { 23 | return OrderMenuDto.of(menu); 24 | }); 25 | return createOrderDto; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/nginx/configs/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | user nginx; 3 | worker_processes auto; 4 | 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main_log_format '$remote_addr - $remote_user [$time_local] ' 18 | '"$request" $status $body_bytes_sent ' 19 | '"$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log off; 22 | log_not_found off; 23 | 24 | error_log /var/log/nginx/error.log crit; 25 | 26 | sendfile on; 27 | 28 | keepalive_timeout 65; 29 | 30 | gzip on; 31 | 32 | include /etc/nginx/conf.d/*.conf; 33 | } 34 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuList/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const MenuListPageWrapper = styled.div` 4 | width: 100%; 5 | `; 6 | 7 | export const MenuListWrapper = styled.ul` 8 | margin: 0 2rem 6.5rem 2rem; 9 | `; 10 | 11 | export const CategoryBarWrapper = styled.ul` 12 | display:flex; 13 | overflow-x: auto; 14 | white-space: nowrap; 15 | border-bottom: 1px solid ${(props) => props.theme.colors.grey200}; 16 | 17 | &::-webkit-scrollbar{ 18 | display:none; 19 | } 20 | `; 21 | 22 | export const CategoryItem = styled.li` 23 | display: inline-block; 24 | padding: 0.2rem 0.5rem 0.2rem 0.5rem; 25 | color: ${(props) => props.theme.colors.secondary}; 26 | font-size: ${(props) => props.theme.font.size.sm}; 27 | cursor: pointer; 28 | `; 29 | -------------------------------------------------------------------------------- /packages/server/src/cafe/dto/CafeMenuRes.dto.ts: -------------------------------------------------------------------------------- 1 | import { Exclude, Expose } from 'class-transformer'; 2 | import { Cafe } from './../entities/cafe.entity'; 3 | import { MenuDto } from './MenuRes.dto'; 4 | 5 | export class CafeMenuResDto { 6 | @Exclude() private readonly _id: number; 7 | @Exclude() readonly _name: string; 8 | @Exclude() readonly _menus: MenuDto[]; 9 | 10 | constructor(cafe: Cafe) { 11 | this._id = cafe.id; 12 | this._name = cafe.name; 13 | this._menus = cafe.cafeMenus.map((cafeMenu) => MenuDto.from(cafeMenu.menu)); 14 | } 15 | 16 | @Expose() 17 | get id(): number { 18 | return this._id; 19 | } 20 | 21 | @Expose() 22 | get name(): string { 23 | return this._name; 24 | } 25 | 26 | @Expose() 27 | get menus(): MenuDto[] { 28 | return this._menus; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/client/src/hooks/useMenuListData.tsx: -------------------------------------------------------------------------------- 1 | import { QUERY_KEYS } from '@/constants'; 2 | import { Menu } from '@/types'; 3 | import useCustomQuery from './useCustomQuery'; 4 | 5 | function useMenuListData() { 6 | const data = useCustomQuery({ 7 | queryKey: [QUERY_KEYS.MENU_LIST_DATA], 8 | url: '/cafe/1/menus', 9 | }); 10 | 11 | if (data) { 12 | return { 13 | menuList: data.data.menus, 14 | categoryList: makeCategoryList(data.data.menus), 15 | }; 16 | } 17 | 18 | return { 19 | menuList: [], 20 | categoryList: [], 21 | }; 22 | } 23 | 24 | function makeCategoryList(menuList: Menu[]) { 25 | if (menuList) { 26 | return Array.from( 27 | new Set(['전체'].concat(menuList.map((menu: Menu) => menu.category))) 28 | ); 29 | } 30 | } 31 | 32 | export default useMenuListData; 33 | -------------------------------------------------------------------------------- /packages/client/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 금액 세자리 단위 콤마 추가 3 | */ 4 | export const getPriceComma = (price: number | string) => { 5 | return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); 6 | }; 7 | 8 | /** 9 | * 문자의 첫 글자만 대문자로 변경 10 | */ 11 | export const getFirstUpper = (text: string) => { 12 | return `${text.slice(0, 1).toUpperCase()}${text.slice(1)}`; 13 | }; 14 | 15 | /** 16 | * 문자열 날짜를 숫자로 변환하여 반환 17 | * 18 | * @params (string) YYYY-MM-DD 19 | * @returns (number) YYYYMMDD 20 | */ 21 | export const getDateNumber = (date: string) => 22 | Number(date.slice(0, 10).replace(/-/g, '')); 23 | 24 | /** 25 | * 주문 내역 날짜를 내림차순으로 정렬 26 | */ 27 | export const sortDateDesc = (prev: string, curr: string) => { 28 | if (prev === '오늘') return -1; 29 | return getDateNumber(curr) - getDateNumber(prev); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuList/components/SnackBar/styled.ts: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const SnackBarWrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | flex-direction: row; 8 | position: fixed; 9 | bottom: 3rem; 10 | width: 100%; 11 | min-width: 320px; 12 | max-width: 480px; 13 | height: 2.5rem; 14 | padding: 0 1rem 0 1rem; 15 | background-color: ${(props) => props.theme.colors.secondary}; 16 | color: white; 17 | font-size: ${(props) => props.theme.font.size.xs}; 18 | font-weight: ${(props) => props.theme.font.weight.bold500}; 19 | `; 20 | 21 | export const CartWrapper = styled.div` 22 | display: flex; 23 | flex-direction: row; 24 | align-items: center; 25 | gap: 0.3rem; 26 | cursor: pointer; 27 | `; 28 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuList/components/SnackBar/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { ReactComponent as Cart } from 'icons/cart.svg'; 4 | import { getCartCount } from 'utils/localStorage'; 5 | import { SnackBarWrapper, CartWrapper } from './styled'; 6 | 7 | function SnackBar() { 8 | const navigate = useNavigate(); 9 | 10 | const handleClickCart = () => { 11 | navigate('/cart'); 12 | }; 13 | 14 | return ( 15 | 16 |

주문할 매장을 선택해주세요

17 | 18 |

{`장바구니 ${getCartCount()}개`}

19 | 20 |
21 |
22 | ); 23 | } 24 | 25 | export default memo(SnackBar); 26 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/cart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/server/src/auth/strategy/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { ExtractJwt, Strategy } from 'passport-jwt'; 5 | @Injectable() 6 | export class JwtStrategy extends PassportStrategy(Strategy) { 7 | constructor(private readonly configService: ConfigService) { 8 | super({ 9 | jwtFromRequest: ExtractJwt.fromExtractors([ 10 | (request) => { 11 | return request?.cookies?.accessToken; 12 | }, 13 | ]), 14 | ignoreExpiration: false, 15 | secretOrKey: configService.get('JWT_SECRET'), 16 | signOptions: { expiresIn: '1h' }, 17 | }); 18 | } 19 | 20 | async validate(payload) { 21 | return { id: parseInt(payload.id), userRole: payload.userRole }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/client/src/pages/customer/MenuList/components/MenuItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { Menu } from '@/types'; 3 | import { getPriceComma } from '@/utils'; 4 | import { MenuImg, MenuWrapper, MenuInfoWrapper } from './styled'; 5 | 6 | function MenuItem(props: Menu) { 7 | const navigate = useNavigate(); 8 | 9 | const handleClickMenuItem = () => { 10 | navigate(`/menu/${props.id}`); 11 | }; 12 | 13 | return ( 14 | 19 | 20 | 21 |

{props.name}

22 |

{getPriceComma(props.price)}원

23 |
24 |
25 | ); 26 | } 27 | 28 | export default MenuItem; 29 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/down_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/client/src/assets/icons/left_arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/server/src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { JwtGuard } from './../auth/guard/jwt.guard'; 2 | import { Controller, Body, Patch, Get, UseGuards, Req } from '@nestjs/common'; 3 | import { UserService } from './user.service'; 4 | import { UpdateUserDto } from './dto/update-user.dto'; 5 | import { Request } from 'express'; 6 | import { JwtPayload } from 'src/auth/interfaces/jwtPayload'; 7 | import { User } from './entities/user.entity'; 8 | 9 | @Controller() 10 | export class UserController { 11 | constructor(private readonly userService: UserService) {} 12 | 13 | // 회원정보(닉네임) 수정 14 | @Patch() 15 | update(@Body() updateUserDto: UpdateUserDto) { 16 | return; 17 | } 18 | 19 | @Get() 20 | @UseGuards(JwtGuard) 21 | async findOne(@Req() req: Request): Promise { 22 | const { id } = req.user as JwtPayload; 23 | return await this.userService.findById(id); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/client/src/components/LeftArrow/styled.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@emotion/react'; 2 | import styled from '@emotion/styled'; 3 | 4 | import { ReactComponent as SVG } from 'icons/left_arrow.svg'; 5 | import { Props } from '.'; 6 | 7 | export const LeftArrowSVG = styled(SVG)` 8 | position: absolute; 9 | top: ${({ top }) => top}rem; 10 | left: ${({ left }) => left}rem; 11 | width: ${({ width }) => width}rem; 12 | height: ${({ height }) => height}rem; 13 | color: ${(props) => props.theme.colors.grey600}; 14 | cursor: pointer; 15 | 16 | & path { 17 | fill: ${({ color, theme }: { color: string; theme: Theme }) => { 18 | if ( 19 | color === 'primary' || 20 | color === 'secondary' || 21 | color === 'tertiary' || 22 | color === 'fourth' 23 | ) 24 | return theme.colors[color]; 25 | return color; 26 | }}; 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /.github/workflows/fe-dev-ci.yml: -------------------------------------------------------------------------------- 1 | name: Frontend dev ci 2 | 3 | on: 4 | pull_request: 5 | branches: ['dev'] 6 | paths: 7 | - 'packages/client/**' 8 | - '.github/workflows/fe-dev-ci.yml' 9 | 10 | jobs: 11 | ci: 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: './packages/client' 16 | 17 | steps: 18 | - name: checkout to the branch 19 | uses: actions/checkout@v3 20 | 21 | - name: node set-up 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18.6.0 25 | 26 | - name: Install dependencies 27 | run: npm install 28 | 29 | - name: Inject Environment Variables 30 | env: 31 | FE_ENV: ${{ secrets.FE_ENV }} 32 | run: echo "$FE_ENV" > .env 33 | 34 | - name: Test 35 | run: npm run test 36 | env: 37 | CI: false 38 | -------------------------------------------------------------------------------- /packages/client/src/components/LeftArrow/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, memo } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { LeftArrowSVG } from './styled'; 4 | 5 | export interface Props { 6 | color: 'primary' | 'secondary' | 'tertiary' | 'fourth' | string; 7 | top: number; 8 | left: number; 9 | width?: number; 10 | height?: number; 11 | } 12 | 13 | function LeftArrow({ color, top, left, width = 1, height = 1 }: Props) { 14 | const navigate = useNavigate(); 15 | 16 | const handleClickBack = useCallback(() => { 17 | // 뒤로가기, window.history.popState() 18 | navigate(-1); 19 | }, [navigate]); 20 | 21 | return ( 22 | 30 | ); 31 | } 32 | 33 | export default memo(LeftArrow); 34 | -------------------------------------------------------------------------------- /packages/client/src/pages/manager/AcceptList/index.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | 3 | import OrderDateList from 'components/OrderDateList'; 4 | import Footer from 'components/Footer'; 5 | import Header from 'components/Header'; 6 | 7 | import { QUERY_KEYS } from '@/constants'; 8 | import { customFetch } from '@/utils/fetch'; 9 | import { Container } from './styled'; 10 | 11 | function AcceptList() { 12 | const { data: list = {} } = useQuery([QUERY_KEYS.ACCEPTED_LIST], async () => { 13 | const res = await customFetch({ url: '/order/accepted', method: 'GET' }); 14 | return res.data; 15 | }); 16 | 17 | return ( 18 | 19 |
20 | {list.orders && ( 21 | 22 | )} 23 |