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

[BE] 브랜치 병합 #260

Merged
merged 47 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8ede780
🔧 fix: stock index 데이터 안 들어왔을 때 예외 처리 추가
sieunie Dec 2, 2024
5d33dea
✅ test: stock index service 테스트 코드 통합 및 수정
sieunie Dec 2, 2024
4d93680
Merge pull request #236 from boostcampwm-2024/test/stockIndex
sieunie Dec 2, 2024
fd3bdde
Revert "[BE] 주가 지수 서비스 테스트 코드 작성"
sieunie Dec 2, 2024
f8b41ef
Merge pull request #239 from boostcampwm-2024/revert-236-test/stockIndex
sieunie Dec 2, 2024
2847ff3
🔧 fix: stock index 데이터 안 들어왔을 때 예외 처리 추가
sieunie Dec 2, 2024
675f1c7
✅ test: stock index service 테스트 코드 통합 및 수정
sieunie Dec 2, 2024
3bfb649
✅ test: topfive 로직 테스트를 위한 목데이터 생성 및 테스트 진행#237
uuuo3o Dec 2, 2024
4cd8976
🚚 rename: 파일명 및 디렉터리 통일#237
uuuo3o Dec 2, 2024
4cfba3b
🚚 rename: 파일명에 누락되어있던 service 추가#237
uuuo3o Dec 2, 2024
361389a
⚙️ chore: 불필요한 코드 삭제 및 테스트 이름 수정#237
uuuo3o Dec 2, 2024
b69ee2c
✅ test: 주식 매수 테스트 코드 작성 #237
sieunie Dec 2, 2024
acc7ae2
✅ test: 주식 매도 테스트 코드 작성 #237
sieunie Dec 2, 2024
a8336ae
✅ test: 주문 취소 테스트 코드 작성 #237
sieunie Dec 2, 2024
50f2216
🚚 rename: 테스트 및 목 데이터 파일명 변경 #237
sieunie Dec 2, 2024
749ad87
🚚 rename: service 구분자 .으로 변경 #237
sieunie Dec 2, 2024
f20dea2
🔧 fix: 필요 없는 모킹 삭제 #237
sieunie Dec 2, 2024
633b885
✅ test: trade-history 로직 테스트를 위한 목데이터 생성 및 테스트 진행#237
uuuo3o Dec 2, 2024
4d5ba50
✅ test: detail 로직 테스트를 위한 목데이터 생성 및 테스트 진행#237
uuuo3o Dec 2, 2024
d32a30e
✅ test: 매수/매도 가능 수량 및 금액 조회 테스트 추가 #237
sieunie Dec 2, 2024
c11dd04
⚙️ chore: 테스트 이름 수정#237
uuuo3o Dec 2, 2024
bdedb24
✅ test: 마이페이지 조회 API 테스트 코드 작성 #237
sieunie Dec 2, 2024
1da5938
🔧 린트 오류 해결 #237
sieunie Dec 2, 2024
576d8c7
♻️ refactor: 사용하지 않는 SSE 코드 제거#250
uuuo3o Dec 2, 2024
ffe39e2
✅ test: 즐겨찾기 등록 API 테스트 코드 작성 #237
sieunie Dec 2, 2024
9c7248a
♻️ refactor: 여러 종목에 대한 구독 해제 요청을 한번에 할 수 있도록 배열로 변경#250
uuuo3o Dec 2, 2024
80752a2
✅ test: 즐겨찾기 취소 API 테스트 코드 작성 #237
sieunie Dec 2, 2024
0e87ffe
✅ test: 즐겨찾기 리스트 조회 API 테스트 코드 작성 #237
sieunie Dec 2, 2024
f5c2fe4
Merge pull request #240 from boostcampwm-2024/test/stockIndex
uuuo3o Dec 3, 2024
b53a05c
Merge pull request #243 from boostcampwm-2024/test/topfive-#237
uuuo3o Dec 3, 2024
e3e4011
Merge pull request #245 from boostcampwm-2024/test/order
uuuo3o Dec 3, 2024
741d015
Merge pull request #246 from boostcampwm-2024/test/tradeHistory-#237
uuuo3o Dec 3, 2024
0a72a7a
Merge pull request #248 from boostcampwm-2024/test/stockDetail-#237
uuuo3o Dec 3, 2024
75b6239
Merge pull request #249 from boostcampwm-2024/test/asset-#237
uuuo3o Dec 3, 2024
1aa4623
Merge pull request #252 from boostcampwm-2024/test/bookmark-#237
uuuo3o Dec 3, 2024
9193a08
Merge pull request #251 from boostcampwm-2024/refactor/socket/unsubsc…
uuuo3o Dec 3, 2024
dae9f7c
✨ feat: redis pub/sub에 이용할 코드 추가
uuuo3o Dec 3, 2024
1c11590
✨ feat: redis에서 한투 구독을 관리할 수 있게 코드 추가
uuuo3o Dec 3, 2024
ed54e27
✨ feat: redis를 이용해 분산해서 세션을 이용할 수 있도록 코드 변경 및 추가
uuuo3o Dec 3, 2024
4cc12c6
♻️ refactor: await 추가
uuuo3o Dec 3, 2024
495fe73
🔧 fix: 자신이 pub한 것도 sub할 수 있게 코드 추가
uuuo3o Dec 3, 2024
0389366
🔧 fix: 테스트코드가 정상적으로 동작하지 않는 부분 수정
uuuo3o Dec 3, 2024
adc5be5
🔧 fix: 린트 오류 수정
uuuo3o Dec 3, 2024
967bb2f
Merge pull request #255 from boostcampwm-2024/feature/socket/redis
uuuo3o Dec 4, 2024
c3cf551
merge
uuuo3o Dec 4, 2024
46a886d
🚑 !HOTFIX: redis pub/sub은 다른 클라이언트 사용하도록 변경
sieunie Dec 4, 2024
aedff42
🚑 !HOTFIX: redis pub/sub export
sieunie Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading