Skip to content

Commit

Permalink
Merge pull request #260 from boostcampwm-2024/back/main
Browse files Browse the repository at this point in the history
[BE] 브랜치 병합
  • Loading branch information
uuuo3o authored Dec 4, 2024
2 parents 72c03e4 + aedff42 commit 1d71e4d
Show file tree
Hide file tree
Showing 29 changed files with 5,051 additions and 189 deletions.
2 changes: 2 additions & 0 deletions BE/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,7 @@ module.exports = {
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/naming-convention': 'off',
'no-restricted-syntax': 'off',
'no-await-in-loop': 'off'
},
};
203 changes: 203 additions & 0 deletions BE/src/asset/asset-service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { Test } from '@nestjs/testing';
import { DeepPartial } from 'typeorm';
import { AssetService } from './asset.service';
import { UserStockRepository } from './user-stock.repository';
import { AssetRepository } from './asset.repository';
import { StockDetailService } from '../stock/detail/stock-detail.service';
import { StockPriceSocketService } from '../stockSocket/stock-price-socket.service';
import { TradeType } from '../stock/order/enum/trade-type';
import { StatusType } from '../stock/order/enum/status-type';
import { Asset } from './asset.entity';
import { AssetResponseDto } from './dto/asset-response.dto';
import { StockElementResponseDto } from './dto/stock-element-response.dto';
import { MypageResponseDto } from './dto/mypage-response.dto';

describe('asset test', () => {
let assetService: AssetService;
let userStockRepository: UserStockRepository;
let assetRepository: AssetRepository;
let stockDetailService: StockDetailService;

beforeEach(async () => {
const mockUserStockRepository = {
findOneBy: jest.fn(),
findUserStockWithNameByUserId: jest.fn(),
findAllDistinctCode: jest.fn(),
find: jest.fn(),
};
const mockAssetRepository = {
findAllPendingOrders: jest.fn(),
findOneBy: jest.fn(),
save: jest.fn(),
};
const mockStockDetailService = { getInquirePrice: jest.fn() };
const mockStockPriceSocketService = { subscribeByCode: jest.fn() };

const module = await Test.createTestingModule({
providers: [
AssetService,
{ provide: UserStockRepository, useValue: mockUserStockRepository },
{ provide: AssetRepository, useValue: mockAssetRepository },
{
provide: StockDetailService,
useValue: mockStockDetailService,
},
{
provide: StockPriceSocketService,
useValue: mockStockPriceSocketService,
},
],
}).compile();

assetService = module.get(AssetService);
userStockRepository = module.get(UserStockRepository);
assetRepository = module.get(AssetRepository);
stockDetailService = module.get(StockDetailService);
});

it('보유 주식과 미체결 주문을 모두 반영한 매도 가능 주식 수를 반환한다.', async () => {
jest.spyOn(userStockRepository, 'findOneBy').mockResolvedValue({
id: 1,
user_id: 1,
stock_code: '005930',
quantity: 1,
avg_price: 1000,
last_updated: new Date(),
});

jest.spyOn(assetRepository, 'findAllPendingOrders').mockResolvedValue([
{
id: 1,
user_id: 1,
stock_code: '005930',
trade_type: TradeType.SELL,
amount: 1,
price: 1000,
status: StatusType.PENDING,
created_at: new Date(),
},
]);

expect(await assetService.getUserStockByCode(1, '005930')).toEqual({
quantity: 0,
avg_price: 1000,
});
});

it('보유 자산과 미체결 주문을 모두 반영한 매수 가능 금액을 반환한다.', async () => {
jest.spyOn(assetRepository, 'findOneBy').mockResolvedValue({
id: 1,
user_id: 1,
stock_balance: 0,
cash_balance: 1000,
total_asset: 1000,
total_profit: 0,
total_profit_rate: 0,
});

jest.spyOn(assetRepository, 'findAllPendingOrders').mockResolvedValue([
{
id: 1,
user_id: 1,
stock_code: '005930',
trade_type: TradeType.BUY,
amount: 1,
price: 1000,
status: StatusType.PENDING,
created_at: new Date(),
},
]);

expect(await assetService.getCashBalance(1)).toEqual({
cash_balance: 0,
});
});

it('마이페이지 조회 시 종목의 현재가를 반영한 총 자산을 반환한다.', async () => {
jest
.spyOn(userStockRepository, 'findUserStockWithNameByUserId')
.mockResolvedValue([
{
user_stocks_id: 1,
user_stocks_user_id: 1,
user_stocks_stock_code: '005930',
user_stocks_quantity: 1,
user_stocks_avg_price: '1000',
user_stocks_last_updated: new Date(),
stocks_code: '005930',
stocks_name: '삼성전자',
stocks_market: 'KOSPI',
},
]);

jest.spyOn(assetRepository, 'findOneBy').mockResolvedValue({
id: 1,
user_id: 1,
stock_balance: 0,
cash_balance: 1000,
total_asset: 1000,
total_profit: 0,
total_profit_rate: 0,
});

jest
.spyOn(userStockRepository, 'findAllDistinctCode')
.mockResolvedValue([{ stock_code: '005930' }]);

jest.spyOn(stockDetailService, 'getInquirePrice').mockResolvedValue({
hts_kor_isnm: '삼성전자',
stck_shrn_iscd: '005930',
stck_prpr: '53600',
prdy_vrss: '-600',
prdy_vrss_sign: '5',
prdy_ctrt: '-1.11',
hts_avls: '3199803',
per: '25.15',
stck_mxpr: '70400',
stck_llam: '38000',
is_bookmarked: false,
});

jest.spyOn(userStockRepository, 'find').mockResolvedValue([
{
id: 1,
user_id: 1,
stock_code: '005930',
quantity: 1,
avg_price: 1000,
last_updated: new Date(),
},
]);

jest
.spyOn(assetRepository, 'save')
.mockImplementation((updatedAsset) =>
Promise.resolve(updatedAsset as DeepPartial<Asset> & Asset),
);

const assetResponse = new AssetResponseDto(
1000,
53600,
54600,
-9945400,
'-99.45',
false,
);
const stockElementResponse = new StockElementResponseDto(
'삼성전자',
'005930',
1,
1000,
'53600',
'-600',
'5',
'-1.11',
);

const expected = new MypageResponseDto();
expected.asset = assetResponse;
expected.stocks = [stockElementResponse];

expect(await assetService.getMyPage(1)).toEqual(expected);
});
});
10 changes: 6 additions & 4 deletions BE/src/asset/asset.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,19 @@ export class AssetService {
const userStocks: UserStock[] =
await this.userStockRepository.findAllDistinctCode(userId);

userStocks.map((userStock) =>
this.stockPriceSocketService.subscribeByCode(userStock.stock_code),
await Promise.all(
userStocks.map((userStock) =>
this.stockPriceSocketService.subscribeByCode(userStock.stock_code),
),
);
}

async unsubscribeMyStocks(userId: number) {
const userStocks: UserStock[] =
await this.userStockRepository.findAllDistinctCode(userId);

userStocks.map((userStock) =>
this.stockPriceSocketService.unsubscribeByCode(userStock.stock_code),
await this.stockPriceSocketService.unsubscribeByCode(
userStocks.map((userStock) => userStock.stock_code),
);
}

Expand Down
35 changes: 31 additions & 4 deletions BE/src/common/redis/redis.domain-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import Redis from 'ioredis';
@Injectable()
export class RedisDomainService {
constructor(
@Inject('REDIS_CLIENT')
private readonly redis: Redis,
@Inject('REDIS_CLIENT') private readonly redis: Redis,
@Inject('REDIS_PUBLISHER') private readonly publisher: Redis,
@Inject('REDIS_SUBSCRIBER') private readonly subscriber: Redis,
) {}

async exists(key: string): Promise<number> {
return this.redis.exists(key);
}

async get(key: string): Promise<string | null> {
async get(key: string): Promise<string | number | null> {
return this.redis.get(key);
}

async set(key: string, value: string, expires?: number): Promise<'OK'> {
async set(key: string, value: number, expires?: number): Promise<'OK'> {
if (expires) {
return this.redis.set(key, value, 'EX', expires);
}
Expand Down Expand Up @@ -62,4 +63,30 @@ export class RedisDomainService {
async expire(key: string, seconds: number): Promise<number> {
return this.redis.expire(key, seconds);
}

async publish(channel: string, message: string) {
return this.publisher.publish(channel, message);
}

async subscribe(channel: string) {
await this.subscriber.subscribe(channel);
}

on(callback: (message: string) => void) {
this.redis.on('message', (message) => {
callback(message);
});
}

async unsubscribe(channel: string) {
return this.redis.unsubscribe(channel);
}

async increment(key: string) {
return this.redis.incr(key);
}

async decrement(key: string) {
return this.redis.decr(key);
}
}
25 changes: 24 additions & 1 deletion BE/src/common/redis/redis.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,31 @@ import { RedisDomainService } from './redis.domain-service';
});
},
},
{
provide: 'REDIS_PUBLISHER',
useFactory: () => {
return new Redis({
host: process.env.REDIS_HOST || 'redis',
port: Number(process.env.REDIS_PORT || 6379),
});
},
},
{
provide: 'REDIS_SUBSCRIBER',
useFactory: () => {
return new Redis({
host: process.env.REDIS_HOST || 'redis',
port: Number(process.env.REDIS_PORT || 6379),
});
},
},
RedisDomainService,
],
exports: [
RedisDomainService,
'REDIS_CLIENT',
'REDIS_PUBLISHER',
'REDIS_SUBSCRIBER',
],
exports: [RedisDomainService, 'REDIS_CLIENT'],
})
export class RedisModule {}
19 changes: 17 additions & 2 deletions BE/src/common/websocket/base-socket.domain-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OnModuleInit,
} from '@nestjs/common';
import { SocketTokenDomainService } from './socket-token.domain-service';
import { RedisDomainService } from '../redis/redis.domain-service';

@Injectable()
export class BaseSocketDomainService implements OnModuleInit {
Expand All @@ -20,6 +21,7 @@ export class BaseSocketDomainService implements OnModuleInit {

constructor(
private readonly socketTokenDomainService: SocketTokenDomainService,
private readonly redisDomainService: RedisDomainService,
) {}

async onModuleInit() {
Expand Down Expand Up @@ -57,11 +59,19 @@ export class BaseSocketDomainService implements OnModuleInit {
}

const dataList = data[3].split('^');

if (Number(dataList[1]) % 500 === 0)
this.logger.log(`한국투자증권 데이터 수신 성공 (5분 단위)`, data[1]);

this.socketDataHandlers[data[1]](dataList);
if (data[1] === 'H0UPCNT0') {
this.socketDataHandlers.H0UPCNT0(dataList);
return;
}

this.redisDomainService
.publish(`stock/${dataList[0]}`, data[3])
.catch((err) => {
throw new InternalServerErrorException(err);
});
};

this.socket.onclose = () => {
Expand All @@ -72,6 +82,11 @@ export class BaseSocketDomainService implements OnModuleInit {
});
}, 60000);
};

this.redisDomainService.on((message) => {
const dataList = message.split('^');
this.socketDataHandlers.H0STCNT0(dataList);
});
}

registerCode(trId: string, trKey: string) {
Expand Down
3 changes: 2 additions & 1 deletion BE/src/news/news.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource, In } from 'typeorm';
import { NaverApiDomianService } from './naver-api-domian.service';
Expand Down Expand Up @@ -47,7 +48,7 @@ export class NewsService {
};
}

// @Cron('*/30 8-16 * * 1-5')
@Cron('*/1 * * * *')
async cronNewsData() {
const queryRunner = this.dataSource.createQueryRunner();

Expand Down
Loading

0 comments on commit 1d71e4d

Please sign in to comment.