From 523ed06685f89790abbdb0845abf1d00c38baea9 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Tue, 12 Nov 2024 19:18:34 +0900 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20feat:=20stock=20Detail=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/stock/domain/stockDetail.entity.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 packages/backend/src/stock/domain/stockDetail.entity.ts diff --git a/packages/backend/src/stock/domain/stockDetail.entity.ts b/packages/backend/src/stock/domain/stockDetail.entity.ts new file mode 100644 index 0000000..d91a807 --- /dev/null +++ b/packages/backend/src/stock/domain/stockDetail.entity.ts @@ -0,0 +1,37 @@ +import { + Column, + Entity, + JoinColumn, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Stock } from './stock.entity'; + +@Entity('stock_detail') +export class StockDetail { + @PrimaryGeneratedColumn() + id: number; + + @OneToOne(() => Stock) + @JoinColumn({ name: 'stock_id' }) + stock: Stock; + + @Column({ + type: 'decimal', + precision: 20, + scale: 2, + }) + marketCap: number; + + @Column({ type: 'integer' }) + eps: number; + + @Column({ type: 'integer' }) + per: number; + + @Column({ type: 'integer' }) + high52w: number; + + @Column({ type: 'integer' }) + low52w: number; +} From 5eff67254916b0f2a8fde768ae6db08231bd41af Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Tue, 12 Nov 2024 19:23:11 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=90=9B=20fix:=20stock=20Live=20Data?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/stock/domain/stockLiveData.entity.ts | 10 ++++++---- packages/backend/src/stock/stockLiveData.subscriber.ts | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/stock/domain/stockLiveData.entity.ts b/packages/backend/src/stock/domain/stockLiveData.entity.ts index 6fd36c8..885ee78 100644 --- a/packages/backend/src/stock/domain/stockLiveData.entity.ts +++ b/packages/backend/src/stock/domain/stockLiveData.entity.ts @@ -4,6 +4,7 @@ import { Column, OneToOne, JoinColumn, + UpdateDateColumn, } from 'typeorm'; import { Stock } from './stock.entity'; @@ -13,10 +14,10 @@ export class StockLiveData { id: number; @Column({ type: 'decimal', precision: 15, scale: 2 }) - current_price: number; + currentPrice: number; @Column({ type: 'decimal', precision: 5, scale: 2 }) - change_rate: number; + changeRate: number; @Column() volume: number; @@ -31,10 +32,11 @@ export class StockLiveData { open: number; @Column({ type: 'decimal', precision: 15, scale: 2 }) - previous_close: number; + previousClose: number; + @UpdateDateColumn() @Column({ type: 'timestamp' }) - updated_at: Date; + updatedAt: Date; @OneToOne(() => Stock) @JoinColumn({ name: 'stock_id' }) diff --git a/packages/backend/src/stock/stockLiveData.subscriber.ts b/packages/backend/src/stock/stockLiveData.subscriber.ts index 2e292df..e28d55e 100644 --- a/packages/backend/src/stock/stockLiveData.subscriber.ts +++ b/packages/backend/src/stock/stockLiveData.subscriber.ts @@ -29,8 +29,8 @@ export class StockLiveDataSubscriber if (updatedStockLiveData?.stock?.id) { const { id: stockId } = updatedStockLiveData.stock; const { - current_price: price, - change_rate: change, + currentPrice: price, + changeRate: change, volume: volume, } = updatedStockLiveData; this.stockGateway.onUpdateStock(stockId, price, change, volume); From f5044f63ac5e2de368daada38f14b4a368a36573 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Tue, 12 Nov 2024 19:47:20 +0900 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20feat:=20stock=20Detail=20DTO?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/stock/dto/stockDetail.response.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/backend/src/stock/dto/stockDetail.response.ts diff --git a/packages/backend/src/stock/dto/stockDetail.response.ts b/packages/backend/src/stock/dto/stockDetail.response.ts new file mode 100644 index 0000000..cf692e5 --- /dev/null +++ b/packages/backend/src/stock/dto/stockDetail.response.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockDetailResponse { + @ApiProperty({ + description: '주식의 시가 총액', + example: 352510000000000, + }) + marketCap: number; + + @ApiProperty({ + description: '주식의 EPS', + example: 4091, + }) + eps: number; + + @ApiProperty({ + description: '주식의 PER', + example: 17.51, + }) + per: number; + + @ApiProperty({ + description: '주식의 52주 최고가', + example: 88000, + }) + high52w: number; + + @ApiProperty({ + description: '주식의 52주 최저가', + example: 53000, + }) + low52w: number; +} From 4b96f3ca874a130cb019657d32e9cd22ef0774e7 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Tue, 12 Nov 2024 19:49:32 +0900 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=90=9B=20fix:=20stock=20Detail=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20PER=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/domain/stockDetail.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/stock/domain/stockDetail.entity.ts b/packages/backend/src/stock/domain/stockDetail.entity.ts index d91a807..a7b548b 100644 --- a/packages/backend/src/stock/domain/stockDetail.entity.ts +++ b/packages/backend/src/stock/domain/stockDetail.entity.ts @@ -26,7 +26,7 @@ export class StockDetail { @Column({ type: 'integer' }) eps: number; - @Column({ type: 'integer' }) + @Column({ type: 'decimal', precision: 6, scale: 3 }) per: number; @Column({ type: 'integer' }) From 835033404b8ed56096b2ab837e0ef32753c2f167 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 13 Nov 2024 00:18:39 +0900 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8=20feat:=20stock=20Detail=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/stock/stock.controller.ts | 41 +++++++++++++++++-- .../backend/src/stock/stockDetail.service.ts | 35 ++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 packages/backend/src/stock/stockDetail.service.ts diff --git a/packages/backend/src/stock/stock.controller.ts b/packages/backend/src/stock/stock.controller.ts index a5506f0..27b87b6 100644 --- a/packages/backend/src/stock/stock.controller.ts +++ b/packages/backend/src/stock/stock.controller.ts @@ -1,6 +1,16 @@ -import { Body, Controller, Delete, HttpCode, Post } from '@nestjs/common'; -import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; +import { + Body, + Controller, + Delete, + Get, + HttpCode, + Param, + Post, +} from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; +import { StockDetailResponse } from './dto/stockDetail.response'; import { StockService } from './stock.service'; +import { StockDetailService } from './stockDetail.service'; import { StockViewsResponse } from '@/stock/dto/stock.Response'; import { StockViewRequest } from '@/stock/dto/stockView.request'; import { @@ -11,7 +21,10 @@ import { UserStockResponse } from '@/stock/dto/userStock.response'; @Controller('stock') export class StockController { - constructor(private readonly stockService: StockService) {} + constructor( + private readonly stockService: StockService, + private readonly stockDetailService: StockDetailService, + ) {} @HttpCode(200) @Post('/view') @@ -80,4 +93,26 @@ export class StockController { '사용자 소유 주식을 삭제했습니다.', ); } + + @ApiOperation({ + summary: '주식 상세 정보 조회 API', + description: '시가 총액, EPS, PER, 52주 최고가, 52주 최저가를 조회합니다', + }) + @ApiOkResponse({ + description: '주식 상세 정보 조회 성공', + example: { + marketCap: 352510000000000, + eps: 4091, + per: 17.51, + high52w: 88000, + low52w: 53000, + }, + }) + @ApiParam({ name: 'stockId', required: true, description: '주식 ID' }) + @Get(':stockId/detail') + async getStockDetail( + @Param('stockId') stockId: string, + ): Promise { + return await this.stockDetailService.getStockDetailByStockId(stockId); + } } diff --git a/packages/backend/src/stock/stockDetail.service.ts b/packages/backend/src/stock/stockDetail.service.ts new file mode 100644 index 0000000..412a474 --- /dev/null +++ b/packages/backend/src/stock/stockDetail.service.ts @@ -0,0 +1,35 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { plainToInstance } from 'class-transformer'; +import { DataSource } from 'typeorm'; +import { Logger } from 'winston'; +import { StockDetail } from './domain/stockDetail.entity'; +import { StockDetailResponse } from './dto/stockDetail.response'; + +@Injectable() +export class StockDetailService { + constructor( + private readonly datasource: DataSource, + @Inject('winston') private readonly logger: Logger, + ) {} + + async getStockDetailByStockId(stockId: string): Promise { + return await this.datasource.transaction(async (manager) => { + const isExists = await manager.existsBy(StockDetail, { + stock: { id: stockId }, + }); + + if (!isExists) { + this.logger.warn(`stock detail not found (stockId: ${stockId})`); + throw new NotFoundException( + `stock detail not found (stockId: ${stockId}`, + ); + } + + const stockDetail = await manager.findBy(StockDetail, { + stock: { id: stockId }, + }); + + return plainToInstance(StockDetailResponse, stockDetail[0]); + }); + } +} From d3df449d511f7a1c9dce590d8d67687499c2b27e Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 13 Nov 2024 00:29:01 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=85=20test:=20stockDetailService=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/stock/stockDetail.service.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/backend/src/stock/stockDetail.service.spec.ts diff --git a/packages/backend/src/stock/stockDetail.service.spec.ts b/packages/backend/src/stock/stockDetail.service.spec.ts new file mode 100644 index 0000000..8423a58 --- /dev/null +++ b/packages/backend/src/stock/stockDetail.service.spec.ts @@ -0,0 +1,70 @@ +import { NotFoundException } from '@nestjs/common'; +import { DataSource } from 'typeorm'; +import { Logger } from 'winston'; +import { StockDetail } from './domain/stockDetail.entity'; +import { StockDetailResponse } from './dto/stockDetail.response'; +import { StockDetailService } from './stockDetail.service'; +import { createDataSourceMock } from '@/user/user.service.spec'; + +const logger: Logger = { + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), +} as unknown as Logger; + +describe('StockDetailService 테스트', () => { + const stockId = 'A005930'; + + test('stockId로 주식 상세 정보를 조회한다.', async () => { + const mockStockDetail = { + stock: { id: stockId }, + marketCap: 352510000000000, + eps: 4091, + per: 17.51, + high52w: 88000, + low52w: 53000, + }; + const managerMock = { + existsBy: jest.fn().mockResolvedValue(true), + findBy: jest.fn().mockResolvedValue([mockStockDetail]), + }; + const dataSource = createDataSourceMock(managerMock); + const stockDetailService = new StockDetailService( + dataSource as DataSource, + logger, + ); + + const result = await stockDetailService.getStockDetailByStockId(stockId); + + expect(managerMock.existsBy).toHaveBeenCalledWith(StockDetail, { + stock: { id: stockId }, + }); + expect(managerMock.findBy).toHaveBeenCalledWith(StockDetail, { + stock: { id: stockId }, + }); + expect(result).toBeInstanceOf(StockDetailResponse); + expect(result.marketCap).toEqual(mockStockDetail.marketCap); + expect(result.eps).toEqual(mockStockDetail.eps); + expect(result.per).toEqual(mockStockDetail.per); + expect(result.high52w).toEqual(mockStockDetail.high52w); + expect(result.low52w).toEqual(mockStockDetail.low52w); + }); + + test('존재하지 않는 stockId로 조회 시 예외를 발생시킨다.', async () => { + const managerMock = { + existsBy: jest.fn().mockResolvedValue(false), + }; + const dataSource = createDataSourceMock(managerMock); + const stockDetailService = new StockDetailService( + dataSource as DataSource, + logger, + ); + + await expect( + stockDetailService.getStockDetailByStockId('nonexistentId'), + ).rejects.toThrow(NotFoundException); + expect(logger.warn).toHaveBeenCalledWith( + `stock detail not found (stockId: nonexistentId)`, + ); + }); +}); From 17098303415d651d1c4939322b1a10196af3ade1 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 13 Nov 2024 02:14:19 +0900 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=90=9B=20fix:=20stockDetail=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20updatedAt=20=EC=BB=AC=EB=9F=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/domain/stockDetail.entity.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/src/stock/domain/stockDetail.entity.ts b/packages/backend/src/stock/domain/stockDetail.entity.ts index a7b548b..db3e75d 100644 --- a/packages/backend/src/stock/domain/stockDetail.entity.ts +++ b/packages/backend/src/stock/domain/stockDetail.entity.ts @@ -4,6 +4,7 @@ import { JoinColumn, OneToOne, PrimaryGeneratedColumn, + UpdateDateColumn, } from 'typeorm'; import { Stock } from './stock.entity'; @@ -34,4 +35,7 @@ export class StockDetail { @Column({ type: 'integer' }) low52w: number; + + @UpdateDateColumn({ type: 'timestamp', name: 'updated_at' }) + updatedAt: Date; } From e9aa117d75e0bcfbfcdac5181778b21a34c4d526 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 13 Nov 2024 02:15:58 +0900 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=90=9B=20fix:=20getStockDetailBySto?= =?UTF-8?q?ckId=20Swagger=20=EB=8D=B0=EC=BD=94=EB=A0=88=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/stock.controller.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/backend/src/stock/stock.controller.ts b/packages/backend/src/stock/stock.controller.ts index 27b87b6..147dd42 100644 --- a/packages/backend/src/stock/stock.controller.ts +++ b/packages/backend/src/stock/stock.controller.ts @@ -100,13 +100,7 @@ export class StockController { }) @ApiOkResponse({ description: '주식 상세 정보 조회 성공', - example: { - marketCap: 352510000000000, - eps: 4091, - per: 17.51, - high52w: 88000, - low52w: 53000, - }, + type: StockDetailResponse, }) @ApiParam({ name: 'stockId', required: true, description: '주식 ID' }) @Get(':stockId/detail') From fa9355267bcf70ddc16b779a2cbf670a390ac705 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 13 Nov 2024 02:24:14 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=E2=9C=85=20test:=20=EA=B0=95=EA=B2=B0?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/stockDetail.service.spec.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/stock/stockDetail.service.spec.ts b/packages/backend/src/stock/stockDetail.service.spec.ts index 8423a58..403b875 100644 --- a/packages/backend/src/stock/stockDetail.service.spec.ts +++ b/packages/backend/src/stock/stockDetail.service.spec.ts @@ -2,7 +2,6 @@ import { NotFoundException } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Logger } from 'winston'; import { StockDetail } from './domain/stockDetail.entity'; -import { StockDetailResponse } from './dto/stockDetail.response'; import { StockDetailService } from './stockDetail.service'; import { createDataSourceMock } from '@/user/user.service.spec'; @@ -42,7 +41,13 @@ describe('StockDetailService 테스트', () => { expect(managerMock.findBy).toHaveBeenCalledWith(StockDetail, { stock: { id: stockId }, }); - expect(result).toBeInstanceOf(StockDetailResponse); + expect(result).toMatchObject({ + marketCap: expect.any(Number), + eps: expect.any(Number), + per: expect.any(Number), + high52w: expect.any(Number), + low52w: expect.any(Number), + }); expect(result.marketCap).toEqual(mockStockDetail.marketCap); expect(result.eps).toEqual(mockStockDetail.eps); expect(result.per).toEqual(mockStockDetail.per); From 3ad766bf670027da643ce9eb80c935525fe68e01 Mon Sep 17 00:00:00 2001 From: demian-m00n Date: Wed, 13 Nov 2024 03:23:30 +0900 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=90=9B=20fix:=20stock=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20StockDetailService=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/stock/stock.module.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/stock/stock.module.ts b/packages/backend/src/stock/stock.module.ts index 74e2d42..bfac281 100644 --- a/packages/backend/src/stock/stock.module.ts +++ b/packages/backend/src/stock/stock.module.ts @@ -4,11 +4,17 @@ import { Stock } from './domain/stock.entity'; import { StockController } from './stock.controller'; import { StockGateway } from './stock.gateway'; import { StockService } from './stock.service'; +import { StockDetailService } from './stockDetail.service'; import { StockLiveDataSubscriber } from './stockLiveData.subscriber'; @Module({ imports: [TypeOrmModule.forFeature([Stock])], controllers: [StockController], - providers: [StockService, StockGateway, StockLiveDataSubscriber], + providers: [ + StockService, + StockGateway, + StockLiveDataSubscriber, + StockDetailService, + ], }) export class StockModule {}