HJW's IT Blog

프로젝트 : 조각집 [2024-09-20 기록] 본문

카테고리 없음

프로젝트 : 조각집 [2024-09-20 기록]

kiki1875 2024. 9. 20. 16:23

주어진 요구사항에 따라 ERD 설계를 시작하였다. 주어진 요구사항은 다음과 같았다

### 그룹

**그룹 등록**

- 그룹명, 대표 이미지, 그룹 소개, 그룹 공개 여부, 비밀번호를 입력하여 그룹을 등록합니다.

**그룹 수정**

- 비밀번호를 입력하여 그룹 등록 시 입력했던 비밀번호와 일치할 경우 그룹 정보 수정이 가능합니다.

**그룹 삭제**

- 비밀번호를 입력하여 그룹 등록 시 입력했던 비밀번호와 일치할 경우 그룹 삭제가 가능합니다.

**그룹 목록 조회**

- 등록된 그룹 목록을 조회할 수 있습니다.
- 각 그룹의 이미지(한 장), 그룹명, 그룹 소개, 그룹 공개 여부, 디데이(생성 후 지난 일수), 획득 배지수, 추억수, 그룹 공감수가 표시됩니다.
- 공개 그룹 목록과 비공개 그룹 목록을 구분하여 조회합니다.
- 최신순, 게시글 많은순, 공감순, 획득 배지순으로 정렬 가능합니다.
- 그룹명으로 검색 가능합니다.

**그룹 상세 조회**

- 그룹 목록 페이지에서 그룹을 클릭할 경우 그룹 상세 조회가 가능합니다.
- 비공개 그룹의 경우 비밀번호를 입력하여 그룹 등록시 입력한 비밀번호와 일치할 경우 조회 가능합니다.
- 각 그룹의 대표 이미지, 그룹명, 그룹 소개, 그룹 공개 여부, 디데이(생성 후 지난 일수), 획득 배지 목록, 추억수, 그룹 공감수가 표시됩니다.
- 공감 보내기 버튼을 클릭할 경우 그룹의 공감수를 높일 수 있으며, 공감은 클릭할 때마다 중복해서 보낼 수 있습니다.
- 해당 그룹의 추억 목록이 표시됩니다.

### 게시글(추억)

**게시글 등록**

- 닉네임, 제목, 이미지(한 장), 본문, 태그, 장소, 추억의 순간, 추억 공개 여부, 비밀번호를 입력하여 추억 등록이 가능합니다.

**게시글 수정**

- 비밀번호를 입력하여 추억 등록 시 입력했던 비밀번호와 일치할 경우 추억 수정이 가능합니다.

**게시글 삭제**

- 비밀번호를 입력하여 추억 등록 시 입력했던 비밀번호와 일치할 경우 추억 삭제가 가능합니다.

**게시글 목록 조회**

- 그룹 상세 조회를 할 경우 그 그룹에 해당되는 추억 목록이 같이 조회됩니다.
- 각 추억의 닉네임, 추억 공개 여부, 제목, 이미지, 태그, 장소, 추억의 순간, 추억 공감수, 댓글수가 표시됩니다.
- 공개 추억 목록과 비공개 추억 목록을 구분하여 조회합니다.
- 최신순, 댓글순, 공감순으로 정렬 가능합니다.
- 제목, 태그로 검색 가능합니다.

**게시글 상세 조회**

- 추억 목록에서 추억을 클릭할 경우 추억 상세 조회가 가능합니다.
- 닉네임, 제목, 이미지(한 장), 본문, 태그, 장소, 추억의 순간, 추억 공개 여부, 추억 공감수, 댓글수가 표시됩니다.
- 공감 보내기 버튼을 클릭할 경우 그룹의 공감수를 높일 수 있으며, 공감은 클릭할 때마다 중복해서 보낼 수 있습니다.
- 해당 추억의 댓글 목록이 조회됩니다.

### 댓글

**댓글 등록**

- 닉네임, 댓글 내용, 비밀번호를 입력하여 댓글 등록이 가능합니다.

**댓글 수정**

- 비밀번호를 입력하여 댓글 등록 시 입력했던 비밀번호와 일치할 경우 댓글 수정이 가능합니다.

**댓글 삭제**

- 비밀번호를 입력하여 댓글 등록 시 입력했던 비밀번호와 일치할 경우 댓글 삭제가 가능합니다.

**댓글 목록 조회**

- 추억을 조회할 경우 그 추억에 해당되는 댓글 목록이 조회됩니다.
- 닉네임, 댓글 생성 날짜, 댓글 내용이 표시됩니다.

### 배지

- 그룹은 일정 조건을 달성하면 자동으로 배지를 획득합니다.
- 배지의 종류
    - 7일 연속 추억 등록
    - 추억 수 20개 이상 등록
    - 그룹 생성 후 1년 달성
    - 그룹 공간 1만 개 이상 받기
    - 추억 공감 1만 개 이상 받기
        - 공감 1만 개 이상의 추억이 하나라도 있으면 획득

 

이에 따라 다음과 같이 ERD 를 설계하였다. 

만들다 보니 GROUP은 SQL 예약어이기 때문에 다른 이름으로 해야겠다는 생각이 들었다.

GROUP 과 BADGE 의 관계는 N:N 관계이기 때문에, 이 두 테이블의 PK를 Composite Key 로 사용하는 GROUP_BADGE 테이블을 생성하였다.

TAG 와 POST 의 관계 또한 N:N 관계이기에 Composite Key 를 생성하였다.

 

현재 INDEX 를 어느 컬럼에 대해 생성 해야 할 지 고민중인데, 후보군은 다음과 같다

  1. Groups 의 Gname : 사용자들이 그룹명으로 검색할 수 있다
  2. Posts 의 Title : 사용자들은 게시글 검색을 할 수 있다
  3. Post 의 CreateDate : 개시글을 최신순으로 정렬할 때
  4. Post의 LikeCount : 개시글을 공감 순으로 정렬할 때

위 인덱스중 정말 필수적이라 생각되는 것은 idx_posts_createddate 인데, 기본적으로 게시글을 최신순으로 보여주는것이 일반적이기 때문이다.

 

이제 프로젝트 기본 세팅을 시작하겠다.

node -v
npm -v
  • node.js 와 npm 이 설치되어 있는지 확인 후,
npm init -y
  • 초기화를 해주었다.
  • 필자는 해당 프로젝트를 typescript로 진행할 것이기에, 필요한 개발 의존성을 설치해야 한다
npm install --save-dev typescript ts-node @types/node @types/express @types/sequelize
  • typescript: TypeScript 컴파일러
  • ts-node: TypeScript 코드를 직접 실행하기 위한 도구
  • @types/*: TypeScript용 타입 정의 파일
  • typescript는 개발에서만 사용되기 때문에 save-dev 옵션을 꼭 사용해야 한다
npx tsc --init
  • tsconfig.json 생성
  • tsc → typescript 컴파일러
//package.json 내에 작성
"scripts": {
  "build": "tsc",
  "start": "node dist/app.js",
  "dev": "ts-node src/app.ts",
  "format": "prettier --write ."
},

npm install express
npm install --save-dev @types/express

npm install sequelize mysql2
npm install --save-dev @types/sequelize

npm install sequelize-typescript

Sequelize 란?

  • DB 작업을 도와주는 ORM(Object Relational Mapping) 라이브러리 이다
  • 자바스크립트 객체와 관계형 DB를 서로 연결해 주는 도구이다.

Model 생성

/src/config/model 내부에 각 테이블의 모델을 생성하였다.

모델의 정의

  • 테이블의 추상화 : 모델은 테이블을 코드 상에서 표현한 것으로 각 테이블의 구조를 클래스나 객체로 나타낸다
  • 객체 지향적 접근을 가능하게 만들어, SQL 쿼리를 직접 작성하지 않고도 객체 지향적 방식으로 DB와 상호작용 할 수 있다
  • 모델은 DB와의 CRUD 작업이 필요한 모든 곳에서 사용된다.
  • 다음은 모델의 예시이다

sequelize.ts

이 코드는 sequelize-typescript 라이브러리 사용시, ORM 인스턴스를 설정하는 코드이다. DB 연결및 관련 설정을 처기화하고, 모델을 DB 테이블과 연결한다.

import { Sequelize } from 'sequelize-typescript';
import path from 'path';

export const sequelize = new Sequelize({
  database: 'ZOGAKZIP',
  dialect: 'mysql',
  username: 'admin',
  password: '1234',
  storage: ':memory:',
  models: [path.resolve(__dirname, '../models')] // 모델 폴더 위치
});
import { Table, Column, Model, DataType, PrimaryKey, AutoIncrement, Default, CreatedAt, UpdatedAt, AllowNull } from 'sequelize-typescript';

@Table({
  tableName: 'Group',
  timestamps: false
})
export default class Group extends Model<Group>{
  
  @Column({
    type: DataType.INTEGER,
    primaryKey: true,
    autoIncrement: true
  })
  GID!:number;

  @Column({
    type: DataType.STRING(100),
    allowNull: false,
  })
  GName!:string;

  @Column({
    type: DataType.STRING(255)
  })
  GImage!:string;

  @Column({
    type: DataType.TEXT
  })
  GIntro!:string;

  @Column({
    type: DataType.BOOLEAN,
    allowNull: false
  })
  IsPublic!: boolean;

  @Column({
    type: DataType.STRING(255),
    allowNull: false
  })
  GPassword!: string;
  
  @CreatedAt
  @Column({
    type: DataType.DATE,
    defaultValue: DataType.NOW
  })
  CreatedDate!: Date;

  @Default(0)
  @Column({
    type: DataType.INTEGER
  })
  GLikes!:number;

  @Default(0)
  @Column({
    type: DataType.INTEGER
  })
  GBadgeCount!: number;

  @Default(0)
  @Column({
    type: DataType.INTEGER
  })
  PostCount!: number;

}

 

MySQL 서버 설정

DB 생성

CREATE DATABASE ZOGAKZIP
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
  • DEFAULT CHARACTER SET utf8mb4
    • 기본 문자 집합을 utf8mb4 로 설정하여, 유니코드 문자를 4바이트 까지 지원하는 문자 집합으로 설정하였다. 이를 이용해, 이모지와 같은 다양한 문자를 저장할 수 있다.
  • DEFAULT COLLATE utf8mb4_unicode_ci
    • 기본 정렬 규칙을 정의하는 것이다.
    • 대소문자를 구분하지 않고 case-insective 문자를 정렬 하고 비교하는 방식

사용자 생성 및 권한 부여

CREATE USER 'admin'@'localhost' IDENTIFIED BY '1234';
GRANT ALL PRIVILEGES ON ZOGAKZIP.* TO 'admin'@'localhost';
FLUSH PRIVILEGES;
  • root user 를 사용하는것은 보안상의 이유로 안전하지 못하므로 admin 을 생성해 주었다.

환경변수 설정

root dirctory 에 .env 파일 생성

DB_USERNAME=admin
DB_PASSWORD=1234
DB_NAME=ZOGAKZIP
DB_HOST=localhost
DB_DIALECT=mysql

마이그레이션

config/config.js

//require('dotenv').config(); 

module.exports = {
  development: {
    username: 'admin',
    password: '1234',
    database: 'ZOGAKZIP',
    host: '127.0.0.1',
    dialect: 'mysql',
  },

};

npm install --save-dev sequelize-cli-typescript
// 마이그레이션 파일 작성
npx sequelize-cli db:migrate
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Group', {
      GID: {
        type: Sequelize.INTEGER,
        primaryKey: true,
        autoIncrement: true,
      },
      GName: {
        type: Sequelize.STRING(100),
        allowNull: false,
      },
      GImage: {
        type: Sequelize.STRING(255),
        allowNull: true,
      },
      GIntro: {
        type: Sequelize.TEXT,
        allowNull: true,
      },
      IsPublic: {
        type: Sequelize.BOOLEAN,
        allowNull: false,
      },
      GPassword: {
        type: Sequelize.STRING(255),
        allowNull: false,
      },
      CreatedDate: {
        type: Sequelize.DATE,
        allowNull: false,
        defaultValue: Sequelize.fn('NOW'),
      },
      GLikes: {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
      GBadgeCount: {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
      PostCount: {
        type: Sequelize.INTEGER,
        allowNull: false,
        defaultValue: 0,
      },
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Group');
  },
};

Sequelize → Prisma 전환

npm install prisma --save-dev
npm install @prisma/client

// schema.prisma 작성

npx prisma migrate dev --name init
npx generate prisma

prisma 설정

// .env
DATABASE_URL="mysql://admin:1234@localhost:3306/zogakzip"
  • 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 Badge {
  BadgeID     Int     @id @default(autoincrement()) @map("BadgeID")
  Name        String  @db.VarChar(100)
  Description String  @db.Text
  Condition   String  @db.Text

  GroupBadges GroupBadge[]

  @@map("Badges")
}

model Comment {
  CommentID   Int     @id @default(autoincrement()) @map("CommentID")
  PostID      Int
  Nickname    String  @db.VarChar(50)
  Content     String  @db.Text
  Password    String  @db.VarChar(255)
  CreatedDate DateTime @default(now()) @map("CreatedDate")

  post Post @relation(fields: [PostID], references: [PostID])

  @@map("Comments")
}

model Group {
  GID          Int     @id @default(autoincrement()) @map("GID")
  GName        String  @db.VarChar(100)
  GImage       String? @db.VarChar(255)
  GIntro       String? @db.Text
  IsPublic     Boolean
  GPassword    String  @db.VarChar(255)
  CreatedDate  DateTime @default(now()) @map("CreatedDate")
  GLikes       Int     @default(0)
  GBadgeCount  Int     @default(0)
  PostCount    Int     @default(0)

  posts        Post[]
  groupBadges  GroupBadge[]

  @@map("Group")
}

model GroupBadge {
  GID          Int
  BadgeID      Int
  ObtainedDate DateTime @default(now()) @map("ObtainedDate")

  group Group @relation(fields: [GID], references: [GID])
  badge Badge @relation(fields: [BadgeID], references: [BadgeID])

  @@id([GID, BadgeID])
  @@map("Group_Badge")
}

model Post {
  PostID       Int      @id @default(autoincrement()) @map("PostID")
  GID          Int
  Nickname     String   @db.VarChar(51)
  Title        String   @db.VarChar(100)
  Image        String?  @db.VarChar(255)
  Content      String   @db.Text
  Location     String?  @db.VarChar(100)
  MemoryMoment DateTime @map("MemoryMoment")
  IsPublic     Boolean
  PPassword    String   @db.VarChar(255)
  CreatedDate  DateTime @default(now()) @map("CreatedDate")
  LikeCount    Int      @default(0)
  CommentCount Int      @default(0)

  group    Group   @relation(fields: [GID], references: [GID])
  comments Comment[]
  postTags PostTag[]

  @@map("Posts")
}

model PostTag {
  PostID Int
  TagID  Int

  post Post @relation(fields: [PostID], references: [PostID])
  tag  Tag  @relation(fields: [TagID], references: [TagID])

  @@id([PostID, TagID])
  @@map("Post_Tag")
}

model Tag {
  TagID Int     @id @default(autoincrement()) @map("TagID")
  Name  String  @db.VarChar(50)

  postTags PostTag[]

  @@map("Tags")
}

Prisma로 변경한 이유

  1. Type Safety → Prisma 는 정적 타입을 보장하며, DB 스키마에 변경이 생기면 자동으로 업데이트 된다
  2. 자동 생성 스키마 → prisma 는 스키마 우선 접근 방식을 제공한다. DB 스키마 변경시, 마이그레이션과 타입이 자동으로 업데이트되어 관리가 편하다
  3. 마이그레이션 시스템 자동화