Skip to content

Commit

Permalink
Merge pull request #175 from boostcampwm-2024/back/main
Browse files Browse the repository at this point in the history
[BE] 브랜치 병합
  • Loading branch information
uuuo3o authored Nov 21, 2024
2 parents e05d39c + bb1c047 commit 71968fb
Show file tree
Hide file tree
Showing 75 changed files with 1,459 additions and 602 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ jobs:
max-parallel: 1
matrix:
app:
[
{ name: 'be', dir: 'BE', port: 3000, container: 'juga-docker-be' },
{ name: 'fe', dir: 'FE', port: 5173, container: 'juga-docker-fe' },
]
[{ name: 'be', dir: 'BE', port: 3000, container: 'juga-docker-be' }]

steps:
- uses: actions/checkout@v4
Expand All @@ -35,11 +32,7 @@ jobs:
- name: Create .env file
run: |
touch ./${{ matrix.app.dir }}/.env
if [ "${{ matrix.app.name }}" = "be" ]; then
echo "${{ secrets.ENV }}" > ./${{matrix.app.dir}}/.env
else
echo "${{ secrets.ENV_FE }}" > ./${{matrix.app.dir}}/.env
fi
echo "${{ secrets.ENV_ALPHA }}" > ./${{matrix.app.dir}}/.env
- name: Install dependencies
working-directory: ./${{matrix.app.dir}}
Expand Down Expand Up @@ -108,12 +101,7 @@ jobs:
port: 22
script: |
docker system prune -af
if [ "${{ matrix.app.name }}" = "be" ]; then
echo "${{ secrets.ENV }}" > .env
else
echo "${{ secrets.ENV_FE }}" > .env
fi
echo "${{ secrets.ENV_ALPHA }}" > .env
docker network create juga-network || true
Expand Down
1 change: 1 addition & 0 deletions BE/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ WORKDIR /var/app
COPY package*.json ./
RUN npm install --only=production
COPY --from=builder /app/dist ./dist
RUN ln -snf /usr/share/zoneinfo/Asia/Seoul /etc/localtime

EXPOSE 3000
CMD ["node", "dist/main.js"]
6 changes: 4 additions & 2 deletions BE/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { StockIndexModule } from './stock/index/stock-index.module';
import { StockTopfiveModule } from './stock/topfive/stock-topfive.module';
import { KoreaInvestmentModule } from './koreaInvestment/korea-investment.module';
import { SocketModule } from './websocket/socket.module';
import { KoreaInvestmentModule } from './common/koreaInvestment/korea-investment.module';
import { SocketModule } from './common/websocket/socket.module';
import { StockOrderModule } from './stock/order/stock-order.module';
import { StockDetailModule } from './stock/detail/stock-detail.module';
import { typeOrmConfig } from './configs/typeorm.config';
import { StockListModule } from './stock/list/stock-list.module';
import { StockTradeHistoryModule } from './stock/trade/history/stock-trade-history.module';
import { RedisModule } from './common/redis/redis.module';
import { HTTPExceptionFilter } from './common/filters/http-exception.filter';
import { RankingModule } from './ranking/ranking.module';

@Module({
imports: [
Expand All @@ -33,6 +34,7 @@ import { HTTPExceptionFilter } from './common/filters/http-exception.filter';
StockListModule,
StockTradeHistoryModule,
RedisModule,
RankingModule,
],
controllers: [AppController],
providers: [
Expand Down
80 changes: 76 additions & 4 deletions BE/src/asset/asset.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,78 @@
import { Controller } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { Request } from 'express';
import { Cron } from '@nestjs/schedule';
import { JwtAuthGuard } from '../auth/jwt-auth-guard';
import { AssetService } from './asset.service';
import { MypageResponseDto } from './dto/mypage-response.dto';

@Controller('/api/assets')
@ApiTags('자산 API')
export class AssetController {}
@ApiTags('사용자 자산 및 보유 주식 API')
export class AssetController {
constructor(private readonly assetService: AssetService) {}

@Get('/stocks/:stockCode')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: '매도 가능 주식 개수 조회 API',
description: '특정 주식 매도 시에 필요한 매도 가능한 주식 개수를 조회한다.',
})
@ApiResponse({
status: 200,
description: '매도 가능 주식 개수 조회 성공',
example: { quantity: 0 },
})
async getUserStockByCode(
@Req() request: Request,
@Param('stockCode') stockCode: string,
) {
return this.assetService.getUserStockByCode(
parseInt(request.user.userId, 10),
stockCode,
);
}

@Get('/cash')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: '매수 가능 금액 조회 API',
description:
'특정 주식 매수 시에 필요한 매수 가능한 금액(현재 가용자산)을 조회한다.',
})
@ApiResponse({
status: 200,
description: '매수 가능 금액 조회 성공',
example: { cash_balance: 0 },
})
async getCashBalance(@Req() request: Request) {
return this.assetService.getCashBalance(parseInt(request.user.userId, 10));
}

@Get()
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: '마이페이지 보유 자산 현황 조회 API',
description: '마이페이지 조회 시 필요한 보유 자산 현황을 조회한다.',
})
@ApiResponse({
status: 200,
description: '매수 가능 금액 조회 성공',
type: MypageResponseDto,
})
async getMyPage(@Req() request: Request) {
return this.assetService.getMyPage(parseInt(request.user.userId, 10));
}

@Cron('*/10 9-16 * * 1-5')
async updateAllAssets() {
await this.assetService.updateAllAssets();
}
}
3 changes: 3 additions & 0 deletions BE/src/asset/asset.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ export class Asset {

@Column({ nullable: true })
last_updated?: Date;

@Column({ default: INIT_ASSET })
prev_total_asset?: number;
}
9 changes: 6 additions & 3 deletions BE/src/asset/asset.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { AssetController } from './asset.controller';
import { AssetService } from './asset.service';
import { AssetRepository } from './asset.repository';
import { Asset } from './asset.entity';
import { UserStock } from './user-stock.entity';
import { UserStockRepository } from './user-stock.repository';
import { StockDetailModule } from '../stock/detail/stock-detail.module';

@Module({
imports: [TypeOrmModule.forFeature([Asset])],
imports: [TypeOrmModule.forFeature([Asset, UserStock]), StockDetailModule],
controllers: [AssetController],
providers: [AssetService, AssetRepository],
exports: [AssetRepository],
providers: [AssetService, AssetRepository, UserStockRepository],
exports: [AssetRepository, UserStockRepository],
})
export class AssetModule {}
7 changes: 7 additions & 0 deletions BE/src/asset/asset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@ export class AssetRepository extends Repository<Asset> {
constructor(@InjectDataSource() dataSource: DataSource) {
super(Asset, dataSource.createEntityManager());
}

async getAssets() {
return this.createQueryBuilder('asset')
.leftJoin('user', 'user', 'asset.user_id = user.id')
.select(['asset.* ', 'user.nickname as nickname'])
.getRawMany();
}
}
115 changes: 114 additions & 1 deletion BE/src/asset/asset.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,117 @@
import { Injectable } from '@nestjs/common';
import { UserStockRepository } from './user-stock.repository';
import { AssetRepository } from './asset.repository';
import { MypageResponseDto } from './dto/mypage-response.dto';
import { StockElementResponseDto } from './dto/stock-element-response.dto';
import { AssetResponseDto } from './dto/asset-response.dto';
import { StockDetailService } from '../stock/detail/stock-detail.service';
import { UserStock } from './user-stock.entity';
import { Asset } from './asset.entity';

@Injectable()
export class AssetService {}
export class AssetService {
constructor(
private readonly userStockRepository: UserStockRepository,
private readonly assetRepository: AssetRepository,
private readonly stockDetailService: StockDetailService,
) {}

async getUserStockByCode(userId: number, stockCode: string) {
const userStock = await this.userStockRepository.findOneBy({
user_id: userId,
stock_code: stockCode,
});

return { quantity: userStock ? userStock.quantity : 0 };
}

async getCashBalance(userId: number) {
const asset = await this.assetRepository.findOneBy({ user_id: userId });

return { cash_balance: asset.cash_balance };
}

async getMyPage(userId: number) {
const userStocks =
await this.userStockRepository.findUserStockWithNameByUserId(userId);
const asset = await this.assetRepository.findOneBy({ user_id: userId });
const newAsset = await this.updateMyAsset(
asset,
await this.getCurrPrices(),
);

const myStocks = userStocks.map((userStock) => {
return new StockElementResponseDto(
userStock.stocks_name,
userStock.stocks_code,
userStock.user_stocks_quantity,
userStock.user_stocks_avg_price,
);
});

const myAsset = new AssetResponseDto(
newAsset.cash_balance,
newAsset.stock_balance,
newAsset.total_asset,
newAsset.total_profit,
newAsset.total_profit_rate,
newAsset.total_profit_rate >= 0,
);

const response = new MypageResponseDto();
response.asset = myAsset;
response.stocks = myStocks;

return response;
}

async updateAllAssets() {
const currPrices = await this.getCurrPrices();
const assets = await this.assetRepository.find();

await Promise.allSettled(
assets.map((asset) => this.updateMyAsset(asset, currPrices)),
);
}

private async updateMyAsset(asset: Asset, currPrices) {
const userId = asset.user_id;
const userStocks = await this.userStockRepository.find({
where: { user_id: userId },
});

const totalPrice = userStocks.reduce(
(sum, userStock) =>
sum + userStock.quantity * currPrices[userStock.stock_code],
0,
);

const updatedAsset = {
...asset,
stock_balance: totalPrice,
total_asset: asset.cash_balance + totalPrice,
total_profit: asset.cash_balance + totalPrice - 10000000,
total_profit_rate: (asset.cash_balance + totalPrice - 10000000) / 100000,
last_updated: new Date(),
prev_total_asset: asset.total_asset,
};
return this.assetRepository.save(updatedAsset);
}

private async getCurrPrices() {
const userStocks: UserStock[] =
await this.userStockRepository.findAllDistinctCode();
const currPrices = {};

await Promise.allSettled(
userStocks.map(async (userStock) => {
const inquirePrice = await this.stockDetailService.getInquirePrice(
userStock.stock_code,
);
currPrices[userStock.stock_code] = Number(inquirePrice.stck_prpr);
}),
);

return currPrices;
}
}
37 changes: 37 additions & 0 deletions BE/src/asset/dto/asset-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';

export class AssetResponseDto {
constructor(
cash_balance,
stock_balance,
total_asset,
total_profit,
total_profit_rate,
is_positive,
) {
this.cash_balance = cash_balance;
this.stock_balance = stock_balance;
this.total_asset = total_asset;
this.total_profit = total_profit;
this.total_profit_rate = total_profit_rate;
this.is_positive = is_positive;
}

@ApiProperty({ description: '보유 현금' })
cash_balance: number;

@ApiProperty({ description: '주식 평가 금액' })
stock_balance: number;

@ApiProperty({ description: '총 자산' })
total_asset: number;

@ApiProperty({ description: '총 수익금' })
total_profit: number;

@ApiProperty({ description: '총 수익률' })
total_profit_rate: number;

@ApiProperty({ description: '수익률 부호' })
is_positive: boolean;
}
17 changes: 17 additions & 0 deletions BE/src/asset/dto/mypage-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { StockElementResponseDto } from './stock-element-response.dto';
import { AssetResponseDto } from './asset-response.dto';

export class MypageResponseDto {
@ApiProperty({
description: '보유 자산',
type: AssetResponseDto,
})
asset: AssetResponseDto;

@ApiProperty({
description: '보유 주식 리스트',
type: [StockElementResponseDto],
})
stocks: StockElementResponseDto[];
}
22 changes: 22 additions & 0 deletions BE/src/asset/dto/stock-element-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';

export class StockElementResponseDto {
constructor(name, code, quantity, avg_price) {
this.name = name;
this.code = code;
this.quantity = quantity;
this.avg_price = avg_price;
}

@ApiProperty({ description: '종목 이름' })
name: string;

@ApiProperty({ description: '종목 코드' })
code: string;

@ApiProperty({ description: '보유량' })
quantity: number;

@ApiProperty({ description: '평균 매수가' })
avg_price: number;
}
Loading

0 comments on commit 71968fb

Please sign in to comment.