Skip to content

Commit

Permalink
Merge pull request #142 from boostcampwm-2024/back/main
Browse files Browse the repository at this point in the history
[4주차] BE alpha 자동배포
  • Loading branch information
uuuo3o authored Nov 21, 2024
2 parents b074a01 + bb1c047 commit b1ff9b3
Show file tree
Hide file tree
Showing 74 changed files with 1,456 additions and 587 deletions.
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;
}
11 changes: 11 additions & 0 deletions BE/src/asset/interface/user-stock.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface UserStockInterface {
user_stocks_id: number;
user_stocks_user_id: number;
user_stocks_stock_code: string;
user_stocks_quantity: number;
user_stocks_avg_price: string;
user_stocks_last_updated: Date;
stocks_code: string;
stocks_name: string;
stocks_market: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {
Column,
Entity,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
} from 'typeorm';

@Entity('user_stocks')
@Unique(['user_id', 'stock_code'])
export class UserStock {
@PrimaryGeneratedColumn()
id: number;
Expand All @@ -19,7 +21,7 @@ export class UserStock {
@Column({ nullable: false })
quantity: number;

@Column('decimal', { nullable: false, precision: 10, scale: 5 })
@Column('float', { nullable: false, scale: 5 })
avg_price: number;

@UpdateDateColumn()
Expand Down
Loading

0 comments on commit b1ff9b3

Please sign in to comment.