우리 팀은 PostgreSQL과 MongoDB방식 중 연습 차원에서 MongoDB를 채택하기로 하였다.
기본적으로 ESM형식을 기반으로 코드를 작성
그러기 위해 package.json에
"type": "module",
을 작성
앞서 개별적으로 배운 내용을 활용하고자 Mermaid ERDiagram을 작성해보았고
파일 구성 Tree는 팀원의 노션 내 ReadME파일을 따랐다.
erDiagram
erDiagram
STYLE {
string _id "PK"
string name "스타일 이름"
string title "스타일 제목"
string description "스타일 설명"
string content "스타일 내용"
string imageUrl "대표 이미지"
string thumbnail "썸네일 이미지"
string[] tags "태그 리스트"
string nickname "작성자"
int viewCount "조회수"
int curationCount "큐레이팅 수"
datetime createdAt "생성 날짜"
}
CATEGORY {
string _id "PK"
string type "의류 종류"
string name "의류명"
string brand "브랜드명"
int price "가격"
string styleId "FK"
}
CURATION {
string _id "PK"
int trendy "트렌디 점수"
int personality "개성 점수"
int practicality "실용성 점수"
int costEffectiveness "가성비 점수"
string content "큐레이팅 한줄 평"
string nickname "작성자 닉네임"
string passwd "비밀번호"
datetime createdAt "생성 날짜"
string styleId "FK"
}
STYLE ||--o{ CATEGORY : "Style Composition"
STYLE ||--o{ CURATION : "Curating"
현재 tistory에서는 mermaid를 지원하지 않는 것 같아 er다이어그램은 사진으로 대체한다.
Tree
style/
├── config/
│ └── database.js # MongoDB 연결 설정
├── middlewares
│ └── errorHandler.js
├── models/
│ └── style.js # 스타일 모델 (스키마)
├── controllers/
│ └── styleController.js # 스타일 관련 로직
├── routes/
│ └── styleRoutes.js # 라우트 설정
├── .env # 환경변수 설정
├── request.http
└── app.js # 테스트 파일
database.js
MongoDB를 활용하기 때문에 config/database.js를 다음과 같이 설정하였다.
import mongoose from "mongoose";
import dotenv from "dotenv";
dotenv.config();
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log("MongoDB 연결 성공");
} catch (error) {
console.error("MongoDB 연결 실패:", error);
process.exit(1);
}
};
export default connectDB;
style.js
위 erDiagram을 기반으로 모델 스타일 스키마를 작성
import mongoose from "mongoose";
// 카테고리 스키마 정의
const CategorySchema = new mongoose.Schema({
type: { type: String, required: true }, // 의류 유형 (예: 상의, 하의, 신발 등)
name: { type: String, required: true },
brand: String,
price: Number,
});
// 스타일 스키마 정의
const StyleSchema = new mongoose.Schema({
name: { type: String, required: true }, // 작성자 이름
title: { type: String, required: true }, // 스타일 제목
description: String, // 스타일 설명
content: String, // 스타일 상세 내용
imageUrl: String, // 스타일 대표 이미지 URL
thumbnail: String, // 스타일 썸네일 이미지 URL
tags: [String], // 스타일과 관련된 태그 목록
nickname: { type: String, required: true }, // 작성자의 닉네임
viewCount: { type: Number, default: 0 }, // 조회수
curationCount: { type: Number, default: 0 }, // 큐레이팅 수
trendy: { type: Number, default: 0 }, // 트렌디 점수
personality: { type: Number, default: 0 }, // 개성 점수
practicality: { type: Number, default: 0 }, // 실용성 점수
costEffectiveness: { type: Number, default: 0 }, // 가성비 점수
categories: [CategorySchema], // 스타일을 구성하는 패션 아이템 목록 (배열 형태)
createdAt: { type: Date, default: Date.now }, // 스타일 등록 날짜 (기본값: 현재 시간)
});
// 스타일 검색을 위한 텍스트 인덱스 설정
StyleSchema.index({ title: "text", description: "text", tags: "text" });
// Style 모델 생성 및 내보내기
export default mongoose.model("Style", StyleSchema);
styleController.js
컨트롤러의 검색 및 정렬 기능
//검색 정렬
export const getStyles = async (req, res, next) => {
try {
const {
page = 1,
limit = 10,
search = "",
sortBy = "createdAt",
order = "desc",
} = req.query;
const query = search
? {
$or: [
{ title: { $regex: search, $options: "i" } },
{ nickname: { $regex: search, $options: "i" } },
{ description: { $regex: search, $options: "i" } },
{ tags: { $regex: search, $options: "i" } },
],
}
: {};
const total = await Style.countDocuments(query);
const styles = await Style.find(query)
.sort({ [sortBy]: order === "desc" ? -1 : 1 })
.skip((page - 1) * limit)
.limit(Number(limit));
[...]
- page, limit, search, sortBy, order 등의 요청 파라미터를 사용하여 스타일 목록을 검색 및 정렬
- search 값이 있으면 제목, 닉네임, 설명, 태그에서 해당 문자열을 포함하는 내용을 검색
- sortBy와 order 값을 기준으로 정렬하며, 기본값은 createdAt 기준 내림차순(desc)으로 설정
- 페이지네이션을 위한 skip, limit을 적용
랭킹 기능 구현
//랭킹
export const getStyleRankings = async (req, res, next) => {
try {
const { sortBy = "curationCount", page = 1, limit = 10 } = req.query;
const validSortFields = [
"trendy",
"personality",
"practicality",
"costEffectiveness",
"viewCount",
"curationCount",
];
if (!validSortFields.includes(sortBy)) {
return res.status(400).json({ error: "잘못된 요청" });
}
const total = await Style.countDocuments();
const styles = await Style.find()
.sort({ [sortBy]: -1 })
.skip((page - 1) * limit)
.limit(Number(limit));
[...]
- sortBy는 trendy, personality, practicality, costEffectiveness, viewCount, curationCount 등으로 랭킹을 매김
- 랭킹 기준에 따라 내림차순 정렬하여 페이지네이션을 적용하여 반환
조회를 통한 조회수 증가는 아래와 같이 구현
const { id } = req.params;
const style = await Style.findByIdAndUpdate(
id,
{ $inc: { viewCount: 1 } },
{ new: true }
조회 혹은 데이터에 응답 요청을 보낼 때 마다 viewCount값이 1씩 증가하는 것을 확인
errorHandler.js
PR 이후 코드 컨벤션을 통해 errorHandler를 작성하여 코드의 가독성을 높이는 것이 어떻냐는 제안을 확인
대부분의 에러를 하드코드로 작성하였기 때문에 필요성을 인지
공통으로 500에러를 작성한 것을 확인 하고 아래와 같이 에러 핸들러를 작성
const errorHandler = (err, req, res, next) => {
console.error(err.message);
const statusCode = res.statusCode < 400 ? 500 : res.statusCode;
res.status(statusCode).json({ error: err.message });
};
export default errorHandler;
next를 활용하여 에러 핸들링 처리
이후 아래와 같이 컨트롤러에서 대부분의 코드 길이와 가독성이 향상
try {
[...]
} catch (error) {
next(error);
}
};
+ ) 중간 피드백 이후 MongoDB보다는 PostgreSQL을 권장한다는 소식을 듣고 전체적으로 형식을 바꿀 필요가 생겼다. 이후 트리 구조 변경과 Prisma를 이용해 DB와 스키마 부터 다시 작성해보기로 했다.
tree
backend/
├── node_modules/
├── src/
│ ├── config/
│ │ └── database.js # 데이터베이스 연결 설정
│ │
│ ├── controllers/ # 비즈니스 로직 처리
│ │ └── styleController.js
│ │
│ ├── routes/ # API 라우트 정의
│ │ └── styles.js # 스타일 관련 API (/api/styles/*)
│ │
│ ├── middlewares/ # 미들웨어
│ │ └── errorHandler.js # 에러 처리 미들웨어
│ │
│ └── services/ # 비즈니스 로직 서비스
├── prisma/ # Prisma ORM 설정
├── app.js # 메인 애플리케이션 파일
├── package.json # 프로젝트 의존성 관리
├── package-lock.json
└── .gitignore
다시 팀원들과 상의 후 ReadME를 통해 트리를 참조하였다.
PostgreSQL 프리즈마로 바꾸었기 때문에 데이터 베이스의 형식을 바꾸었다.
database.js
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
이후 모델 스타일 스키마에 해당하는 부분도 프리즈마 형식에 맞추어 다시 작성하였다.
schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Style {
id Int @id @default(autoincrement())
name String
title String
description String?
content String?
imageUrl String?
thumbnail String?
nickname String
password String? // 스타일 작성자 비밀번호 필드 추가
viewCount Int @default(0)
curationCount Int @default(0)
trendy Int @default(0)
personality Int @default(0)
practicality Int @default(0)
costEffectiveness Int @default(0)
categories Category[]
tags Tag[] @relation("StyleTags")
images StyleImage[] // 1:N 관계 추가
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Curation Curation[]
}
model Category {
id Int @id @default(autoincrement())
type String
name String
brand String?
price Float?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
styleId Int
style Style @relation(fields: [styleId], references: [id])
}
이후 기존 MongoDB형식으로 작성 된 코드들을 찾아 PostgreSQL형식으로 바꾸어야 한다.
임포트 해야 할 곳을 바꿔주고 프리즈마를 지정
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
Router.js
라우터는 기존과 동일하게 설정하고 설명을 위한 주석을 추가
import express from "express";
const router = express.Router();
import {
getStyleRankings,
createStyle,
getStyles,
updateStyle,
deleteStyle,
getStyleById,
} from "../controllers/styleController.js";
// 스타일 랭킹 조회
router.get("/rankings", getStyleRankings);
// 스타일 등록
router.post("/", createStyle);
// 스타일 목록 조회
router.get("/", getStyles);
// 스타일 상세 조회
router.get("/:id", getStyleById);
// 스타일 수정
router.put("/:id", updateStyle);
// 스타일 삭제
router.delete("/:id", deleteStyle);
export default router;
사실 처음 작성할 때 ESM형식을 사용하였기에 몇몇 세부적인 부분을 제외하면 크게 바꿀 부분이 없었다..
대부분의 유틸 함수나 프론트와 연결을 위해 수정 된 부분은 팀장님이 많은 부분을 수정해 주었기 때문에
받아온 결과로 실행하는게 전부였다.
'프로젝트 회고' 카테고리의 다른 글
part2 중급 프로젝트 개별 보고서 (1) | 2025.05.13 |
---|