Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[김민선] sprint10 #31

Open
wants to merge 24 commits into
base: express-김민선
Choose a base branch
from

Conversation

alscksdlek
Copy link
Collaborator

@alscksdlek alscksdlek commented Nov 12, 2024

요구사항

기본

Github PR

  • Github에 위클리 미션 PR을 만들어 주세요.
  • RESTful를 설계하고 백엔드 코드를 변경하세요.
  • 프론트엔드 코드에 맞게 백엔드 코드를 변경해 주세요.
  • 백엔드 코드에 swagger를 추가해 API 명세 문서를 생성해 주세요.

상품 등록

  • "상품 등록하기" 버튼 클릭 시, 상품 정보 등록 API 엔드포인트에 요청을 보내 상품을 등록합니다.
  • 상품 등록 시 필요한 필드(이름, 설명, 가격 등)의 유효성을 검증하는 미들웨어를 구현합니다.
  • multer 미들웨어를 사용하여 이미지 업로드 API를 구현해 주세요.
  • 업로드된 이미지는 서버에 저장하고, 해당 이미지의 경로를 response 객체에 포함해 반환합니다.

상품 상세

  • 상품을 조회할 때, 해당 상품에 대한 댓글 리스트, 사용자가 '좋아요'를 눌렀는지 여부를 확인할 수 있도록 응답 객체에 포함시켜 반환해 주세요.

좋아요 기능

  • '좋아요' API를 만들어 주세요.
  • 사용자는 상품 또는 게시글에 '좋아요'를 할 수 있습니다.
  • $transaction을 사용해 주세요.
  • '좋아요' 취소 API를 만들어 주세요.
  • 사용자는 상품 또는 게시글에 '좋아요'를 취소할 수 있습니다.
  • $transaction을 사용해 주세요.
  • 상품 또는 게시글을 조회할 때, 사용자가 '좋아요'를 누른 항목인지 확인할 수 있도록 isLiked 필드를 응답 객체에 포함시켜 반환해 주세요.

에러 처리

  • 모든 예외 상황을 처리할 수 있는 에러 핸들러 미들웨어를 구현합니다.
  • 서버 오류(500), 사용자 입력 오류(400 시리즈), 리소스 찾을 수 없음(404) 등 상황에 맞는 상태값을 반환합니다.

라우트 중복 제거

  • 중복되는 라우트 경로(예: /users에 대한 get 및 post 요청)를 app.route()로 통합해 중복을 제거합니다.
  • express.Router()를 활용하여 중고마켓/자유게시판 관련 라우트를 별도의 모듈로 구분합니다.

인증

  • User 스키마를 작성해 주세요.
  • id, email, nickname, image, encryptedPassword, createdAt, updatedAt 필드를 가집니다.
  • 회원가입 API를 만들어 주세요.
  • email, nickname, password 를 입력하여 회원가입을 진행합니다.
  • password는 해싱해 저장합니다.
  • 로그인 API를 만들어 주세요.
  • 사용자의 신원을 확인하고, 성공적인 인증 후에는 액세스 토큰을 발급해 response 객체에 포함해 반환합니다.

상품 기능 인가

  • 로그인한 사용자만 상품을 등록할 수 있습니다.
  • 상품을 등록한 사용자만 해당 상품의 정보를 수정 및 삭제를 할 수 있습니다.
  • 로그인한 사용자만 상품에 '좋아요'를 추가하거나 삭제할 수 있습니다.

게시글 기능 인가

  • 로그인한 사용자만 게시글을 등록할 수 있습니다.
  • 게시글을 등록한 사용자만 해당 게시글 정보를 수정 및 삭제할 수 있습니다.
  • 로그인한 사용자만 게시글에 '좋아요'를 추가하거나 삭제할 수 있습니다.

댓글 기능 인가

  • 로그인한 사용자만 상품에 댓글을 등록할 수 있습니다.
  • 로그인한 사용자만 게시글에 댓글을 등록할 수 있습니다.
  • 댓글을 등록한 사용자만 댓글을 수정하거나 삭제할 수 있습니다.

심화

인증

  • 토큰 기반 방식을 사용할 경우, 만료된 액세스 토큰을 새로 발급하는 리프레시 토큰 발급 기능을 구현합니다.(jwt sliding session 적용)

OAuth를 활용한 인증

  • 구글 OAuth를 사용하여 회원가입 및 로그인 기능을 구현합니다.

프로젝트 구조 변경

  • 프로젝트의 구조와 복잡성을 관리하기 위해 MVC 패턴이나 Layered Architecture와 같은 설계 방식을 적용해 보세요.

자유게시판 게시물 등록

  • 게시물 등록 시 필요한 필드(제목, 내용 등)의 유효성 검증하는 미들웨어를 구현합니다.
  • multer 미들웨어를 사용하여 이미지 업로드 API를 구현해 주세요.
  • 업로드된 이미지는 서버에 저장하고, 해당 이미지의 경로를 response 객체에 포함해 반환합니다.

주요 변경사항

스크린샷

image

멘토에게

  • 회원가입 로그인 유효성 검사 추가하기, 커스텀 에러 함수, multer는 일요일 저녁에 할거예요!

@alscksdlek alscksdlek self-assigned this Nov 12, 2024
@alscksdlek alscksdlek added 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 진행 중 🏃 아직 스프린트 미션 제출일이 아닙니다. 새로 커밋된 내용에 대해 코드리뷰 해주세요! labels Nov 12, 2024
@MCprotein
Copy link
Collaborator

@alscksdlek 컴파일된건 왜올리셨나요?

name String
description String
price Int
tags String[] @default([])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 왜 배열로 했나요?

Copy link
Collaborator Author

@alscksdlek alscksdlek Nov 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tag들이 여러개가 있고 문자열이기 때문에 배열을 사용하면 tag들을 묶기 편할 것 같아서 선택했습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek db 성능과 연관지어서 생각해보시겠어요?
그리고 tag는 재사용이 될 수 있는 요소인데 배열로 묶어버리면 그건 string[] 처럼 작동해서 하드코딩으로 들어갈텐데 괜찮을까요?

@alscksdlek
Copy link
Collaborator Author

@alscksdlek 컴파일된건 왜올리셨나요?

.gitignore 파일에 dist 폴더 넣는 것을 깜빡하였습니다 삭제하였습니다!

"main": "index.js",
"scripts": {
"dev": "tsx watch src/app.ts",
"build": "tsc --outDir dist",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 이건 tsconfig에서 지정할 수 있는데 여기다가 쓰신 이유가 있을까요?

Comment on lines +8 to +12
await prisma.like.deleteMany();
await prisma.comment.deleteMany();
await prisma.article.deleteMany();
await prisma.product.deleteMany();
await prisma.user.deleteMany();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 이건 Promise.all 로 묶으면 안되나요?

Comment on lines +15 to +34
await prisma.user.createMany({
data: USERS,
skipDuplicates: true,
});
await prisma.product.createMany({
data: PRODUCTS,
skipDuplicates: true,
});
await prisma.article.createMany({
data: ARTICLES,
skipDuplicates: true,
});
await prisma.comment.createMany({
data: COMMENTS,
skipDuplicates: true,
});
await prisma.like.createMany({
data: LIKES,
skipDuplicates: true,
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 이것도 promise.all 로 묶으면안되나요?

.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 주는게 무슨 의미인지 아시나요?

import { ProductRepository } from "../repositories/productRepository";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek prismaClient를 여러곳에서 생성하면 connection이 여러개 맺어질텐데 확인해보셨나요?
singleton 구조 내부에서는 connection 은 하나로만 해도 충분할거같아요
실제로 connection이 너무 많으면 부하가 심하거나 예상치못한문제가 생길수도있어요

export class ProductController {
constructor(private service: ProductService) {}

getProductById = asyncErrorHandler(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek controller의 메소드는 그 자체의 내용만 필요해요
async error handler가 controller에 속한 내용인지, util or middleware 성격인지 생각해보세요


getProductById = asyncErrorHandler(
async (req: Request, res: Response): Promise<any> => {
const { id } = req.params;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek id 유효성검사해야돼요
id 가 없거나 비정상적이면 이후 로직은 전부 실패할텐데, 실패할 로직을 실행할 이유가 없으니까요

const { id } = req.params;
const product = await this.service.getProductById(Number(id));
if (!product) {
return res.status(404).json({ message: "Not Found" });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek product의 유효성 검사를 한다면 service 에서 해야죠. controller는 http 요청만 처리하는곳이고 리소스에 대한 관리는 서비스에서 해야돼요
그리고 404를 반환하는게아니라 에러를 던져야돼요 그래야 전역에러핸들러에서 받으니까요


createUser = asyncErrorHandler(
async (req: Request, res: Response): Promise<any> => {
const user = await this.service.createUser(req.body);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek body를 통째로 보낸다면, body에 엉뚱한게 들어있어도 서비스단으로 들어가는건데 이거맞나요?

Comment on lines +19 to +22
const user = await this.service.getUser(email, password);
const accessToken = this.service.createToken(user.id);
const refreshToken = this.service.createToken(user.id, "refresh");
await this.service.updateUser(user.id, { refreshToken });
Copy link
Collaborator

@MCprotein MCprotein Nov 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 여러 서비스 메소드를 사용하지말고 loginUser api 요청에 대해 적절한 service 메소드를 하나만 쓰고, 그 메소드가 여러 private method 를 사용하도록 구성해보세요

import { AuthRequest } from "../types/requestType";

export const verifyAccessToken = expressjwt({
secret: process.env.JWT_SECRET as string,
Copy link
Collaborator

@MCprotein MCprotein Nov 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 왜 이쪽에는 전부 타입단언을 사용하셨나요?

.get(productContainer.productController.getProductById)
.patch(
verifyAccessToken,
verifyProductAuth(productContainer.productRepository),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek routes 는 컨트롤러의 상위 레이어인데 repository에 의존성을 가지면 안돼요

Comment on lines +26 to +27
const error = new Error("product not found");
throw error;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바로 throw 할 . 수있는데 error 변수로 초기화한 이유가 있나요?

Comment on lines +27 to +46
if (error.message === "User already exists") {
return res.status(422).json({
message: error.message,
email: req.body.email,
});
} else if (error.message === "Unauthorized") {
return res.status(401).json({ message: error.message });
} else if (
error instanceof Prisma.PrismaClientValidationError ||
error.name === "StructError"
) {
return res.status(400).json({ message: error.message });
} else if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === "P2025"
) {
return res.status(404).json({ message: error.message });
} else {
return res.status(500).json({ message: error.message });
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 하드코딩 빼고 커스텀에러클래스 만들어서 사용해보세요

assert(req.body, CreateProduct);
next();
} catch (error) {
next(error);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek superstruct 에러를 에러 핸들러에서 조건걸어 처리해야하는 번거로움이 있다면 아예 글로벌 에러 핸들러로 보내기 전에 우리의 에러로 변환해서 내보내도 되지 않을까싶어요

Comment on lines +12 to +23
createProduct = async (req: any): Promise<Product> => {
return await this.prisma.product.create({
data: req,
});
};

updateProduct = async (req: any, id: number): Promise<Product> => {
return await this.prisma.product.update({
where: { id },
data: req,
});
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek repository layer에서는 request 객체에 의존하면 안돼요

Comment on lines +6 to +16
findByEmail = async (email: any): Promise<any> => {
return await this.prisma.user.findUnique({
where: { email },
});
};

createUser = async (user: any): Promise<User> => {
return await this.prisma.user.create({
data: user,
});
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 왜 any 죠?

Comment on lines +9 to +19
userRouter.route("/").post(userContainer.userController.createUser);
userRouter.route("/login").post(userContainer.userController.loginUser);
userRouter
.route("/token/refresh")
.post(
verifyRefreshToken,
debugRefreshToken,
userContainer.userController.refreshAccessToken
);

return userRouter;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alscksdlek 이걸 controller가 아니라 route 클래스를 만들어서 작성한이유가있나요/

@MCprotein MCprotein mentioned this pull request Nov 17, 2024
47 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. 진행 중 🏃 아직 스프린트 미션 제출일이 아닙니다. 새로 커밋된 내용에 대해 코드리뷰 해주세요!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants