Super Kawaii Cute Cat Kaoani
본문 바로가기
{Extracurricular Activities}/UMC 7기 - Node.js

[UMC 7th Server] Chapter 6. ORM 사용해보기

by wonee1 2024. 11. 7.
반응형

Chapter 6. ORM 사용해보기

 

 

 

 

 

☑️ 실습 인증


 

 

🔨ORM 사용해보기 실습

더보기

 

 다음 명령어로 prisma 라이브러리를 설치했습니다.

npm install @prisma/client prisma

 

 

👉 Prisma 설정 파일 만들기

 

  1. 다음과 같은 명령어를 사용해서 prisma 설정 파일을 만들어주었습니다.
npm exec prisma init

 

 

 


2. 또한 provider 를 postgresql에서 mysql로 변경해줬습니다. (prisma는 postgresql이 디폴트)

 

 

 

👉 Prisma Schema에 Model 추가하기

 

 

과제로 진행했던 데이터베이스의 users를 prisma schema로 바꿨습니다.

 

 

// 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 users {
  user_id        Int      @id @default(autoincrement())
  user_name      String   @unique @db.VarChar(50)
  password       String   @db.VarChar(255)
  email          String   @unique @db.VarChar(100)
  created_at     DateTime @default(now()) @db.Timestamp(6)
  gender         String   @db.VarChar(15)
  birth          DateTime? @db.Date
  address        String   @db.VarChar(255)
  detailAddress  String?  @map("detail_address") @db.VarChar(255)
  phoneNumber    String   @map("phone_number") @db.VarChar(20)
}

 

 

 

 

 

👉 Client 코드 만들기

 

1. 다음과 같은 명령어를 실행하여 generate를 하였습니다.

 

npm exec prisma generate

 

 

 

또한 package.json에 dev 코드도 추가해줬습니다.

 

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node src/index.js",
     "dev": "nodemon -e js,json,prisma --exec \"prisma generate && node src/index.js\""
  },

 

 

 

 

👉 ORM 적용하기

 

 

  1. users 뿐만 아니라 전체 모델 코드를 적용해주었습니다
// 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")
}


// Enum 정의
enum MissionStatus {
  IN_PROGRESS
  COMPLETED
}

enum UserMissionStatus {
  IN_PROGRESS
  COMPLETED
}

// 지역 모델
model regions {
  id          Int           @id @default(autoincrement())
  region_name String        @unique @db.VarChar(100)

  stores      stores[]
  missions    missions[]

  @@map("regions")
}

// 사용자 모델
model users {
  id                Int                 @id @default(autoincrement())
  user_name         String              @unique @db.VarChar(50)
  password          String              @db.VarChar(255)
  email             String              @unique @db.VarChar(100)
  created_at        DateTime            @default(now()) @db.Timestamp(6)
  gender            String              @db.VarChar(15)
  birth             DateTime?           @db.Date
  address           String              @db.VarChar(255)
  detailAddress     String?             @map("detail_address") @db.VarChar(255)
  phoneNumber       String              @map("phone_number") @db.VarChar(20)

  reviews           reviews[]
  user_points       user_points[]
  user_missions     user_missions[]
  user_favor_category user_favor_category[]

  @@map("users")
}

// 리뷰 모델
model reviews {
  id          Int       @id @default(autoincrement())
  user        users     @relation(fields: [userId], references: [id])
  userId      Int       @map("user_id")
  store       stores    @relation(fields: [storeId], references: [id])
  storeId     Int       @map("store_id")
  rating      Int       @db.Int
  review_text String?   @db.Text
  created_at  DateTime  @default(now()) @db.Timestamp(6)

  @@map("reviews")
}

// 미션 모델
model missions {
  id             Int               @id @default(autoincrement())
  region         regions           @relation(fields: [regionId], references: [id])
  regionId       Int               @map("region_id")
  mission_status MissionStatus
  description    String            @db.Text
  created_at     DateTime          @default(now()) @db.Timestamp(6)

  user_missions  user_missions[]

  @@map("missions")
}

// 가게 모델
model stores {
  id            Int              @id @default(autoincrement())
  store_name    String           @db.VarChar(100)
  region        regions?         @relation(fields: [regionId], references: [id])
  regionId      Int?             @map("region_id")
  store_address String?          @db.VarChar(255)

  reviews       reviews[]
  user_missions user_missions[]

  @@map("stores")
}

// 사용자 포인트 모델
model user_points {
  id          Int           @id @default(autoincrement())
  user        users         @relation(fields: [userId], references: [id])
  userId      Int           @map("user_id")
  points      Int           @default(0)
  updated_at  DateTime      @default(now()) @db.Timestamp(6)

  user_missions user_missions[]

  @@map("user_points")
}

// 사용자 미션 모델
model user_missions {
  id             Int             @id @default(autoincrement())
  user           users           @relation(fields: [userId], references: [id])
  userId         Int             @map("user_id")
  mission        missions        @relation(fields: [missionId], references: [id])
  missionId      Int             @map("mission_id")
  store          stores          @relation(fields: [storeId], references: [id])
  storeId        Int             @map("store_id")
  point          user_points     @relation(fields: [pointId], references: [id])
  pointId        Int             @map("point_id")
  status         UserMissionStatus
  completed_at   DateTime?

  @@map("user_missions")
}

// 음식 카테고리 모델
model food_category {
  id                Int                   @id @default(autoincrement())
  name              String                @unique @db.VarChar(100)

  user_favor_category user_favor_category[]

  @@map("food_category")
}

// 사용자 선호 카테고리 모델
model user_favor_category {
  id             Int           @id @default(autoincrement())
  user           users         @relation(fields: [userId], references: [id])
  userId         Int           @map("user_id")
  foodCategory   food_category @relation(fields: [foodCategoryId], references: [id])
  foodCategoryId Int           @map("food_category_id")

  @@index([foodCategoryId], map: "f_category_id")
  @@index([userId], map: "user_id_idx")
  @@map("user_favor_category")
}

 

 

 2. src/repositories/user.repository.js , src/dtos/user.dto.js 수정

 

 

💠src/repositories/user.repository.js 

import { pool } from "../db.config.js";
import { prisma } from "../db.config.js";

// User 데이터 삽입
export const addUser = async (data) => {
  const user = await prisma.users.findFirst({ where: { email: data.email } });
  if (user) {
    return null;
  }

  const created = await prisma.users.create({ data: data });
  return created.id;
};

// 사용자 정보 얻기
export const getUser = async (userId) => {
  const user = await prisma.users.findFirstOrThrow({ where: { id: userId } });
  return user;
};

// 음식 선호 카테고리 매핑
export const setPreference = async (userId, foodCategoryId) => {
  await prisma.user_favor_category.create({
    data: {
      userId: userId,
      foodCategoryId: foodCategoryId,
    },
  });
};

// 사용자 선호 카테고리 반환
export const getUserPreferencesByUserId = async (userId) => {
  const preferences = await prisma.user_favor_category.findMany({
    select: {
      id: true,
      userId: true,
      foodCategoryId: true,
      foodCategory: {
        select: {
          id: true,
          name: true,
        },
      },
    },
    where: { userId: userId },
    orderBy: { foodCategoryId: "asc" },
  });

  return preferences;
};

 

 

 

💠 src/dtos/user.dto.js

export const bodyToUser = (body) => {
    const birth = new Date(body.birth);
  
    return {
      email: body.email,
      name: body.name,
      gender: body.gender,
      birth:birth.gender,
      address: body.address || "",
      detailAddress: body.detailAddress || "",
      phoneNumber: body.phoneNumber,
      preferences: body.preferences,
    };
  };

export const responseFromUser = ({ user, preferences }) => {
  const preferFoods = preferences.map(
    (preference) => preference.foodCategory.name
  );

  return {
    email: user.email,
    name: user.name,
    preferCategory: preferFoods,
  };
};

 

npm exec prisma migrate dev 명령어 실행하여 테이블 생성

 

 

 

👉 마무리

 

생성된 테이블을 확인하기 위해서 다음 명령어 실행

npx prisma studio

 

 

 

 

 

 

🍍 DB 마이그레이션 파일 관리해보기

 

더보기

 

👉 npm exec prisma migrate dev

 

다음 명령어 사용하여 마이그레이션 하였습니다.

 

 

 

 

👉npx prisma studio 명령어로 테이블 생성확인

 

 

 

 

 

 

 

 

📜 목록 API 만들어보기

 

더보기

 

1. DTO 파일: review.dto.js

단일 리뷰를 변환하는 responseFromReview와 리뷰 목록을 변환하는 responseFromReviews 함수를 구현했습니다.

 

// src/dtos/review.dto.js

export const bodyToReview = (body) => ({
  userId: body.userId,
  storeId: body.storeId,
  rating: body.rating,
  reviewText: body.reviewText,
});

export const responseFromReview = (review) => ({
  reviewId: review.id,
  userId: review.user?.id,
  storeId: review.store?.id,
  rating: review.rating,
  reviewText: review.review_text,
  createdAt: review.created_at,
  user: review.user
    ? {
        userName: review.user.user_name,
        email: review.user.email,
        gender: review.user.gender,
        birth: review.user.birth,
        address: review.user.address,
        detailAddress: review.user.detailAddress,
        phoneNumber: review.user.phoneNumber,
      }
    : null,
  store: review.store
    ? {
        storeName: review.store.store_name,
      }
    : null,
}); // 단일 리뷰 변환 

export const responseFromReviews = (reviews) => {
  if (!Array.isArray(reviews) || reviews.length === 0) {
    return {
      data: [],
      pagination: { cursor: null },
    };
  }

  return {
    data: reviews.map((review) => responseFromReview(review)),
    pagination: {
      cursor: reviews.length ? reviews[reviews.length - 1].id : null,
    },
  };
};// 전체 리뷰 목록 변환

 

 

 

2. 레포지토리 파일: user.repository.js

getAllStoreReviews로 특정 가게의 리뷰 목록을 데이터베이스에서 가져오는 역할을 합니다.

 

export const getAllStoreReviews = async (storeId, cursor) => {
  const reviews = await prisma.reviews.findMany({
    select: {
      id: true,
      review_text: true, // 리뷰 내용
      rating: true,      // 리뷰 평점
      created_at: true,  // 작성일
      store: {
        select: { id: true, store_name: true },
      },
      user: {
        select: {
          id: true,
          user_name: true,
          email: true,
          gender: true,
          birth: true,
          address: true,
          detailAddress: true,
          phoneNumber: true,
        },
      },
    },
    where: { storeId: storeId, id: { gt: cursor } },
    orderBy: { id: "asc" },
    take: 5,
  });
  console.log("Reviews found:", reviews); // 쿼리 결과 로그


  return reviews;
};

 

 

 

 

3. 서비스 파일: store.service.js

데이터베이스에서 getAllStoreReviews로 리뷰 목록을 조회하고, DTO를 통해 응답 형식으로 변환한 후 반환했습니다.

 

// src/services/store.service.js
import { addStore } from "../repositories/store.repository.js";
import { responseFromStore } from "../dtos/store.dto.js";
import { getAllStoreReviews } from '../repositories/user.repository.js'; 
import {responseFromReviews} from '../dtos/review.dto.js'

export const storeSignUp = async (data) => {
    const storeId = await addStore({
        storeName: data.storeName,
        address: data.address,
        regionId: data.regionId,
    });

    if (!storeId) {
        throw new Error("가게를 추가할 수 없습니다.");
    }

    return responseFromStore({ storeId, ...data });
};

export const listStoreReviews = async (storeId, cursor) => {
  const reviews = await getAllStoreReviews(storeId, cursor);
  console.log("Raw reviews from DB:", reviews); // 리뷰 배열이 제대로 반환되는지 확인
  return responseFromReviews(reviews); // 리뷰 배열을 변환하여 반환
}; 

 

 

 

4. Controller 파일: store.controller.js

컨트롤러에서 listStoreReviews 함수를 호출하여 최종 데이터를 반환하게 코드를 짰습니다.

(클라이언트로는 JSON 형식으로 응답)

// src/controller/store.controller.js
import { StatusCodes } from "http-status-codes";
import { bodyToStore } from "../dtos/store.dto.js";
import { storeSignUp } from "../services/store.service.js";
import { listStoreReviews } from "../services/store.service.js";

export const handleStoreSignUp = async (req, res, next) => {
    try {
        const store = await storeSignUp(bodyToStore(req.body));
        res.status(StatusCodes.CREATED).json({ result: store });
    } catch (error) {
        res.status(StatusCodes.BAD_REQUEST).json({ error: error.message });
    }
};

export const handleListStoreReviews = async (req, res, next) => {
  try {
    const storeId = parseInt(req.params.storeId, 10);
    const cursor = req.query.cursor ? parseInt(req.query.cursor, 10) : 0;
    const reviews = await listStoreReviews(storeId, cursor);
    res.status(StatusCodes.OK).json(reviews); // 데이터를 JSON으로 반환
  } catch (error) {
    console.error(error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: '리뷰 조회에 실패했습니다.' });
  }
};

 

 

최종 index.js 파일

 

// 작성한 리뷰 목록 확인 
app.get("/api/stores/:storeId/reviews", handleListStoreReviews)

 

 

☑️실행결과 

 

### 특정 가게의 리뷰 목록 조회 - 첫 페이지
GET <http://localhost:3000/api/stores/1/reviews?cursor>
Content-Type: application/json

 

 

 

 

 

 


 

 

 

🎯핵심 키워드 정리


 

 

 

💠ORM

 

🔹ORM 이란?

Object-Relational Mapping

  • 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 것을 말한다
    • 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다
    • 객체 모델과 관계형 모델 간에 불일치가 존재한다
    • ORM을 통해 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 불일치를 해결해준다

 

 

 

 

💠 Prisma 문서 살펴보기

 

🔹  Prisma의 Connection Pool 관리 방법

 

Connection Pool을 관리하는 방법은 기본적으로 데이터베이스 클라이언트 라이브러리를 통해 이루어진다.

Prisma는 데이터베이스 연결을 관리할 때, 기본적으로 각 요청마다 새 연결을 생성하기보다 데이터베이스와의 지속적인 연결을 유지하고 재사용하여 성능을 최적화한다.

 

Prisma에서 Connection Pool을 관리하기 위해서는:

  • DATABASE_URL에서 connection_limit 등의 매개변수를 지정
  • 서버리스 환경에서는 전역 변수로 Prisma Client를 유지해 연결 재사용
  • 추가적인 세부 관리가 필요할 경우, 서드파티 라이브러리와 Prisma를 조합

 

 

🔹 Prisma의 Migration 관리 방법

 

 

  1. 마이그레이션 설정 준비
  2. 새로운 마이그레이션 생성
npx prisma migrate dev --name <이름>

     

     3. 마이그레이션 적용

npx prisma migrate deploy

 

 

 

 

💠 ORM(Prisma)을 사용하여 좋은 점과 나쁜 점

 

 

🔹 ORM 장점

  • 객체 지향적인 코드로 인해 더 직관적이고, 비즈니스 로직에 더 집중할 수 있게 도와준다. 
  • 재사용성 및 유지보수의 편리성이 증가
  • DBMS에 대한 종속성이 줄어든다

 

🔹 ORM 단점

  • 완벽한 ORM으로만 서비스를 구현하기가 어렵다.
    • 프로젝트의 복잡성이 커질 경우 난이도 또한 올라갈 수 있다.
    • 잘못 구현된 경우에 속도 저하 및 심각할 경우 일관성이 무너지는 문제점이 생길 수 있다.
  • 프로시저가 많은 시스템에서는 ORM의 객체 지향적인 장점을 활용하기 어렵다.
    • 이미 프로시저가 많은 시스템에서는 다시 객체로 바꿔야함, 그 과정에서 생산성 저하나 리스크가 많이 발생할 수 있다.

 

 

 

 

💠 다양한 ORM 라이브러리 살펴보기

 

🔹  Sequelize

 

  • Node.js 환경에서 MySQL, PostgreSQL, SQLite, MSSQL 등 여러 관계형 데이터베이스를 쉽게 다룰 수 있도록 지원하는 ORM(Object-Relational Mapping) 라이브러리

🔹  TypeORM

 

  • TypeORM은 데코레이터 기반의 문법을 사용해 객체 모델을 정의하고 이를 데이터베이스 테이블로 매핑하여 관리한다.

 

 

 

 

 


 

 

✏️6주차 필기 및 마인드맵 


 

 

 

 

 

 

 

 

 

 


 

 

 

🔥 미션 인증


 

 

미션 기록🍀

 

기본적으로 cursor 페이지네이션을 사용하여 구현하였습니다.

또한 미션을 진행하면서 db를 보완, 수정하는 과정도 함께 기록하였습니다.

 

 

 

Prisma ORM 초기 셋팅

 

더보기

저는 다음과 같이 model을 정의하였습니다.⬇️

// 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")
}

// Enum 정의
enum MissionStatus {
  IN_PROGRESS
  COMPLETED
}

enum UserMissionStatus {
  IN_PROGRESS
  COMPLETED
}

// 지역 모델
model regions {
  id          Int           @id @default(autoincrement())
  region_name String        @unique @db.VarChar(100)

  stores      stores[]
  missions    missions[]

  @@map("regions")
}

// 사용자 모델
model users {
  id                Int                 @id @default(autoincrement())
  user_name         String              @unique @db.VarChar(50)
  password          String              @db.VarChar(255)
  email             String              @unique @db.VarChar(100)
  created_at        DateTime            @default(now()) @db.Timestamp(6)
  gender            String              @db.VarChar(15)
  birth             DateTime?           @db.Date
  address           String              @db.VarChar(255)
  detailAddress     String?             @map("detail_address") @db.VarChar(255)
  phoneNumber       String              @map("phone_number") @db.VarChar(20)

  reviews           reviews[]
  user_points       user_points[]
  user_missions     user_missions[]
  user_favor_category user_favor_category[]

  @@map("users")
}

// 리뷰 모델
model reviews {
  id          Int       @id @default(autoincrement())
  user        users     @relation(fields: [userId], references: [id])
  userId      Int       @map("user_id")
  store       stores    @relation(fields: [storeId], references: [id])
  storeId     Int       @map("store_id")
  rating      Int       @db.Int
  review_text String?   @db.Text
  created_at  DateTime  @default(now()) @db.Timestamp(6)

  @@map("reviews")
}

// 미션 모델
model missions {
  id             Int               @id @default(autoincrement())
  region         regions           @relation(fields: [regionId], references: [id])
  regionId       Int               @map("region_id")
  mission_status MissionStatus
  description    String            @db.Text
  created_at     DateTime          @default(now()) @db.Timestamp(6)

  user_missions  user_missions[]

  @@map("missions")
}

// 가게 모델
model stores {
  id            Int              @id @default(autoincrement())
  store_name    String           @db.VarChar(100)
  region        regions?         @relation(fields: [regionId], references: [id])
  regionId      Int?             @map("region_id")
  store_address String?          @db.VarChar(255)

  reviews       reviews[]
  user_missions user_missions[]

  @@map("stores")
}

// 사용자 포인트 모델
model user_points {
  id          Int           @id @default(autoincrement())
  user        users         @relation(fields: [userId], references: [id])
  userId      Int           @map("user_id")
  points      Int           @default(0)
  updated_at  DateTime      @default(now()) @db.Timestamp(6)

  user_missions user_missions[]

  @@map("user_points")
}

// 사용자 미션 모델
model user_missions {
  id             Int             @id @default(autoincrement())
  user           users           @relation(fields: [userId], references: [id])
  userId         Int             @map("user_id")
  mission        missions        @relation(fields: [missionId], references: [id])
  missionId      Int             @map("mission_id")
  store          stores          @relation(fields: [storeId], references: [id])
  storeId        Int             @map("store_id")
  point          user_points     @relation(fields: [pointId], references: [id])
  pointId        Int             @map("point_id")
  status         UserMissionStatus
  completed_at   DateTime?

  @@map("user_missions")
}

// 음식 카테고리 모델
model food_category {
  id                Int                   @id @default(autoincrement())
  name              String                @unique @db.VarChar(100)

  user_favor_category user_favor_category[]

  @@map("food_category")
}

// 사용자 선호 카테고리 모델
model user_favor_category {
  id             Int           @id @default(autoincrement())
  user           users         @relation(fields: [userId], references: [id])
  userId         Int           @map("user_id")
  foodCategory   food_category @relation(fields: [foodCategoryId], references: [id])
  foodCategoryId Int           @map("food_category_id")

  @@index([foodCategoryId], map: "f_category_id")
  @@index([userId], map: "user_id_idx")
  @@map("user_favor_category")
}

 

 

 

기존에 구현했던 API의 Repository 함수를 ORM을 사용하여 변경

 

 

더보기

challenge.repository 수정 전

import pool from "../db.config.js";

export const addChallenge = async (data) => {
  const existing = await pool.query(
    `SELECT * FROM user_missions WHERE user_id = ? AND mission_id = ? AND status = '진행 중'`,
    [data.userId, data.missionId]
  );

  if (existing.length > 0) {
    throw new Error("이미 도전 중인 미션입니다.");
  }

  const result = await pool.query(
    `INSERT INTO user_missions (user_id, mission_id, store_id, status) VALUES (?, ?, ?, ?)`,
    [data.userId, data.missionId, data.storeId, data.status]
  );

  return result.insertId;
};

☑️challenge.repository 수정 후

import { prisma } from "../db.config.js";

export const addChallenge = async (data) => {
  const existing = await prisma.user_missions.findFirst({
    where: {
      user_id: data.userId,
      mission_id: data.missionId,
      status: '진행 중',
    },
  });

  if (existing) {
    throw new Error("이미 도전 중인 미션입니다.");
  }

  const result = await prisma.user_missions.create({
    data: {
      user_id: data.userId,
      mission_id: data.missionId,
      store_id: data.storeId,
      status: data.status,
    },
  });

  return result.id;
};

mission.repository.js 수정 전

import pool from "../db.config.js";

export const addMission = async (data) => {
  const result = await pool.query(
    `INSERT INTO missions (region_id, description, mission_status) VALUES (?, ?, ?)`,
    [data.regionId, data.description, data.missionStatus]
  );

  return result.insertId;
};

☑️mission.repository.js 수정 후

import { prisma } from "../db.config.js";

export const addMission = async (data) => {
  const result = await prisma.missions.create({
    data: {
      region_id: data.regionId,
      description: data.description,
      mission_status: data.missionStatus,
    },
  });
  return result.id;
};

review.repository.js 수정 전

import pool from "../db.config.js";

export const addReview = async (data) => {
  const result = await pool.query(
    `INSERT INTO reviews (user_id, store_id, rating, review_text) VALUES (?, ?, ?, ?)`,
    [data.userId, data.storeId, data.rating, data.reviewText]
  );

  return result.insertId;
};

export const checkStoreExists = async (storeId) => {
  const store = await pool.query(`SELECT * FROM stores WHERE id = ?`, [storeId]);
  return store.length > 0;
};

☑️review.repository.js 수정 후

import { prisma } from "../db.config.js";

export const addReview = async (data) => {
  const result = await prisma.reviews.create({
    data: {
      user_id: data.userId,
      store_id: data.storeId,
      rating: data.rating,
      review_text: data.reviewText,
    },
  });
  return result.id;
};

export const checkStoreExists = async (storeId) => {
  const store = await prisma.stores.findUnique({
    where: { id: storeId },
  });
  return !!store;
};

store.repository.js 수정 전

import pool from "../db.config.js";

export const addStore = async (data) => {
  const result = await pool.query(
    `INSERT INTO stores (store_name, store_address, region_id) VALUES (?, ?, ?)`,
    [data.storeName, data.address, data.regionId]
  );

  return result.insertId;
};

☑️store.repository.js 수정 후

import { prisma } from "../db.config.js";

export const addStore = async (data) => {
  const result = await prisma.stores.create({
    data: {
      store_name: data.storeName,
      store_address: data.address,
      region_id: data.regionId,
    },
  });
  return result.id;
};

user는 실습에서 따로 진행하였기 때문에 생략합니다!

 

 

 

DB 데이터 추가

 

더보기

API 요청을 확인하기 위해 더미데이터를 추가하였습니다.

-- 지역 데이터 생성
INSERT INTO regions (region_name) VALUES ('Seoul');

-- 가게 데이터 생성
INSERT INTO stores (store_name, region_id, store_address) 
VALUES ('Test Store', 1, '123 Test Address');

-- 사용자 데이터 생성
INSERT INTO users (user_name, password, email, gender, birth, address, detail_address, phone_number) 
VALUES 
('testuser1', 'password123', 'testuser1@example.com', '남성', '1990-01-01', 'Seoul, South Korea', '101-202', '010-1234-5678'),
('testuser2', 'password123', 'testuser2@example.com', '여성', '1988-05-12', 'Busan, South Korea', '203-304', '010-5678-1234');

-- 리뷰 데이터 생성
INSERT INTO reviews (user_id, store_id, rating, review_text, created_at) 
VALUES 
(1, 1, 5, 'Great place!', NOW()),
(2, 1, 4, 'Nice ambiance but a bit pricey.', NOW());

-- 미션 데이터 생성
INSERT INTO missions (region_id, mission_status, description, created_at) 
VALUES 
(1, 'IN_PROGRESS', 'Promote local store', NOW()),
(1, 'COMPLETED', 'Complete a customer feedback survey', NOW());

-- 사용자 포인트 데이터 생성
INSERT INTO user_points (user_id, points, updated_at) 
VALUES 
(1, 100, NOW()),
(2, 150, NOW());

-- 사용자 미션 데이터 생성
INSERT INTO user_missions (user_id, mission_id, store_id, point_id, status, completed_at) 
VALUES 
(1, 1, 1, 1, 'IN_PROGRESS', NULL),
(2, 2, 1, 2, 'COMPLETED', NOW());

-- 음식 카테고리 데이터 생성
INSERT INTO food_category (name) 
VALUES ('Vegetarian'), ('Fast Food'), ('Desserts');

-- 사용자 선호 카테고리 데이터 생성
INSERT INTO user_favor_category (user_id, food_category_id) 
VALUES (1, 1), (2, 2);

 

 

 

내가 작성한 리뷰 목록

더보기

1. getUserReviews 함수 생성

  • userId를 기반으로 사용자가 작성한 리뷰 목록을 조회하는 함수를 user.repository.js 파일에 추가했습니다.
export const getUserReviews = async (userId, cursor = 0) => {
  const reviews = await prisma.reviews.findMany({
    select: {
      id: true,
      review_text: true,    // 리뷰 내용
      rating: true,         // 리뷰 평점
      created_at: true,     // 작성일
      store: {
        select: { id: true, store_name: true },
      },
    },
    where: { userId: userId }, // userId 조건으로 조회
    orderBy: { id: "asc" },
    take: 5,
  });
  console.log("User Reviews found:", reviews); // 쿼리 결과 로그
  return reviews;
};

2. review.dto.js

  • 데이터베이스에서 가져온 리뷰 데이터를 클라이언트에 반환할 형식으로 변환하는 함수를 작성했습니다.
  • responseFromReview는 단일 리뷰를, responseFromReviews는 리뷰 목록을 변환합니다.
export const responseFromReview = (review) => ({
  reviewId: review.id,
  userId: review.user?.id,
  storeId: review.store?.id,
  rating: review.rating,
  reviewText: review.review_text,
  createdAt: review.created_at,
  store: review.store
    ? {
        storeName: review.store.store_name,
      }
    : null,
});

export const responseFromReviews = (reviews) => ({
  data: reviews.map((review) => responseFromReview(review)),
  pagination: {
    cursor: reviews.length ? reviews[reviews.length - 1].id : null,
  },
});

3. user.service.js

  • listUserReviews 함수는 getUserReviews 함수를 호출해 데이터를 가져오고, DTO 변환을 수행한 뒤 클라이언트에 반환할 형식으로 만들었습니다.
import { getUserReviews } from '../repositories/user.repository.js';
import { responseFromReviews } from '../dtos/review.dto.js';

export const listUserReviews = async (userId, cursor) => {
  const reviews = await getUserReviews(userId, cursor);
  return responseFromReviews(reviews); // 변환된 리뷰 데이터 반환
};

4. user.controller.js

  • 클라이언트의 요청을 받아 listUserReviews 함수를 호출하는 코드를 작성해줬습니다.
export const handleListUserReviews = async (req, res) => {
  try {
    const userId = parseInt(req.params.userId, 10); // URL에서 userId 가져오기
    const cursor = req.query.cursor ? parseInt(req.query.cursor, 10) : 0; // cursor 기본값 설정
    const reviews = await listUserReviews(userId, cursor);
    res.status(StatusCodes.OK).json(reviews);
  } catch (error) {
    console.error(error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: '리뷰 조회에 실패했습니다.' });
  }
};

5. index.js - 경로 설정 및 서버 실행

  • 내가 작성한 리뷰 목록을 확인하는 api를 설정했습니다. (import는 생략)
// 내가 작성한 리뷰 목록 확인 
app.get('/api/users/:userId/reviews', handleListUserReviews);

 

특정 가게의 미션 목록

더보기

☑️ db 수정

- missions 테이블에 storeId 컬럼 추가 ,-missions.store_id를 stores.id와 연결하는 외래 키 제약 조건 추가 → 이게 없어서 미션조회가 안되는 현상이 발생!

- missions 테이블에 storeId 컬럼 추가
ALTER TABLE missions
ADD COLUMN store_id INT;
- missions.store_id를 stores.id와 연결하는 외래 키 제약 조건 추가
ALTER TABLE missions
ADD CONSTRAINT fk_store_missions
FOREIGN KEY (store_id) REFERENCES stores(id)
ON DELETE SET NULL; -- 스토어 삭제 시 NULL로 설정 (필요에 따라 설정 가능)

☑️ 체크를 위한 더미 데이터 입력

-- 1. 지역 데이터 생성
INSERT INTO regions (region_name) 
VALUES 
('Seoul'),
('Busan');

-- 2. 가게 데이터 생성
INSERT INTO stores (store_name, region_id, store_address) 
VALUES 
('Test Store', 1, '123 Test Address, Seoul'),
('Seafood Delight', 2, '789 Ocean Road, Busan');

-- 3. 사용자 데이터 생성
INSERT INTO users (user_name, password, email, gender, birth, address, detail_address, phone_number) 
VALUES 
('testuser1', 'password123', 'testuser1@example.com', '남성', '1990-01-01', 'Seoul, South Korea', '101-202', '010-1234-5678'),
('testuser2', 'password123', 'testuser2@example.com', '여성', '1988-05-12', 'Busan, South Korea', '203-304', '010-5678-1234');

-- 4. 미션 데이터 생성
INSERT INTO missions (region_id, store_id, mission_status, description, created_at) 
VALUES 
(1, 1, 'IN_PROGRESS', 'Promote local store', NOW()),
(2, 2, 'COMPLETED', 'Organize a seafood tasting event', NOW());

-- 5. 리뷰 데이터 생성
INSERT INTO reviews (user_id, store_id, rating, review_text, created_at) 
VALUES 
(1, 1, 5, 'Great place!', NOW()),
(2, 1, 4, 'Nice ambiance but a bit pricey.', NOW());

-- 6. 사용자 포인트 데이터 생성
INSERT INTO user_points (user_id, points, updated_at) 
VALUES 
(1, 100, NOW()),
(2, 150, NOW());

-- 7. 사용자 미션 데이터 생성
INSERT INTO user_missions (user_id, mission_id, store_id, point_id, status, completed_at) 
VALUES 
(1, 1, 1, 1, 'IN_PROGRESS', NULL),
(2, 2, 2, 2, 'COMPLETED', NOW());

-- 8. 음식 카테고리 데이터 생성
INSERT INTO food_category (name) 
VALUES 
('Vegetarian'), 
('Fast Food'), 
('Desserts');

-- 9. 사용자 선호 카테고리 데이터 생성
INSERT INTO user_favor_category (user_id, food_category_id) 
VALUES 
(1, 1), 
(2, 2);


1. mission.repository.js

  • storeId를 기준으로 미션 목록을 조회하는 함수입니다
  • . 페이지네이션을 위해 cursor를 사용해 특정 ID 이후의 데이터를 가져오도록 설정했습니다.
// src/repositories/mission.repository.js
import { prisma } from "../db.config.js";

export const getStoreMissions = async (storeId, cursor = 0) => {
  const missions = await prisma.missions.findMany({
    where: { storeId: storeId }, // storeId로 필터링
    select: {
      id: true,
      description: true,
      mission_status: true,
      created_at: true,
      region: {
        select: { id: true, region_name: true },
      },
    },
    orderBy: { id: "asc" },
    take: 10,
    skip: cursor,
  });
  return missions;
};

2. mission.dto.js

  • 데이터베이스에서 가져온 미션 데이터를 클라이언트에 반환할 형식으로 변환하는 함수를 적어줬습니다.
// src/dtos/mission.dto.js

export const responseFromMission = (mission) => ({
  missionId: mission.id,
  description: mission.description,
  status: mission.mission_status,
  createdAt: mission.created_at,
  region: mission.region ? mission.region.region_name : null,
});

export const responseFromMissions = (missions) => ({
  data: missions.map((mission) => responseFromMission(mission)),
  pagination: {
    cursor: missions.length ? missions[missions.length - 1].id : null,
  },
});

3. mission.service.js

  • getStoreMissions 함수를 호출하여 미션 목록을 가져온 후, DTO로 변환하여 반환하는 서비스 함수를 구현했습니다.
// src/services/mission.service.js
import { getStoreMissions } from '../repositories/mission.repository.js';
import { responseFromMissions } from '../dtos/mission.dto.js';

export const listStoreMissions = async (storeId, cursor) => {
  const missions = await getStoreMissions(storeId, cursor);
  return responseFromMissions(missions); // 변환된 미션 데이터 반환
};

4. mission.controller.js

  • 클라이언트의 요청을 받아 listStoreMissions 함수를 호출하도록 구현했습니다
// src/controllers/mission.controller.js
import { listStoreMissions } from '../services/mission.service.js';
import { StatusCodes } from 'http-status-codes';

export const handleListStoreMissions = async (req, res) => {
  try {
    const storeId = parseInt(req.params.storeId, 10); // URL에서 storeId 가져오기
    const cursor = req.query.cursor ? parseInt(req.query.cursor, 10) : 0; // 페이지네이션을 위한 cursor 설정
    const missions = await listStoreMissions(storeId, cursor);
    res.status(StatusCodes.OK).json(missions);
  } catch (error) {
    console.error(error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: '미션 목록 조회에 실패했습니다.' });
  }
};

5. index.js

  • 특정 가게의 미션 목록 조회 API를 설정했습니다.
// 특정 가게의 미션 목록 조회 API
app.get('/api/stores/:storeId/missions', handleListStoreMissions);

 

 

내가 진행 중인 미션 목록

더보기

1. mission.repository.js

  • 사용자가 진행 중인 미션 목록을 데이터베이스에서 조회하는 함수를 작성했습니다.
  • 이 함수는 user_missions 테이블에서 userId와 status가 IN_PROGRESS인 데이터를 가져옵니다.
// src/repositories/mission.repository.js
import { prisma } from "../db.config.js";

export const getUserInProgressMissions = async (userId, cursor = 0) => {
  const missions = await prisma.user_missions.findMany({
    where: {
      userId: userId,
      status: 'IN_PROGRESS', // 진행 중인 미션만 조회
    },
    select: {
      mission: {
        select: {
          id: true,
          description: true,
          mission_status: true,
          created_at: true,
          region: {
            select: { id: true, region_name: true },
          },
        },
      },
    },
    orderBy: { missionId: "asc" },
    take: 10,
    skip: cursor,
  });
  return missions.map((userMission) => userMission.mission);
};

2. mission.dto.js

  • 데이터베이스에서 가져온 미션 데이터를 클라이언트에 반환할 형식으로 변환하는 코드를 작성해줬습니다 (이 코드는 기존에 가게 미션 조회 때 작성한 코드와 동일하므로 따로 첨부하지 않았습니다)
// src/services/mission.service.js
import { getUserInProgressMissions } from '../repositories/mission.repository.js';
import { responseFromMissions } from '../dtos/mission.dto.js';

export const listUserInProgressMissions = async (userId, cursor) => {
  const missions = await getUserInProgressMissions(userId, cursor);
  return responseFromMissions(missions); // 변환된 미션 데이터 반환
};

3. mission.service.js

  • getUserInProgressMissions 함수를 호출해 진행 중인 미션 목록을 가져오고, DTO로 변환하여 반환하는 서비스 함수를 구현했습니다.
// src/services/mission.service.js
import { getUserInProgressMissions } from '../repositories/mission.repository.js';
import { responseFromMissions } from '../dtos/mission.dto.js';

export const listUserInProgressMissions = async (userId, cursor) => {
  const missions = await getUserInProgressMissions(userId, cursor);
  return responseFromMissions(missions); // 변환된 미션 데이터 반환
};

4. mission.controller.js

  • 클라이언트의 요청을 받아 listUserInProgressMissions 함수를 호출하도록 작성했습니다.
// src/controllers/mission.controller.js
import { listUserInProgressMissions } from '../services/mission.service.js';
import { StatusCodes } from 'http-status-codes';

export const handleListUserInProgressMissions = async (req, res) => {
  try {
    const userId = parseInt(req.params.userId, 10); // URL에서 userId 가져오기
    const cursor = req.query.cursor ? parseInt(req.query.cursor, 10) : 0; // 페이지네이션을 위한 cursor 설정
    const missions = await listUserInProgressMissions(userId, cursor);
    res.status(StatusCodes.OK).json(missions);
  } catch (error) {
    console.error(error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: '진행 중인 미션 목록 조회에 실패했습니다.' });
  }
};

5. index.js

  • 특정 사용자의 진행 중인 미션 목록 조회 API를 설정했습니다.
// 특정 사용자의 진행 중인 미션 목록 조회 API
app.get('/api/users/:userId/missions/in-progress', handleListUserInProgressMissions);

 

내가 진행 중인 미션을 진행 완료로 바꾸기

더보기

1. mission.repository.js

  • userId와 missionId를 기준으로 미션 상태를 COMPLETED로 업데이트하는 함수를 구현했습니다.
// src/repositories/mission.repository.js
import { prisma } from "../db.config.js";

export const completeUserMission = async (userId, missionId) => {
  return await prisma.user_missions.updateMany({
    where: {
      userId: userId,
      missionId: missionId,
      status: 'IN_PROGRESS', // 진행 중인 미션만 업데이트
    },
    data: {
      status: 'COMPLETED',
      completed_at: new Date(), // 완료 시간 기록
    },
  });
};

2. mission.service.js

  • completeUserMission 함수를 호출하여 미션 상태를 업데이트하는 서비스 함수입니다.
// src/services/mission.service.js
import { completeUserMission } from '../repositories/mission.repository.js';

export const markMissionAsCompleted = async (userId, missionId) => {
  const result = await completeUserMission(userId, missionId);
  if (result.count === 0) {
    throw new Error('해당 미션을 찾을 수 없거나 이미 완료된 상태입니다.');
  }
  return { message: '미션이 완료되었습니다.' };
};

3.mission.controller.js

  • 클라이언트의 요청을 받아 markMissionAsCompleted 함수를 호출하도록 코드를 작성했습니다.
// src/controllers/mission.controller.js
import { markMissionAsCompleted } from '../services/mission.service.js';
import { StatusCodes } from 'http-status-codes';

export const handleCompleteUserMission = async (req, res) => {
  try {
    const userId = parseInt(req.params.userId, 10); // URL에서 userId 가져오기
    const missionId = parseInt(req.params.missionId, 10); // URL에서 missionId 가져오기

    const result = await markMissionAsCompleted(userId, missionId);
    res.status(StatusCodes.OK).json(result);
  } catch (error) {
    console.error(error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: error.message || '미션 완료 처리에 실패했습니다.' });
  }
};

4. index.js

  • 특정 사용자의 미션을 완료로 변경하는 API 경로를 설정했습니다.
// src/index.js
import express from 'express';
import { handleCompleteUserMission } from './controllers/mission.controller.js';

const app = express();

// 특정 사용자의 진행 중인 미션 완료로 업데이트 API
app.put('/api/users/:userId/missions/:missionId/complete', handleCompleteUserMission);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

 

테스트 스크린샷 (test.http로 리퀘스트 보내고 확인)

 

더보기

이 게시물을 참고하였습니다⬇️

https://velog.io/@iberis/REST-Client-VS-code-에서-http-request-보내는-익스텐션

 

[node.js] VS code 에서 http request 보내기

REST Client 익스텐션 설치.http 확장자 파일 생성대문자 method url 작성 후 Send Request 버튼 클릭각 요청 사이에 - request body 는 한 줄 띄고 { }\` 안에 작성

velog.io

 

 

 

  1. 내가 작성한 리뷰 목록 테스트
### 내 리뷰 목록 조회 
GET <http://localhost:3000/api/users/1/reviews?cursor>
Content-Type: application/json

 

 

☑️실행결과

 

 

 

 

  2. 특정 가게의 미션 목록

###특정 가게 미션 조회 
GET <http://localhost:3000/api/stores/1/missions?cursor>
Content-Type: application/json

 

 

☑️실행결과  

 

 

 

 

   3. 내가 진행중인 미션 목록 조회

###내가 진행중인 미션목록 조회 
GET <http://localhost:3000/api/users/1/missions/in-progress?cursor=0>
Content-Type: application/json

 

 

☑️ 실행결과

 

 

 

 

  4. 내가 진행 중인 미션을 진행 완료로 바꾸기

 

PUT http://localhost:3000/api/users/1/missions/1/complete
Content-Type: application/json

//리소스의 일부 속성을 업데이트하는 것이기 때문에 put을 사용했습니다.

 

 

☑️ 실행결과

 

 

 

 

 

 


 

⚡6주차 트러블 슈팅 기록


 

 

트러블 슈팅 기록

더보기

NO.1

이슈

👉 Argument storeId: Invalid value provided. Expected IntFilter or Int, provided String 오류 발생

Argument storeId: Invalid value provided. Expected IntFilter or Int, provided String.
    at Dn (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:114:8082)
    at Mn.handleRequestError (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:7396)
    at Mn.handleAndLogRequestError (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:7061)
    at Mn.request (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:6745)
    at async l (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:130:9633)
    at async getAllStoreReviews (file:///D:/umc-week5/src/repositories/user.repository.js:54:19)
    at async listStoreReviews (file:///D:/umc-week5/src/services/store.service.js:21:19)
    at async handleListStoreReviews (file:///D:/umc-week5/src/controller/store.controller.js:17:19) {
  clientVersion: '5.21.1'
}

문제

👉

  • Prisma에서 storeId가 Int 형식으로 지정되어 있는데, 문자열로 제공했기 때문에 발생
  • storeId는 Int 타입이므로 Prisma가 문자열 "1"을 받아들이지 못하고 오류를 발생시켰던 것

해결

👉 storeId를 정수로 변환:

storeId가 문자열이 아니라 정수로 전달되도록 컨트롤러나 서비스 레이어에서 변환

export const handleListStoreReviews = async (req, res, next) => {
  try {
    const storeId = parseInt(req.params.storeId, 10); // storeId를 정수로 변환
    const cursor = req.query.cursor ? parseInt(req.query.cursor, 10) : 0; // cursor 기본값 설정
    const reviews = await listStoreReviews(storeId, cursor);
    res.status(StatusCodes.OK).json(reviews);
  } catch (error) {
    console.error(error);
    res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: '리뷰 조회에 실패했습니다.' });
  }
};

NO.2

이슈

👉 user와 store를 null로 리턴하는 현상

TTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Content-Length: 26
ETag: W/"1a-cu5+mc44qDfmQ9TfrCaIxJwtVWU"
Date: Sun, 27 Oct 2024 11:43:09 GMT
Connection: close

{
  "user": null,
  "store": null
}

문제

👉

  • responseFromReviews 함수가 제대로 호출되지 않고, 단일 리뷰 객체에 대한 DTOresponseFromReview가 직접 호출됨.
  • listStoreReviews에서 잘못된 데이터가 반환되었기 때문에 발생한 현상

해결

👉 listStoreReviews 함수에서 responseFromReviews가 아닌, responseFromReview가 잘못 호출된 것이 아닌지 확인했습니다.

→ 확인 결과 잘못 호출하였다는 걸 알게 되었고, 바로 수정하였습니다.

 


NO.3

이슈

👉

PrismaClientValidationError: 
Invalid prisma.missions.findMany() invocation:

{
  select: {
    id: true,
    description: true,
    mission_status: true,
    created_at: true,
    region: {
      select: {
        id: true,
        region_name: true
      }
    }
  },
  where: {
    storeId: 1,
    ~~~~~~~
?   AND?: missionsWhereInput | missionsWhereInput[],
?   OR?: missionsWhereInput[],
?   NOT?: missionsWhereInput | missionsWhereInput[],
?   id?: IntFilter | Int,
?   regionId?: IntFilter | Int,
?   mission_status?: EnumMissionStatusFilter | MissionStatus,
?   description?: StringFilter | String,
?   created_at?: DateTimeFilter | DateTime,
?   region?: RegionsRelationFilter | regionsWhereInput,
?   user_missions?: User_missionsListRelationFilter
  },
  orderBy: {
    id: "asc"
  },
  take: 5
}

Unknown argument storeId. Available options are marked with ?.      
    at Dn (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:114:8082)
    at Mn.handleRequestError (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:7396)
    at Mn.handleAndLogRequestError (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:7061)
    at Mn.request (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:6745)
    at async l (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:130:9633)
    at async getStoreMissions (file:///D:/umc-week5/src/repositories/mission.repository.js:17:20)
    at async listStoreMissions (file:///D:/umc-week5/src/services/mission.service.js:15:22)
    at async handleListStoreMissions (file:///D:/umc-week5/src/controller/mission.controller.js:19:22) {
  clientVersion: '5.21.1'
}
PrismaClientValidationError: 
Invalid prisma.missions.findMany() invocation:

{
  select: {
    id: true,
    description: true,
    mission_status: true,
    created_at: true,
    region: {
      select: {
        id: true,
        region_name: true
      }
    }
  },
  where: {
    storeId: 1,
    ~~~~~~~
?   AND?: missionsWhereInput | missionsWhereInput[],
?   OR?: missionsWhereInput[],
?   NOT?: missionsWhereInput | missionsWhereInput[],
?   id?: IntFilter | Int,
?   regionId?: IntFilter | Int,
?   mission_status?: EnumMissionStatusFilter | MissionStatus,
?   description?: StringFilter | String,
?   created_at?: DateTimeFilter | DateTime,
?   region?: RegionsRelationFilter | regionsWhereInput,
?   user_missions?: User_missionsListRelationFilter
  },
  orderBy: {
    id: "asc"
  },
  take: 5
}

Unknown argument storeId. Available options are marked with ?.      
    at Dn (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:114:8082)
    at Mn.handleRequestError (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:7396)
    at Mn.handleAndLogRequestError (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:7061)
    at Mn.request (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:121:6745)
    at async l (D:\\umc-week5\\node_modules\\@prisma\\client\\runtime\\library.js:130:9633)
    at async getStoreMissions (file:///D:/umc-week5/src/repositories/mission.repository.js:17:20)
    at async listStoreMissions (file:///D:/umc-week5/src/services/mission.service.js:15:22)
    at async handleListStoreMissions (file:///D:/umc-week5/src/controller/mission.controller.js:19:22) {
  clientVersion: '5.21.1'
}

 

문제

👉missions 모델에 storeId 필드가 존재하지 않기 때문에 발생!

 

 

해결

👉
스키마에서 missions가 regions나 stores와 연결된 방식에 따라 적절히 수정하였습니다. 또한 MYSQL도 같이 수정해주었습니다,

 

MYSQL 수정

-- `missions` 테이블에 `storeId` 컬럼 추가
ALTER TABLE missions
ADD COLUMN store_id INT;

-- `missions.store_id`를 `stores.id`와 연결하는 외래 키 제약 조건 추가
ALTER TABLE missions
ADD CONSTRAINT fk_store_missions
FOREIGN KEY (store_id) REFERENCES stores(id)
ON DELETE SET NULL; -- 스토어 삭제 시 NULL로 설정 (필요에 따라 설정 가능)

Prisma 수정

// stores 모델
model stores {
  id            Int              @id @default(autoincrement())
  store_name    String           @db.VarChar(100)
  region        regions?         @relation(fields: [regionId], references: [id])
  regionId      Int?             @map("region_id")
  store_address String?          @db.VarChar(255)

  reviews       reviews[]
  user_missions user_missions[]
  missions      missions[]       // stores와 missions 간의 관계 설정

  @@map("stores")
}

// missions 모델
model missions {
  id             Int               @id @default(autoincrement())
  description    String            @db.Text
  mission_status MissionStatus
  created_at     DateTime          @default(now()) @db.Timestamp(6)
  region         regions?          @relation(fields: [regionId], references: [id])
  regionId       Int?              @map("region_id")
  store          stores?           @relation(fields: [storeId], references: [id]) // stores와 관계 설정
  storeId        Int?              @map("store_id")                               // stores의 외래 키

  user_missions  user_missions[]

  @@map("missions")
}

참고 레퍼런스


NO.4

이슈

👉 깃 이슈 발생

$ git checkout feature/mission-06
error: Your local changes to the following files would be overwritten by checkout:
        package-lock.json
        package.json
        src/controller/challenge.controller.js
        src/controller/mission.controller.js
        src/controller/review.controller.js
        src/controller/store.controller.js
        src/controller/user.controller.js
        src/db.config.js
        src/dtos/challenge.dto.js
        src/dtos/mission.dto.js
        src/dtos/review.dto.js
        src/dtos/store.dto.js
        src/dtos/user.dto.js
        src/index.js
        src/repositories/challenge.repository.js
        src/repositories/mission.repository.js
        src/repositories/review.repository.js
        src/repositories/store.repository.js
        src/repositories/user.repository.js
        src/services/challenge.service.js
        src/services/mission.service.js
        src/services/review.service.js
        src/services/store.service.js
        src/services/user.service.js
        test.http
Please commit your changes or stash them before you switch branches.
Aborting

문제

👉

feature/mission-06 브랜치로 체크아웃이 성공했으나 git log를 보면 여전히 first commit만 있는 상태

또, 추가된 파일들(prisma/migrations/...)이 있으며, 이전 브랜치에서의 모든 파일들이 현재 커밋에 포함되지 않은 상태

해결

👉 git log를 활용하여 커밋로그를 본 다음 수정

ommit ac2dc78b90046b77a1e9cf71ffd2c1c34766c7bf (HEAD -> feature/mission-06, origin/main, origin/feature/mission-06)      
Author: gimchaewon <gimchaewon417@gmail.com>
Date:   Mon Oct 14 12:03:20 2024 +0900

    first commit
# 먼저 feature/mission-05 브랜치로 돌아가서 확인
git checkout feature/mission-05

# feature/mission-06 브랜치로 돌아가기
git checkout feature/mission-06

# feature/mission-05의 변경 사항을 feature/mission-06으로 병합
git merge feature/mission-05

참고 레퍼런스


NO.5

이슈

👉 ReferenceError: router is not defined 오류 발생

file:///D:/umc-week5/src/index.js:33
router.get('/api/users/:userId/reviews', handleListUserReviews);      
^

ReferenceError: router is not defined
    at file:///D:/umc-week5/src/index.js:33:1
    at ModuleJob.run (node:internal/modules/esm/module_job:222:25)    
    at async ModuleLoader.import (node:internal/modules/esm/loader:316:24)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:123:5)

문제

👉 이 오류는 router 변수가 선언되지 않았기 때문에 발생!

해결

👉router를 사용하지 않고 그냥 app로 일단 구현했습니다. 추후에 router를 사용하는 방식으로 바꿀 예정입니다.

// src/index.js
import express from "express";
import cors from "cors";
import { handleStoreSignUp,handleListStoreReviews} from "./controller/store.controller.js";
import { handleReviewSignUp } from "./controller/review.controller.js";
import { handleMissionSignUp,handleListStoreMissions,handleListUserInProgressMissions,handleCompleteUserMission   } from "./controller/mission.controller.js";
import { handleChallengeSignUp } from "./controller/challenge.controller.js";
import { handleListUserReviews } from './controller/user.controller.js';

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());
app.use(cors());

// 가게 추가 API
app.post("/api/stores", handleStoreSignUp);

// 가게 리뷰 추가 API
app.post("/api/reviews", handleReviewSignUp);

// 가게 미션 추가 API
app.post("/api/missions", handleMissionSignUp);

// 가게의 미션 도전하기 API
app.post("/api/challenges", handleChallengeSignUp);

// 리뷰 목록 확인 
app.get("/api/stores/:storeId/reviews", handleListStoreReviews)

// 내가 작성한 리뷰 목록 확인 
app.get('/api/users/:userId/reviews', handleListUserReviews);

// 특정 가게의 미션 목록 조회 API
app.get('/api/stores/:storeId/missions', handleListStoreMissions);

// 특정 사용자의 진행 중인 미션 목록 조회 API
app.get('/api/users/:userId/missions/in-progress', handleListUserInProgressMissions);

// 특정 사용자의 진행 중인 미션 완료로 업데이트 API
app.put('/api/users/:userId/missions/:missionId/complete', handleCompleteUserMission);

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

 

 


 

 

📢 6주차 학습 후기


 

 

😺: 이번주 핵심 키워드는 ORM이었는데 전에 프로젝트를 할 때 ORM을 사용했던 경험이 있어서 이번 워크북 작성 때 이를 리마인드 할 수 있어서 좋았습니다. 또한 다양한 ORM에 대해서 알 수 있어서 좋았습니다.
반응형