From 6e494c16978032205ebc8601d841e7e964021728 Mon Sep 17 00:00:00 2001 From: DongKey777 Date: Tue, 19 Nov 2024 19:36:33 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[feat]=20=EC=98=A4=EB=8D=94=20DTO=20?= =?UTF-8?q?=EC=97=AD=ED=95=A0=EB=B3=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/database/database.service.ts | 4 +- apps/backend/src/order/dto/baseOrder.dto.ts | 16 ++++++++ apps/backend/src/order/dto/limitOrder.dto.ts | 10 +++++ apps/backend/src/order/dto/order.dto.ts | 27 ++++++++---- apps/backend/src/order/dto/orderBook.dto.ts | 20 +++++++-- apps/backend/src/order/enums/orderType.ts | 16 ++++++++ apps/backend/src/order/matching.service.ts | 15 ++++++- apps/backend/src/order/order.controller.ts | 27 +++++++----- apps/backend/src/order/order.module.ts | 7 +++- apps/backend/src/order/order.repository.ts | 41 +++++++++++++++++++ apps/backend/src/order/order.service.ts | 28 ++++++++----- apps/backend/src/order/orderBook.service.ts | 10 +++-- .../backend/src/order/utils/dtoTransformer.ts | 32 +++++++++++++++ 13 files changed, 215 insertions(+), 38 deletions(-) create mode 100644 apps/backend/src/order/dto/baseOrder.dto.ts create mode 100644 apps/backend/src/order/dto/limitOrder.dto.ts create mode 100644 apps/backend/src/order/enums/orderType.ts create mode 100644 apps/backend/src/order/order.repository.ts create mode 100644 apps/backend/src/order/utils/dtoTransformer.ts diff --git a/apps/backend/src/database/database.service.ts b/apps/backend/src/database/database.service.ts index aa234cf..f8dd410 100644 --- a/apps/backend/src/database/database.service.ts +++ b/apps/backend/src/database/database.service.ts @@ -5,6 +5,7 @@ import { Client, QueryResult } from 'pg'; @Injectable() export class DatabaseService implements OnModuleInit { private client: Client; + constructor(private configService: ConfigService) {} onModuleInit() { @@ -21,7 +22,8 @@ export class DatabaseService implements OnModuleInit { .catch((error: Error) => console.error('Failed to connect to PostgreSQL database', error)); } - async query(query: string, params: string[] = []): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async query(query: string, params: any[] = []): Promise { return this.client.query(query, params); } diff --git a/apps/backend/src/order/dto/baseOrder.dto.ts b/apps/backend/src/order/dto/baseOrder.dto.ts new file mode 100644 index 0000000..f4a2a72 --- /dev/null +++ b/apps/backend/src/order/dto/baseOrder.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { OrderType, TradingType } from '../enums/orderType'; + +export class BaseOrderDto { + @ApiProperty({ description: '상품 ID', example: 1 }) + cropId: number; + + @ApiProperty({ description: '회원 ID', example: 123 }) + memberId: number; + + @ApiProperty({ description: '주문 유형', example: 'buy' }) + orderType: OrderType; + + @ApiProperty({ description: '거래 유형', example: 'limit' }) + tradingType: TradingType; +} diff --git a/apps/backend/src/order/dto/limitOrder.dto.ts b/apps/backend/src/order/dto/limitOrder.dto.ts new file mode 100644 index 0000000..6604ff3 --- /dev/null +++ b/apps/backend/src/order/dto/limitOrder.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { BaseOrderDto } from './baseOrder.dto'; + +export class LimitOrderDto extends BaseOrderDto { + @ApiProperty({ description: '주문 수량', example: 100 }) + quantity: number; + + @ApiProperty({ description: '주문 가격', example: 50 }) + price: number; +} diff --git a/apps/backend/src/order/dto/order.dto.ts b/apps/backend/src/order/dto/order.dto.ts index de8ba55..78eaf91 100644 --- a/apps/backend/src/order/dto/order.dto.ts +++ b/apps/backend/src/order/dto/order.dto.ts @@ -1,13 +1,26 @@ -export class OrderDto { - orderId: number; - cropId: number; +import { ApiProperty } from '@nestjs/swagger'; +import { BaseOrderDto } from './baseOrder.dto'; +import { OrderStatus } from '../enums/orderType'; + +export class OrderDto extends BaseOrderDto { + @ApiProperty({ description: '회원 번호', example: 15 }) memberId: number; - orderType: 'buy' | 'sell'; + + @ApiProperty({ description: '주문 상태', example: 'pending' }) + status: OrderStatus; + + @ApiProperty({ description: '주문 가격', example: 1500 }) + price: number; + + @ApiProperty({ description: '주문 시간', example: new Date() }) time: Date; + + @ApiProperty({ description: '주문 수량', example: 100 }) quantity: number; - price: number; - status: 'pending' | 'partially_filled' | 'completed' | 'canceled'; + + @ApiProperty({ description: '체결된 수량', example: 50 }) filledQuantity: number; - tradingType: 'limit' | 'market'; + + @ApiProperty({ description: '미체결 수량', example: 50 }) unfilledQuantity: number; } diff --git a/apps/backend/src/order/dto/orderBook.dto.ts b/apps/backend/src/order/dto/orderBook.dto.ts index 1882d67..8d434a7 100644 --- a/apps/backend/src/order/dto/orderBook.dto.ts +++ b/apps/backend/src/order/dto/orderBook.dto.ts @@ -1,8 +1,22 @@ -export interface OrderBookDto { +import { ApiProperty } from '@nestjs/swagger'; +import { OrderType } from '../enums/orderType'; + +export class OrderBookDto { + @ApiProperty({ description: '주문 ID', example: 1 }) orderId: number; + + @ApiProperty({ description: '상품 ID', example: 1 }) cropId: number; - orderType: 'buy' | 'sell'; + + @ApiProperty({ description: '주문 유형', example: 'buy' }) + orderType: OrderType; + + @ApiProperty({ description: '가격', example: 50 }) price: number; + + @ApiProperty({ description: '미체결 수량', example: 100 }) unfilledQuantity: number; - timestamp: number; + + @ApiProperty({ description: '주문 시간', example: new Date() }) + time: Date; } diff --git a/apps/backend/src/order/enums/orderType.ts b/apps/backend/src/order/enums/orderType.ts new file mode 100644 index 0000000..daa128a --- /dev/null +++ b/apps/backend/src/order/enums/orderType.ts @@ -0,0 +1,16 @@ +export enum OrderType { + BUY = 'buy', + SELL = 'sell' +} + +export enum TradingType { + LIMIT = 'limit', + MARKET = 'market' +} + +export enum OrderStatus { + PENDING = 'pending', + PARTIALLY_FILLED = 'partially_filled', + COMPLETED = 'completed', + CANCELED = 'canceled' +} diff --git a/apps/backend/src/order/matching.service.ts b/apps/backend/src/order/matching.service.ts index a05c4c5..5572c48 100644 --- a/apps/backend/src/order/matching.service.ts +++ b/apps/backend/src/order/matching.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { OrderBookService } from './orderBook.service'; +import { OrderType } from './enums/orderType'; @Injectable() export class MatchingService { @@ -24,8 +25,18 @@ export class MatchingService { //TODO DB 트랜잭션 업데이트,체결 이벤트 발생 - await this.orderBookService.updateOrder(cropId, 'buy', buyOrder.orderId, matchedQuantity); - await this.orderBookService.updateOrder(cropId, 'sell', sellOrder.orderId, matchedQuantity); + await this.orderBookService.updateOrder( + cropId, + OrderType.BUY, + buyOrder.orderId, + matchedQuantity + ); + await this.orderBookService.updateOrder( + cropId, + OrderType.SELL, + sellOrder.orderId, + matchedQuantity + ); if (sellOrder.unfilledQuantity <= matchedQuantity) { sellIndex++; diff --git a/apps/backend/src/order/order.controller.ts b/apps/backend/src/order/order.controller.ts index c3e1206..2961acf 100644 --- a/apps/backend/src/order/order.controller.ts +++ b/apps/backend/src/order/order.controller.ts @@ -1,8 +1,10 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { OrderService } from './order.service'; import { OrderBookService } from './orderBook.service'; -import { OrderDto } from './dto/order.dto'; import { OrderBookDto } from './dto/orderBook.dto'; +import DtoTransformer from './utils/dtoTransformer'; +import { LimitOrderDto } from './dto/limitOrder.dto'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; @Controller('api/order') export class OrderController { @@ -12,28 +14,34 @@ export class OrderController { ) {} @Post('buy') - async createBuyOrder(@Body() orderDto: OrderDto): Promise { - // TODO: 주문 데이터를 데이터베이스에 저장 - await this.orderService.placeOrder(orderDto); + @ApiOperation({ summary: '구매 주문 생성' }) + @ApiResponse({ status: 200, description: '구매 주문이 성공적으로 생성되었습니다.' }) + async createBuyOrder(@Body() limitOrderDto: LimitOrderDto): Promise { + const orderDto = DtoTransformer.toOrderDto(limitOrderDto); + const orderId = await this.orderService.saveOrder(orderDto); + + await this.orderService.saveOrderToOrderBook(orderDto, orderId); return '구매 주문이 성공적으로 생성되었습니다.'; } @Post('sell') - async createSellOrder(@Body() orderDto: OrderDto): Promise { - // TODO: 주문 데이터를 데이터베이스에 저장 - await this.orderService.placeOrder(orderDto); + @ApiOperation({ summary: '판매 주문 생성' }) + @ApiResponse({ status: 200, description: '판매 주문이 성공적으로 생성되었습니다.' }) + async createSellOrder(@Body() limitOrderDto: LimitOrderDto): Promise { + const orderDto = DtoTransformer.toOrderDto(limitOrderDto); + const orderId = await this.orderService.saveOrder(orderDto); + + await this.orderService.saveOrderToOrderBook(orderDto, orderId); return '판매 주문이 성공적으로 생성되었습니다.'; } @Get('buy/:cropId') async getBuyOrders(@Param('cropId') cropId: number): Promise { - // TODO: 필요 시 데이터베이스에서 데이터를 가져옴 return await this.orderBookService.getBuyOrders(cropId); } @Get('sell/:cropId') async getSellOrders(@Param('cropId') cropId: number): Promise { - // TODO: 필요 시 데이터베이스에서 데이터를 가져옴 return await this.orderBookService.getSellOrders(cropId); } @@ -42,7 +50,6 @@ export class OrderController { @Body() { cropId, orderId, orderType }: { cropId: number; orderId: number; orderType: 'buy' | 'sell' } ): Promise { - // TODO: 데이터베이스에서 주문 상태를 "취소됨"으로 업데이트 await this.orderBookService.removeOrder(cropId, orderId, orderType); return '주문이 성공적으로 취소되었습니다.'; } diff --git a/apps/backend/src/order/order.module.ts b/apps/backend/src/order/order.module.ts index ab46d0b..ab022dc 100644 --- a/apps/backend/src/order/order.module.ts +++ b/apps/backend/src/order/order.module.ts @@ -2,9 +2,12 @@ import { Module } from '@nestjs/common'; import { OrderService } from './order.service'; import { OrderController } from './order.controller'; import { OrderBookService } from './orderBook.service'; +import { OrderRepository } from './order.repository'; +import { DatabaseModule } from '../database/database.module'; @Module({ - providers: [OrderService, OrderBookService], - controllers: [OrderController] + providers: [OrderService, OrderBookService, OrderRepository], + controllers: [OrderController], + imports: [DatabaseModule] }) export class OrderModule {} diff --git a/apps/backend/src/order/order.repository.ts b/apps/backend/src/order/order.repository.ts new file mode 100644 index 0000000..84dfdff --- /dev/null +++ b/apps/backend/src/order/order.repository.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { DatabaseService } from '../database/database.service'; +import { OrderDto } from './dto/order.dto'; +import { OrderStatus } from './enums/orderType'; + +@Injectable() +export class OrderRepository { + constructor(private readonly databaseService: DatabaseService) {} + + async saveOrder(order: OrderDto): Promise { + const query = ` + INSERT INTO orders (crop_id, + member_id, + order_type, + trading_type, + quantity, + price, + status, + filled_quantity, + unfilled_quantity, + time) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING order_id + `; + + const values = [ + order.cropId, + order.memberId, + order.orderType, + order.tradingType, + order.quantity, + order.price, + order.status || OrderStatus.PENDING, // 기본값 설정 + order.filledQuantity || 0, + order.unfilledQuantity || order.quantity, + order.time || new Date() + ]; + + const result = await this.databaseService.query(query, values); + return result.rows[0].order_id; + } +} diff --git a/apps/backend/src/order/order.service.ts b/apps/backend/src/order/order.service.ts index f8c77ed..daea44d 100644 --- a/apps/backend/src/order/order.service.ts +++ b/apps/backend/src/order/order.service.ts @@ -1,20 +1,28 @@ import { Injectable } from '@nestjs/common'; import { OrderBookService } from './orderBook.service'; +import { OrderRepository } from './order.repository'; import { OrderDto } from './dto/order.dto'; +import { LimitOrderDto } from './dto/limitOrder.dto'; +import DtoTransformer from './utils/dtoTransformer'; @Injectable() export class OrderService { - constructor(private readonly orderBookService: OrderBookService) {} + constructor( + private readonly orderBookService: OrderBookService, + private readonly orderRepository: OrderRepository + ) {} - async placeOrder(order: OrderDto): Promise { - const orderBookDto = { - orderId: order.orderId, - cropId: order.cropId, - orderType: order.orderType, - price: order.price, - unfilledQuantity: order.quantity, - timestamp: Date.now() - }; + async saveOrder(createOrderDto: LimitOrderDto): Promise { + const orderDto: OrderDto = DtoTransformer.toOrderDto(createOrderDto); + const orderId = await this.orderRepository.saveOrder(orderDto); + + await this.saveOrderToOrderBook(orderDto, orderId); + + return orderId; + } + + async saveOrderToOrderBook(order: OrderDto, orderId: number): Promise { + const orderBookDto = DtoTransformer.toOrderBookDto(order, orderId); await this.orderBookService.addOrder(orderBookDto); } } diff --git a/apps/backend/src/order/orderBook.service.ts b/apps/backend/src/order/orderBook.service.ts index df53474..e8e2e12 100644 --- a/apps/backend/src/order/orderBook.service.ts +++ b/apps/backend/src/order/orderBook.service.ts @@ -1,6 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { RedisClientType } from 'redis'; import { OrderBookDto } from './dto/orderBook.dto'; +import { OrderType } from './enums/orderType'; @Injectable() export class OrderBookService { @@ -36,12 +37,14 @@ export class OrderBookService { async updateOrder( cropId: number, - orderType: 'buy' | 'sell', + orderType: OrderType, orderId: number, filledQuantity: number ): Promise { const orders = - orderType === 'buy' ? await this.getBuyOrders(cropId) : await this.getSellOrders(cropId); + orderType === OrderType.BUY + ? await this.getBuyOrders(cropId) + : await this.getSellOrders(cropId); const targetOrder = orders.find(order => order.orderId === orderId); if (!targetOrder) { @@ -57,7 +60,8 @@ export class OrderBookService { } private serializeOrder(order: OrderBookDto): string { - return JSON.stringify(order); + const time = order.time.toISOString(); + return JSON.stringify({ ...order, time }); } private deserializeOrder(orderData: string): OrderBookDto { diff --git a/apps/backend/src/order/utils/dtoTransformer.ts b/apps/backend/src/order/utils/dtoTransformer.ts new file mode 100644 index 0000000..d9a4858 --- /dev/null +++ b/apps/backend/src/order/utils/dtoTransformer.ts @@ -0,0 +1,32 @@ +import { OrderDto } from '../dto/order.dto'; +import { OrderBookDto } from '../dto/orderBook.dto'; +import { LimitOrderDto } from '../dto/limitOrder.dto'; +import { OrderStatus } from '../enums/orderType'; + +export default class DtoTransformer { + static toOrderBookDto(order: OrderDto, orderId: number): OrderBookDto { + return { + orderId, + cropId: order.cropId, + orderType: order.orderType, + price: order.price, + unfilledQuantity: order.quantity, + time: order.time + }; + } + + static toOrderDto(limitOrderDto: LimitOrderDto): OrderDto { + return { + cropId: limitOrderDto.cropId, + memberId: limitOrderDto.memberId, + orderType: limitOrderDto.orderType, + tradingType: limitOrderDto.tradingType, + time: new Date(), // 현재 시간으로 설정 + quantity: limitOrderDto.quantity, + price: limitOrderDto.price, + status: OrderStatus.PENDING, // 초기 상태를 기본값으로 설정 + filledQuantity: 0, // 초기 체결 수량 + unfilledQuantity: limitOrderDto.quantity // 전체 수량을 미체결로 설정 + }; + } +} From efee39ac40bbadf5967a52b9d1da04398fed9be4 Mon Sep 17 00:00:00 2001 From: edder773 Date: Wed, 20 Nov 2024 10:58:03 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[fix]=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/auth/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index 6e9070a..d6c6103 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -4,7 +4,7 @@ import { SignUpDto } from './dto/signUp.dto'; import { successhandler, successMessage } from 'src/global/successhandler'; import { ApiOperation } from '@nestjs/swagger'; import { LoginDto } from './dto/login.dto'; -import { signUpResponseDecorator } from './decorator/signup.decorator'; +import { signUpResponseDecorator } from './decorator/signUp.decorator'; import { loginResponseDecorator } from './decorator/login.decorator'; import { AuthGuard } from '@nestjs/passport'; import { GoogleLoginDto } from './dto/googleLogin.dto';