TIL

Node.js 백오피스 팀 프로젝트 (1)

황민도 2024. 6. 19. 23:06

내일배움캠프 스파르타 코딩클럽

 

< 디렉터리 >

 

< README.md >

# 🍜 육개장 (pet-sitter-platform backend)

안녕하세요 팀 6조 육개장입니다.

## (●'◡'●) 팀 소개
- 팀장 : 🫅모전하
- 팀원 : 👨‍🎓조민수
- 팀원 : 👩‍🎓박서진
- 팀원 : 👨‍🎓황민도
- 팀원 : 👩‍🎓이연서

## 프로젝트 소개
 - 프로젝트 명 : 백오피 프로젝트 - petching
 - 소개
    - 한 줄 정리 : 유저와 펫시터를 연결해주는 플랫폼

## 🚦 Project Rules

 # 개발환경
 - OS: Window / Mac
 - Code editor: Visual Studio Code
 - Client-Tool : Insomnia
 - DB-Tool: DBeaver
 - Database: AWS/RDS (MySQL)
 - Server: AWS/EC2

 # 개발언어
 - Front-End : Html, CSS, Javascript
 - Back-End : Javascript
    - Node.js, Express.js
 - Database: MySQL
    - ORM: Prisma
 
## 역할
- 모전하 : 팀장!!!! 프로필 기능, 프로필 사지 업로드
- 조민수 : 발표!!!! 펫시터 기능, 북마크 기능
- 박서진 : 리뷰 기능
- 황민도 : 회원가입 및 로그인 기능
- 이연서 : 예약 기능, 펫시터 기능


## 와이어 프레임 
![와이어프레임](https://github.com/ysys29/petching/assets/167045109/7c0acbc7-45a8-45c9-9044-3cad824bd9c4)


## API 명세서
![스크린샷 2024-06-20 122138](https://github.com/ysys29/petching/assets/167045109/9785874b-9a97-41c2-9c5c-c3bf8e399432)
![스크린샷 2024-06-20 122213](https://github.com/ysys29/petching/assets/167045109/e7f22075-de97-4ec1-9f24-dfa4f813c2fd)
![스크린샷 2024-06-20 122224](https://github.com/ysys29/petching/assets/167045109/f9bd9d35-c505-4dc0-a19e-69185180381c)




## ERD
![스크린샷 2024-06-20 122512](https://github.com/ysys29/petching/assets/167045109/d967a0ca-3e95-4924-ae1b-47c31f248714)

## Github Rules
![스크린샷 2024-06-20 122604](https://github.com/ysys29/petching/assets/167045109/587c93d5-a7bc-463b-b8e2-dee7d0a14972)


## Code Convention
![스크린샷 2024-06-20 122649](https://github.com/ysys29/petching/assets/167045109/1e1bcf89-5f8f-4c3f-a0f5-15fa72fe7e76)


## 실행 방법

- 필요한 패키지 설치

```sh
yarn
```

- 서버 실행 (개발용)

```sh
yarn dev
```

 

< package.json >

{
  "name": "petching",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js",
    "seed": "node prisma/seed.js",
    "format": "prettier --write *.js **/*.js",
    "test": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest --forceExit",
    "test:unit": "cross-env NODE_ENV=test NODE_OPTIONS=--experimental-vm-modules jest __tests__/unit --forceExit"
  },
  "dependencies": {
    "@aws-sdk/client-s3": "^3.600.0",
    "@prisma/client": "^5.15.0",
    "aws-sdk": "^2.1644.0",
    "bcrypt": "^5.1.1",
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "joi": "^17.13.1",
    "jsonwebtoken": "^9.0.2",
    "multer": "^1.4.5-lts.1",
    "multer-s3": "^3.0.1",
    "prisma": "^5.15.0",
    "winston": "^3.13.0"
  },
  "devDependencies": {
    "@jest/globals": "^29.7.0",
    "cross-env": "^7.0.3",
    "jest": "^29.7.0",
    "nodemon": "^3.1.3",
    "prettier": "^3.3.2"
  }
}

 

< .prettierrc.json >

{
  "trailingComma": "es5",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "arrowParens": "always"
}

 

< .gitignore >

node_modules
# Keep environment variables out of version control
.env

 

< .env.example >

DATABASE_URL =
SERVER_PORT =
ACCESS_TOKEN_SECRET =
REFRESH_TOKEN_SECRET =
AWS_ACCESS_KEY_ID =
AWS_SECRET_ACCESS_KEY =
AWS_REGION =
AWS_S3_BUCKET_NAME =

 

< jest.config.js >

export default {
  // 해당 패턴에 일치하는 경로가 존재할 경우 테스트를 하지 않고 넘어갑니다.
  testPathIgnorePatterns: ['/node_modules/'],
  // 테스트 실행 시 각 TestCase에 대한 출력을 해줍니다.
  verbose: true,
  // *.test.js, *.spec.js 파일만 테스트 파일로 인식해서 실행합니다.
  testRegex: '.*\\.(test|spec)\\.js$',
  transform: {},
};

 

< schema.prisma >

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id           Int     @id @default(autoincrement()) @map("id")
  email        String  @unique @map("email")
  password     String  @map("password")
  name         String  @map("name")
  introduce    String? @map("introduce") @db.Text
  profileImage String? @map("profile_image")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  booking      Booking[]
  refreshToken RefreshToken?
  bookmark     Bookmark[]
  review       Review[]

  @@map("users")
}

model Petsitter {
  id           Int     @id @default(autoincrement()) @map("id")
  name         String  @map("name")
  experience   Int     @default(0) @map("experience")
  email        String  @unique @map("email")
  password     String  @map("password")
  profileImage String? @map("profile_image")
  introduce    String  @default("안녕하세요.") @map("introduce")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  booking           Booking[]
  petsitterService  PetsitterService[]
  petsitterLocation PetsitterLocation[]
  review            Review[]
  bookmark          Bookmark[]

  @@map("petsitters")
}

model PetsitterService {
  id          Int         @id @default(autoincrement()) @map("id")
  petsitterId Int         @map("petsitter_id")
  animalType  AnimalType  @map("animal_type")
  serviceType ServiceType @map("service_type")
  price       Int         @map("price")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  petsitter Petsitter @relation(fields: [petsitterId], references: [id], onDelete: Cascade)

  @@map("petsitter_services")
}

enum AnimalType {
  DOG
  CAT
  ETC
}

enum ServiceType {
  WALK
  SHOWER
  PICKUP
  FEED
}

model PetsitterLocation {
  id          Int    @id @default(autoincrement()) @map("id")
  petsitterId Int    @map("petsitter_id")
  location    String @map("location")
  surcharge   Int    @default(0) @map("surcharge")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  petsitter Petsitter @relation(fields: [petsitterId], references: [id], onDelete: Cascade)

  @@map("petsitter_locations")
}

model Booking {
  id          Int         @id @default(autoincrement()) @map("id")
  userId      Int         @map("user_id")
  petsitterId Int         @map("petsitter_id")
  animalType  AnimalType  @map("animal_type")
  serviceType ServiceType @map("service")
  location    String      @map("location")
  content     String?     @map("content") @db.Text //요구사항
  date        DateTime    @map("date")
  totalPrice  Int         @map("total_price")
  status      Status      @default(PENDING) @map("status")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  petsitter Petsitter @relation(fields: [petsitterId], references: [id], onDelete: Cascade)

  @@map("bookings")
}

enum Status {
  REJECTED //거절
  CANCELED //취소
  PENDING //보류
  APPROVED //승인
  DONE //끝남
}

model Bookmark {
  id          Int @id @default(autoincrement()) @map("id")
  userId      Int @unique @map("user_id")
  petsitterId Int @map("petsitter_id")

  createdAt DateTime @default(now()) @map("create_at")

  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  petsitter Petsitter @relation(fields: [petsitterId], references: [id], onDelete: Cascade)

  @@map("bookmarks")
}

model Review {
  id          Int    @id @default(autoincrement()) @map("id")
  userId      Int    @map("user_id")
  petsitterId Int    @map("petsitter_id")
  rating      Int    @map("rating")
  comment     String @map("comment")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  petsitter Petsitter @relation(fields: [petsitterId], references: [id], onDelete: Cascade)

  @@map("reviews")
}

model RefreshToken {
  id           Int     @id @default(autoincrement()) @map("id")
  userId       Int     @unique @map("user_id")
  refreshToken String? @map("refresh_token")

  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("refresh_tokens")
}

 

< seed.js >

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  // 펫시터 데이터
  const petsitter1 = await prisma.petsitter.create({
    data: {
      name: '펫시터1',
      experience: 1,
      email: 'petsitter1@example.com',
      password: '$2b$10$PeRLSlDLgtIyHYm3xEWeOu7ttFUzwlA1X6ysqaFQmCiM.riFhlaIW', //123 해싱된 값
    },
  });
  const petsitter2 = await prisma.petsitter.create({
    data: {
      name: '펫시터2',
      experience: 2,
      email: 'petsitter2@example.com',
      password: '$2b$10$PeRLSlDLgtIyHYm3xEWeOu7ttFUzwlA1X6ysqaFQmCiM.riFhlaIW',
    },
  });
  const petsitter3 = await prisma.petsitter.create({
    data: {
      name: '펫시터3',
      experience: 3,
      email: 'petsitter3@example.com',
      password: '$2b$10$PeRLSlDLgtIyHYm3xEWeOu7ttFUzwlA1X6ysqaFQmCiM.riFhlaIW',
    },
  });

  //펫시터 서비스 데이터
  await prisma.petsitterService.create({
    data: {
      petsitterId: petsitter1.id,
      animalType: 'DOG',
      serviceType: 'FEED',
      price: 0,
    },
  });
  await prisma.petsitterService.create({
    data: {
      petsitterId: petsitter1.id,
      animalType: 'ETC',
      serviceType: 'WALK',
      price: 0,
    },
  });
  await prisma.petsitterService.create({
    data: {
      petsitterId: petsitter2.id,
      animalType: 'ETC',
      serviceType: 'WALK',
      price: 0,
    },
  });
  await prisma.petsitterService.create({
    data: {
      petsitterId: petsitter3.id,
      animalType: 'CAT',
      serviceType: 'SHOWER',
      price: 0,
    },
  });

  //펫시터 서비스 장소
  await prisma.petsitterLocation.create({
    data: {
      petsitterId: petsitter1.id,
      location: '서울',
    },
  });
  await prisma.petsitterLocation.create({
    data: {
      petsitterId: petsitter1.id,
      location: '대구',
      surcharge: 40000,
    },
  });
  await prisma.petsitterLocation.create({
    data: {
      petsitterId: petsitter2.id,
      location: '경주',
    },
  });
  await prisma.petsitterLocation.create({
    data: {
      petsitterId: petsitter3.id,
      location: '인천',
    },
  });
}

main()
  .then(() => {
    console.log('데이터 베이스 삽입 성공');
    prisma.$disconnect();
  })
  .catch((err) => {
    console.log('데이터 베이스 삽입 실패');
    prisma.$disconnect();
  });

 

 

< app.js >

import express from 'express';
import 'dotenv/config';
import { SERVER_PORT } from './constants/env.constant.js';
import { apiRouter } from './routers/index.js';
import errorHandler from './middlewares/error-handler.middleware.js';
import { HTTP_STATUS } from './constants/http-status.constant.js';
import logMiddleware from './middlewares/log.middleware.js';

const app = express();

app.use(express.json());

app.use(logMiddleware);

app.get('/health-check', (req, res, next) => {
  res.status(HTTP_STATUS.OK).json({ message: "I'm healthy" });
});

app.use('/', apiRouter);

app.use(errorHandler);

app.listen(SERVER_PORT, () => {
  console.log(`${SERVER_PORT}번 포트로 서버가 열렸습니다.`);
});