post-service는 유저가 함께 운동할 사람을 구인한다던지, 아니면 운동 방법에 대한 조언을 구한다던지 다양한 종류의 게시글을 작성할 수 있게 해주는 서비스입니다. 게시글의 종류는 다음과 같습니다.
1) 함께해요: 함께 운동할 사람을 구할 수 있는 게시글입니다. 게시글의 댓글을 통해서 함께 운동할 사람을 구할 수 있는데 예상 시나리오는 다음과 같습니다.
시나리오 1)
시나리오 2)
2) 도와주세요: 도와주세요는 운동에 관한 방법을 물어보는 게시글입니다. 예상 시나리오는 다음과 같습니다.
시나리오 1)
이상으로 게시글 타입과 관련하여 예상 시나리오를 작성해보았고, 이를 바탕으로 post-service를 작성해보도록 하겠습니다.
다음의 명령어로 post-service를 만들도록 하겠습니다.
nest new post-service
cd post-service
nest generate module post
nest generate service post
컨트롤러부터 작성을 해보겠습니다.
import { Controller, Delete, Get, HttpStatus, Param, Post } from "@nestjs/common";
import { Builder } from "builder-pattern";
import { statusConstants } from "./constants/status.constants";
import { PostDto } from "./dto/post.dto";
import { PostService } from "./post/post.service";
import { RequestPost } from "./vo/request.post";
import { ResponsePost } from "./vo/response.post";
@Controller('post-service')
export class AppController {
constructor(private readonly postService: PostService) {}
@Post('/')
public async write(vo: RequestPost): Promise<any> {
try {
const dto: any = await this.postService.write(Builder(PostDto).type(vo.type)
.title(vo.title)
.content(vo.content)
.userId(vo.userId)
.writer(vo.writer)
.rental(vo.rental ? vo.rental : null)
.build());
if(dto.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dto.message,
});
}
return await Object.assign({
status: HttpStatus.CREATED,
payload: Builder(ResponsePost).type(dto.payload.type)
.title(dto.payload.title)
.content(dto.payload.content)
.userId(dto.payload.userId)
.writer(dto.payload.writer)
.createdAt(dto.payload.createdAt)
.rental(dto.payload.rental)
.build(),
message: "Get post data"
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err
});
}
}
@Get('/')
public async getAll(): Promise<any> {
try {
const dtos: any = await this.postService.getAll();
if(dtos.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dtos.message,
});
}
const responsePosts: Array<ResponsePost> = [];
for(const dto of dtos.payload) {
responsePosts.push(dto);
}
return await Object.assign({
status: HttpStatus.OK,
payload: responsePosts,
message: "Get posts data"
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err
});
}
}
@Get('posts/type/:type')
public async getPostsByType(@Param('type') type: string): Promise<any> {
try {
const dtos: any = await this.postService.getPostsByType(type);
if(dtos.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dtos.message,
});
}
const responsePosts: Array<ResponsePost> = [];
for(const dto of dtos.payload) {
responsePosts.push(dto);
}
return await Object.assign({
status: HttpStatus.OK,
payload: responsePosts,
message: "Get posts data",
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err,
});
}
}
@Get(':_id/post')
public async getOne(@Param('_id') _id: string): Promise<any> {
try {
const dto: any = await this.postService.getOne(_id);
if(dto.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dto.message,
});
}
return await Object.assign({
status: HttpStatus.OK,
payload: Builder(ResponsePost)._id(dto.payload._id)
.type(dto.payload.type)
.title(dto.payload.title)
.content(dto.payload.content)
.userId(dto.payload.userId)
.writer(dto.payload.writer)
.createdAt(dto.payload.createdAt)
.rental(dto.payload.rental)
.build(),
message: "Get post data",
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err
});
}
}
@Get(':userId/posts')
public async getPostsByUserId(@Param('userId') userId: string): Promise<any> {
try {
const dtos: any = await this.postService.getPostsByUserId(userId);
if(dtos.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dtos.message,
});
}
const responsePosts: Array<ResponsePost> = [];
for(const dto of dtos.payload) {
responsePosts.push(dto);
}
return await Object.assign({
status: HttpStatus.OK,
payload: responsePosts,
message: "Get posts data"
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err,
});
}
}
@Get('posts/keyword/:keyword')
public async getPostsByKeyword(@Param('keyword') keyword: string): Promise<any> {
try {
const dtos: any = await this.postService.getPostsByKeyword(keyword);
if(dtos.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dtos.message,
});
}
const responsePosts: Array<ResponsePost> = [];
for(const dto of dtos.payload) {
responsePosts.push(dto);
}
return await Object.assign({
status: HttpStatus.OK,
payload: responsePosts,
message: "Get posts data"
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err
});
}
}
@Delete(':_id/post')
public async deletePost(@Param('_id') _id: string): Promise<any> {
try {
const result: any = await this.postService.deletePost(_id);
if(result.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + result.message,
});
}
return await Object.assign({
status: HttpStatus.NO_CONTENT,
payload: null,
message: "Delete post"
})
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err
});
}
}
}
컨트롤러의 메서드를 살펴 보겠습니다.
1) write: 게시글을 생성하는 메서드입니다. vo 객체를 전달받아 객체 내에 존재하는 type, title, content, userId, writer, rental 요소들을 dto 객체로 변환하여 전달하고 결과값을 반환받아 이 값을 유저에게 보여줍니다.
2) getAll: 게시글 홈에서 사용될 메서드입니다. 게시글 홈에서는 생성된 전체 게시글을 받아와 사용자 화면에 띄워 줍니다.
3) getPostsByType: 게시글 홈에 있는 정렬탭에서 사용될 메서드입니다. 사용자는 도와주세요, 함께해요의 값을 가진 타입을 기반으로 게시글들을 볼 수 있습니다.
4) getOne: 상세 게시글을 볼 수 있는 메서드입니다. 사용자는 게시글 리스트 중 원하는 게시글을 클릭하여 상세 게시글을 볼 수 있는데 이 때 사용되는 메서드입니다.
5) getPostsByUserId: 마이페이지의 게시글 내역에서 활용될 메서드입니다. 게시글 내역 클릭 시 userId를 기반으로 사용자가 작성한 게시글들을 불러오는 메서드입니다.
6) getPostsByKeyword: 게시글 홈 최상단에 있는 검색탭에서 사용될 메서드입니다. 사용자는 원하는 키워드를 바탕으로 게시글을 불러올 수 있습니다.
7) deletePost: 게시글을 삭제하는 메서드입니다. 사용자는 게시글 내역에 있는 리스트 중 원하는 게시글을 삭제할 수 있습니다.
컨트롤러의 오류코드들을 통과하기 위한 클래스들을 작성하도록 하겠습니다.
npm install --save class-validator class-transformer builder-pattern
import { IsString } from "class-validator";
export class PostDto {
@IsString()
_id: string;
@IsString()
type: string;
@IsString()
title: string;
@IsString()
content: string;
@IsString()
userId: string;
@IsString()
writer: string;
@IsString()
createdAt: string;
rental: any;
}
import { IsString } from "class-validator";
export class RequestPost {
@IsString()
type: string;
@IsString()
title: string;
@IsString()
content: string;
@IsString()
userId: string;
@IsString()
writer: string;
rental: any;
}
import { IsString } from "class-validator";
export class ResponsePost {
@IsString()
_id: string;
@IsString()
type: string;
@IsString()
title: string;
@IsString()
content: string;
@IsString()
userId: string;
@IsString()
writer: string;
@IsString()
createdAt: string;
rental: any;
}
export const statusConstants = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
};
여기까지 작성을 하고 컨트롤러에서 클래스들을 import하면 service 메서드만 남게 됩ㄴ디다. 이어서 service클래스를 작성하도록 하겠습니다.
PostService를 작성하기 전에 mongoose 라이브러리를 설치하도록 하겠습니다.
npm install --save mongoose @nestjs/mongoose
post 스키마에는 rental 데이터가 들어갈 수도 들어가지 않을수도 있습니다. 따라서 rental데이터에 대한 인터페이스를 만들도록 하겠습니다.
import { Payment } from "./payment.interface";
export class Rental {
rentalId: string;
price: number;
borrower: string;
tel: string;
userId: string;
date: string;
time: string;
mapId: string;
mapName: string;
status: string;
createdAt: string;
payment: Payment;
}
export class Payment {
paymentName: string;
payer: string;
rentalId: string;
paymentId: string;
price: number;
createdAt: string;
}
이어서 post 스키마를 작성하도록 하겠습니다.
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { SchemaTypes, Types } from "mongoose";
export type PostDocument = Post & Document;
@Schema()
export class Post {
@Prop({ required: true })
type: string;
@Prop({ required: true})
title: string;
@Prop({ required: true })
content: string;
@Prop({ required: true })
userId: string;
@Prop({ required: true })
writer: string;
@Prop({ required: true })
createdAt: string;
@Prop()
rental: any;
}
export const PostSchema = SchemaFactory.createForClass(Post);
mongoose 모듈을 import 하겠습니다.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Post, PostSchema } from 'src/schema/post.schema';
import { PostService } from './post.service';
@Module({
imports: [
MongooseModule.forFeature([{
name: Post.name,
schema: PostSchema,
}])
],
providers: [PostService],
exports: [PostService]
})
export class PostModule {}
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostModule } from './post/post.module';
@Module({
imports: [
MongooseModule.forRoot("mongodb://localhost:27017/POSTSERVICE?readPreference=primary&appname=MongoDB%20Compass&directConnection=true&ssl=false"),
PostModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
모듈을 import했으니 service에서 model을 호출하여 비즈니스 로직을 구현하도록 하겠습니다.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Builder } from 'builder-pattern';
import { Model } from 'mongoose';
import { statusConstants } from 'src/constants/status.constants';
import { PostDto } from 'src/dto/post.dto';
import { Post, PostDocument } from 'src/schema/post.schema';
@Injectable()
export class PostService {
constructor(@InjectModel(Post.name) private postModel: Model<PostDocument>) {}
public async write(dto: PostDto): Promise<any> {
try {
const entity: any = await new this.postModel(Builder(Post).title(dto.title)
.content(dto.content)
.type(dto.type)
.userId(dto.userId)
.writer(dto.writer)
.createdAt(new Date().toString())
.rental(dto.rental ? dto.rental : null)
.build())
.save();
if(!entity) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: Database error!"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: Builder(PostDto).title(entity.title)
.content(entity.content)
.type(entity.type)
.userId(entity.userId)
.writer(entity.writer)
.createdAt(entity.createdAt)
.rental(entity.rental)
.build(),
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "post-service: " + err,
});
}
}
public async getAll(): Promise<any> {
try {
const entities: any = await this.postModel.find();
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist posts data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(entity);
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err
});
}
}
public async getOne(_id: string): Promise<any> {
try {
const entity: any = await this.postModel.findOne({ _id: _id });
if(!entity) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: Builder(PostDto)._id(String(entity._id))
.title(entity.title)
.content(entity.content)
.type(entity.type)
.userId(entity.userId)
.writer(entity.writer)
.createdAt(entity.createdAt)
.rental(entity.rental)
.build(),
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
public async getPostsByType(type: string): Promise<any> {
try {
const entities: any = await this.postModel.find({ type: type });
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(entity);
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err
});
}
}
public async getPostsByUserId(userId: string): Promise<any> {
try {
const entities: any = await this.postModel.find({ userId: userId });
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(entity);
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
public async getPostsByKeyword(keyword: string): Promise<any> {
try {
const entities: any = await this.postModel.aggregate([{
$group: {
$or: [
{ title: { $regex: '.*' + keyword + '.*' }},
{ content: { $regex: '.*' + keyword + '.*' }}
]
}
}]);
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(entity);
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
public async deletePost(_id: string): Promise<any> {
try {
const result: any = await this.postModel.deleteOne({ _id: _id });
if(!result) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Not exist post data"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
}
컨트롤러에 필요한 7개의 메서드를 완성했습니다. 메서드를 살펴보겠습니다.
1) write: 글쓰기를 위한 메서드입니다. dto 객체를 전달받아 이를 이용하여 entity를 구성하고, document로 저장합니다. 그리고 이 entity를 이용하여 다시 dto객체로 변환시킨 후 컨트롤러 레이어로 보내는 역할을 합니다.
2) getAll: 전체 글을 불러오는 메서드입니다. 전달받는 인자는 없으며 모든 게시글을 탐색하여 dto 배열에 담아 컨트롤러에 전송합니다.
3) getOne: 게시글 상세 페이지를 위한 메서드입니다. mongodb는 데이터 저장시 자체적으로 object형의 id값을 만드는데 이를 이용하여 하나의 entity를 불러와 dto 객체로 변환시킨 뒤 컨트롤러에 전송합니다.
4) getPostsByType: 정렬 탭에서 사용될 메서드입니다. 게시글은 기본적으로 '함께해요', '도와주세요' 라는 타입을 가지고 있기 때문에 사용자가 원하는 타입의 글만 불러오도록 하는 메서드입니다.
5) getPostsByUserId: 마이페이지의 게시글 내역에서 사용될 메서드입니다. 사용자는 자신이 작성한 게시글을 해당 메서드를 이용하여 파악할 수 있습니다.
6) getPostsByKeyword: 게시글 홈 최상단에 위치한 검색 바에서 사용될 메서드입니다. title, content를 중심으로 keyword에 해당하는 데이터를 찾고, 중복이 있는 데이터는 제거한 후 반환시켜주는 쿼리를 가지고 있습니다.
7) deletePost: 마이 페이지의 게시글 내역에서 사용될 포스트 제거 기능입니다. id값을 이용하여 원하는 게시글을 삭제할 수 있습니다.
이상으로 post-service에서 댓글을 제외한 부분을 완성했으니 UI작업을 다음 포스트에서 진행해보도록 하겠습니다.