From 5d5318b7b62d7f6bba257db237d120a940f5d470 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 7 Nov 2024 16:49:06 +0900 Subject: [PATCH 001/158] =?UTF-8?q?=E2=9E=95=20add=20:=20express=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EC=9D=98=20=20Request=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EC=84=A0=EC=96=B8=20=EC=9E=AC=EC=A0=95=EC=9D=98(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/types/express.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 BE/src/types/express.d.ts diff --git a/BE/src/types/express.d.ts b/BE/src/types/express.d.ts new file mode 100644 index 00000000..9cf89153 --- /dev/null +++ b/BE/src/types/express.d.ts @@ -0,0 +1,11 @@ +import { Request as Req } from 'express'; +import { UUID } from 'crypto'; + +declare module 'express' { + interface Request extends Req { + user: { + kakaoId?: number; + userId?: UUID; + }; + } +} From 0a276f9bb58f07e6266b23bb53c5a621a4ee8b08 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 7 Nov 2024 17:03:40 +0900 Subject: [PATCH 002/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20typeOr?= =?UTF-8?q?mModule=EC=9D=B4=20=EB=AA=A8=EB=93=A0=20entity=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 36681ad9..1628ef25 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -5,7 +5,6 @@ import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; -import { User } from './auth/user.entity'; import { StockIndexModule } from './stock/index/stock.index.module'; import { SocketService } from './websocket/socket.service'; import { SocketGateway } from './websocket/socket.gateway'; @@ -23,7 +22,7 @@ import { KoreaInvestmentModule } from './koreaInvestment/korea.investment.module username: process.env.DB_USERNAME, password: process.env.DB_PASSWD, database: process.env.DB_DATABASE, - entities: [User], + entities: [__dirname + '/../**/*.entity.{js,ts}'], synchronize: true, }), KoreaInvestmentModule, From 4357a549cee01d89ffca865a492cc095f851a154 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 7 Nov 2024 17:48:16 +0900 Subject: [PATCH 003/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20refresh=20Token?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 12 +---- BE/src/auth/auth.controller.ts | 1 + BE/src/auth/auth.module.ts | 16 +++++-- BE/src/auth/auth.service.ts | 64 +++++++++++++++++++++++++- BE/src/auth/dto/authCredentials.dto.ts | 40 +++++++++++----- BE/src/auth/user.entity.ts | 6 +++ BE/src/auth/user.repository.ts | 13 ++++++ BE/src/configs/typeorm.config.ts | 15 ++++++ BE/tsconfig.json | 3 +- 9 files changed, 140 insertions(+), 30 deletions(-) create mode 100644 BE/src/configs/typeorm.config.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 1628ef25..b161c923 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -10,21 +10,13 @@ import { SocketService } from './websocket/socket.service'; import { SocketGateway } from './websocket/socket.gateway'; import { StockTopfiveModule } from './stock/topfive/stock.topfive.module'; import { KoreaInvestmentModule } from './koreaInvestment/korea.investment.module'; +import { typeOrmConfig } from './configs/typeorm.config'; @Module({ imports: [ ScheduleModule.forRoot(), ConfigModule.forRoot(), - TypeOrmModule.forRoot({ - type: 'mysql', // 데이터베이스 타입 - host: process.env.DB_HOST, - port: 3306, - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWD, - database: process.env.DB_DATABASE, - entities: [__dirname + '/../**/*.entity.{js,ts}'], - synchronize: true, - }), + TypeOrmModule.forRoot(typeOrmConfig), KoreaInvestmentModule, AuthModule, StockIndexModule, diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 46eb0d6a..2bfffe99 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -11,6 +11,7 @@ import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { Request } from 'express'; @Controller('auth') export class AuthController { diff --git a/BE/src/auth/auth.module.ts b/BE/src/auth/auth.module.ts index a3ba6f04..dac8c17f 100644 --- a/BE/src/auth/auth.module.ts +++ b/BE/src/auth/auth.module.ts @@ -7,16 +7,22 @@ import { AuthService } from './auth.service'; import { User } from './user.entity'; import { UserRepository } from './user.repository'; import { JwtStrategy } from './jwt.strategy'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ TypeOrmModule.forFeature([User]), + ConfigModule, PassportModule.register({ defaultStrategy: 'jwt' }), - JwtModule.register({ - secret: 'Juga16', - signOptions: { - expiresIn: 3600, - }, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET'), + signOptions: { + expiresIn: configService.get('JWT_ACCESS_EXPIRATION_TIME'), + }, + }), + inject: [ConfigService], }), ], controllers: [AuthController], diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index cbc4a69f..00738ec5 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -4,6 +4,7 @@ import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { UserRepository } from './user.repository'; import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class AuthService { @@ -11,6 +12,7 @@ export class AuthService { @InjectRepository(UserRepository) private userRepository: UserRepository, private jwtService: JwtService, + private readonly configService: ConfigService, ) {} async signUp(authCredentialsDto: AuthCredentialsDto): Promise { @@ -24,10 +26,68 @@ export class AuthService { const user = await this.userRepository.findOne({ where: { email } }); if (user && (await bcrypt.compare(password, user.password))) { - const payload = { email }; - const accessToken = this.jwtService.sign(payload); + const { accessToken, refreshToken } = + await this.getJWTToken(authCredentialsDto); + + this.setCurrentRefreshToken(refreshToken, user.id); + return { accessToken }; } throw new UnauthorizedException('Please check your login credentials'); } + + async getJWTToken(authCredentialsDto: AuthCredentialsDto) { + const accessToken = await this.generateAccessToken(authCredentialsDto); + const refreshToken = await this.generateRefreshToken(authCredentialsDto); + return { accessToken, refreshToken }; + } + + async generateAccessToken( + authCredentialsDto: AuthCredentialsDto, + ): Promise { + return authCredentialsDto.email + ? this.jwtService.sign({ email: authCredentialsDto.email }) + : this.jwtService.sign({ kakaoId: authCredentialsDto.kakaoId }); + } + + async generateRefreshToken( + authCredentialsDto: AuthCredentialsDto, + ): Promise { + if (authCredentialsDto.email) { + return this.jwtService.sign( + { email: authCredentialsDto.email }, + { + secret: this.configService.get('JWT_REFRESH_SECRET'), + expiresIn: this.configService.get( + 'JWT_REFRESH_EXPIRATION_TIME', + ), + }, + ); + } else { + return this.jwtService.sign( + { kakaoId: authCredentialsDto.kakaoId }, + { + secret: this.configService.get('JWT_REFRESH_SECRET'), + expiresIn: this.configService.get( + 'JWT_REFRESH_EXPIRATION_TIME', + ), + }, + ); + } + } + + async setCurrentRefreshToken(refreshToken: string, userId: number) { + const currentDate = new Date(); + const salt = await bcrypt.genSalt(); + const currentRefreshToken = await bcrypt.hash(refreshToken, salt); + const currentRefreshTokenExpiresAt = new Date( + currentDate.getTime() + + parseInt(this.configService.get('JWT_REFRESH_EXPIRATION_TIME')), + ); + + this.userRepository.update(userId, { + currentRefreshToken, + currentRefreshTokenExpiresAt, + }); + } } diff --git a/BE/src/auth/dto/authCredentials.dto.ts b/BE/src/auth/dto/authCredentials.dto.ts index fb1199ed..07332ed8 100644 --- a/BE/src/auth/dto/authCredentials.dto.ts +++ b/BE/src/auth/dto/authCredentials.dto.ts @@ -1,27 +1,43 @@ -import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; +import { + IsString, + Matches, + MaxLength, + MinLength, + IsEmail, + ValidateNested, + IsOptional, +} from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class AuthCredentialsDto { @ApiProperty({ description: '유저 이메일', - minLength: 4, - maxLength: 20, - type: 'string', }) @IsString() - @MinLength(4) - @MaxLength(20) - email: string; + email?: string; @ApiProperty({ description: '유저 비밀번호', - minLength: 4, - maxLength: 20, - type: 'string', }) @IsString() @MinLength(4) @MaxLength(20) - @Matches(/^[a-zA-Z0-9]*$/) - password: string; + @Matches(/^[a-zA-Z0-9]*$/, { + message: '비밀번호는 영문과 숫자만 사용가능합니다', + }) + password?: string; + + @ApiProperty({ + description: '카카오 ID', + }) + @IsString() + @IsOptional() + kakaoId?: string; + + @ApiProperty({ + description: '카카오 액세스 토큰', + }) + @IsString() + @IsOptional() + kakaoAccessToken?: string; } diff --git a/BE/src/auth/user.entity.ts b/BE/src/auth/user.entity.ts index cf6b130a..fbdacb39 100644 --- a/BE/src/auth/user.entity.ts +++ b/BE/src/auth/user.entity.ts @@ -16,4 +16,10 @@ export class User extends BaseEntity { @Column({ default: -1 }) kakaoId: number; + + @Column({ default: '' }) + currentRefreshToken: string; + + @Column({ type: 'datetime', nullable: true }) + currentRefreshTokenExpiresAt: Date; } diff --git a/BE/src/auth/user.repository.ts b/BE/src/auth/user.repository.ts index 0a23f980..ac571fe2 100644 --- a/BE/src/auth/user.repository.ts +++ b/BE/src/auth/user.repository.ts @@ -18,4 +18,17 @@ export class UserRepository extends Repository { const user = this.create({ email, password: hashedPassword }); await this.save(user); } + + async updateUserWithRefreshToken( + id: number, + { + refreshToken, + refreshTokenExpiresAt, + }: { refreshToken: string; refreshTokenExpiresAt: Date }, + ) { + const user = await this.findOne({ where: { id } }); + user.currentRefreshToken = refreshToken; + user.currentRefreshTokenExpiresAt = refreshTokenExpiresAt; + await this.save(user); + } } diff --git a/BE/src/configs/typeorm.config.ts b/BE/src/configs/typeorm.config.ts new file mode 100644 index 00000000..dd42cdbe --- /dev/null +++ b/BE/src/configs/typeorm.config.ts @@ -0,0 +1,15 @@ +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +export const typeOrmConfig: TypeOrmModuleOptions = { + type: 'mysql', + host: process.env.DB_HOST, + port: 3306, + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWD, + database: process.env.DB_DATABASE, + entities: [__dirname + '/../**/*.entity{.js,.ts}'], + synchronize: true, +}; diff --git a/BE/tsconfig.json b/BE/tsconfig.json index 95f5641c..52a4d6a7 100644 --- a/BE/tsconfig.json +++ b/BE/tsconfig.json @@ -16,6 +16,7 @@ "noImplicitAny": false, "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "typeRoots": ["node_modules/@types", "./src/types"] } } From a9c95d76a624a605a10a382d464a7e7da9b69de9 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 7 Nov 2024 18:52:23 +0900 Subject: [PATCH 004/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20Token=20Refresh?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20API=20=EA=B5=AC=ED=98=84(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/package-lock.json | 33 +++++++++++++++++++++++++ BE/package.json | 4 ++- BE/src/auth/auth.controller.ts | 45 +++++++++++++++++++++++++++++++--- BE/src/auth/auth.service.ts | 39 +++++++++++++++++++++++++++-- BE/src/auth/user.entity.ts | 10 ++++++++ BE/src/main.ts | 2 ++ 6 files changed, 126 insertions(+), 7 deletions(-) diff --git a/BE/package-lock.json b/BE/package-lock.json index cb72a103..4f1add7a 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -20,11 +20,13 @@ "@nestjs/swagger": "^8.0.1", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.4.7", + "@types/cookie-parser": "^1.4.7", "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "cross-env": "^7.0.3", "docker": "^1.0.0", "dotenv": "^16.4.5", @@ -1961,6 +1963,15 @@ "version": "0.4.1", "license": "MIT" }, + "node_modules/@types/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "dev": true, @@ -3782,6 +3793,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "license": "MIT" diff --git a/BE/package.json b/BE/package.json index 224dc019..8c4cf8f8 100644 --- a/BE/package.json +++ b/BE/package.json @@ -30,12 +30,14 @@ "@nestjs/schedule": "^4.1.1", "@nestjs/swagger": "^8.0.1", "@nestjs/typeorm": "^10.0.2", - "@types/passport-jwt": "^4.0.1", "@nestjs/websockets": "^10.4.7", + "@types/cookie-parser": "^1.4.7", + "@types/passport-jwt": "^4.0.1", "axios": "^1.7.7", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "cross-env": "^7.0.3", "docker": "^1.0.0", "dotenv": "^16.4.5", diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 2bfffe99..b95a78ae 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -3,19 +3,24 @@ import { Post, Get, Body, - Req, ValidationPipe, UseGuards, + Req, + Res, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { AuthCredentialsDto } from './dto/authCredentials.dto'; -import { Request } from 'express'; +import { Request, Response } from 'express'; +import { ConfigService } from '@nestjs/config'; @Controller('auth') export class AuthController { - constructor(private authService: AuthService) {} + constructor( + private authService: AuthService, + private configService: ConfigService, + ) {} @ApiOperation({ summary: '회원 가입 API' }) @Post('/signup') @@ -24,7 +29,7 @@ export class AuthController { } @ApiOperation({ summary: '로그인 API' }) - @Get('/login') + @Post('/login') loginWithCredentials( @Body(ValidationPipe) authCredentialsDto: AuthCredentialsDto, ) { @@ -37,4 +42,36 @@ export class AuthController { test(@Req() req: Request) { return req; } + + @ApiOperation({ summary: 'Kakao 로그인 API' }) + @Get('/kakao') + async kakaoLogin( + @Body() authCredentialsDto: AuthCredentialsDto, + @Res() res: Response, + ) { + const { accessToken, refreshToken } = + await this.authService.kakaoLoginUser(authCredentialsDto); + + res.cookie('refreshToken', refreshToken, { httpOnly: true }); + res.cookie('accessToken', accessToken, { httpOnly: true }); + res.cookie('isRefreshToken', true, { httpOnly: true }); + return res.redirect(this.configService.get('CLIENT_URL')); + } + + @ApiOperation({ summary: 'Refresh Token 요청 API' }) + @Get('/refresh') + async refresh(@Req() req: Request, @Res() res: Response) { + const refreshToken = req.cookies['refreshToken']; + const accessToken = req.cookies['accessToken']; + + if (!refreshToken || !accessToken) { + return res.status(401).send(); + } + + const newAccessToken = await this.authService.refreshToken(refreshToken); + + res.cookie('accessToken', newAccessToken, { httpOnly: true }); + res.cookie('isRefreshToken', true, { httpOnly: true }); + return res.status(200).send(); + } } diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 00738ec5..026ff074 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -21,7 +21,7 @@ export class AuthService { async loginUser( authCredentialsDto: AuthCredentialsDto, - ): Promise<{ accessToken: string }> { + ): Promise<{ accessToken: string; refreshToken: string }> { const { email, password } = authCredentialsDto; const user = await this.userRepository.findOne({ where: { email } }); @@ -31,11 +31,16 @@ export class AuthService { this.setCurrentRefreshToken(refreshToken, user.id); - return { accessToken }; + return { accessToken, refreshToken }; } throw new UnauthorizedException('Please check your login credentials'); } + async kakaoLoginUser( + authCredentialsDto: AuthCredentialsDto, + ): Promise<{ accessToken: string; refreshToken: string }> { + return await this.getJWTToken(authCredentialsDto); + } async getJWTToken(authCredentialsDto: AuthCredentialsDto) { const accessToken = await this.generateAccessToken(authCredentialsDto); const refreshToken = await this.generateRefreshToken(authCredentialsDto); @@ -90,4 +95,34 @@ export class AuthService { currentRefreshTokenExpiresAt, }); } + + async refreshToken(refreshToken: string): Promise { + try { + const decodedRefreshToken = this.jwtService.verify(refreshToken, { + secret: this.configService.get('JWT_REFRESH_SECRET'), + }); + + const user = decodedRefreshToken.email + ? await this.userRepository.findOne({ + where: { email: decodedRefreshToken.email }, + }) + : await this.userRepository.findOne({ + where: { kakaoId: decodedRefreshToken.kakaoId }, + }); + + const isRefreshTokenMatching = await bcrypt.compare( + refreshToken, + user.currentRefreshToken, + ); + + if (!isRefreshTokenMatching) { + throw new UnauthorizedException('Invalid Token'); + } + + const accessToken = this.generateAccessToken(user.toAuthCredentialsDto()); + return accessToken; + } catch (error) { + throw new UnauthorizedException('Invalid Token'); + } + } } diff --git a/BE/src/auth/user.entity.ts b/BE/src/auth/user.entity.ts index fbdacb39..4dfc6541 100644 --- a/BE/src/auth/user.entity.ts +++ b/BE/src/auth/user.entity.ts @@ -1,4 +1,5 @@ import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { AuthCredentialsDto } from './dto/authCredentials.dto'; @Entity() export class User extends BaseEntity { @@ -22,4 +23,13 @@ export class User extends BaseEntity { @Column({ type: 'datetime', nullable: true }) currentRefreshTokenExpiresAt: Date; + + toAuthCredentialsDto(): AuthCredentialsDto { + if (this.kakaoId === -1) { + return { + email: this.email, + password: this.password, + }; + } + } } diff --git a/BE/src/main.ts b/BE/src/main.ts index bbc099aa..37ceb7cf 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core'; import { Logger } from '@nestjs/common'; import { AppModule } from './app.module'; import { setupSwagger } from './util/swagger'; +import * as cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -14,6 +15,7 @@ async function bootstrap() { optionsSuccessStatus: 204, }); + app.use(cookieParser()); await app.listen(process.env.PORT ?? 3000); } From 91607bdd2c058e7f97a5041b5bb2ee1d5ca27418 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 7 Nov 2024 19:05:58 +0900 Subject: [PATCH 005/158] =?UTF-8?q?=E2=9E=95=20add=20:=20kakao=20strategy?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/package-lock.json | 45 ++++++++++++++++++++++ BE/package.json | 1 + BE/src/auth/auth.controller.ts | 3 +- BE/src/auth/auth.module.ts | 5 ++- BE/src/auth/{ => strategy}/jwt.strategy.ts | 4 +- BE/src/auth/strategy/kakao.strategy.ts | 32 +++++++++++++++ 6 files changed, 85 insertions(+), 5 deletions(-) rename BE/src/auth/{ => strategy}/jwt.strategy.ts (88%) create mode 100644 BE/src/auth/strategy/kakao.strategy.ts diff --git a/BE/package-lock.json b/BE/package-lock.json index 4f1add7a..5cdd9bb9 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -34,6 +34,7 @@ "mysql2": "^3.11.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "passport-kakao": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.8.1", @@ -8332,6 +8333,12 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, "node_modules/oauth-sign": { "version": "0.8.2", "license": "Apache-2.0", @@ -8644,6 +8651,29 @@ "passport-strategy": "^1.0.0" } }, + "node_modules/passport-kakao": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/passport-kakao/-/passport-kakao-1.0.1.tgz", + "integrity": "sha512-uItaYRVrTHL6iGPMnMZvPa/O1GrAdh/V6EMjOHcFlQcVroZ9wgG7BZ5PonMNJCxfHQ3L2QVNRnzhKWUzSsumbw==", + "license": "MIT", + "dependencies": { + "passport-oauth2": "~1.1.2", + "pkginfo": "~0.3.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.1.2.tgz", + "integrity": "sha512-wpsGtJDHHQUjyc9WcV9FFB0bphFExpmKtzkQrxpH1vnSr6RcWa3ZEGHx/zGKAh2PN7Po9TKYB1fJeOiIBspNPA==", + "dependencies": { + "oauth": "0.9.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/passport-strategy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", @@ -8802,6 +8832,15 @@ "node": ">=8" } }, + "node_modules/pkginfo": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz", + "integrity": "sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "dev": true, @@ -10934,6 +10973,12 @@ "node": ">=8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/unbounded": { "version": "1.3.0", "license": "MIT", diff --git a/BE/package.json b/BE/package.json index 8c4cf8f8..52432b78 100644 --- a/BE/package.json +++ b/BE/package.json @@ -45,6 +45,7 @@ "mysql2": "^3.11.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "passport-kakao": "^1.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.8.1", diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index b95a78ae..44f31114 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -38,13 +38,14 @@ export class AuthController { @ApiOperation({ summary: 'Token 인증 테스트 API' }) @Get('/test') - @UseGuards(AuthGuard()) + @UseGuards(AuthGuard('jwt')) test(@Req() req: Request) { return req; } @ApiOperation({ summary: 'Kakao 로그인 API' }) @Get('/kakao') + @UseGuards(AuthGuard('kakao')) async kakaoLogin( @Body() authCredentialsDto: AuthCredentialsDto, @Res() res: Response, diff --git a/BE/src/auth/auth.module.ts b/BE/src/auth/auth.module.ts index dac8c17f..87a398ba 100644 --- a/BE/src/auth/auth.module.ts +++ b/BE/src/auth/auth.module.ts @@ -6,8 +6,9 @@ import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { User } from './user.entity'; import { UserRepository } from './user.repository'; -import { JwtStrategy } from './jwt.strategy'; +import { JwtStrategy } from './strategy/jwt.strategy'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { KakaoStrategy } from './strategy/kakao.strategy'; @Module({ imports: [ @@ -26,7 +27,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; }), ], controllers: [AuthController], - providers: [AuthService, UserRepository, JwtStrategy], + providers: [AuthService, UserRepository, JwtStrategy, KakaoStrategy], exports: [JwtStrategy, PassportModule], }) export class AuthModule {} diff --git a/BE/src/auth/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts similarity index 88% rename from BE/src/auth/jwt.strategy.ts rename to BE/src/auth/strategy/jwt.strategy.ts index 350d622d..b60a0596 100644 --- a/BE/src/auth/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -2,8 +2,8 @@ import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { UserRepository } from './user.repository'; -import { User } from './user.entity'; +import { UserRepository } from '../user.repository'; +import { User } from '../user.entity'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { diff --git a/BE/src/auth/strategy/kakao.strategy.ts b/BE/src/auth/strategy/kakao.strategy.ts new file mode 100644 index 00000000..f47b3eeb --- /dev/null +++ b/BE/src/auth/strategy/kakao.strategy.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { Profile, Strategy } from 'passport-kakao'; + +@Injectable() +export class KakaoStrategy extends PassportStrategy(Strategy) { + constructor(private readonly configService: ConfigService) { + super({ + clientID: configService.get('KAKAO_CLIENT_ID'), + clientSecret: '', + callbackURL: `${configService.get('BACKEND_URL')}/auth/kakao`, + }); + } + + async validate( + accessToken: string, + refreshToken: string, + profile: Profile, + done: (error: any, user?: any, info?: any) => void, + ) { + try { + const { _json } = profile; + const user = { + kakaoId: _json.id, + }; + done(null, user); + } catch (error) { + done(error); + } + } +} From 99362cdf5c55fad734d74a05168779e10aa7bd24 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 8 Nov 2024 10:13:25 +0900 Subject: [PATCH 006/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20getFul?= =?UTF-8?q?lUrl,=20getHeader=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../korea.investment.service.ts | 14 ++++---- BE/src/stock/index/stock.index.service.ts | 34 +++++++------------ BE/src/websocket/socket.service.ts | 6 ++-- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/BE/src/koreaInvestment/korea.investment.service.ts b/BE/src/koreaInvestment/korea.investment.service.ts index 57458372..73ebe901 100644 --- a/BE/src/koreaInvestment/korea.investment.service.ts +++ b/BE/src/koreaInvestment/korea.investment.service.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { getFullURL } from '../util/getFullURL'; export class KoreaInvestmentService { private accessToken: string; @@ -9,14 +10,11 @@ export class KoreaInvestmentService { if (this.accessToken && this.tokenExpireTime > new Date()) { return this.accessToken; } - const response = await axios.post( - `${process.env.KOREA_INVESTMENT_BASE_URL}/oauth2/tokenP`, - { - grant_type: 'client_credentials', - appkey: process.env.KOREA_INVESTMENT_APP_KEY, - appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, - }, - ); + const response = await axios.post(getFullURL('/oauth2/tokenP'), { + grant_type: 'client_credentials', + appkey: process.env.KOREA_INVESTMENT_APP_KEY, + appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, + }); const { data } = response; diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock.index.service.ts index 875be12d..42bb056b 100644 --- a/BE/src/stock/index/stock.index.service.ts +++ b/BE/src/stock/index/stock.index.service.ts @@ -7,6 +7,8 @@ import { StockIndexChartInterface, StockIndexValueInterface, } from './interface/stock.index.interface'; +import { getFullURL } from '../../util/getFullURL'; +import { getHeader } from '../../util/getHeader'; @Injectable() export class StockIndexService { @@ -39,12 +41,14 @@ export class StockIndexService { if (result.rt_cd !== '0') throw new Error('데이터를 정상적으로 조회하지 못했습니다.'); + const data = result.output; + return new StockIndexValueElementDto( code, - result.output.bstp_nmix_prpr, - result.output.bstp_nmix_prdy_vrss, - result.output.bstp_nmix_prdy_ctrt, - result.output.prdy_vrss_sign, + data.bstp_nmix_prpr, + data.bstp_nmix_prdy_vrss, + data.bstp_nmix_prdy_ctrt, + data.prdy_vrss_sign, ); } @@ -53,16 +57,9 @@ export class StockIndexService { accessToken: string, ) { const response = await axios.get( - `${process.env.KOREA_INVESTMENT_BASE_URL}/uapi/domestic-stock/v1/quotations/inquire-index-timeprice`, + getFullURL('/uapi/domestic-stock/v1/quotations/inquire-index-timeprice'), { - headers: { - 'content-type': 'application/json; charset=utf-8', - authorization: `Bearer ${accessToken}`, - appkey: process.env.KOREA_INVESTMENT_APP_KEY, - appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, - tr_id: 'FHPUP02110200', - custtype: 'P', - }, + headers: getHeader(accessToken, 'FHPUP02110200'), params: { fid_input_hour_1: 300, fid_cond_mrkt_div_code: 'U', @@ -79,16 +76,9 @@ export class StockIndexService { accessToken: string, ) { const response = await axios.get( - `${process.env.KOREA_INVESTMENT_BASE_URL}/uapi/domestic-stock/v1/quotations/inquire-index-price`, + getFullURL('/uapi/domestic-stock/v1/quotations/inquire-index-price'), { - headers: { - 'content-type': 'application/json; charset=utf-8', - authorization: `Bearer ${accessToken}`, - appkey: process.env.KOREA_INVESTMENT_APP_KEY, - appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, - tr_id: 'FHPUP02100000', - custtype: 'P', - }, + headers: getHeader(accessToken, 'FHPUP02100000'), params: { fid_cond_mrkt_div_code: 'U', fid_input_iscd: code, diff --git a/BE/src/websocket/socket.service.ts b/BE/src/websocket/socket.service.ts index 0c25bf03..68893600 100644 --- a/BE/src/websocket/socket.service.ts +++ b/BE/src/websocket/socket.service.ts @@ -4,6 +4,7 @@ import axios from 'axios'; import { SocketGateway } from './socket.gateway'; import { StockIndexValueElementDto } from '../stock/index/dto/stock.index.value.element.dto'; import { SocketConnectTokenInterface } from './interface/socket.interface'; +import { getFullURL } from '../util/getFullURL'; @Injectable() export class SocketService implements OnModuleInit { @@ -17,8 +18,7 @@ export class SocketService implements OnModuleInit { async onModuleInit() { const socketConnectionKey = await this.getSocketConnectionKey(); - const url = 'ws://ops.koreainvestment.com:21000'; - this.socket = new WebSocket(url); + this.socket = new WebSocket(process.env.KOREA_INVESTMENT_SOCKET_URL); this.socket.onopen = () => { this.registerStockIndexByCode('0001', socketConnectionKey); // 코스피 @@ -53,7 +53,7 @@ export class SocketService implements OnModuleInit { private async getSocketConnectionKey() { const response = await axios.post( - `${process.env.KOREA_INVESTMENT_BASE_URL}/oauth2/Approval`, + getFullURL('/oauth2/Approval'), { grant_type: 'client_credentials', appkey: process.env.KOREA_INVESTMENT_APP_KEY, From 6549725d9e02ef975f98efc3fd562ea26ba4bf91 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 8 Nov 2024 10:59:58 +0900 Subject: [PATCH 007/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20stock?= =?UTF-8?q?=20index=20=EA=B4=80=EB=A0=A8=20=EC=86=8C=EC=BC=93=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8,=20response=20=ED=98=95=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../index/dto/stock.index.list.element.dto.ts | 17 -------------- .../dto/stock.index.response.element.dto.ts | 14 +++++------- .../dto/stock.index.value.element.dto.ts | 22 +++++-------------- BE/src/stock/index/stock.index.controller.ts | 11 +++++----- BE/src/stock/index/stock.index.service.ts | 17 +++++--------- BE/src/websocket/socket.gateway.ts | 8 +++---- BE/src/websocket/socket.service.ts | 9 +++++++- 7 files changed, 35 insertions(+), 63 deletions(-) delete mode 100644 BE/src/stock/index/dto/stock.index.list.element.dto.ts diff --git a/BE/src/stock/index/dto/stock.index.list.element.dto.ts b/BE/src/stock/index/dto/stock.index.list.element.dto.ts deleted file mode 100644 index 03ec0176..00000000 --- a/BE/src/stock/index/dto/stock.index.list.element.dto.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { StockIndexListChartElementDto } from './stock.index.list.chart.element.dto'; - -export class StockIndexListElementDto { - constructor(code: string, chart: StockIndexListChartElementDto[]) { - this.code = code; - this.chart = chart; - } - - @ApiProperty({ - description: '코스피: 0001, 코스닥: 1001, 코스피200: 2001, KSQ150: 3003', - }) - code: string; - - @ApiProperty({ type: [StockIndexListChartElementDto] }) - chart: StockIndexListChartElementDto[]; -} diff --git a/BE/src/stock/index/dto/stock.index.response.element.dto.ts b/BE/src/stock/index/dto/stock.index.response.element.dto.ts index 7ce0ef1a..09e21102 100644 --- a/BE/src/stock/index/dto/stock.index.response.element.dto.ts +++ b/BE/src/stock/index/dto/stock.index.response.element.dto.ts @@ -1,16 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; import { StockIndexValueElementDto } from './stock.index.value.element.dto'; -import { StockIndexListElementDto } from './stock.index.list.element.dto'; +import { StockIndexListChartElementDto } from './stock.index.list.chart.element.dto'; export class StockIndexResponseElementDto { - @ApiProperty({ - description: '코스피: 0001, 코스닥: 1001, 코스피200: 2001, KSQ150: 3003', - }) - code: string; - @ApiProperty({ description: '실시간 값', type: StockIndexValueElementDto }) value: StockIndexValueElementDto; - @ApiProperty({ description: '실시간 차트', type: StockIndexListElementDto }) - chart: StockIndexListElementDto; + @ApiProperty({ + description: '실시간 차트', + type: [StockIndexListChartElementDto], + }) + chart: StockIndexListChartElementDto[]; } diff --git a/BE/src/stock/index/dto/stock.index.value.element.dto.ts b/BE/src/stock/index/dto/stock.index.value.element.dto.ts index 88ca4949..1eb73a50 100644 --- a/BE/src/stock/index/dto/stock.index.value.element.dto.ts +++ b/BE/src/stock/index/dto/stock.index.value.element.dto.ts @@ -1,33 +1,21 @@ import { ApiProperty } from '@nestjs/swagger'; export class StockIndexValueElementDto { - constructor( - code: string, - value: string, - diff: string, - diffRate: string, - sign: string, - ) { - this.code = code; - this.value = value; + constructor(value: string, diff: string, diffRate: string, sign: string) { + this.curr_value = value; this.diff = diff; - this.diffRate = diffRate; + this.diff_rate = diffRate; this.sign = sign; } - @ApiProperty({ - description: '코스피: 0001, 코스닥: 1001, 코스피200: 2001, KSQ150: 3003', - }) - code: string; - @ApiProperty({ description: '주가 지수' }) - value: string; + curr_value: string; @ApiProperty({ description: '전일 대비 등락' }) diff: string; @ApiProperty({ description: '전일 대비 등락률' }) - diffRate: string; + diff_rate: string; @ApiProperty({ description: '부호... 인데 추후에 알아봐야 함' }) sign: string; diff --git a/BE/src/stock/index/stock.index.controller.ts b/BE/src/stock/index/stock.index.controller.ts index 949df5a5..7625e044 100644 --- a/BE/src/stock/index/stock.index.controller.ts +++ b/BE/src/stock/index/stock.index.controller.ts @@ -70,22 +70,18 @@ export class StockIndexController { const stockIndexResponse = new StockIndexResponseDto(); stockIndexResponse.KOSPI = { - code: '0001', value: kospiValue, chart: kospiChart, }; stockIndexResponse.KOSDAQ = { - code: '1001', value: kosdaqValue, chart: kosdaqChart, }; stockIndexResponse.KOSPI200 = { - code: '2001', value: kospi200Value, chart: kospi200Chart, }; stockIndexResponse.KSQ150 = { - code: '3003', value: ksq150Value, chart: ksq150Chart, }; @@ -115,6 +111,11 @@ export class StockIndexController { ), // KSQ150 ]); - this.socketGateway.sendStockIndexListToClient(stockLists); + this.socketGateway.sendStockIndexListToClient({ + KOSPI: stockLists[0], + KOSDAQ: stockLists[1], + KOSPI200: stockLists[2], + KSQ150: stockLists[3], + }); } } diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock.index.service.ts index 42bb056b..0d4a97c0 100644 --- a/BE/src/stock/index/stock.index.service.ts +++ b/BE/src/stock/index/stock.index.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import axios from 'axios'; import { StockIndexListChartElementDto } from './dto/stock.index.list.chart.element.dto'; -import { StockIndexListElementDto } from './dto/stock.index.list.element.dto'; import { StockIndexValueElementDto } from './dto/stock.index.value.element.dto'; import { StockIndexChartInterface, @@ -21,15 +20,12 @@ export class StockIndexService { if (result.rt_cd !== '0') throw new Error('데이터를 정상적으로 조회하지 못했습니다.'); - return new StockIndexListElementDto( - code, - result.output.map((element) => { - return new StockIndexListChartElementDto( - element.bsop_hour, - element.bstp_nmix_prpr, - ); - }), - ); + return result.output.map((element) => { + return new StockIndexListChartElementDto( + element.bsop_hour, + element.bstp_nmix_prpr, + ); + }); } async getDomesticStockIndexValueByCode(code: string, accessToken: string) { @@ -44,7 +40,6 @@ export class StockIndexService { const data = result.output; return new StockIndexValueElementDto( - code, data.bstp_nmix_prpr, data.bstp_nmix_prdy_vrss, data.bstp_nmix_prdy_ctrt, diff --git a/BE/src/websocket/socket.gateway.ts b/BE/src/websocket/socket.gateway.ts index 921d9503..e65eac69 100644 --- a/BE/src/websocket/socket.gateway.ts +++ b/BE/src/websocket/socket.gateway.ts @@ -6,11 +6,11 @@ export class SocketGateway { @WebSocketServer() private server: Server; - sendStockIndexListToClient(stockIndex) { - this.server.emit('index', stockIndex); + sendStockIndexListToClient(stockChart) { + this.server.emit('chart', stockChart); } - sendStockIndexValueToClient(stockIndexValue) { - this.server.emit('indexValue', stockIndexValue); + sendStockIndexValueToClient(event, stockIndexValue) { + this.server.emit(event, stockIndexValue); } } diff --git a/BE/src/websocket/socket.service.ts b/BE/src/websocket/socket.service.ts index 68893600..042cd57a 100644 --- a/BE/src/websocket/socket.service.ts +++ b/BE/src/websocket/socket.service.ts @@ -13,6 +13,13 @@ export class SocketService implements OnModuleInit { H0UPCNT0: this.handleStockIndexValue.bind(this), }; + private STOCK_CODE = { + '0001': 'KOSPI', + '1001': 'KOSDAQ', + '2001': 'KOSPI200', + '3003': 'KSQ150', + }; + constructor(private readonly socketGateway: SocketGateway) {} async onModuleInit() { @@ -41,8 +48,8 @@ export class SocketService implements OnModuleInit { private handleStockIndexValue(responseData: string) { const responseList = responseData.split('^'); this.socketGateway.sendStockIndexValueToClient( + this.STOCK_CODE[responseList[0]], new StockIndexValueElementDto( - responseList[0], responseList[2], responseList[4], responseList[9], From d65c70bfef23fc17362613566ac1a07033f24e2d Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 8 Nov 2024 11:24:23 +0900 Subject: [PATCH 008/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20service=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EB=A5=BC=20http=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../korea.investment.service.ts | 15 +++-- BE/src/stock/index/stock.index.service.ts | 60 +++++++++++-------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/BE/src/koreaInvestment/korea.investment.service.ts b/BE/src/koreaInvestment/korea.investment.service.ts index 73ebe901..886b68cb 100644 --- a/BE/src/koreaInvestment/korea.investment.service.ts +++ b/BE/src/koreaInvestment/korea.investment.service.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { UnauthorizedException } from '@nestjs/common'; import { getFullURL } from '../util/getFullURL'; export class KoreaInvestmentService { @@ -10,11 +11,15 @@ export class KoreaInvestmentService { if (this.accessToken && this.tokenExpireTime > new Date()) { return this.accessToken; } - const response = await axios.post(getFullURL('/oauth2/tokenP'), { - grant_type: 'client_credentials', - appkey: process.env.KOREA_INVESTMENT_APP_KEY, - appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, - }); + const response = await axios + .post(getFullURL('/oauth2/tokenP'), { + grant_type: 'client_credentials', + appkey: process.env.KOREA_INVESTMENT_APP_KEY, + appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, + }) + .catch((err) => { + throw new UnauthorizedException('액세스 토큰을 조회하지 못했습니다.'); + }); const { data } = response; diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock.index.service.ts index 0d4a97c0..3a14c7fc 100644 --- a/BE/src/stock/index/stock.index.service.ts +++ b/BE/src/stock/index/stock.index.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import axios from 'axios'; import { StockIndexListChartElementDto } from './dto/stock.index.list.chart.element.dto'; import { StockIndexValueElementDto } from './dto/stock.index.value.element.dto'; @@ -17,9 +17,6 @@ export class StockIndexService { accessToken, ); - if (result.rt_cd !== '0') - throw new Error('데이터를 정상적으로 조회하지 못했습니다.'); - return result.output.map((element) => { return new StockIndexListChartElementDto( element.bsop_hour, @@ -34,9 +31,6 @@ export class StockIndexService { accessToken, ); - if (result.rt_cd !== '0') - throw new Error('데이터를 정상적으로 조회하지 못했습니다.'); - const data = result.output; return new StockIndexValueElementDto( @@ -51,17 +45,25 @@ export class StockIndexService { code: string, accessToken: string, ) { - const response = await axios.get( - getFullURL('/uapi/domestic-stock/v1/quotations/inquire-index-timeprice'), - { - headers: getHeader(accessToken, 'FHPUP02110200'), - params: { - fid_input_hour_1: 300, - fid_cond_mrkt_div_code: 'U', - fid_input_iscd: code, + const response = await axios + .get( + getFullURL( + '/uapi/domestic-stock/v1/quotations/inquire-index-timeprice', + ), + { + headers: getHeader(accessToken, 'FHPUP02110200'), + params: { + fid_input_hour_1: 300, + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }, }, - }, - ); + ) + .catch((err) => { + throw new InternalServerErrorException( + '주가 지수 차트 정보를 조회하지 못했습니다.', + ); + }); return response.data; } @@ -70,16 +72,22 @@ export class StockIndexService { code: string, accessToken: string, ) { - const response = await axios.get( - getFullURL('/uapi/domestic-stock/v1/quotations/inquire-index-price'), - { - headers: getHeader(accessToken, 'FHPUP02100000'), - params: { - fid_cond_mrkt_div_code: 'U', - fid_input_iscd: code, + const response = await axios + .get( + getFullURL('/uapi/domestic-stock/v1/quotations/inquire-index-price'), + { + headers: getHeader(accessToken, 'FHPUP02100000'), + params: { + fid_cond_mrkt_div_code: 'U', + fid_input_iscd: code, + }, }, - }, - ); + ) + .catch((err) => { + throw new InternalServerErrorException( + '주가 지수 값 정보를 조회하지 못했습니다.', + ); + }); return response.data; } From 462c8a189747ec8e15cb25eec87b8e4936543db3 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Fri, 8 Nov 2024 11:42:36 +0900 Subject: [PATCH 009/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20getAccessToken=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface/korea.investment.interface.ts | 6 ++++++ BE/src/koreaInvestment/korea.investment.service.ts | 7 ++++--- BE/src/stock/index/stock.index.service.ts | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 BE/src/koreaInvestment/interface/korea.investment.interface.ts diff --git a/BE/src/koreaInvestment/interface/korea.investment.interface.ts b/BE/src/koreaInvestment/interface/korea.investment.interface.ts new file mode 100644 index 00000000..e290a32c --- /dev/null +++ b/BE/src/koreaInvestment/interface/korea.investment.interface.ts @@ -0,0 +1,6 @@ +export interface AccessTokenInterface { + access_token: string; + access_token_token_expired: string; + token_type: string; + expires_in: number; +} diff --git a/BE/src/koreaInvestment/korea.investment.service.ts b/BE/src/koreaInvestment/korea.investment.service.ts index 886b68cb..727c044c 100644 --- a/BE/src/koreaInvestment/korea.investment.service.ts +++ b/BE/src/koreaInvestment/korea.investment.service.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import { UnauthorizedException } from '@nestjs/common'; import { getFullURL } from '../util/getFullURL'; +import { AccessTokenInterface } from './interface/korea.investment.interface'; export class KoreaInvestmentService { private accessToken: string; @@ -12,19 +13,19 @@ export class KoreaInvestmentService { return this.accessToken; } const response = await axios - .post(getFullURL('/oauth2/tokenP'), { + .post(getFullURL('/oauth2/tokenP'), { grant_type: 'client_credentials', appkey: process.env.KOREA_INVESTMENT_APP_KEY, appsecret: process.env.KOREA_INVESTMENT_APP_SECRET, }) - .catch((err) => { + .catch(() => { throw new UnauthorizedException('액세스 토큰을 조회하지 못했습니다.'); }); const { data } = response; this.accessToken = data.access_token; - this.tokenExpireTime = new Date(Date.now() + +data.expires_in); + this.tokenExpireTime = new Date(data.access_token_token_expired); return this.accessToken; } diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock.index.service.ts index 3a14c7fc..587af43d 100644 --- a/BE/src/stock/index/stock.index.service.ts +++ b/BE/src/stock/index/stock.index.service.ts @@ -59,7 +59,7 @@ export class StockIndexService { }, }, ) - .catch((err) => { + .catch(() => { throw new InternalServerErrorException( '주가 지수 차트 정보를 조회하지 못했습니다.', ); @@ -83,7 +83,7 @@ export class StockIndexService { }, }, ) - .catch((err) => { + .catch(() => { throw new InternalServerErrorException( '주가 지수 값 정보를 조회하지 못했습니다.', ); From f210aa0780a7722ab4b74e8d7ec552810fb61fb1 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Sun, 10 Nov 2024 00:35:32 +0900 Subject: [PATCH 010/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=B0=A8=ED=8A=B8?= =?UTF-8?q?=20response=EC=97=90=20=EC=A0=84=EC=9D=BC=20=EB=8C=80=EB=B9=84?= =?UTF-8?q?=20=EA=B0=92=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/index/dto/stock.index.list.chart.element.dto.ts | 6 +++++- BE/src/stock/index/stock.index.service.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts b/BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts index 1f2abdce..ba6ce1f2 100644 --- a/BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts +++ b/BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts @@ -1,9 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; export class StockIndexListChartElementDto { - constructor(time: string, value: string) { + constructor(time: string, value: string, diff: string) { this.time = time; this.value = value; + this.diff = diff; } @ApiProperty({ description: 'HHMMSS', example: '130500' }) @@ -11,4 +12,7 @@ export class StockIndexListChartElementDto { @ApiProperty({ description: '주가 지수' }) value: string; + + @ApiProperty({ description: '전일 대비 주가 지수' }) + diff: string; } diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock.index.service.ts index 587af43d..f15326f5 100644 --- a/BE/src/stock/index/stock.index.service.ts +++ b/BE/src/stock/index/stock.index.service.ts @@ -21,6 +21,7 @@ export class StockIndexService { return new StockIndexListChartElementDto( element.bsop_hour, element.bstp_nmix_prpr, + element.bstp_nmix_prdy_vrss, ); }); } From 19fccb72de3a981e3a60a3724245163d04397953 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 11 Nov 2024 10:57:13 +0900 Subject: [PATCH 011/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20?= =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20=ED=8C=8C=EC=9D=BC=EB=AA=85=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 6 +++--- BE/src/auth/auth.controller.ts | 2 +- BE/src/auth/auth.service.ts | 2 +- ...{authCredentials.dto.ts => auth-credentials.dto.ts} | 0 BE/src/auth/user.repository.ts | 2 +- ...ment.interface.ts => korea-investment.interface.ts} | 0 ...investment.module.ts => korea-investment.module.ts} | 2 +- ...vestment.service.ts => korea-investment.service.ts} | 4 ++-- BE/src/stock/enum/{MarketType.ts => market-type.ts} | 0 ...nt.dto.ts => stock-index-list-chart.element.dto.ts} | 0 ...ment.dto.ts => stock-index-response-element.dto.ts} | 4 ++-- ...dex.response.dto.ts => stock-index-response.dto.ts} | 2 +- ...element.dto.ts => stock-index-value-element.dto.ts} | 0 ...ock.index.interface.ts => stock-index.interface.ts} | 0 ...k.index.controller.ts => stock-index.controller.ts} | 6 +++--- .../{stock.index.module.ts => stock-index.module.ts} | 6 +++--- .../{stock.index.service.ts => stock-index.service.ts} | 10 +++++----- ...topfive.interface.ts => stock-topfive.interface.ts} | 0 ...pfive.controller.ts => stock-topfive.controller.ts} | 4 ++-- ...stock.topfive.module.ts => stock-topfive.module.ts} | 6 +++--- ...ock.topfive.service.ts => stock-topfive.service.ts} | 10 +++++----- BE/src/util/{getFullURL.ts => get-full-URL.ts} | 0 BE/src/util/{getHeader.ts => get-header.ts} | 0 BE/src/websocket/socket.service.ts | 4 ++-- BE/test/stock/index/stock.index.list.e2e-spec.ts | 2 +- BE/test/stock/index/stock.index.value.e2e-spec.ts | 2 +- 26 files changed, 37 insertions(+), 37 deletions(-) rename BE/src/auth/dto/{authCredentials.dto.ts => auth-credentials.dto.ts} (100%) rename BE/src/koreaInvestment/interface/{korea.investment.interface.ts => korea-investment.interface.ts} (100%) rename BE/src/koreaInvestment/{korea.investment.module.ts => korea-investment.module.ts} (76%) rename BE/src/koreaInvestment/{korea.investment.service.ts => korea-investment.service.ts} (89%) rename BE/src/stock/enum/{MarketType.ts => market-type.ts} (100%) rename BE/src/stock/index/dto/{stock.index.list.chart.element.dto.ts => stock-index-list-chart.element.dto.ts} (100%) rename BE/src/stock/index/dto/{stock.index.response.element.dto.ts => stock-index-response-element.dto.ts} (71%) rename BE/src/stock/index/dto/{stock.index.response.dto.ts => stock-index-response.dto.ts} (88%) rename BE/src/stock/index/dto/{stock.index.value.element.dto.ts => stock-index-value-element.dto.ts} (100%) rename BE/src/stock/index/interface/{stock.index.interface.ts => stock-index.interface.ts} (100%) rename BE/src/stock/index/{stock.index.controller.ts => stock-index.controller.ts} (96%) rename BE/src/stock/index/{stock.index.module.ts => stock-index.module.ts} (71%) rename BE/src/stock/index/{stock.index.service.ts => stock-index.service.ts} (87%) rename BE/src/stock/topfive/interface/{stock.topfive.interface.ts => stock-topfive.interface.ts} (100%) rename BE/src/stock/topfive/{stock.topfive.controller.ts => stock-topfive.controller.ts} (88%) rename BE/src/stock/topfive/{stock.topfive.module.ts => stock-topfive.module.ts} (68%) rename BE/src/stock/topfive/{stock.topfive.service.ts => stock-topfive.service.ts} (95%) rename BE/src/util/{getFullURL.ts => get-full-URL.ts} (100%) rename BE/src/util/{getHeader.ts => get-header.ts} (100%) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index fbef051f..114ecadf 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -6,9 +6,9 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { User } from './auth/user.entity'; -import { StockIndexModule } from './stock/index/stock.index.module'; -import { StockTopfiveModule } from './stock/topfive/stock.topfive.module'; -import { KoreaInvestmentModule } from './koreaInvestment/korea.investment.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'; @Module({ diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 8411eb48..aee104f0 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -10,7 +10,7 @@ import { import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; import { AuthService } from './auth.service'; -import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; @Controller('auth') export class AuthController { diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index cbc4a69f..6f751e1b 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { UserRepository } from './user.repository'; -import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; @Injectable() export class AuthService { diff --git a/BE/src/auth/dto/authCredentials.dto.ts b/BE/src/auth/dto/auth-credentials.dto.ts similarity index 100% rename from BE/src/auth/dto/authCredentials.dto.ts rename to BE/src/auth/dto/auth-credentials.dto.ts diff --git a/BE/src/auth/user.repository.ts b/BE/src/auth/user.repository.ts index 0a23f980..429437e7 100644 --- a/BE/src/auth/user.repository.ts +++ b/BE/src/auth/user.repository.ts @@ -3,7 +3,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { User } from './user.entity'; -import { AuthCredentialsDto } from './dto/authCredentials.dto'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; @Injectable() export class UserRepository extends Repository { diff --git a/BE/src/koreaInvestment/interface/korea.investment.interface.ts b/BE/src/koreaInvestment/interface/korea-investment.interface.ts similarity index 100% rename from BE/src/koreaInvestment/interface/korea.investment.interface.ts rename to BE/src/koreaInvestment/interface/korea-investment.interface.ts diff --git a/BE/src/koreaInvestment/korea.investment.module.ts b/BE/src/koreaInvestment/korea-investment.module.ts similarity index 76% rename from BE/src/koreaInvestment/korea.investment.module.ts rename to BE/src/koreaInvestment/korea-investment.module.ts index 69679c6b..8e7dd10a 100644 --- a/BE/src/koreaInvestment/korea.investment.module.ts +++ b/BE/src/koreaInvestment/korea-investment.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { KoreaInvestmentService } from './korea.investment.service'; +import { KoreaInvestmentService } from './korea-investment.service'; @Module({ imports: [], diff --git a/BE/src/koreaInvestment/korea.investment.service.ts b/BE/src/koreaInvestment/korea-investment.service.ts similarity index 89% rename from BE/src/koreaInvestment/korea.investment.service.ts rename to BE/src/koreaInvestment/korea-investment.service.ts index 727c044c..29dc323c 100644 --- a/BE/src/koreaInvestment/korea.investment.service.ts +++ b/BE/src/koreaInvestment/korea-investment.service.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { UnauthorizedException } from '@nestjs/common'; -import { getFullURL } from '../util/getFullURL'; -import { AccessTokenInterface } from './interface/korea.investment.interface'; +import { getFullURL } from '../util/get-full-URL'; +import { AccessTokenInterface } from './interface/korea-investment.interface'; export class KoreaInvestmentService { private accessToken: string; diff --git a/BE/src/stock/enum/MarketType.ts b/BE/src/stock/enum/market-type.ts similarity index 100% rename from BE/src/stock/enum/MarketType.ts rename to BE/src/stock/enum/market-type.ts diff --git a/BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts b/BE/src/stock/index/dto/stock-index-list-chart.element.dto.ts similarity index 100% rename from BE/src/stock/index/dto/stock.index.list.chart.element.dto.ts rename to BE/src/stock/index/dto/stock-index-list-chart.element.dto.ts diff --git a/BE/src/stock/index/dto/stock.index.response.element.dto.ts b/BE/src/stock/index/dto/stock-index-response-element.dto.ts similarity index 71% rename from BE/src/stock/index/dto/stock.index.response.element.dto.ts rename to BE/src/stock/index/dto/stock-index-response-element.dto.ts index 09e21102..79cf6af5 100644 --- a/BE/src/stock/index/dto/stock.index.response.element.dto.ts +++ b/BE/src/stock/index/dto/stock-index-response-element.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { StockIndexValueElementDto } from './stock.index.value.element.dto'; -import { StockIndexListChartElementDto } from './stock.index.list.chart.element.dto'; +import { StockIndexValueElementDto } from './stock-index-value-element.dto'; +import { StockIndexListChartElementDto } from './stock-index-list-chart.element.dto'; export class StockIndexResponseElementDto { @ApiProperty({ description: '실시간 값', type: StockIndexValueElementDto }) diff --git a/BE/src/stock/index/dto/stock.index.response.dto.ts b/BE/src/stock/index/dto/stock-index-response.dto.ts similarity index 88% rename from BE/src/stock/index/dto/stock.index.response.dto.ts rename to BE/src/stock/index/dto/stock-index-response.dto.ts index 7f26af32..026fecf3 100644 --- a/BE/src/stock/index/dto/stock.index.response.dto.ts +++ b/BE/src/stock/index/dto/stock-index-response.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { StockIndexResponseElementDto } from './stock.index.response.element.dto'; +import { StockIndexResponseElementDto } from './stock-index-response-element.dto'; export class StockIndexResponseDto { @ApiProperty({ diff --git a/BE/src/stock/index/dto/stock.index.value.element.dto.ts b/BE/src/stock/index/dto/stock-index-value-element.dto.ts similarity index 100% rename from BE/src/stock/index/dto/stock.index.value.element.dto.ts rename to BE/src/stock/index/dto/stock-index-value-element.dto.ts diff --git a/BE/src/stock/index/interface/stock.index.interface.ts b/BE/src/stock/index/interface/stock-index.interface.ts similarity index 100% rename from BE/src/stock/index/interface/stock.index.interface.ts rename to BE/src/stock/index/interface/stock-index.interface.ts diff --git a/BE/src/stock/index/stock.index.controller.ts b/BE/src/stock/index/stock-index.controller.ts similarity index 96% rename from BE/src/stock/index/stock.index.controller.ts rename to BE/src/stock/index/stock-index.controller.ts index 7625e044..330685e2 100644 --- a/BE/src/stock/index/stock.index.controller.ts +++ b/BE/src/stock/index/stock-index.controller.ts @@ -1,9 +1,9 @@ import { Controller, Get } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Cron } from '@nestjs/schedule'; -import { StockIndexService } from './stock.index.service'; -import { StockIndexResponseDto } from './dto/stock.index.response.dto'; -import { KoreaInvestmentService } from '../../koreaInvestment/korea.investment.service'; +import { StockIndexService } from './stock-index.service'; +import { StockIndexResponseDto } from './dto/stock-index-response.dto'; +import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; import { SocketGateway } from '../../websocket/socket.gateway'; @Controller('/api/stocks/index') diff --git a/BE/src/stock/index/stock.index.module.ts b/BE/src/stock/index/stock-index.module.ts similarity index 71% rename from BE/src/stock/index/stock.index.module.ts rename to BE/src/stock/index/stock-index.module.ts index 1227e4f7..64e0c575 100644 --- a/BE/src/stock/index/stock.index.module.ts +++ b/BE/src/stock/index/stock-index.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { StockIndexController } from './stock.index.controller'; -import { StockIndexService } from './stock.index.service'; -import { KoreaInvestmentModule } from '../../koreaInvestment/korea.investment.module'; +import { StockIndexController } from './stock-index.controller'; +import { StockIndexService } from './stock-index.service'; +import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; import { SocketModule } from '../../websocket/socket.module'; @Module({ diff --git a/BE/src/stock/index/stock.index.service.ts b/BE/src/stock/index/stock-index.service.ts similarity index 87% rename from BE/src/stock/index/stock.index.service.ts rename to BE/src/stock/index/stock-index.service.ts index f15326f5..fa05ed4c 100644 --- a/BE/src/stock/index/stock.index.service.ts +++ b/BE/src/stock/index/stock-index.service.ts @@ -1,13 +1,13 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import axios from 'axios'; -import { StockIndexListChartElementDto } from './dto/stock.index.list.chart.element.dto'; -import { StockIndexValueElementDto } from './dto/stock.index.value.element.dto'; +import { StockIndexListChartElementDto } from './dto/stock-index-list-chart.element.dto'; +import { StockIndexValueElementDto } from './dto/stock-index-value-element.dto'; import { StockIndexChartInterface, StockIndexValueInterface, -} from './interface/stock.index.interface'; -import { getFullURL } from '../../util/getFullURL'; -import { getHeader } from '../../util/getHeader'; +} from './interface/stock-index.interface'; +import { getFullURL } from '../../util/get-full-URL'; +import { getHeader } from '../../util/get-header'; @Injectable() export class StockIndexService { diff --git a/BE/src/stock/topfive/interface/stock.topfive.interface.ts b/BE/src/stock/topfive/interface/stock-topfive.interface.ts similarity index 100% rename from BE/src/stock/topfive/interface/stock.topfive.interface.ts rename to BE/src/stock/topfive/interface/stock-topfive.interface.ts diff --git a/BE/src/stock/topfive/stock.topfive.controller.ts b/BE/src/stock/topfive/stock-topfive.controller.ts similarity index 88% rename from BE/src/stock/topfive/stock.topfive.controller.ts rename to BE/src/stock/topfive/stock-topfive.controller.ts index f90d300a..bac275e3 100644 --- a/BE/src/stock/topfive/stock.topfive.controller.ts +++ b/BE/src/stock/topfive/stock-topfive.controller.ts @@ -1,8 +1,8 @@ import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; import { Controller, Get, Query } from '@nestjs/common'; -import { StockTopfiveService } from './stock.topfive.service'; +import { StockTopfiveService } from './stock-topfive.service'; import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; -import { MarketType } from '../enum/MarketType'; +import { MarketType } from '../enum/market-type'; @Controller('/api/stocks') export class StockTopfiveController { diff --git a/BE/src/stock/topfive/stock.topfive.module.ts b/BE/src/stock/topfive/stock-topfive.module.ts similarity index 68% rename from BE/src/stock/topfive/stock.topfive.module.ts rename to BE/src/stock/topfive/stock-topfive.module.ts index 33a63aae..5be62a4f 100644 --- a/BE/src/stock/topfive/stock.topfive.module.ts +++ b/BE/src/stock/topfive/stock-topfive.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { StockTopfiveController } from './stock.topfive.controller'; -import { StockTopfiveService } from './stock.topfive.service'; -import { KoreaInvestmentModule } from '../../koreaInvestment/korea.investment.module'; +import { StockTopfiveController } from './stock-topfive.controller'; +import { StockTopfiveService } from './stock-topfive.service'; +import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; @Module({ imports: [ConfigModule, KoreaInvestmentModule], diff --git a/BE/src/stock/topfive/stock.topfive.service.ts b/BE/src/stock/topfive/stock-topfive.service.ts similarity index 95% rename from BE/src/stock/topfive/stock.topfive.service.ts rename to BE/src/stock/topfive/stock-topfive.service.ts index d8da1643..82f659d5 100644 --- a/BE/src/stock/topfive/stock.topfive.service.ts +++ b/BE/src/stock/topfive/stock-topfive.service.ts @@ -3,14 +3,14 @@ import { Injectable, Logger } from '@nestjs/common'; import { StockRankingQueryParameterDto } from './dto/stock-ranking-request.dto'; import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; import { StockRankingDataDto } from './dto/stock-ranking-data.dto'; -import { MarketType } from '../enum/MarketType'; +import { MarketType } from '../enum/market-type'; import { StockApiOutputData, StockApiResponse, -} from './interface/stock.topfive.interface'; -import { getHeader } from '../../util/getHeader'; -import { getFullURL } from '../../util/getFullURL'; -import { KoreaInvestmentService } from '../../koreaInvestment/korea.investment.service'; +} from './interface/stock-topfive.interface'; +import { getHeader } from '../../util/get-header'; +import { getFullURL } from '../../util/get-full-URL'; +import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; @Injectable() export class StockTopfiveService { diff --git a/BE/src/util/getFullURL.ts b/BE/src/util/get-full-URL.ts similarity index 100% rename from BE/src/util/getFullURL.ts rename to BE/src/util/get-full-URL.ts diff --git a/BE/src/util/getHeader.ts b/BE/src/util/get-header.ts similarity index 100% rename from BE/src/util/getHeader.ts rename to BE/src/util/get-header.ts diff --git a/BE/src/websocket/socket.service.ts b/BE/src/websocket/socket.service.ts index 042cd57a..0bcb7b64 100644 --- a/BE/src/websocket/socket.service.ts +++ b/BE/src/websocket/socket.service.ts @@ -2,9 +2,9 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { WebSocket } from 'ws'; import axios from 'axios'; import { SocketGateway } from './socket.gateway'; -import { StockIndexValueElementDto } from '../stock/index/dto/stock.index.value.element.dto'; +import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; import { SocketConnectTokenInterface } from './interface/socket.interface'; -import { getFullURL } from '../util/getFullURL'; +import { getFullURL } from '../util/get-full-URL'; @Injectable() export class SocketService implements OnModuleInit { diff --git a/BE/test/stock/index/stock.index.list.e2e-spec.ts b/BE/test/stock/index/stock.index.list.e2e-spec.ts index 7518b75d..9a844581 100644 --- a/BE/test/stock/index/stock.index.list.e2e-spec.ts +++ b/BE/test/stock/index/stock.index.list.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import axios from 'axios'; -import { StockIndexService } from '../../../src/stock/index/stock.index.service'; +import { StockIndexService } from '../../../src/stock/index/stock-index.service'; import { STOCK_INDEX_LIST_MOCK } from './mockdata/stock.index.list.mockdata'; jest.mock('axios'); diff --git a/BE/test/stock/index/stock.index.value.e2e-spec.ts b/BE/test/stock/index/stock.index.value.e2e-spec.ts index 12ff2590..2008f2e4 100644 --- a/BE/test/stock/index/stock.index.value.e2e-spec.ts +++ b/BE/test/stock/index/stock.index.value.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import axios from 'axios'; -import { StockIndexService } from '../../../src/stock/index/stock.index.service'; +import { StockIndexService } from '../../../src/stock/index/stock-index.service'; import { STOCK_INDEX_VALUE_MOCK } from './mockdata/stock.index.value.mockdata'; jest.mock('axios'); From b8720660909c042b2b2ad0f69bad3c6634a98772 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 13:00:24 +0900 Subject: [PATCH 012/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20lint?= =?UTF-8?q?=20=EC=97=90=20=EC=9C=84=EB=B0=B0=EB=90=98=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/package-lock.json | 3 ++ BE/package.json | 1 + BE/src/app.module.ts | 1 - BE/src/auth/auth.controller.ts | 16 +++++---- BE/src/auth/auth.module.ts | 4 +-- BE/src/auth/auth.service.ts | 41 +++++++++++----------- BE/src/auth/dto/auth-credentials.dto.ts | 2 -- BE/src/auth/strategy/kakao.strategy.ts | 45 +++++++++++++++++++------ BE/src/auth/user.entity.ts | 2 ++ BE/src/configs/typeorm.config.ts | 2 +- BE/src/main.ts | 2 +- 11 files changed, 75 insertions(+), 44 deletions(-) diff --git a/BE/package-lock.json b/BE/package-lock.json index 5cdd9bb9..f9b6300f 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -30,6 +30,7 @@ "cross-env": "^7.0.3", "docker": "^1.0.0", "dotenv": "^16.4.5", + "express": "^4.21.1", "fastify-swagger": "^5.1.1", "mysql2": "^3.11.3", "passport": "^0.7.0", @@ -5133,6 +5134,8 @@ }, "node_modules/express": { "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", diff --git a/BE/package.json b/BE/package.json index 52432b78..86c3538b 100644 --- a/BE/package.json +++ b/BE/package.json @@ -41,6 +41,7 @@ "cross-env": "^7.0.3", "docker": "^1.0.0", "dotenv": "^16.4.5", + "express": "^4.21.1", "fastify-swagger": "^5.1.1", "mysql2": "^3.11.3", "passport": "^0.7.0", diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 639c4d3d..8a1dab3d 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -5,7 +5,6 @@ import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; -import { User } from './auth/user.entity'; import { StockIndexModule } from './stock/index/stock-index.module'; import { StockTopfiveModule } from './stock/topfive/stock-topfive.module'; import { KoreaInvestmentModule } from './koreaInvestment/korea-investment.module'; diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index d88c7c39..631aa0e2 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -10,10 +10,10 @@ import { } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; -import { AuthService } from './auth.service'; -import { AuthCredentialsDto } from './dto/auth-credentials.dto'; import { Request, Response } from 'express'; import { ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; +import { AuthCredentialsDto } from './dto/auth-credentials.dto'; @Controller('auth') export class AuthController { @@ -62,13 +62,15 @@ export class AuthController { @ApiOperation({ summary: 'Refresh Token 요청 API' }) @Get('/refresh') async refresh(@Req() req: Request, @Res() res: Response) { - const refreshToken = req.cookies['refreshToken']; - const accessToken = req.cookies['accessToken']; - - if (!refreshToken || !accessToken) { - return res.status(401).send(); + if ( + typeof req.cookies.refreshToken !== 'string' || + typeof req.cookies.accessToken !== 'string' + ) { + return res.status(400).send(); } + const { refreshToken } = req.cookies; + const newAccessToken = await this.authService.refreshToken(refreshToken); res.cookie('accessToken', newAccessToken, { httpOnly: true }); diff --git a/BE/src/auth/auth.module.ts b/BE/src/auth/auth.module.ts index 87a398ba..522e5199 100644 --- a/BE/src/auth/auth.module.ts +++ b/BE/src/auth/auth.module.ts @@ -2,12 +2,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { User } from './user.entity'; import { UserRepository } from './user.repository'; import { JwtStrategy } from './strategy/jwt.strategy'; -import { ConfigModule, ConfigService } from '@nestjs/config'; import { KakaoStrategy } from './strategy/kakao.strategy'; @Module({ @@ -17,7 +17,7 @@ import { KakaoStrategy } from './strategy/kakao.strategy'; PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.registerAsync({ imports: [ConfigModule], - useFactory: async (configService: ConfigService) => ({ + useFactory: (configService: ConfigService) => ({ secret: configService.get('JWT_SECRET'), signOptions: { expiresIn: configService.get('JWT_ACCESS_EXPIRATION_TIME'), diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 1b913fdf..2de79cd8 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -2,9 +2,9 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; +import { ConfigService } from '@nestjs/config'; import { UserRepository } from './user.repository'; import { AuthCredentialsDto } from './dto/auth-credentials.dto'; -import { ConfigService } from '@nestjs/config'; @Injectable() export class AuthService { @@ -29,7 +29,7 @@ export class AuthService { const { accessToken, refreshToken } = await this.getJWTToken(authCredentialsDto); - this.setCurrentRefreshToken(refreshToken, user.id); + await this.setCurrentRefreshToken(refreshToken, user.id); return { accessToken, refreshToken }; } @@ -39,8 +39,9 @@ export class AuthService { async kakaoLoginUser( authCredentialsDto: AuthCredentialsDto, ): Promise<{ accessToken: string; refreshToken: string }> { - return await this.getJWTToken(authCredentialsDto); + return this.getJWTToken(authCredentialsDto); } + async getJWTToken(authCredentialsDto: AuthCredentialsDto) { const accessToken = await this.generateAccessToken(authCredentialsDto); const refreshToken = await this.generateRefreshToken(authCredentialsDto); @@ -51,15 +52,15 @@ export class AuthService { authCredentialsDto: AuthCredentialsDto, ): Promise { return authCredentialsDto.email - ? this.jwtService.sign({ email: authCredentialsDto.email }) - : this.jwtService.sign({ kakaoId: authCredentialsDto.kakaoId }); + ? this.jwtService.signAsync({ email: authCredentialsDto.email }) + : this.jwtService.signAsync({ kakaoId: authCredentialsDto.kakaoId }); } async generateRefreshToken( authCredentialsDto: AuthCredentialsDto, ): Promise { if (authCredentialsDto.email) { - return this.jwtService.sign( + return this.jwtService.signAsync( { email: authCredentialsDto.email }, { secret: this.configService.get('JWT_REFRESH_SECRET'), @@ -68,17 +69,16 @@ export class AuthService { ), }, ); - } else { - return this.jwtService.sign( - { kakaoId: authCredentialsDto.kakaoId }, - { - secret: this.configService.get('JWT_REFRESH_SECRET'), - expiresIn: this.configService.get( - 'JWT_REFRESH_EXPIRATION_TIME', - ), - }, - ); } + return this.jwtService.signAsync( + { kakaoId: authCredentialsDto.kakaoId }, + { + secret: this.configService.get('JWT_REFRESH_SECRET'), + expiresIn: this.configService.get( + 'JWT_REFRESH_EXPIRATION_TIME', + ), + }, + ); } async setCurrentRefreshToken(refreshToken: string, userId: number) { @@ -87,10 +87,13 @@ export class AuthService { const currentRefreshToken = await bcrypt.hash(refreshToken, salt); const currentRefreshTokenExpiresAt = new Date( currentDate.getTime() + - parseInt(this.configService.get('JWT_REFRESH_EXPIRATION_TIME')), + parseInt( + this.configService.get('JWT_REFRESH_EXPIRATION_TIME'), + 10, + ), ); - this.userRepository.update(userId, { + await this.userRepository.update(userId, { currentRefreshToken, currentRefreshTokenExpiresAt, }); @@ -120,7 +123,7 @@ export class AuthService { } const accessToken = this.generateAccessToken(user.toAuthCredentialsDto()); - return accessToken; + return await accessToken; } catch (error) { throw new UnauthorizedException('Invalid Token'); } diff --git a/BE/src/auth/dto/auth-credentials.dto.ts b/BE/src/auth/dto/auth-credentials.dto.ts index 07332ed8..31bc69b6 100644 --- a/BE/src/auth/dto/auth-credentials.dto.ts +++ b/BE/src/auth/dto/auth-credentials.dto.ts @@ -3,8 +3,6 @@ import { Matches, MaxLength, MinLength, - IsEmail, - ValidateNested, IsOptional, } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; diff --git a/BE/src/auth/strategy/kakao.strategy.ts b/BE/src/auth/strategy/kakao.strategy.ts index f47b3eeb..e69805dc 100644 --- a/BE/src/auth/strategy/kakao.strategy.ts +++ b/BE/src/auth/strategy/kakao.strategy.ts @@ -3,30 +3,53 @@ import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { Profile, Strategy } from 'passport-kakao'; +interface KakaoStrategyOptions { + clientID: string; + clientSecret: string; + callbackURL: string; +} + +interface KakaoProfile extends Profile { + id: number; + _json: { + id: number; + }; +} + +interface KakaoUser { + kakaoId: number; +} + @Injectable() -export class KakaoStrategy extends PassportStrategy(Strategy) { +export class KakaoStrategy extends PassportStrategy( + Strategy, + 'kakao', +) { constructor(private readonly configService: ConfigService) { - super({ - clientID: configService.get('KAKAO_CLIENT_ID'), + const options: KakaoStrategyOptions = { + clientID: configService.get('KAKAO_CLIENT_ID') || '', clientSecret: '', - callbackURL: `${configService.get('BACKEND_URL')}/auth/kakao`, - }); + callbackURL: `${configService.get('BACKEND_URL') || ''}/auth/kakao`, + }; + + super(options); } - async validate( + validate( accessToken: string, refreshToken: string, - profile: Profile, - done: (error: any, user?: any, info?: any) => void, + profile: KakaoProfile, + done: (error: Error, user?: KakaoUser) => void, ) { try { - const { _json } = profile; + // eslint-disable-next-line no-underscore-dangle + const kakaoId = profile._json.id; const user = { - kakaoId: _json.id, + kakaoId, }; done(null, user); } catch (error) { - done(error); + done(error instanceof Error ? error : new Error(String(error))); } } } diff --git a/BE/src/auth/user.entity.ts b/BE/src/auth/user.entity.ts index e052393a..8c574cd4 100644 --- a/BE/src/auth/user.entity.ts +++ b/BE/src/auth/user.entity.ts @@ -31,5 +31,7 @@ export class User extends BaseEntity { password: this.password, }; } + + throw new Error('Cannot convert Kakao user to auth credentials'); } } diff --git a/BE/src/configs/typeorm.config.ts b/BE/src/configs/typeorm.config.ts index dd42cdbe..c10d56ef 100644 --- a/BE/src/configs/typeorm.config.ts +++ b/BE/src/configs/typeorm.config.ts @@ -10,6 +10,6 @@ export const typeOrmConfig: TypeOrmModuleOptions = { username: process.env.DB_USERNAME, password: process.env.DB_PASSWD, database: process.env.DB_DATABASE, - entities: [__dirname + '/../**/*.entity{.js,.ts}'], + entities: [`${__dirname}/../**/*.entity{.js,.ts}`], synchronize: true, }; diff --git a/BE/src/main.ts b/BE/src/main.ts index 37ceb7cf..bc11d55f 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -1,8 +1,8 @@ import { NestFactory } from '@nestjs/core'; import { Logger } from '@nestjs/common'; +import * as cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; import { setupSwagger } from './util/swagger'; -import * as cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(AppModule); From afc77fbdda087353cddee53e110f15a599cda62b Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 14:15:10 +0900 Subject: [PATCH 013/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20API=20=EC=9A=94=EC=B2=AD=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20=EC=9D=91=EB=8B=B5=EA=B0=92=20interface?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface/stock-detail.interface.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 BE/src/stock/detail/interface/stock-detail.interface.ts diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail.interface.ts new file mode 100644 index 00000000..00eb2045 --- /dev/null +++ b/BE/src/stock/detail/interface/stock-detail.interface.ts @@ -0,0 +1,85 @@ +export interface InquirePriceOutputData { + iscd_stat_cls_code: string; + marg_rate: string; + rprs_mrkt_kor_name: string; + new_hgpr_lwpr_cls_code: string; + btsp_kor_isnm: string; + temp_stop_yn: string; + oprc_rang_cont_yn: string; + clpr_rang_cont_yn: string; + crdt_able_yn: string; + grmn_rate_cls_code: string; + elw_pblc_yn: string; + stck_prpr: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + acml_tr_pbmn: string; + acml_vol: string; + prdy_vrss_vol_rate: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + stck_mxpr: string; + stck_llam: string; + stck_sdpr: string; + wghn_avrg_stck_prc: string; + hts_frgn_ehrt: string; + frgn_ntby_qty: string; + pgtr_ntby_qty: string; + dmrs_val: string; + dmsp_val: string; + cpfn: string; + rstc_wdth_prc: string; + stck_fcam: string; + stck_sspr: string; + aspr_unit: string; + hts_deal_qty_unit_val: string; + lstn_stcn: string; + hts_avls: string; + per: string; + pbr: string; + stac_month: string; + vol_tnrt: string; + eps: string; + bps: string; + d250_hgpr: string; + d250_hgpr_date: string; + d250_hgpr_vrss_prpr_rate: string; + d250_lwpr: string; + d250_lwpr_date: string; + d250_lwpr_vrss_prpr_rate: string; + stck_dryy_hgpr: string; + dryy_hgpr_vrss_prpr_rate: string; + dryy_hgpr_date: string; + stck_dryy_lwpr: string; + dryy_lwpr_vrss_prpr_rate: string; + dryy_lwpr_date: string; + w52_hgpr: string; + w52_hgpr_vrss_prpr_ctrt: string; + w52_hgpr_date: string; + w52_lwpr: string; + w52_lwpr_vrss_prpr_ctrt: string; + w52_lwpr_date: string; + whol_loan_rmnd_rate: string; + ssts_yn: string; + stck_shrn_iscd: string; + fcam_cnnm: string; + cpfn_cnnm: string; + apprch_rate: string; + frgn_hldn_qty: string; + vi_cls_code: string; + ovtm_vi_cls_code: string; + last_ssts_cntg_qty: string; + invt_caful_yn: string; + mrkt_warn_cls_code: string; + short_over_yn: string; + sltr_yn: string; +} + +export interface InquirePriceApiResponse { + output: InquirePriceOutputData[]; + rt_cd: string; + msg_cd: string; + msg1: string; +} From 1a48a8b83aca488979f4a9aed07fb0fd0093d79a Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 14:15:29 +0900 Subject: [PATCH 014/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20API=20=EC=9A=94=EC=B2=AD=EC=97=90?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BF=BC=EB=A6=AC=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20DTO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/detail/dto/stock-detail-request.dto.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 BE/src/stock/detail/dto/stock-detail-request.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-request.dto.ts new file mode 100644 index 00000000..46392101 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-request.dto.ts @@ -0,0 +1,16 @@ +/** + * 주식 현재가 시세 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO + */ +export class StockDetailQueryParameterDto { + /** + * 조건 시장 분류 코드 + * 'J' 주식 + */ + fid_cond_mrkt_div_code: string; + + /** + * 주식 종목 코드 + * (ex) 005930 + */ + fid_input_iscd: string; +} From 3053dd2df0249186ff07f76b037141c5c87ef0d6 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 14:17:45 +0900 Subject: [PATCH 015/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 62 +++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 BE/src/stock/detail/stock-detail.service.ts diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts new file mode 100644 index 00000000..ff7a5864 --- /dev/null +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -0,0 +1,62 @@ +import axios from 'axios'; +import { Injectable, Logger } from '@nestjs/common'; +import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; +import { getHeader } from '../../util/get-header'; +import { getFullURL } from '../../util/get-full-URL'; +import { StockDetailQueryParameterDto } from './dto/stock-detail-request.dto'; +import { InquirePriceApiResponse } from './interface/stock-detail.interface'; + +@Injectable() +export class StockDetailService { + private readonly logger = new Logger(); + + constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + + /** + * @private 한국투자 Open API - [국내주식] 기본시세 - 주식현재가 시세 호출 함수 + * @param {StockDetailQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - 주식현재가 시세 데이터 + * + * @author uuuo3o + */ + private async requestApi(queryParams: StockDetailQueryParameterDto) { + try { + const accessToken = await this.koreaInvetmentService.getAccessToken(); + const headers = getHeader(accessToken, 'FHKST01010100'); + const url = getFullURL( + '/uapi/domestic-stock/v1/quotations/inquire-price', + ); + const params = this.getInquirePriceParams(queryParams); + + const response = await axios.get(url, { + headers, + params, + }); + + return response.data; + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, + message: error.message, + }); + throw error; + } + } + + /** + * @private 주식현재가 시세 요청을 위한 쿼리 파라미터 객체 생성 함수 + * @param {StockDetailQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO + * @returns - API 요청에 필요한 쿼리 파라미터 객체 + * + * @author uuuo3o + */ + private getInquirePriceParams(params: StockDetailQueryParameterDto) { + return { + fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, + fid_input_iscd: params.fid_input_iscd, + }; + } +} From 05200cf7a2ce5dd293eb224b29b762251ea238b7 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 14:57:29 +0900 Subject: [PATCH 016/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20output=EC=97=90?= =?UTF-8?q?=20=EB=8C=80=ED=95=9C=20=EC=A0=95=EB=B3=B4=EA=B0=80=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/interface/stock-detail.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail.interface.ts index 00eb2045..9a7dc8c4 100644 --- a/BE/src/stock/detail/interface/stock-detail.interface.ts +++ b/BE/src/stock/detail/interface/stock-detail.interface.ts @@ -78,7 +78,7 @@ export interface InquirePriceOutputData { } export interface InquirePriceApiResponse { - output: InquirePriceOutputData[]; + output: InquirePriceOutputData; rt_cd: string; msg_cd: string; msg1: string; From 35b332dfe8bdaa158bec5ae9989c740427c10e9b Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 15:00:34 +0900 Subject: [PATCH 017/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20API=EB=A1=9C=EB=B6=80=ED=84=B0=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A0=95=EC=A0=9C=20=EB=B0=8F=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/dto/stock-detail-request.dto.ts | 5 ++ BE/src/stock/detail/stock-detail.service.ts | 52 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-request.dto.ts index 46392101..3f5655d6 100644 --- a/BE/src/stock/detail/dto/stock-detail-request.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-request.dto.ts @@ -13,4 +13,9 @@ export class StockDetailQueryParameterDto { * (ex) 005930 */ fid_input_iscd: string; + + constructor(fid_cond_mrkt_div_code: string, fid_input_iscd: string) { + this.fid_cond_mrkt_div_code = fid_cond_mrkt_div_code; + this.fid_input_iscd = fid_input_iscd; + } } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index ff7a5864..d8e6dbaf 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -4,7 +4,11 @@ import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.s import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; import { StockDetailQueryParameterDto } from './dto/stock-detail-request.dto'; -import { InquirePriceApiResponse } from './interface/stock-detail.interface'; +import { + InquirePriceApiResponse, + InquirePriceOutputData, +} from './interface/stock-detail.interface'; +import { StockDetailDataDto } from './dto/stock-detail-data.dto'; @Injectable() export class StockDetailService { @@ -12,6 +16,32 @@ export class StockDetailService { constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + /** + * 특정 주식의 주식현재가 시세 데이터를 반환하는 함수 + * @param {string} stockCode - 종목코드 + * @returns - 주식현재가 시세 데이터 객체 반환 + * + * @author uuuo3o + */ + async getInquirePrice(stockCode: string) { + try { + const queryParams = new StockDetailQueryParameterDto('J', stockCode); + + const response = await this.requestApi(queryParams); + + return this.formatStockData(response.output); + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + /** * @private 한국투자 Open API - [국내주식] 기본시세 - 주식현재가 시세 호출 함수 * @param {StockDetailQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO @@ -46,6 +76,26 @@ export class StockDetailService { } } + /** + * @private API에서 받은 주식현재가 시세 데이터를 필요한 정보로 정제하는 함수 + * @param {InquirePriceOutputData} stock - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatStockData(stock: InquirePriceOutputData) { + const stockData = new StockDetailDataDto(); + stockData.stck_shrn_iscd = stock.stck_shrn_iscd; + stockData.stck_prpr = stock.stck_prpr; + stockData.prdy_vrss = stock.prdy_vrss; + stockData.prdy_vrss_sign = stock.prdy_vrss_sign; + stockData.prdy_ctrt = stock.prdy_ctrt; + stockData.hts_avls = stock.hts_avls; + stockData.per = stock.per; + + return stockData; + } + /** * @private 주식현재가 시세 요청을 위한 쿼리 파라미터 객체 생성 함수 * @param {StockDetailQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO From 08bb77d92b86e2b50ec04a623e22384d2cf2e0fb Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 15:00:56 +0900 Subject: [PATCH 018/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=A0=95?= =?UTF-8?q?=20=EC=A3=BC=EC=8B=9D=EC=9D=98=20=EC=A3=BC=EC=8B=9D=ED=98=84?= =?UTF-8?q?=EC=9E=AC=EA=B0=80=20=EC=8B=9C=EC=84=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8A=94=20con?= =?UTF-8?q?troller=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 ++ .../stock/detail/stock-detail.controller.ts | 27 +++++++++++++++++++ BE/src/stock/detail/stock-detail.module.ts | 11 ++++++++ 3 files changed, 40 insertions(+) create mode 100644 BE/src/stock/detail/stock-detail.controller.ts create mode 100644 BE/src/stock/detail/stock-detail.module.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 114ecadf..4773fba1 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -10,6 +10,7 @@ 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 { StockDetailModule } from './stock/detail/stock-detail.module'; @Module({ imports: [ @@ -30,6 +31,7 @@ import { SocketModule } from './websocket/socket.module'; StockIndexModule, StockTopfiveModule, SocketModule, + StockDetailModule, ], controllers: [AppController], providers: [AppService], diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts new file mode 100644 index 00000000..346e13b7 --- /dev/null +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { StockDetailService } from './stock-detail.service'; +import { StockDetailDataDto } from './dto/stock-detail-data.dto'; + +@Controller('/api/stocks') +export class StockDetailController { + constructor(private readonly stockDetailService: StockDetailService) {} + + @Get(':stockCode') + @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: '단일 주식 종목 기본값 조회 성공', + type: StockDetailDataDto, + }) + getStockDetail(@Param('stockCode') stockCode: string) { + return this.stockDetailService.getInquirePrice(stockCode); + } +} diff --git a/BE/src/stock/detail/stock-detail.module.ts b/BE/src/stock/detail/stock-detail.module.ts new file mode 100644 index 00000000..cfb2b57b --- /dev/null +++ b/BE/src/stock/detail/stock-detail.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; +import { StockDetailController } from './stock-detail.controller'; +import { StockDetailService } from './stock-detail.service'; + +@Module({ + imports: [KoreaInvestmentModule], + controllers: [StockDetailController], + providers: [StockDetailService], +}) +export class StockDetailModule {} From 1c7c1e954524544547a1ba09e6ed49d0ba31fd36 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 15:01:55 +0900 Subject: [PATCH 019/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20alpha?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EB=B0=B0=ED=8F=AC=EC=99=80=20production?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=20CI/CD=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{deploy.yml => deploy-production.yml} | 49 ++++---- .github/workflows/deply-alpha.yml | 119 ++++++++++++++++++ FE/Dockerfile | 17 +++ FE/tsconfig.app.tsbuildinfo | 1 + FE/tsconfig.node.tsbuildinfo | 1 + FE/vite.config.ts | 1 + 6 files changed, 167 insertions(+), 21 deletions(-) rename .github/workflows/{deploy.yml => deploy-production.yml} (65%) create mode 100644 .github/workflows/deply-alpha.yml create mode 100644 FE/Dockerfile create mode 100644 FE/tsconfig.app.tsbuildinfo create mode 100644 FE/tsconfig.node.tsbuildinfo diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy-production.yml similarity index 65% rename from .github/workflows/deploy.yml rename to .github/workflows/deploy-production.yml index f1ca50c3..cf0c3cd9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy-production.yml @@ -2,9 +2,9 @@ name: deploy on: push: - branches: [main, alpha] + branches: [main] pull_request: - branches: [main, alpha] + branches: [main] env: DOCKER_IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/juga-docker @@ -13,6 +13,13 @@ env: jobs: build-and-deploy: runs-on: ubuntu-latest + strategy: + matrix: + app: + [ + { name: 'be', dir: 'BE', port: 3000, container: 'juga-docker-be' }, + { name: 'fe', dir: 'FE', port: 5173, container: 'juga-docker-fe' }, + ] steps: - uses: actions/checkout@v3 @@ -22,29 +29,29 @@ jobs: with: node-version: '20' cache: 'npm' - cache-dependency-path: ./BE/package-lock.json + cache-dependency-path: ./${{matrix.app.dir}}/package-lock.json - name: Create .env file run: | - touch ./BE/.env - echo "${{ secrets.ENV }}" > ./BE/.env + touch ./${{ matrix.app.dir }}/.env + echo "${{ secrets.ENV }}" > ./${{matrix.app.dir}}/.env - name: Install dependencies - working-directory: ./BE + working-directory: ./${{matrix.app.dir}} run: npm ci - name: Run tests - working-directory: ./BE + working-directory: ./${{matrix.app.dir}} run: npm test env: CI: true - name: Run linter - working-directory: ./BE + working-directory: ./${{matrix.app.dir}} run: npm run lint - name: Build application - working-directory: ./BE + working-directory: ./${{matrix.app.dir}} run: npm run build - name: Login to Docker Hub @@ -54,16 +61,16 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Docker Image - working-directory: ./BE + working-directory: ./${{ matrix.app.dir }} env: NCP_ACCESS_KEY: ${{ secrets.NCP_ACCESS_KEY }} NCP_SECRET_KEY: ${{ secrets.NCP_SECRET_KEY }} run: | - docker build -t ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }} . - docker tag ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }} ${{ env.DOCKER_IMAGE }}:latest + docker build -t ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} . + docker tag ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:latest - docker push ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }} - docker push ${{ env.DOCKER_IMAGE }}:latest + docker push ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + docker push ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:latest - name: Get Github Actions IP id: ip @@ -95,15 +102,15 @@ jobs: script: | docker system prune -af echo "${{ secrets.ENV }}" > .env - - docker pull ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }} - docker stop juga-docker || true - docker rm juga-docker || true + + docker pull ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + docker stop ${{ matrix.app.container }} || true + docker rm ${{ matrix.app.container }} || true docker run -d \ - --name juga-docker \ - -p 3000:3000 \ + --name ${{ matrix.app.container }} \ + -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ --env-file .env \ - ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }} + ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} - name: Remove Github Action Ip to Security group run: | diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml new file mode 100644 index 00000000..155e349b --- /dev/null +++ b/.github/workflows/deply-alpha.yml @@ -0,0 +1,119 @@ +name: deploy + +on: + push: + branches: [alpha] + pull_request: + branches: [alpha] + +env: + DOCKER_IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/juga-docker + DOCKER_TAG: ${{ github.sha }} + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + strategy: + matrix: + app: + [ + { name: 'be', dir: 'BE', port: 3000, container: 'juga-docker-be' }, + { name: 'fe', dir: 'FE', port: 5173, container: 'juga-docker-fe' }, + ] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: ./${{matrix.app.dir}}/package-lock.json + + - name: Create .env file + run: | + touch ./${{ matrix.app.dir }}/.env + echo "${{ secrets.ENV }}" > ./${{matrix.app.dir}}/.env + + - name: Install dependencies + working-directory: ./${{matrix.app.dir}} + run: npm ci + + - name: Run tests + working-directory: ./${{matrix.app.dir}} + run: npm test + env: + CI: true + + - name: Run linter + working-directory: ./${{matrix.app.dir}} + run: npm run lint + + - name: Build application + working-directory: ./${{matrix.app.dir}} + run: npm run build + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and Push Docker Image + working-directory: ./${{ matrix.app.dir }} + env: + NCP_ACCESS_KEY: ${{ secrets.NCP_ACCESS_KEY }} + NCP_SECRET_KEY: ${{ secrets.NCP_SECRET_KEY }} + run: | + docker build -t ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} . + docker tag ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:latest + + docker push ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + docker push ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:latest + + - name: Get Github Actions IP + id: ip + uses: haythem/public-ip@v1.2 + + - name: Setting NCP CLI & Credentials + run: | + cd ~ + wget https://www.ncloud.com/api/support/download/5/65 + unzip 65 + mkdir ~/.ncloud + echo -e "[DEFAULT]\nncloud_access_key_id = ${{ secrets.NCP_ACCESS_KEY }}\nncloud_secret_access_key = ${{ secrets.NCP_SECRET_KEY }}\nncloud_api_url = ${{ secrets.NCP_API_URI }}" >> ~/.ncloud/configure + + - name: Add Github Action Ip to Security group + run: | + cd ~ + ls -la + chmod -R 777 ~/cli_linux + cd ~/cli_linux + ./ncloud vserver addAccessControlGroupInboundRule --regionCode KR --vpcNo ${{ secrets.NCP_VPC_ID }} --accessControlGroupNo ${{ secrets.NCP_ACG_ID }} --accessControlGroupRuleList "protocolTypeCode='TCP', ipBlock='${{ steps.ip.outputs.ipv4 }}/32', portRange='${{ secrets.SSH_PORT }}'" + + - name: Deploy to NCP Server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.NCP_ALPHA_SERVER_HOST }} + username: ${{ secrets.NCP_ALPHA_SERVER_USERNAME }} + key: ${{ secrets.NCP_ALPHA_SERVER_SSH_KEY }} + port: 22 + script: | + docker system prune -af + echo "${{ secrets.ENV }}" > .env + + docker pull ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + docker stop ${{ matrix.app.container }} || true + docker rm ${{ matrix.app.container }} || true + docker run -d \ + --name ${{ matrix.app.container }} \ + -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ + --env-file .env \ + ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + + - name: Remove Github Action Ip to Security group + run: | + chmod -R 777 ~/cli_linux + cd ~/cli_linux + ./ncloud vserver removeAccessControlGroupInboundRule --regionCode KR --vpcNo ${{ secrets.NCP_VPC_ID }} --accessControlGroupNo ${{ secrets.NCP_ACG_ID }} --accessControlGroupRuleList "protocolTypeCode='TCP', ipBlock='${{ steps.ip.outputs.ipv4 }}/32', portRange='${{ secrets.SSH_PORT }}'" diff --git a/FE/Dockerfile b/FE/Dockerfile new file mode 100644 index 00000000..6773e6a4 --- /dev/null +++ b/FE/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20 + +RUN mkdir -p /var/app +WORKDIR /var/app + +COPY package*.json ./ +RUN npm install + +COPY . . + +RUN npm run build + +RUN npm install -g serve + +EXPOSE 5173 + +CMD ["serve", "-s", "dist", "-l", "5173"] \ No newline at end of file diff --git a/FE/tsconfig.app.tsbuildinfo b/FE/tsconfig.app.tsbuildinfo new file mode 100644 index 00000000..bbaf2949 --- /dev/null +++ b/FE/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/header.tsx","./src/page/home.tsx"],"version":"5.6.3"} \ No newline at end of file diff --git a/FE/tsconfig.node.tsbuildinfo b/FE/tsconfig.node.tsbuildinfo new file mode 100644 index 00000000..75ea0011 --- /dev/null +++ b/FE/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"version":"5.6.3"} \ No newline at end of file diff --git a/FE/vite.config.ts b/FE/vite.config.ts index 539e2463..be05188f 100644 --- a/FE/vite.config.ts +++ b/FE/vite.config.ts @@ -6,6 +6,7 @@ import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ plugins: [react(), tsconfigPaths()], server: { + host: true, open: true, }, }); From 697eb50bad0611fe013ab4848dcdf499dc29287c Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 11 Nov 2024 15:03:35 +0900 Subject: [PATCH 020/158] =?UTF-8?q?=E2=9C=A8=20feat:=20jwt=20authGuard=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 --- BE/src/auth/jwt-auth-guard.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 BE/src/auth/jwt-auth-guard.ts diff --git a/BE/src/auth/jwt-auth-guard.ts b/BE/src/auth/jwt-auth-guard.ts new file mode 100644 index 00000000..0aa58fd8 --- /dev/null +++ b/BE/src/auth/jwt-auth-guard.ts @@ -0,0 +1,5 @@ +import { AuthGuard } from '@nestjs/passport'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') {} From e1d5582f19f6ebf9317e88b2dace8629a4742324 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 11 Nov 2024 15:04:16 +0900 Subject: [PATCH 021/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EB=A7=A4=EC=88=98=20API=20=EA=B5=AC=ED=98=84=20#49?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 5 +- .../order/dto/stock-order-request.dto.ts | 19 ++++++++ BE/src/stock/order/enum/status-type.ts | 4 ++ BE/src/stock/order/enum/trade-type.ts | 4 ++ .../order/interface/request.interface.ts | 9 ++++ BE/src/stock/order/stock-order.controller.ts | 42 +++++++++++++++++ BE/src/stock/order/stock-order.entity.ts | 46 +++++++++++++++++++ BE/src/stock/order/stock-order.module.ts | 13 ++++++ BE/src/stock/order/stock-order.repository.ts | 11 +++++ BE/src/stock/order/stock-order.service.ts | 23 ++++++++++ BE/src/util/swagger.ts | 1 + 11 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 BE/src/stock/order/dto/stock-order-request.dto.ts create mode 100644 BE/src/stock/order/enum/status-type.ts create mode 100644 BE/src/stock/order/enum/trade-type.ts create mode 100644 BE/src/stock/order/interface/request.interface.ts create mode 100644 BE/src/stock/order/stock-order.controller.ts create mode 100644 BE/src/stock/order/stock-order.entity.ts create mode 100644 BE/src/stock/order/stock-order.module.ts create mode 100644 BE/src/stock/order/stock-order.repository.ts create mode 100644 BE/src/stock/order/stock-order.service.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 114ecadf..dee9d8fa 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -10,6 +10,8 @@ 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 { StockOrderModule } from './stock/order/stock-order.module'; +import { Order } from './stock/order/stock-order.entity'; @Module({ imports: [ @@ -22,7 +24,7 @@ import { SocketModule } from './websocket/socket.module'; username: process.env.DB_USERNAME, password: process.env.DB_PASSWD, database: process.env.DB_DATABASE, - entities: [User], + entities: [User, Order], synchronize: true, }), KoreaInvestmentModule, @@ -30,6 +32,7 @@ import { SocketModule } from './websocket/socket.module'; StockIndexModule, StockTopfiveModule, SocketModule, + StockOrderModule, ], controllers: [AppController], providers: [AppService], diff --git a/BE/src/stock/order/dto/stock-order-request.dto.ts b/BE/src/stock/order/dto/stock-order-request.dto.ts new file mode 100644 index 00000000..b06a3dcb --- /dev/null +++ b/BE/src/stock/order/dto/stock-order-request.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsNumber, IsPositive } from 'class-validator'; + +export class StockOrderRequestDto { + @ApiProperty({ description: '주식 id' }) + @IsInt() + @IsPositive() + stock_id: number; + + @ApiProperty({ description: '매수/매도 희망 가격' }) + @IsNumber() + @IsPositive() + price: number; + + @ApiProperty({ description: '매수/매도 희망 수량' }) + @IsInt() + @IsPositive() + amount: number; +} diff --git a/BE/src/stock/order/enum/status-type.ts b/BE/src/stock/order/enum/status-type.ts new file mode 100644 index 00000000..04b3caeb --- /dev/null +++ b/BE/src/stock/order/enum/status-type.ts @@ -0,0 +1,4 @@ +export enum StatusType { + PENDING = 'PENDING', + COMPLETE = 'COMPLETE', +} diff --git a/BE/src/stock/order/enum/trade-type.ts b/BE/src/stock/order/enum/trade-type.ts new file mode 100644 index 00000000..c07270e3 --- /dev/null +++ b/BE/src/stock/order/enum/trade-type.ts @@ -0,0 +1,4 @@ +export enum TradeType { + SELL = 'SELL', + BUY = 'BUY', +} diff --git a/BE/src/stock/order/interface/request.interface.ts b/BE/src/stock/order/interface/request.interface.ts new file mode 100644 index 00000000..d7616da0 --- /dev/null +++ b/BE/src/stock/order/interface/request.interface.ts @@ -0,0 +1,9 @@ +export interface RequestInterface { + user: { + id: number; + email: string; + password: string; + tutorial: boolean; + kakaoId: number; + }; +} diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts new file mode 100644 index 00000000..218e5ce2 --- /dev/null +++ b/BE/src/stock/order/stock-order.controller.ts @@ -0,0 +1,42 @@ +import { + Body, + Controller, + Post, + Req, + UseGuards, + ValidationPipe, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; +import { StockOrderService } from './stock-order.service'; +import { StockOrderRequestDto } from './dto/stock-order-request.dto'; +import { JwtAuthGuard } from '../../auth/jwt-auth-guard'; +import { RequestInterface } from './interface/request.interface'; + +@Controller('/api/stocks/trade') +@ApiTags('주식 매수/매도 API') +export class StockOrderController { + constructor(private readonly stockTradeService: StockOrderService) {} + + @Post('/buy') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '주식 매수 API', + description: '주식 id, 매수 가격, 수량으로 주식을 매수한다.', + }) + @ApiResponse({ + status: 201, + description: '주식 매수 성공', + }) + async buy( + @Req() request: RequestInterface, + @Body(ValidationPipe) stockOrderRequest: StockOrderRequestDto, + ) { + await this.stockTradeService.buy(request.user.id, stockOrderRequest); + } +} diff --git a/BE/src/stock/order/stock-order.entity.ts b/BE/src/stock/order/stock-order.entity.ts new file mode 100644 index 00000000..a88de180 --- /dev/null +++ b/BE/src/stock/order/stock-order.entity.ts @@ -0,0 +1,46 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { TradeType } from './enum/trade-type'; +import { StatusType } from './enum/status-type'; + +@Entity('orders') +export class Order { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: false }) + user_id: number; + + @Column({ nullable: false }) + stock_id: number; + + @Column({ + type: 'enum', + enum: TradeType, + nullable: false, + }) + trade_type: TradeType; + + @Column({ nullable: false }) + amount: number; + + @Column({ nullable: false }) + price: number; + + @Column({ + type: 'enum', + enum: StatusType, + nullable: false, + }) + status: StatusType; + + @CreateDateColumn() + created_at: Date; + + @Column({ nullable: true }) + completed_at?: Date; +} diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts new file mode 100644 index 00000000..b5c54337 --- /dev/null +++ b/BE/src/stock/order/stock-order.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StockOrderController } from './stock-order.controller'; +import { StockOrderService } from './stock-order.service'; +import { Order } from './stock-order.entity'; +import { StockOrderRepository } from './stock-order.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([Order])], + controllers: [StockOrderController], + providers: [StockOrderService, StockOrderRepository], +}) +export class StockOrderModule {} diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts new file mode 100644 index 00000000..cec0080a --- /dev/null +++ b/BE/src/stock/order/stock-order.repository.ts @@ -0,0 +1,11 @@ +import { DataSource, Repository } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; +import { Order } from './stock-order.entity'; + +@Injectable() +export class StockOrderRepository extends Repository { + constructor(@InjectDataSource() dataSource: DataSource) { + super(Order, dataSource.createEntityManager()); + } +} diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts new file mode 100644 index 00000000..a8b771f1 --- /dev/null +++ b/BE/src/stock/order/stock-order.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { StockOrderRequestDto } from './dto/stock-order-request.dto'; +import { StockOrderRepository } from './stock-order.repository'; +import { TradeType } from './enum/trade-type'; +import { StatusType } from './enum/status-type'; + +@Injectable() +export class StockOrderService { + constructor(private readonly stockOrderRepository: StockOrderRepository) {} + + async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { + const order = this.stockOrderRepository.create({ + user_id: userId, + stock_id: stockOrderRequest.stock_id, + trade_type: TradeType.BUY, + amount: stockOrderRequest.amount, + price: stockOrderRequest.price, + status: StatusType.PENDING, + }); + + await this.stockOrderRepository.save(order); + } +} diff --git a/BE/src/util/swagger.ts b/BE/src/util/swagger.ts index 44de0c96..e820d85e 100644 --- a/BE/src/util/swagger.ts +++ b/BE/src/util/swagger.ts @@ -13,6 +13,7 @@ export function setupSwagger(app: INestApplication): void { .setTitle('Juga API') .setDescription('Juga API 문서입니다.') .setVersion('1.0.0') + .addBearerAuth() .build(); const document = SwaggerModule.createDocument(app, options); From 54218604bd896b7890411ce819e83d8973325f4a Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 11 Nov 2024 15:19:15 +0900 Subject: [PATCH 022/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EB=A7=A4=EB=8F=84=20API=20=EA=B5=AC=ED=98=84=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.controller.ts | 20 +++++++++++++++++++- BE/src/stock/order/stock-order.service.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index 218e5ce2..e7696bb4 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -31,7 +31,7 @@ export class StockOrderController { }) @ApiResponse({ status: 201, - description: '주식 매수 성공', + description: '주식 매수 예약 등록 성공', }) async buy( @Req() request: RequestInterface, @@ -39,4 +39,22 @@ export class StockOrderController { ) { await this.stockTradeService.buy(request.user.id, stockOrderRequest); } + + @Post('/sell') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '주식 매도 API', + description: '주식 id, 매도 가격, 수량으로 주식을 매도한다.', + }) + @ApiResponse({ + status: 201, + description: '주식 매도 예약 등록 성공', + }) + async sell( + @Req() request: RequestInterface, + @Body(ValidationPipe) stockOrderRequest: StockOrderRequestDto, + ) { + await this.stockTradeService.sell(request.user.id, stockOrderRequest); + } } diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index a8b771f1..a5c2cbba 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -20,4 +20,17 @@ export class StockOrderService { await this.stockOrderRepository.save(order); } + + async sell(userId: number, stockOrderRequest: StockOrderRequestDto) { + const order = this.stockOrderRepository.create({ + user_id: userId, + stock_id: stockOrderRequest.stock_id, + trade_type: TradeType.SELL, + amount: stockOrderRequest.amount, + price: stockOrderRequest.price, + status: StatusType.PENDING, + }); + + await this.stockOrderRepository.save(order); + } } From bbcb40ea467711c6c65c000be71aee0b5b89ff22 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 15:24:03 +0900 Subject: [PATCH 023/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20FE=20l?= =?UTF-8?q?int=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/src/components/TopFive/Nav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FE/src/components/TopFive/Nav.tsx b/FE/src/components/TopFive/Nav.tsx index 941c8249..5fc2bef3 100644 --- a/FE/src/components/TopFive/Nav.tsx +++ b/FE/src/components/TopFive/Nav.tsx @@ -43,7 +43,7 @@ export default function Nav() { key={market} ref={(el) => (buttonRefs.current[index] = el)} onClick={() => handleMarketChange(market)} - className={`relative px-2 py-2`} + className={'relative px-2 py-2'} > {market} From 59ecabb10fca05c92a6105237a8759cd69d1122b Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 15:28:18 +0900 Subject: [PATCH 024/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=8A=B9=EC=A0=95?= =?UTF-8?q?=20=EC=A3=BC=EC=8B=9D=EC=9D=98=20=EC=A3=BC=EC=8B=9D=ED=98=84?= =?UTF-8?q?=EC=9E=AC=EA=B0=80=20=EC=8B=9C=EC=84=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=B0=98=ED=99=98=20DTO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/detail/dto/stock-detail-data.dto.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 BE/src/stock/detail/dto/stock-detail-data.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-data.dto.ts b/BE/src/stock/detail/dto/stock-detail-data.dto.ts new file mode 100644 index 00000000..0ecaba3e --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-data.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockDetailDataDto { + @ApiProperty({ description: 'HTS 한글 종목명' }) + hts_kor_isnm: string; + + @ApiProperty({ description: '종목코드' }) + stck_shrn_iscd: string; + + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; + + @ApiProperty({ description: 'HTS 시가총액' }) + hts_avls: string; + + @ApiProperty({ description: 'per' }) + per: string; +} From c1ff7ee752f696f674707a31900680dc59f3d3aa Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 15:31:06 +0900 Subject: [PATCH 025/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20=EB=A6=B0?= =?UTF-8?q?=ED=8A=B8=20=EA=B7=9C=EC=B9=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/.eslintrc.js b/BE/.eslintrc.js index 0cea1c7c..b54cc5a8 100644 --- a/BE/.eslintrc.js +++ b/BE/.eslintrc.js @@ -34,5 +34,6 @@ module.exports = { 'class-methods-use-this': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/naming-convention': 'off', }, }; From 2298435c4c013ef9fee379d5783237c25510b4e7 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Mon, 11 Nov 2024 16:05:40 +0900 Subject: [PATCH 026/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EB=A7=A4=EB=8F=84/=EB=A7=A4=EC=88=98=20=EC=B7=A8=EC=86=8C?= =?UTF-8?q?=20API=20=EA=B5=AC=ED=98=84=20#51=20#52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.controller.ts | 20 +++++++++++++++++++ BE/src/stock/order/stock-order.service.ts | 21 +++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index e7696bb4..82d72083 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, + Delete, + Param, Post, Req, UseGuards, @@ -57,4 +59,22 @@ export class StockOrderController { ) { await this.stockTradeService.sell(request.user.id, stockOrderRequest); } + + @Delete('/:order_id') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '주식 매도/매수 취소 API', + description: '주문 id로 미체결된 주문을 취소한다.', + }) + @ApiResponse({ + status: 200, + description: '주식 매도/매수 취소 성공', + }) + async cancel( + @Req() request: RequestInterface, + @Param('order_id') orderId: number, + ) { + await this.stockTradeService.cancel(request.user.id, orderId); + } } diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index a5c2cbba..997de306 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -1,4 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { + ConflictException, + ForbiddenException, + Injectable, +} from '@nestjs/common'; +import { NotFoundError } from 'rxjs'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; @@ -33,4 +38,18 @@ export class StockOrderService { await this.stockOrderRepository.save(order); } + + async cancel(userId: number, orderId: number) { + const order = await this.stockOrderRepository.findOneBy({ id: orderId }); + + if (!order) throw new NotFoundError('주문을 찾을 수 없습니다.'); + + if (order.user_id !== userId) + throw new ForbiddenException('다른 사용자의 주문은 취소할 수 없습니다.'); + + if (order.status === StatusType.COMPLETE) + throw new ConflictException('이미 체결된 주문은 취소할 수 없습니다.'); + + await this.stockOrderRepository.remove(order); + } } From b760a202d76abfefffd029c5483addbd367f3d1e Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 16:20:48 +0900 Subject: [PATCH 027/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EA=B5=AD?= =?UTF-8?q?=EB=82=B4=EC=A3=BC=EC=8B=9D=EA=B8=B0=EA=B0=84=EB=B3=84=EC=8B=9C?= =?UTF-8?q?=EC=84=B8=20API=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20interf?= =?UTF-8?q?ace=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface/stock-detail.interface.ts | 103 +++++++----------- 1 file changed, 37 insertions(+), 66 deletions(-) diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail.interface.ts index 9a7dc8c4..1169eb90 100644 --- a/BE/src/stock/detail/interface/stock-detail.interface.ts +++ b/BE/src/stock/detail/interface/stock-detail.interface.ts @@ -1,84 +1,55 @@ -export interface InquirePriceOutputData { - iscd_stat_cls_code: string; - marg_rate: string; - rprs_mrkt_kor_name: string; - new_hgpr_lwpr_cls_code: string; - btsp_kor_isnm: string; - temp_stop_yn: string; - oprc_rang_cont_yn: string; - clpr_rang_cont_yn: string; - crdt_able_yn: string; - grmn_rate_cls_code: string; - elw_pblc_yn: string; - stck_prpr: string; +export interface InquirePriceOutput1Data { prdy_vrss: string; prdy_vrss_sign: string; prdy_ctrt: string; - acml_tr_pbmn: string; + stck_prdy_clpr: string; acml_vol: string; - prdy_vrss_vol_rate: string; + acml_tr_pbmn: string; + hts_kor_isnm: string; + stck_prpr: string; + stck_shrn_iscd: string; + prdy_vol: string; + stck_mxpr: string; + stck_llam: string; stck_oprc: string; stck_hgpr: string; stck_lwpr: string; - stck_mxpr: string; - stck_llam: string; - stck_sdpr: string; - wghn_avrg_stck_prc: string; - hts_frgn_ehrt: string; - frgn_ntby_qty: string; - pgtr_ntby_qty: string; - dmrs_val: string; - dmsp_val: string; - cpfn: string; - rstc_wdth_prc: string; + stck_prdy_oprc: string; + stck_prdy_hgpr: string; + stck_prdy_lwpr: string; + askp: string; + bidp: string; + prdy_vrss_vol: string; + vol_tnrt: string; stck_fcam: string; - stck_sspr: string; - aspr_unit: string; - hts_deal_qty_unit_val: string; lstn_stcn: string; + cpfn: string; hts_avls: string; per: string; - pbr: string; - stac_month: string; - vol_tnrt: string; eps: string; - bps: string; - d250_hgpr: string; - d250_hgpr_date: string; - d250_hgpr_vrss_prpr_rate: string; - d250_lwpr: string; - d250_lwpr_date: string; - d250_lwpr_vrss_prpr_rate: string; - stck_dryy_hgpr: string; - dryy_hgpr_vrss_prpr_rate: string; - dryy_hgpr_date: string; - stck_dryy_lwpr: string; - dryy_lwpr_vrss_prpr_rate: string; - dryy_lwpr_date: string; - w52_hgpr: string; - w52_hgpr_vrss_prpr_ctrt: string; - w52_hgpr_date: string; - w52_lwpr: string; - w52_lwpr_vrss_prpr_ctrt: string; - w52_lwpr_date: string; - whol_loan_rmnd_rate: string; - ssts_yn: string; - stck_shrn_iscd: string; - fcam_cnnm: string; - cpfn_cnnm: string; - apprch_rate: string; - frgn_hldn_qty: string; - vi_cls_code: string; - ovtm_vi_cls_code: string; - last_ssts_cntg_qty: string; - invt_caful_yn: string; - mrkt_warn_cls_code: string; - short_over_yn: string; - sltr_yn: string; + pbr: string; + itewhol_loan_rmnd_ratem_name: string; +} + +export interface InquirePriceOutput2Data { + stck_bsop_date: string; + stck_clpr: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + acml_vol: string; + acml_tr_pbmn: string; + flng_cls_code: string; + prtt_rate: string; + mod_yn: string; + prdy_vrss_sign: string; + prdy_vrss: string; + revl_issu_reas: string; } export interface InquirePriceApiResponse { - output: InquirePriceOutputData; + output1: InquirePriceOutput1Data; + output2: InquirePriceOutput2Data[]; rt_cd: string; msg_cd: string; msg1: string; From 7f20998350f66ddfe93943cfdc20d0696a766aa7 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 16:39:30 +0900 Subject: [PATCH 028/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20refreshToken,?= =?UTF-8?q?=20accessToken=20response=20=EB=B0=A9=EC=8B=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 631aa0e2..aa3085dc 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -30,10 +30,17 @@ export class AuthController { @ApiOperation({ summary: '로그인 API' }) @Post('/login') - loginWithCredentials( + async loginWithCredentials( @Body(ValidationPipe) authCredentialsDto: AuthCredentialsDto, + @Res() res: Response, ) { - return this.authService.loginUser(authCredentialsDto); + const { accessToken, refreshToken } = + await this.authService.loginUser(authCredentialsDto); + + res.cookie('refreshToken', refreshToken, { httpOnly: true }); + res.cookie('isRefreshToken', true, { httpOnly: true }); + res.json(accessToken); + return res.redirect(this.configService.get('CLIENT_URL')); } @ApiOperation({ summary: 'Token 인증 테스트 API' }) @@ -54,9 +61,8 @@ export class AuthController { await this.authService.kakaoLoginUser(authCredentialsDto); res.cookie('refreshToken', refreshToken, { httpOnly: true }); - res.cookie('accessToken', accessToken, { httpOnly: true }); res.cookie('isRefreshToken', true, { httpOnly: true }); - return res.redirect(this.configService.get('CLIENT_URL')); + return res.status(200).json({ accessToken }); } @ApiOperation({ summary: 'Refresh Token 요청 API' }) From e1dda3fb46ced9779e9ae21029ff906a016c787e Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 16:48:54 +0900 Subject: [PATCH 029/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20jwt=20secret?= =?UTF-8?q?=20.env=EC=97=90=EC=84=9C=20=EC=9D=BD=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20refresh=20API=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 10 +++++----- BE/src/auth/strategy/jwt.strategy.ts | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index aa3085dc..684384be 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -7,6 +7,7 @@ import { UseGuards, Req, Res, + UnauthorizedException, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; @@ -39,8 +40,7 @@ export class AuthController { res.cookie('refreshToken', refreshToken, { httpOnly: true }); res.cookie('isRefreshToken', true, { httpOnly: true }); - res.json(accessToken); - return res.redirect(this.configService.get('CLIENT_URL')); + return res.status(200).json({ accessToken }); } @ApiOperation({ summary: 'Token 인증 테스트 API' }) @@ -72,15 +72,15 @@ export class AuthController { typeof req.cookies.refreshToken !== 'string' || typeof req.cookies.accessToken !== 'string' ) { - return res.status(400).send(); + throw new UnauthorizedException('Invalid refresh token'); } const { refreshToken } = req.cookies; const newAccessToken = await this.authService.refreshToken(refreshToken); - res.cookie('accessToken', newAccessToken, { httpOnly: true }); + res.cookie('refreshToken', refreshToken, { httpOnly: true }); res.cookie('isRefreshToken', true, { httpOnly: true }); - return res.status(200).send(); + return res.status(200).json({ accessToken: newAccessToken }); } } diff --git a/BE/src/auth/strategy/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts index b60a0596..a6393996 100644 --- a/BE/src/auth/strategy/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -4,14 +4,16 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { UserRepository } from '../user.repository'; import { User } from '../user.entity'; +import { ConfigService } from '@nestjs/config'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( @InjectRepository(UserRepository) private userRepository: UserRepository, + private readonly configService: ConfigService, ) { super({ - secretOrKey: 'Juga16', + secretOrKey: configService.get('JWT_SECRET'), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), }); } From 1c163ea419199815146205ea1d659ec84cea42d5 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 16:54:16 +0900 Subject: [PATCH 030/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B5=AD=EB=82=B4?= =?UTF-8?q?=EC=A3=BC=EC=8B=9D=EA=B8=B0=EA=B0=84=EB=B3=84=EC=8B=9C=EC=84=B8?= =?UTF-8?q?=20API=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20DTO=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ata.dto.ts => stock-detail-output1.dto.ts} | 4 +- .../detail/dto/stock-detail-output2.dto.ts | 42 +++++++++++++++++++ .../detail/dto/stock-detail-request.dto.ts | 27 ++++++++++-- .../detail/dto/stock-detail-response.dto.ts | 14 +++++++ .../stock/detail/stock-detail.controller.ts | 4 +- 5 files changed, 83 insertions(+), 8 deletions(-) rename BE/src/stock/detail/dto/{stock-detail-data.dto.ts => stock-detail-output1.dto.ts} (88%) create mode 100644 BE/src/stock/detail/dto/stock-detail-output2.dto.ts create mode 100644 BE/src/stock/detail/dto/stock-detail-response.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-data.dto.ts b/BE/src/stock/detail/dto/stock-detail-output1.dto.ts similarity index 88% rename from BE/src/stock/detail/dto/stock-detail-data.dto.ts rename to BE/src/stock/detail/dto/stock-detail-output1.dto.ts index 0ecaba3e..aa911bd1 100644 --- a/BE/src/stock/detail/dto/stock-detail-data.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-output1.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class StockDetailDataDto { +export class InquirePriceOutput1Dto { @ApiProperty({ description: 'HTS 한글 종목명' }) hts_kor_isnm: string; @@ -22,6 +22,6 @@ export class StockDetailDataDto { @ApiProperty({ description: 'HTS 시가총액' }) hts_avls: string; - @ApiProperty({ description: 'per' }) + @ApiProperty({ description: 'PER' }) per: string; } diff --git a/BE/src/stock/detail/dto/stock-detail-output2.dto.ts b/BE/src/stock/detail/dto/stock-detail-output2.dto.ts new file mode 100644 index 00000000..c6df900f --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-output2.dto.ts @@ -0,0 +1,42 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class InquirePriceOutput2Dto { + @ApiProperty({ description: '주식 영업 일자' }) + stck_bsop_date: string; + + @ApiProperty({ description: '주식 종가' }) + stck_clpr: string; + + @ApiProperty({ description: '주식 시가' }) + stck_oprc: string; + + @ApiProperty({ description: '주식 최고가' }) + stck_hgpr: string; + + @ApiProperty({ description: '주식 최저가' }) + stck_lwpr: string; + + @ApiProperty({ description: '누적 거래량' }) + acml_vol: string; + + @ApiProperty({ description: '누적 거래 대금' }) + acml_tr_pbmn: string; + + @ApiProperty({ description: '락 구분 코드' }) + flng_cls_code: string; + + @ApiProperty({ description: '분할 비율' }) + prtt_rate: string; + + @ApiProperty({ description: '분할변경여부' }) + mod_yn: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '재평가사유코드' }) + revl_issu_reas: string; +} diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-request.dto.ts index 3f5655d6..0fae03b9 100644 --- a/BE/src/stock/detail/dto/stock-detail-request.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-request.dto.ts @@ -14,8 +14,27 @@ export class StockDetailQueryParameterDto { */ fid_input_iscd: string; - constructor(fid_cond_mrkt_div_code: string, fid_input_iscd: string) { - this.fid_cond_mrkt_div_code = fid_cond_mrkt_div_code; - this.fid_input_iscd = fid_input_iscd; - } + /** + * 조회 시작일자 + * (ex) 20220501 + */ + fid_input_date_1: string; + + /** + * 조회 종료일자 + * (ex) 20220530 + */ + fid_input_date_2: string; + + /** + * 기간 분류 코드 + * D:일봉, W:주봉, M:월봉, Y:년봉 + */ + fid_period_div_code: string; + + /** + * 수정주가 원주가 가격 여부 + * 0:수정주가 1:원주가 + */ + fid_org_adj_prc: string; } diff --git a/BE/src/stock/detail/dto/stock-detail-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts new file mode 100644 index 00000000..765394f7 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-response.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { InquirePriceOutput1Dto } from './stock-detail-output1.dto'; +import { InquirePriceOutput2Dto } from './stock-detail-output2.dto'; + +/** + * 순위 정렬 후 FE에 보낼 DTO + */ +export class InquirePriceResponseDto { + @ApiProperty({ type: InquirePriceOutput1Dto, description: '상승률 순위' }) + output1: InquirePriceOutput1Dto; + + @ApiProperty({ type: [InquirePriceOutput2Dto], description: '하락률 순위' }) + output2: InquirePriceOutput2Dto[]; +} diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 346e13b7..090d2528 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; -import { StockDetailDataDto } from './dto/stock-detail-data.dto'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; @Controller('/api/stocks') export class StockDetailController { @@ -19,7 +19,7 @@ export class StockDetailController { @ApiResponse({ status: 200, description: '단일 주식 종목 기본값 조회 성공', - type: StockDetailDataDto, + type: InquirePriceResponseDto, }) getStockDetail(@Param('stockCode') stockCode: string) { return this.stockDetailService.getInquirePrice(stockCode); From e8969c7ad3eacccb22ef6f7c6286b5fc7d32a259 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:01:06 +0900 Subject: [PATCH 031/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20lint=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 1 - BE/src/auth/dto/auth-credentials.dto.ts | 17 ----------------- BE/src/auth/strategy/jwt.strategy.ts | 2 +- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 639c4d3d..8a1dab3d 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -5,7 +5,6 @@ import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; -import { User } from './auth/user.entity'; import { StockIndexModule } from './stock/index/stock-index.module'; import { StockTopfiveModule } from './stock/topfive/stock-topfive.module'; import { KoreaInvestmentModule } from './koreaInvestment/korea-investment.module'; diff --git a/BE/src/auth/dto/auth-credentials.dto.ts b/BE/src/auth/dto/auth-credentials.dto.ts index de6420bf..31bc69b6 100644 --- a/BE/src/auth/dto/auth-credentials.dto.ts +++ b/BE/src/auth/dto/auth-credentials.dto.ts @@ -5,7 +5,6 @@ import { MinLength, IsOptional, } from 'class-validator'; -import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class AuthCredentialsDto { @@ -17,20 +16,6 @@ export class AuthCredentialsDto { @ApiProperty({ description: '유저 비밀번호', - minLength: 4, - maxLength: 20, - type: 'string', - }) - @IsString() - @MinLength(4) - @MaxLength(20) - email: string; - - @ApiProperty({ - description: '유저 비밀번호', - minLength: 4, - maxLength: 20, - type: 'string', }) @IsString() @MinLength(4) @@ -53,6 +38,4 @@ export class AuthCredentialsDto { @IsString() @IsOptional() kakaoAccessToken?: string; - @Matches(/^[a-zA-Z0-9]*$/) - password: string; } diff --git a/BE/src/auth/strategy/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts index a6393996..e6e85d05 100644 --- a/BE/src/auth/strategy/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -2,9 +2,9 @@ import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { UserRepository } from '../user.repository'; import { User } from '../user.entity'; -import { ConfigService } from '@nestjs/config'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { From ddbbd79b466bd000a3a92e7adea57331180293bd Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:04:34 +0900 Subject: [PATCH 032/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20lint=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 1 - BE/src/auth/dto/auth-credentials.dto.ts | 17 ----------------- BE/src/auth/strategy/jwt.strategy.ts | 2 +- 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 639c4d3d..8a1dab3d 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -5,7 +5,6 @@ import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; -import { User } from './auth/user.entity'; import { StockIndexModule } from './stock/index/stock-index.module'; import { StockTopfiveModule } from './stock/topfive/stock-topfive.module'; import { KoreaInvestmentModule } from './koreaInvestment/korea-investment.module'; diff --git a/BE/src/auth/dto/auth-credentials.dto.ts b/BE/src/auth/dto/auth-credentials.dto.ts index de6420bf..31bc69b6 100644 --- a/BE/src/auth/dto/auth-credentials.dto.ts +++ b/BE/src/auth/dto/auth-credentials.dto.ts @@ -5,7 +5,6 @@ import { MinLength, IsOptional, } from 'class-validator'; -import { IsString, Matches, MaxLength, MinLength } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class AuthCredentialsDto { @@ -17,20 +16,6 @@ export class AuthCredentialsDto { @ApiProperty({ description: '유저 비밀번호', - minLength: 4, - maxLength: 20, - type: 'string', - }) - @IsString() - @MinLength(4) - @MaxLength(20) - email: string; - - @ApiProperty({ - description: '유저 비밀번호', - minLength: 4, - maxLength: 20, - type: 'string', }) @IsString() @MinLength(4) @@ -53,6 +38,4 @@ export class AuthCredentialsDto { @IsString() @IsOptional() kakaoAccessToken?: string; - @Matches(/^[a-zA-Z0-9]*$/) - password: string; } diff --git a/BE/src/auth/strategy/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts index a6393996..e6e85d05 100644 --- a/BE/src/auth/strategy/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -2,9 +2,9 @@ import { PassportStrategy } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { UserRepository } from '../user.repository'; import { User } from '../user.entity'; -import { ConfigService } from '@nestjs/config'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { From 80dd58304c36cc119845d9f14442d0aa5a375c57 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 17:11:46 +0900 Subject: [PATCH 033/158] =?UTF-8?q?=F0=9F=9A=9A=20rename:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=EC=9D=84=20=EB=AA=85=ED=99=95=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-detail-request.dto.ts => stock-detail-query-parameter.dto.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename BE/src/stock/detail/dto/{stock-detail-request.dto.ts => stock-detail-query-parameter.dto.ts} (100%) diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts similarity index 100% rename from BE/src/stock/detail/dto/stock-detail-request.dto.ts rename to BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts From 14aa4eb65dc69baea6b74d8765579c3f34276378 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:22:38 +0900 Subject: [PATCH 034/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20deploy?= =?UTF-8?q?=20alpha=20=EB=94=94=EB=B2=84=EA=B7=B8=20=EC=BD=94=EB=93=9C=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 --- .github/workflows/deply-alpha.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index 155e349b..b6151cb5 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -24,6 +24,10 @@ jobs: steps: - uses: actions/checkout@v3 + - name: debug matrix + run: | + echo "Matrix app directory: ${{matrix.app.dir}}" + - name: Set up Node.js uses: actions/setup-node@v4 with: From 3f0992717581cd43931f7e8cd0beb12f1f55c73e Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:28:33 +0900 Subject: [PATCH 035/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20npm=20?= =?UTF-8?q?install=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index b6151cb5..deda0f34 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -42,8 +42,9 @@ jobs: - name: Install dependencies working-directory: ./${{matrix.app.dir}} - run: npm ci - + run: | + npm cache clean --force + npm install - name: Run tests working-directory: ./${{matrix.app.dir}} run: npm test From 12abb63fc14c029b5b3d33095b067003d9cded60 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 17:33:24 +0900 Subject: [PATCH 036/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EA=B5=AD=EB=82=B4?= =?UTF-8?q?=EC=A3=BC=EC=8B=9D=EA=B8=B0=EA=B0=84=EB=B3=84=EC=8B=9C=EC=84=B8?= =?UTF-8?q?=20API=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=20service,=20controller=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/dto/stock-detail-request.dto.ts | 17 ++++ .../stock/detail/stock-detail.controller.ts | 30 +++++-- BE/src/stock/detail/stock-detail.service.ts | 78 +++++++++++++------ 3 files changed, 96 insertions(+), 29 deletions(-) create mode 100644 BE/src/stock/detail/dto/stock-detail-request.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-request.dto.ts new file mode 100644 index 00000000..7bd1b1d8 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-request.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; + +/** + * 국내주식기간별시세(일/주/월/년) API를 이용할 때 필요한 요청 데이터를 담고 있는 DTO + */ +export class StockDetailRequestDto { + @ApiProperty({ description: '조회 시작일자 (ex) 20220501' }) + fid_input_date_1: string; + + @ApiProperty({ description: '조회 종료일자 (ex) 20220530' }) + fid_input_date_2: string; + + @ApiProperty({ + description: '기간 분류 코드 (ex) D(일봉) W(주봉) M(월봉) Y(년봉)', + }) + fid_period_div_code: string; +} diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 090d2528..c89a00e0 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,27 +1,45 @@ -import { Controller, Get, Param } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { Body, Controller, Param, Post } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; +import { StockDetailRequestDto } from './dto/stock-detail-request.dto'; import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; @Controller('/api/stocks') export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} - @Get(':stockCode') + @Post(':stockCode') @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) @ApiParam({ name: 'stockCode', required: true, description: - '종목 코드\n' + + '종목 코드\n\n' + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', }) + @ApiBody({ + description: + '주식 상세 조회에 필요한 데이터\n\n' + + 'fid_input_date_1: 조회 시작일자 (ex) 20240505\n\n' + + 'fid_input_date_2: 조회 종료일자 (ex) 20241111\n\n' + + 'fid_period_div_code: 기간 분류 코드 (ex) D(일봉), W(주봉), M(월봉), Y(년봉)', + type: StockDetailRequestDto, + }) @ApiResponse({ status: 200, description: '단일 주식 종목 기본값 조회 성공', type: InquirePriceResponseDto, }) - getStockDetail(@Param('stockCode') stockCode: string) { - return this.stockDetailService.getInquirePrice(stockCode); + getStockDetail( + @Param('stockCode') stockCode: string, + @Body() body: StockDetailRequestDto, + ) { + const { fid_input_date_1, fid_input_date_2, fid_period_div_code } = body; + return this.stockDetailService.getInquirePrice( + stockCode, + fid_input_date_1, + fid_input_date_2, + fid_period_div_code, + ); } } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index d8e6dbaf..de6a4adb 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -3,12 +3,9 @@ import { Injectable, Logger } from '@nestjs/common'; import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; -import { StockDetailQueryParameterDto } from './dto/stock-detail-request.dto'; -import { - InquirePriceApiResponse, - InquirePriceOutputData, -} from './interface/stock-detail.interface'; -import { StockDetailDataDto } from './dto/stock-detail-data.dto'; +import { InquirePriceApiResponse } from './interface/stock-detail.interface'; +import { StockDetailQueryParameterDto } from './dto/stock-detail-query-parameter.dto'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; @Injectable() export class StockDetailService { @@ -17,19 +14,32 @@ export class StockDetailService { constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} /** - * 특정 주식의 주식현재가 시세 데이터를 반환하는 함수 + * 특정 주식의 기간별시세 데이터를 반환하는 함수 * @param {string} stockCode - 종목코드 - * @returns - 주식현재가 시세 데이터 객체 반환 + * @param {string} date1 - 조회 시작일자 + * @param {string} date2 - 조회 종료일자 + * @param {string} periodDivCode - 기간 분류 코드 + * @returns - 특정 주식의 기간별시세 데이터 객체 반환 * * @author uuuo3o */ - async getInquirePrice(stockCode: string) { + async getInquirePrice( + stockCode: string, + date1: string, + date2: string, + periodDivCode: string, + ) { try { - const queryParams = new StockDetailQueryParameterDto('J', stockCode); + const queryParams = new StockDetailQueryParameterDto(); + queryParams.fid_cond_mrkt_div_code = 'J'; + queryParams.fid_input_iscd = stockCode; + queryParams.fid_input_date_1 = date1; + queryParams.fid_input_date_2 = date2; + queryParams.fid_period_div_code = periodDivCode; const response = await this.requestApi(queryParams); - return this.formatStockData(response.output); + return this.formatStockData(response); } catch (error) { this.logger.error('API Error Details:', { status: error.response?.status, @@ -52,9 +62,9 @@ export class StockDetailService { private async requestApi(queryParams: StockDetailQueryParameterDto) { try { const accessToken = await this.koreaInvetmentService.getAccessToken(); - const headers = getHeader(accessToken, 'FHKST01010100'); + const headers = getHeader(accessToken, 'FHKST03010100'); const url = getFullURL( - '/uapi/domestic-stock/v1/quotations/inquire-price', + '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', ); const params = this.getInquirePriceParams(queryParams); @@ -78,20 +88,38 @@ export class StockDetailService { /** * @private API에서 받은 주식현재가 시세 데이터를 필요한 정보로 정제하는 함수 - * @param {InquirePriceOutputData} stock - API 응답에서 받은 원시 데이터 + * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 * @returns - 필요한 정보만 추출한 데이터 배열 * * @author uuuo3o */ - private formatStockData(stock: InquirePriceOutputData) { - const stockData = new StockDetailDataDto(); - stockData.stck_shrn_iscd = stock.stck_shrn_iscd; - stockData.stck_prpr = stock.stck_prpr; - stockData.prdy_vrss = stock.prdy_vrss; - stockData.prdy_vrss_sign = stock.prdy_vrss_sign; - stockData.prdy_ctrt = stock.prdy_ctrt; - stockData.hts_avls = stock.hts_avls; - stockData.per = stock.per; + private formatStockData(response: InquirePriceApiResponse) { + const stockData = new InquirePriceResponseDto(); + const { output1, output2 } = response; + + const { + hts_kor_isnm, + stck_shrn_iscd, + stck_prpr, + prdy_vrss, + prdy_vrss_sign, + prdy_ctrt, + hts_avls, + per, + } = output1; + + stockData.output1 = { + hts_kor_isnm, + stck_shrn_iscd, + stck_prpr, + prdy_vrss, + prdy_vrss_sign, + prdy_ctrt, + hts_avls, + per, + }; + + stockData.output2 = output2; return stockData; } @@ -107,6 +135,10 @@ export class StockDetailService { return { fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, fid_input_iscd: params.fid_input_iscd, + fid_input_date_1: params.fid_input_date_1, + fid_input_date_2: params.fid_input_date_2, + fid_period_div_code: params.fid_period_div_code, + fid_org_adj_prc: 0, }; } } From 5061b7fecc236d6076c8e9ba55efa1170ec29d11 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:37:02 +0900 Subject: [PATCH 037/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20action?= =?UTF-8?q?=20=EB=B2=84=EC=A0=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index deda0f34..908a4fc5 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -22,11 +22,7 @@ jobs: ] steps: - - uses: actions/checkout@v3 - - - name: debug matrix - run: | - echo "Matrix app directory: ${{matrix.app.dir}}" + - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 @@ -42,9 +38,8 @@ jobs: - name: Install dependencies working-directory: ./${{matrix.app.dir}} - run: | - npm cache clean --force - npm install + run: npm ci + - name: Run tests working-directory: ./${{matrix.app.dir}} run: npm test From 2cf4769cdb1cf4866b6c205b6ffb962323e14424 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:38:54 +0900 Subject: [PATCH 038/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20audit?= =?UTF-8?q?=20=EB=AC=B4=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index 908a4fc5..5eebd458 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -38,7 +38,7 @@ jobs: - name: Install dependencies working-directory: ./${{matrix.app.dir}} - run: npm ci + run: npm ci --no-audit - name: Run tests working-directory: ./${{matrix.app.dir}} From 053c7a9555f71df7d2d8b14054e8bb6222b2f976 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:40:42 +0900 Subject: [PATCH 039/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20contin?= =?UTF-8?q?ue=20on=20error=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index 5eebd458..353b766c 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -38,6 +38,7 @@ jobs: - name: Install dependencies working-directory: ./${{matrix.app.dir}} + continue-on-error: true run: npm ci --no-audit - name: Run tests From b933cb77c8d668c3f8a14bf285a8e9fd6e47ddef Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 17:44:33 +0900 Subject: [PATCH 040/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20npm=20?= =?UTF-8?q?run=20test=20BE=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index 353b766c..cab94eb5 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -39,9 +39,10 @@ jobs: - name: Install dependencies working-directory: ./${{matrix.app.dir}} continue-on-error: true - run: npm ci --no-audit + run: npm ci - name: Run tests + if: ${{ matrix.app.name == 'be' }} # BE일 때만 실행 working-directory: ./${{matrix.app.dir}} run: npm test env: From 351c474a9447bb608410dfc26eb3cf8fc6602375 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 17:51:18 +0900 Subject: [PATCH 041/158] =?UTF-8?q?=F0=9F=92=A1=20comment:=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/detail/dto/stock-detail-query-parameter.dto.ts | 6 ------ BE/src/stock/detail/dto/stock-detail-response.dto.ts | 2 +- BE/src/stock/detail/stock-detail.service.ts | 8 ++++---- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts b/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts index 0fae03b9..feb9ca0f 100644 --- a/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts @@ -31,10 +31,4 @@ export class StockDetailQueryParameterDto { * D:일봉, W:주봉, M:월봉, Y:년봉 */ fid_period_div_code: string; - - /** - * 수정주가 원주가 가격 여부 - * 0:수정주가 1:원주가 - */ - fid_org_adj_prc: string; } diff --git a/BE/src/stock/detail/dto/stock-detail-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts index 765394f7..a86f9e04 100644 --- a/BE/src/stock/detail/dto/stock-detail-response.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-response.dto.ts @@ -3,7 +3,7 @@ import { InquirePriceOutput1Dto } from './stock-detail-output1.dto'; import { InquirePriceOutput2Dto } from './stock-detail-output2.dto'; /** - * 순위 정렬 후 FE에 보낼 DTO + * 국내주식기간별시세(일/주/월/년) API 응답값 정제 후 FE에 보낼 DTO */ export class InquirePriceResponseDto { @ApiProperty({ type: InquirePriceOutput1Dto, description: '상승률 순위' }) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index de6a4adb..c0629406 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -53,9 +53,9 @@ export class StockDetailService { } /** - * @private 한국투자 Open API - [국내주식] 기본시세 - 주식현재가 시세 호출 함수 + * @private 한국투자 Open API - [국내주식] 기본시세 - 국내주식기간별시세(일/주/월/년) 호출 함수 * @param {StockDetailQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO - * @returns - 주식현재가 시세 데이터 + * @returns - 국내주식기간별시세(일/주/월/년) 데이터 * * @author uuuo3o */ @@ -87,7 +87,7 @@ export class StockDetailService { } /** - * @private API에서 받은 주식현재가 시세 데이터를 필요한 정보로 정제하는 함수 + * @private API에서 받은 국내주식기간별시세(일/주/월/년) 데이터를 필요한 정보로 정제하는 함수 * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 * @returns - 필요한 정보만 추출한 데이터 배열 * @@ -125,7 +125,7 @@ export class StockDetailService { } /** - * @private 주식현재가 시세 요청을 위한 쿼리 파라미터 객체 생성 함수 + * @private 국내주식기간별시세(일/주/월/년) 요청을 위한 쿼리 파라미터 객체 생성 함수 * @param {StockDetailQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO * @returns - API 요청에 필요한 쿼리 파라미터 객체 * From c3ccb20bd4c55b352097416f0dcd0e02010ed9d3 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 18:10:31 +0900 Subject: [PATCH 042/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20key=20?= =?UTF-8?q?secret=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index cab94eb5..51bffea9 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -42,7 +42,7 @@ jobs: run: npm ci - name: Run tests - if: ${{ matrix.app.name == 'be' }} # BE일 때만 실행 + if: ${{ matrix.app.name == 'be' }} working-directory: ./${{matrix.app.dir}} run: npm test env: @@ -99,7 +99,7 @@ jobs: with: host: ${{ secrets.NCP_ALPHA_SERVER_HOST }} username: ${{ secrets.NCP_ALPHA_SERVER_USERNAME }} - key: ${{ secrets.NCP_ALPHA_SERVER_SSH_KEY }} + key: ${{ secrets.NCP_SERVER_SSH_KEY }} port: 22 script: | docker system prune -af From cce5466cd25f9adef3a81429eda88ef1c7f028f0 Mon Sep 17 00:00:00 2001 From: sieun <147706431+sieunie@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:14:40 +0900 Subject: [PATCH 043/158] merge conflict resolve app.module.ts --- BE/src/app.module.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index d3c27a75..99a51b90 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -17,16 +17,6 @@ import { typeOrmConfig } from './configs/typeorm.config'; imports: [ ScheduleModule.forRoot(), ConfigModule.forRoot(), - TypeOrmModule.forRoot({ - type: 'mysql', // 데이터베이스 타입 - host: process.env.DB_HOST, - port: 3306, - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWD, - database: process.env.DB_DATABASE, - entities: [User, Order], - synchronize: true, - }), TypeOrmModule.forRoot(typeOrmConfig), KoreaInvestmentModule, AuthModule, From 0cf7bca47202d9e0dec8e84558d5fa95be0b9de3 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 18:16:00 +0900 Subject: [PATCH 044/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=97=90=20=EB=91=90=20=EA=B0=80=EC=A7=80=20?= =?UTF-8?q?job=20=EC=9D=B4=20=EC=8B=A4=ED=96=89=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index 51bffea9..ec6ec9ce 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -14,6 +14,7 @@ jobs: build-and-deploy: runs-on: ubuntu-latest strategy: + max-parallel: 1 matrix: app: [ From 8ecc4225172285cf2c68702cb72f4dc7ed0bb721 Mon Sep 17 00:00:00 2001 From: sieun <147706431+sieunie@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:16:23 +0900 Subject: [PATCH 045/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20remove?= =?UTF-8?q?=20unused=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 99a51b90..ac06a59c 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -10,7 +10,6 @@ import { StockTopfiveModule } from './stock/topfive/stock-topfive.module'; import { KoreaInvestmentModule } from './koreaInvestment/korea-investment.module'; import { SocketModule } from './websocket/socket.module'; import { StockOrderModule } from './stock/order/stock-order.module'; -import { Order } from './stock/order/stock-order.entity'; import { typeOrmConfig } from './configs/typeorm.config'; @Module({ From 344278662f511e223880b8f54322c3b241e30de0 Mon Sep 17 00:00:00 2001 From: JIN Date: Mon, 11 Nov 2024 18:30:27 +0900 Subject: [PATCH 046/158] =?UTF-8?q?=F0=9F=93=9D=20docs:=20swagger=EC=97=90?= =?UTF-8?q?=20=EC=9E=98=EB=AA=BB=20=EC=9E=91=EC=84=B1=EB=90=9C=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index c89a00e0..5c4cfa85 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -26,7 +26,7 @@ export class StockDetailController { type: StockDetailRequestDto, }) @ApiResponse({ - status: 200, + status: 201, description: '단일 주식 종목 기본값 조회 성공', type: InquirePriceResponseDto, }) From a0534d63ca6ee98b5a721ccc1fb21798bd2edea5 Mon Sep 17 00:00:00 2001 From: jinddings Date: Mon, 11 Nov 2024 18:40:39 +0900 Subject: [PATCH 047/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20deploy?= =?UTF-8?q?=20production(dev)=20yml=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index cf0c3cd9..e2cdb2a2 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -2,9 +2,9 @@ name: deploy on: push: - branches: [main] + branches: [dev] pull_request: - branches: [main] + branches: [dev] env: DOCKER_IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/juga-docker @@ -14,6 +14,7 @@ jobs: build-and-deploy: runs-on: ubuntu-latest strategy: + max-parallel: 1 matrix: app: [ @@ -22,7 +23,7 @@ jobs: ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 @@ -38,9 +39,11 @@ jobs: - name: Install dependencies working-directory: ./${{matrix.app.dir}} + continue-on-error: true run: npm ci - name: Run tests + if: ${{ matrix.app.name == 'be' }} working-directory: ./${{matrix.app.dir}} run: npm test env: From d9da2393059da7473887d117ca91556b06825b51 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 12:39:34 +0900 Subject: [PATCH 048/158] =?UTF-8?q?=E2=9E=95=20add=20:=20frontend=20.doker?= =?UTF-8?q?ignore=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FE/.dockerignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 FE/.dockerignore diff --git a/FE/.dockerignore b/FE/.dockerignore new file mode 100644 index 00000000..6ccb2fdd --- /dev/null +++ b/FE/.dockerignore @@ -0,0 +1,4 @@ +.git +Dockerfile +node_modules +dist From 5d82ee6ecd67258007ac9bd6325dab7f72642874 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 12:45:45 +0900 Subject: [PATCH 049/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20docker?= =?UTF-8?q?=20ignore=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/.dockerignore | 1 + FE/.dockerignore | 1 + 2 files changed, 2 insertions(+) diff --git a/BE/.dockerignore b/BE/.dockerignore index 6ccb2fdd..7bd5f175 100644 --- a/BE/.dockerignore +++ b/BE/.dockerignore @@ -2,3 +2,4 @@ Dockerfile node_modules dist +.env* \ No newline at end of file diff --git a/FE/.dockerignore b/FE/.dockerignore index 6ccb2fdd..7bd5f175 100644 --- a/FE/.dockerignore +++ b/FE/.dockerignore @@ -2,3 +2,4 @@ Dockerfile node_modules dist +.env* \ No newline at end of file From 0f266d0d94159b3a498d9234ef3ffe121d016363 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 13:16:18 +0900 Subject: [PATCH 050/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20docker=20?= =?UTF-8?q?=EB=A9=80=ED=8B=B0=20=EC=8A=A4=ED=85=8C=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/Dockerfile | 19 ++++++++++++++----- FE/Dockerfile | 15 +++++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/BE/Dockerfile b/BE/Dockerfile index e23c0848..60079503 100644 --- a/BE/Dockerfile +++ b/BE/Dockerfile @@ -1,8 +1,17 @@ -FROM node:20 -RUN mkdir -p /var/app -WORKDIR /var/app -COPY . . +# 빌드 스테이지 +FROM node:20-slim as builder +WORKDIR /app +COPY package*.json ./ RUN npm install +COPY . . RUN npm run build + +# 실행 스테이지 +FROM node:20-slim +WORKDIR /var/app +COPY package*.json ./ +RUN npm install --only=production +COPY --from=builder /app/dist ./dist + EXPOSE 3000 -CMD [ "node", "dist/main.js" ] \ No newline at end of file +CMD ["node", "dist/main.js"] \ No newline at end of file diff --git a/FE/Dockerfile b/FE/Dockerfile index 6773e6a4..c4389e8c 100644 --- a/FE/Dockerfile +++ b/FE/Dockerfile @@ -1,17 +1,16 @@ -FROM node:20 - -RUN mkdir -p /var/app -WORKDIR /var/app - +# 빌드 스테이지 +FROM node:20-slim as builder +WORKDIR /app COPY package*.json ./ RUN npm install - COPY . . - RUN npm run build +# 실행 스테이지 +FROM node:20-slim +WORKDIR /app RUN npm install -g serve +COPY --from=builder /app/dist ./dist EXPOSE 5173 - CMD ["serve", "-s", "dist", "-l", "5173"] \ No newline at end of file From 0772d2ea86bc5400a8320cac0eba9057b7a671ee Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 12 Nov 2024 13:45:50 +0900 Subject: [PATCH 051/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20cors=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BE/src/main.ts b/BE/src/main.ts index bc11d55f..f64342a0 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -9,7 +9,11 @@ async function bootstrap() { setupSwagger(app); app.enableCors({ - origin: ['http://localhost:5173', 'http://223.130.151.42:3000'], + origin: [ + 'http://localhost:5173', + 'http://223.130.151.42:5173', + 'http://223.130.151.42:3000', + ], methods: 'GET, HEAD, PUT, PATH, POST, DELETE', preflightContinue: false, optionsSuccessStatus: 204, From 9923b18d2a869ee7278da09a942dec8aa5cfd59b Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 14:49:33 +0900 Subject: [PATCH 052/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:login=20redirect?= =?UTF-8?q?=20=ED=98=95=EC=8B=9D=20=EC=88=98=EC=A0=95(#7):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 684384be..cb7605d6 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -8,6 +8,7 @@ import { Req, Res, UnauthorizedException, + Redirect, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; @@ -59,10 +60,10 @@ export class AuthController { ) { const { accessToken, refreshToken } = await this.authService.kakaoLoginUser(authCredentialsDto); - + res.cookie('accessToken', accessToken, { httpOnly: true }); res.cookie('refreshToken', refreshToken, { httpOnly: true }); res.cookie('isRefreshToken', true, { httpOnly: true }); - return res.status(200).json({ accessToken }); + return res.redirect(this.configService.get('FRONTEND_URL')); } @ApiOperation({ summary: 'Refresh Token 요청 API' }) @@ -79,8 +80,9 @@ export class AuthController { const newAccessToken = await this.authService.refreshToken(refreshToken); + res.cookie('accessToken', newAccessToken, { httpOnly: true }); res.cookie('refreshToken', refreshToken, { httpOnly: true }); res.cookie('isRefreshToken', true, { httpOnly: true }); - return res.status(200).json({ accessToken: newAccessToken }); + return res.redirect(this.configService.get('FRONTEND_URL')); } } From fd6cab7f3cef336d8a27f5726230d50960a087a8 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 14:51:40 +0900 Subject: [PATCH 053/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20imp?= =?UTF-8?q?ort=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index cb7605d6..d7cdd7c0 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -8,7 +8,6 @@ import { Req, Res, UnauthorizedException, - Redirect, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiOperation } from '@nestjs/swagger'; From 23c4f66c6f7817a3a9c7b982a73bdf0244ee0113 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 15:10:59 +0900 Subject: [PATCH 054/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20CI=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 32bcc36b..a2210afd 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -4,7 +4,7 @@ on: push: branches: [back/main, front/main] pull_request: - branches: [main, back/main, dev] + branches: [back/main, front/main] jobs: BE-test-and-build: From 8e12ab01b432ff9a947e1db5e68b591ddb97da0f Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 15:13:02 +0900 Subject: [PATCH 055/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore=20:=20FE=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20cache-dependancy-path=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 --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a2210afd..4d7679eb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -49,7 +49,7 @@ jobs: with: node-version: '20' cache: 'npm' - cache-dependency-path: ./BE/package-lock.json + cache-dependency-path: ./FE/package-lock.json - name: Install dependencies working-directory: ./FE From 788b002f2b1b07ffce50692b106c06b0995b0c4b Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 12 Nov 2024 16:24:40 +0900 Subject: [PATCH 056/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20BaseSo?= =?UTF-8?q?cketService=EB=A5=BC=20=EB=A7=8C=EB=93=A4=EC=96=B4=20=EC=97=AC?= =?UTF-8?q?=EB=9F=AC=20=EA=B0=9C=EC=9D=98=20=EC=86=8C=EC=BC=93=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EB=A5=BC=20=EC=89=BD=EA=B2=8C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.service.ts | 1 - BE/src/websocket/base-socket.service.ts | 57 ++++++++++++ BE/src/websocket/socket-token.service.ts | 25 +++++ BE/src/websocket/socket.module.ts | 5 +- BE/src/websocket/socket.service.ts | 93 ------------------- .../websocket/stock-index-socket.service.ts | 40 ++++++++ 6 files changed, 125 insertions(+), 96 deletions(-) create mode 100644 BE/src/websocket/base-socket.service.ts create mode 100644 BE/src/websocket/socket-token.service.ts delete mode 100644 BE/src/websocket/socket.service.ts create mode 100644 BE/src/websocket/stock-index-socket.service.ts diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 3b5e7e07..997de306 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -4,7 +4,6 @@ import { Injectable, } from '@nestjs/common'; import { NotFoundError } from 'rxjs'; -import { Injectable } from '@nestjs/common'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts new file mode 100644 index 00000000..31c174bc --- /dev/null +++ b/BE/src/websocket/base-socket.service.ts @@ -0,0 +1,57 @@ +import { WebSocket } from 'ws'; +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { SocketGateway } from './socket.gateway'; +import { SocketTokenService } from './socket-token.service'; + +@Injectable() +export abstract class BaseSocketService implements OnModuleInit { + protected socket: WebSocket; + protected abstract tradeHandler: Record void>; + protected socketConnectionKey: string; + + constructor( + protected readonly socketTokenService: SocketTokenService, + protected readonly socketGateway: SocketGateway, + ) {} + + async onModuleInit() { + this.socketConnectionKey = + await this.socketTokenService.getSocketConnectionKey(); + this.socket = new WebSocket(process.env.KOREA_INVESTMENT_SOCKET_URL); + + this.socket.onopen = () => { + this.handleSocketOpen(); + }; + + this.socket.onmessage = (event) => { + const data = + typeof event.data === 'string' + ? event.data.split('|') + : JSON.stringify(event.data); + if (data.length < 2) return; + + (this.tradeHandler[data[1]] as (data) => void)(data[3]); + }; + } + + protected abstract handleSocketOpen(): void; + + protected registerCode(trId: string, trKey: string) { + this.socket.send( + JSON.stringify({ + header: { + approval_key: this.socketConnectionKey, + custtype: 'P', + tr_type: '1', + 'content-type': 'utf-8', + }, + body: { + input: { + tr_id: trId, + tr_key: trKey, + }, + }, + }), + ); + } +} diff --git a/BE/src/websocket/socket-token.service.ts b/BE/src/websocket/socket-token.service.ts new file mode 100644 index 00000000..f4ff9e3c --- /dev/null +++ b/BE/src/websocket/socket-token.service.ts @@ -0,0 +1,25 @@ +import axios from 'axios'; +import { SocketConnectTokenInterface } from './interface/socket.interface'; +import { getFullURL } from '../util/get-full-URL'; + +export class SocketTokenService { + private approvalKey: string; + + async getSocketConnectionKey() { + if (this.approvalKey) { + return this.approvalKey; + } + + const response = await axios.post( + getFullURL('/oauth2/Approval'), + { + grant_type: 'client_credentials', + appkey: process.env.KOREA_INVESTMENT_APP_KEY, + secretkey: process.env.KOREA_INVESTMENT_APP_SECRET, + }, + ); + + this.approvalKey = response.data.approval_key; + return this.approvalKey; + } +} diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts index 4e9e2eb2..026ed02b 100644 --- a/BE/src/websocket/socket.module.ts +++ b/BE/src/websocket/socket.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; -import { SocketService } from './socket.service'; +import { StockIndexSocketService } from './stock-index-socket.service'; import { SocketGateway } from './socket.gateway'; +import { SocketTokenService } from './socket-token.service'; @Module({ imports: [], controllers: [], - providers: [SocketService, SocketGateway], + providers: [SocketTokenService, StockIndexSocketService, SocketGateway], exports: [SocketGateway], }) export class SocketModule {} diff --git a/BE/src/websocket/socket.service.ts b/BE/src/websocket/socket.service.ts deleted file mode 100644 index 0bcb7b64..00000000 --- a/BE/src/websocket/socket.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { WebSocket } from 'ws'; -import axios from 'axios'; -import { SocketGateway } from './socket.gateway'; -import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; -import { SocketConnectTokenInterface } from './interface/socket.interface'; -import { getFullURL } from '../util/get-full-URL'; - -@Injectable() -export class SocketService implements OnModuleInit { - private socket: WebSocket; - private tradeHandler = { - H0UPCNT0: this.handleStockIndexValue.bind(this), - }; - - private STOCK_CODE = { - '0001': 'KOSPI', - '1001': 'KOSDAQ', - '2001': 'KOSPI200', - '3003': 'KSQ150', - }; - - constructor(private readonly socketGateway: SocketGateway) {} - - async onModuleInit() { - const socketConnectionKey = await this.getSocketConnectionKey(); - - this.socket = new WebSocket(process.env.KOREA_INVESTMENT_SOCKET_URL); - - this.socket.onopen = () => { - this.registerStockIndexByCode('0001', socketConnectionKey); // 코스피 - this.registerStockIndexByCode('1001', socketConnectionKey); // 코스닥 - this.registerStockIndexByCode('2001', socketConnectionKey); // 코스피200 - this.registerStockIndexByCode('3003', socketConnectionKey); // KSQ150 - }; - - this.socket.onmessage = (event) => { - const data = - typeof event.data === 'string' - ? event.data.split('|') - : JSON.stringify(event.data); - if (data.length < 2) return; - - (this.tradeHandler[data[1]] as (data) => void)(data[3]); - }; - } - - private handleStockIndexValue(responseData: string) { - const responseList = responseData.split('^'); - this.socketGateway.sendStockIndexValueToClient( - this.STOCK_CODE[responseList[0]], - new StockIndexValueElementDto( - responseList[2], - responseList[4], - responseList[9], - responseList[3], - ), - ); - } - - private async getSocketConnectionKey() { - const response = await axios.post( - getFullURL('/oauth2/Approval'), - { - grant_type: 'client_credentials', - appkey: process.env.KOREA_INVESTMENT_APP_KEY, - secretkey: process.env.KOREA_INVESTMENT_APP_SECRET, - }, - ); - - const result = response.data; - return result.approval_key; - } - - private registerStockIndexByCode(code, socketConnectionKey) { - this.socket.send( - JSON.stringify({ - header: { - approval_key: socketConnectionKey, - custtype: 'P', - tr_type: '1', - 'content-type': 'utf-8', - }, - body: { - input: { - tr_id: 'H0UPCNT0', - tr_key: code, - }, - }, - }), - ); - } -} diff --git a/BE/src/websocket/stock-index-socket.service.ts b/BE/src/websocket/stock-index-socket.service.ts new file mode 100644 index 00000000..a5e61853 --- /dev/null +++ b/BE/src/websocket/stock-index-socket.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; +import { SocketConnectTokenInterface } from './interface/socket.interface'; +import { getFullURL } from '../util/get-full-URL'; +import { BaseSocketService } from './base-socket.service'; + +@Injectable() +export class StockIndexSocketService extends BaseSocketService { + protected tradeHandler = { + H0UPCNT0: this.handleStockIndexValue.bind(this), + }; + + private STOCK_CODE = { + '0001': 'KOSPI', + '1001': 'KOSDAQ', + '2001': 'KOSPI200', + '3003': 'KSQ150', + }; + + protected handleSocketOpen() { + this.registerCode('H0UPCNT0', '0001'); // 코스피 + this.registerCode('H0UPCNT0', '1001'); // 코스닥 + this.registerCode('H0UPCNT0', '2001'); // 코스피200 + this.registerCode('H0UPCNT0', '3003'); // KSQ150 + } + + private handleStockIndexValue(responseData: string) { + const responseList = responseData.split('^'); + this.socketGateway.sendStockIndexValueToClient( + this.STOCK_CODE[responseList[0]], + new StockIndexValueElementDto( + responseList[2], + responseList[4], + responseList[9], + responseList[3], + ), + ); + } +} From 4262fe52d7c372045763218a5b7cc129e56daf98 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 12 Nov 2024 16:27:29 +0900 Subject: [PATCH 057/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20import=EB=AC=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/stock-index-socket.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/BE/src/websocket/stock-index-socket.service.ts b/BE/src/websocket/stock-index-socket.service.ts index a5e61853..04e61460 100644 --- a/BE/src/websocket/stock-index-socket.service.ts +++ b/BE/src/websocket/stock-index-socket.service.ts @@ -1,8 +1,5 @@ import { Injectable } from '@nestjs/common'; -import axios from 'axios'; import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; -import { SocketConnectTokenInterface } from './interface/socket.interface'; -import { getFullURL } from '../util/get-full-URL'; import { BaseSocketService } from './base-socket.service'; @Injectable() From 4c1250af65d465af4db2283a8bb161f909877cae Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 16:42:57 +0900 Subject: [PATCH 058/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20handle?= =?UTF-8?q?SocketData=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/base-socket.service.ts | 5 +++-- BE/src/websocket/stock-index-socket.service.ts | 17 ++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 31c174bc..9322196f 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -6,7 +6,6 @@ import { SocketTokenService } from './socket-token.service'; @Injectable() export abstract class BaseSocketService implements OnModuleInit { protected socket: WebSocket; - protected abstract tradeHandler: Record void>; protected socketConnectionKey: string; constructor( @@ -30,11 +29,13 @@ export abstract class BaseSocketService implements OnModuleInit { : JSON.stringify(event.data); if (data.length < 2) return; - (this.tradeHandler[data[1]] as (data) => void)(data[3]); + const dataList = data[3].split('^'); + this.handleSocketData(dataList); }; } protected abstract handleSocketOpen(): void; + protected abstract handleSocketData(dataList): void; protected registerCode(trId: string, trKey: string) { this.socket.send( diff --git a/BE/src/websocket/stock-index-socket.service.ts b/BE/src/websocket/stock-index-socket.service.ts index 04e61460..4febad48 100644 --- a/BE/src/websocket/stock-index-socket.service.ts +++ b/BE/src/websocket/stock-index-socket.service.ts @@ -4,10 +4,6 @@ import { BaseSocketService } from './base-socket.service'; @Injectable() export class StockIndexSocketService extends BaseSocketService { - protected tradeHandler = { - H0UPCNT0: this.handleStockIndexValue.bind(this), - }; - private STOCK_CODE = { '0001': 'KOSPI', '1001': 'KOSDAQ', @@ -22,15 +18,14 @@ export class StockIndexSocketService extends BaseSocketService { this.registerCode('H0UPCNT0', '3003'); // KSQ150 } - private handleStockIndexValue(responseData: string) { - const responseList = responseData.split('^'); + protected handleSocketData(dataList: string[]) { this.socketGateway.sendStockIndexValueToClient( - this.STOCK_CODE[responseList[0]], + this.STOCK_CODE[dataList[0]], new StockIndexValueElementDto( - responseList[2], - responseList[4], - responseList[9], - responseList[3], + dataList[2], + dataList[4], + dataList[9], + dataList[3], ), ); } From 94364bb1a5af7cbea5155f93510a2e6d0f03fff7 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 16:50:40 +0900 Subject: [PATCH 059/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=EC=B9=B4?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=20login=20=EC=8B=9C=EC=97=90=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EB=90=98=EC=96=B4=20=EC=9E=88=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9C=BC=EB=A9=B4=20=20User=20=EC=A0=95=EB=B3=B4=20=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 10 ++++++---- BE/src/auth/auth.service.ts | 7 +++++++ BE/src/auth/strategy/kakao.strategy.ts | 6 ++++++ BE/src/auth/user.entity.ts | 6 +++--- BE/src/auth/user.repository.ts | 8 ++++++++ BE/src/stock/order/stock-order.service.ts | 1 - BE/src/types/express.d.ts | 3 ++- 7 files changed, 32 insertions(+), 9 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index d7cdd7c0..98c3d7ad 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -53,12 +53,14 @@ export class AuthController { @ApiOperation({ summary: 'Kakao 로그인 API' }) @Get('/kakao') @UseGuards(AuthGuard('kakao')) - async kakaoLogin( - @Body() authCredentialsDto: AuthCredentialsDto, - @Res() res: Response, - ) { + async kakaoLogin(@Req() req: Request, @Res() res: Response) { + const authCredentialsDto: AuthCredentialsDto = { + email: req.user.email, + kakaoId: req.user.kakaoId, + }; const { accessToken, refreshToken } = await this.authService.kakaoLoginUser(authCredentialsDto); + res.cookie('accessToken', accessToken, { httpOnly: true }); res.cookie('refreshToken', refreshToken, { httpOnly: true }); res.cookie('isRefreshToken', true, { httpOnly: true }); diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 2de79cd8..54320e87 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -39,6 +39,13 @@ export class AuthService { async kakaoLoginUser( authCredentialsDto: AuthCredentialsDto, ): Promise<{ accessToken: string; refreshToken: string }> { + const user = await this.userRepository.findOne({ + where: { kakaoId: authCredentialsDto.kakaoId }, + }); + + if (!user) { + await this.userRepository.registerKakaoUser(authCredentialsDto); + } return this.getJWTToken(authCredentialsDto); } diff --git a/BE/src/auth/strategy/kakao.strategy.ts b/BE/src/auth/strategy/kakao.strategy.ts index e69805dc..00d6d96f 100644 --- a/BE/src/auth/strategy/kakao.strategy.ts +++ b/BE/src/auth/strategy/kakao.strategy.ts @@ -13,6 +13,9 @@ interface KakaoProfile extends Profile { id: number; _json: { id: number; + kakao_account: { + email: string; + }; }; } @@ -44,7 +47,10 @@ export class KakaoStrategy extends PassportStrategy( try { // eslint-disable-next-line no-underscore-dangle const kakaoId = profile._json.id; + // eslint-disable-next-line no-underscore-dangle + const { email } = profile._json.kakao_account; const user = { + email, kakaoId, }; done(null, user); diff --git a/BE/src/auth/user.entity.ts b/BE/src/auth/user.entity.ts index 8c574cd4..16773177 100644 --- a/BE/src/auth/user.entity.ts +++ b/BE/src/auth/user.entity.ts @@ -15,8 +15,8 @@ export class User extends BaseEntity { @Column({ default: false }) tutorial: boolean; - @Column({ default: -1 }) - kakaoId: number; + @Column({ default: '' }) + kakaoId: string; @Column({ default: '' }) currentRefreshToken: string; @@ -25,7 +25,7 @@ export class User extends BaseEntity { currentRefreshTokenExpiresAt: Date; toAuthCredentialsDto(): AuthCredentialsDto { - if (this.kakaoId === -1) { + if (this.kakaoId === '') { return { email: this.email, password: this.password, diff --git a/BE/src/auth/user.repository.ts b/BE/src/auth/user.repository.ts index 442c73f4..b7a06fa4 100644 --- a/BE/src/auth/user.repository.ts +++ b/BE/src/auth/user.repository.ts @@ -19,6 +19,14 @@ export class UserRepository extends Repository { await this.save(user); } + async registerKakaoUser(authCredentialsDto: AuthCredentialsDto) { + const { kakaoId, email } = authCredentialsDto; + const salt: string = await bcrypt.genSalt(); + const hashedPassword: string = await bcrypt.hash(String(kakaoId), salt); + const user = this.create({ email, kakaoId, password: hashedPassword }); + await this.save(user); + } + async updateUserWithRefreshToken( id: number, { diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 3b5e7e07..997de306 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -4,7 +4,6 @@ import { Injectable, } from '@nestjs/common'; import { NotFoundError } from 'rxjs'; -import { Injectable } from '@nestjs/common'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; diff --git a/BE/src/types/express.d.ts b/BE/src/types/express.d.ts index 9cf89153..2060c079 100644 --- a/BE/src/types/express.d.ts +++ b/BE/src/types/express.d.ts @@ -4,8 +4,9 @@ import { UUID } from 'crypto'; declare module 'express' { interface Request extends Req { user: { - kakaoId?: number; + kakaoId?: string; userId?: UUID; + email?: string; }; } } From 4c211227d8d28d242d7fcf3e52583519e2d7cd5a Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 17:08:09 +0900 Subject: [PATCH 060/158] =?UTF-8?q?=E2=9E=95=20add=20:=20stockList=20contr?= =?UTF-8?q?oller,=20service,=20=20module=20=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/stock-list.controller.ts | 0 BE/src/stock/list/stock-list.module.ts | 0 BE/src/stock/list/stock-list.service.ts | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 BE/src/stock/list/stock-list.controller.ts create mode 100644 BE/src/stock/list/stock-list.module.ts create mode 100644 BE/src/stock/list/stock-list.service.ts diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts new file mode 100644 index 00000000..e69de29b diff --git a/BE/src/stock/list/stock-list.module.ts b/BE/src/stock/list/stock-list.module.ts new file mode 100644 index 00000000..e69de29b diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts new file mode 100644 index 00000000..e69de29b From 16440dcc755c53f5d9a716217fd64b27ccd3ebf6 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 19:42:46 +0900 Subject: [PATCH 061/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20socket?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/index/stock-index.module.ts | 1 - BE/src/stock/order/stock-order.controller.ts | 8 ++-- BE/src/websocket/base-socket.service.ts | 44 +++++++++++++------ BE/src/websocket/socket.module.ts | 13 +++++- .../websocket/stock-index-socket.service.ts | 37 +++++++++------- 5 files changed, 66 insertions(+), 37 deletions(-) diff --git a/BE/src/stock/index/stock-index.module.ts b/BE/src/stock/index/stock-index.module.ts index 64e0c575..498ddd8b 100644 --- a/BE/src/stock/index/stock-index.module.ts +++ b/BE/src/stock/index/stock-index.module.ts @@ -8,6 +8,5 @@ import { SocketModule } from '../../websocket/socket.module'; imports: [KoreaInvestmentModule, SocketModule], controllers: [StockIndexController], providers: [StockIndexService], - exports: [StockIndexService], }) export class StockIndexModule {} diff --git a/BE/src/stock/order/stock-order.controller.ts b/BE/src/stock/order/stock-order.controller.ts index 82d72083..31960590 100644 --- a/BE/src/stock/order/stock-order.controller.ts +++ b/BE/src/stock/order/stock-order.controller.ts @@ -22,7 +22,7 @@ import { RequestInterface } from './interface/request.interface'; @Controller('/api/stocks/trade') @ApiTags('주식 매수/매도 API') export class StockOrderController { - constructor(private readonly stockTradeService: StockOrderService) {} + constructor(private readonly stockOrderService: StockOrderService) {} @Post('/buy') @ApiBearerAuth() @@ -39,7 +39,7 @@ export class StockOrderController { @Req() request: RequestInterface, @Body(ValidationPipe) stockOrderRequest: StockOrderRequestDto, ) { - await this.stockTradeService.buy(request.user.id, stockOrderRequest); + await this.stockOrderService.buy(request.user.id, stockOrderRequest); } @Post('/sell') @@ -57,7 +57,7 @@ export class StockOrderController { @Req() request: RequestInterface, @Body(ValidationPipe) stockOrderRequest: StockOrderRequestDto, ) { - await this.stockTradeService.sell(request.user.id, stockOrderRequest); + await this.stockOrderService.sell(request.user.id, stockOrderRequest); } @Delete('/:order_id') @@ -75,6 +75,6 @@ export class StockOrderController { @Req() request: RequestInterface, @Param('order_id') orderId: number, ) { - await this.stockTradeService.cancel(request.user.id, orderId); + await this.stockOrderService.cancel(request.user.id, orderId); } } diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 9322196f..7796e669 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -1,17 +1,22 @@ import { WebSocket } from 'ws'; -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { + Injectable, + InternalServerErrorException, + OnModuleInit, +} from '@nestjs/common'; import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; @Injectable() -export abstract class BaseSocketService implements OnModuleInit { - protected socket: WebSocket; - protected socketConnectionKey: string; +export class BaseSocketService implements OnModuleInit { + private socket: WebSocket; + private socketConnectionKey: string; + private socketOpenHandlers: (() => void | Promise)[] = []; + private socketDataHandlers: { + [key: string]: (data) => void; + } = {}; - constructor( - protected readonly socketTokenService: SocketTokenService, - protected readonly socketGateway: SocketGateway, - ) {} + constructor(private readonly socketTokenService: SocketTokenService) {} async onModuleInit() { this.socketConnectionKey = @@ -19,7 +24,13 @@ export abstract class BaseSocketService implements OnModuleInit { this.socket = new WebSocket(process.env.KOREA_INVESTMENT_SOCKET_URL); this.socket.onopen = () => { - this.handleSocketOpen(); + Promise.all( + this.socketOpenHandlers.map(async (socketOpenHandler) => { + await socketOpenHandler(); + }), + ).catch(() => { + throw new InternalServerErrorException(); + }); }; this.socket.onmessage = (event) => { @@ -30,14 +41,11 @@ export abstract class BaseSocketService implements OnModuleInit { if (data.length < 2) return; const dataList = data[3].split('^'); - this.handleSocketData(dataList); + this.socketDataHandlers[data[1]](dataList); }; } - protected abstract handleSocketOpen(): void; - protected abstract handleSocketData(dataList): void; - - protected registerCode(trId: string, trKey: string) { + registerCode(trId: string, trKey: string) { this.socket.send( JSON.stringify({ header: { @@ -55,4 +63,12 @@ export abstract class BaseSocketService implements OnModuleInit { }), ); } + + registerSocketOpenHandler(handler: () => void | Promise) { + this.socketOpenHandlers.push(handler); + } + + registerSocketDataHandler(tradeCode: string, handler: (data) => void) { + this.socketDataHandlers[tradeCode] = handler; + } } diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts index 026ed02b..ee2ed0bc 100644 --- a/BE/src/websocket/socket.module.ts +++ b/BE/src/websocket/socket.module.ts @@ -2,11 +2,20 @@ import { Module } from '@nestjs/common'; import { StockIndexSocketService } from './stock-index-socket.service'; import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; +import { StockPriceSocketService } from './stock/price/stock-price-socket.service'; +import { StockItemModule } from '../stock/item/stock-item.module'; +import { BaseSocketService } from './base-socket.service'; @Module({ - imports: [], + imports: [StockItemModule], controllers: [], - providers: [SocketTokenService, StockIndexSocketService, SocketGateway], + providers: [ + SocketTokenService, + StockIndexSocketService, + SocketGateway, + StockPriceSocketService, + BaseSocketService, + ], exports: [SocketGateway], }) export class SocketModule {} diff --git a/BE/src/websocket/stock-index-socket.service.ts b/BE/src/websocket/stock-index-socket.service.ts index 4febad48..f6a37940 100644 --- a/BE/src/websocket/stock-index-socket.service.ts +++ b/BE/src/websocket/stock-index-socket.service.ts @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; import { BaseSocketService } from './base-socket.service'; +import { SocketGateway } from './socket.gateway'; @Injectable() -export class StockIndexSocketService extends BaseSocketService { +export class StockIndexSocketService { + private TRADE_CODE = 'H0UPCNT0'; private STOCK_CODE = { '0001': 'KOSPI', '1001': 'KOSDAQ', @@ -11,22 +13,25 @@ export class StockIndexSocketService extends BaseSocketService { '3003': 'KSQ150', }; - protected handleSocketOpen() { - this.registerCode('H0UPCNT0', '0001'); // 코스피 - this.registerCode('H0UPCNT0', '1001'); // 코스닥 - this.registerCode('H0UPCNT0', '2001'); // 코스피200 - this.registerCode('H0UPCNT0', '3003'); // KSQ150 - } + constructor( + private readonly socketGateway: SocketGateway, + private readonly baseSocketService: BaseSocketService, + ) { + baseSocketService.registerSocketOpenHandler(() => { + this.baseSocketService.registerCode(this.TRADE_CODE, '0001'); // 코스피 + this.baseSocketService.registerCode(this.TRADE_CODE, '1001'); // 코스닥 + this.baseSocketService.registerCode(this.TRADE_CODE, '2001'); // 코스피200 + this.baseSocketService.registerCode(this.TRADE_CODE, '3003'); // KSQ150 + }); - protected handleSocketData(dataList: string[]) { - this.socketGateway.sendStockIndexValueToClient( - this.STOCK_CODE[dataList[0]], - new StockIndexValueElementDto( - dataList[2], - dataList[4], - dataList[9], - dataList[3], - ), + baseSocketService.registerSocketDataHandler( + this.TRADE_CODE, + (data: string[]) => { + this.socketGateway.sendStockIndexValueToClient( + this.STOCK_CODE[data[0]], + new StockIndexValueElementDto(data[2], data[4], data[9], data[3]), + ); + }, ); } } From 79a49020c7be31b711525d07b12fb6dd0765f0ae Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 19:50:30 +0900 Subject: [PATCH 062/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20lint=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/base-socket.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 7796e669..9c39d431 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -4,7 +4,6 @@ import { InternalServerErrorException, OnModuleInit, } from '@nestjs/common'; -import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; @Injectable() From df7eb3ba5103117c8b10e8c460a69bf87b25d6bc Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 19:53:19 +0900 Subject: [PATCH 063/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20lint=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EB=8B=A4=EC=8B=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/socket.module.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts index ee2ed0bc..21bbbf2e 100644 --- a/BE/src/websocket/socket.module.ts +++ b/BE/src/websocket/socket.module.ts @@ -2,18 +2,15 @@ import { Module } from '@nestjs/common'; import { StockIndexSocketService } from './stock-index-socket.service'; import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; -import { StockPriceSocketService } from './stock/price/stock-price-socket.service'; -import { StockItemModule } from '../stock/item/stock-item.module'; import { BaseSocketService } from './base-socket.service'; @Module({ - imports: [StockItemModule], + imports: [], controllers: [], providers: [ SocketTokenService, StockIndexSocketService, SocketGateway, - StockPriceSocketService, BaseSocketService, ], exports: [SocketGateway], From 437f622460e73665e6907f5e5be029f413e68641 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 20:11:37 +0900 Subject: [PATCH 064/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20TR=5FID?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/stock-index-socket.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BE/src/websocket/stock-index-socket.service.ts b/BE/src/websocket/stock-index-socket.service.ts index f6a37940..604096c6 100644 --- a/BE/src/websocket/stock-index-socket.service.ts +++ b/BE/src/websocket/stock-index-socket.service.ts @@ -5,7 +5,7 @@ import { SocketGateway } from './socket.gateway'; @Injectable() export class StockIndexSocketService { - private TRADE_CODE = 'H0UPCNT0'; + private TR_ID = 'H0UPCNT0'; private STOCK_CODE = { '0001': 'KOSPI', '1001': 'KOSDAQ', @@ -18,14 +18,14 @@ export class StockIndexSocketService { private readonly baseSocketService: BaseSocketService, ) { baseSocketService.registerSocketOpenHandler(() => { - this.baseSocketService.registerCode(this.TRADE_CODE, '0001'); // 코스피 - this.baseSocketService.registerCode(this.TRADE_CODE, '1001'); // 코스닥 - this.baseSocketService.registerCode(this.TRADE_CODE, '2001'); // 코스피200 - this.baseSocketService.registerCode(this.TRADE_CODE, '3003'); // KSQ150 + this.baseSocketService.registerCode(this.TR_ID, '0001'); // 코스피 + this.baseSocketService.registerCode(this.TR_ID, '1001'); // 코스닥 + this.baseSocketService.registerCode(this.TR_ID, '2001'); // 코스피200 + this.baseSocketService.registerCode(this.TR_ID, '3003'); // KSQ150 }); baseSocketService.registerSocketDataHandler( - this.TRADE_CODE, + this.TR_ID, (data: string[]) => { this.socketGateway.sendStockIndexValueToClient( this.STOCK_CODE[data[0]], From 890e447d38bb052cc2db2146c6813788432df6c4 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 20:13:56 +0900 Subject: [PATCH 065/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/stock-index-socket.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/BE/src/websocket/stock-index-socket.service.ts b/BE/src/websocket/stock-index-socket.service.ts index 604096c6..751807f6 100644 --- a/BE/src/websocket/stock-index-socket.service.ts +++ b/BE/src/websocket/stock-index-socket.service.ts @@ -29,7 +29,12 @@ export class StockIndexSocketService { (data: string[]) => { this.socketGateway.sendStockIndexValueToClient( this.STOCK_CODE[data[0]], - new StockIndexValueElementDto(data[2], data[4], data[9], data[3]), + new StockIndexValueElementDto( + data[2], // 주가 지수 + data[4], // 전일 대비 등락 + data[9], // 전일 대비 등락률 + data[3], // 부호 + ), ); }, ); From 7a53c3f409d9f732fb853b31fb3215300e84bf01 Mon Sep 17 00:00:00 2001 From: jindding Date: Tue, 12 Nov 2024 20:43:06 +0900 Subject: [PATCH 066/158] =?UTF-8?q?=E2=9C=A8feat:=20financeDataReader?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EC=A3=BC=EC=8B=9D=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/stock-list.script.py | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 scripts/stock-list.script.py diff --git a/scripts/stock-list.script.py b/scripts/stock-list.script.py new file mode 100644 index 00000000..df5290aa --- /dev/null +++ b/scripts/stock-list.script.py @@ -0,0 +1,46 @@ +import FinanceDataReader as fdr +import json +import os +import mysql.connector +from dotenv import load_dotenv +from pathlib import Path + +root_dir = Path(__file__).parent +env_path = os.path.join(root_dir, '.env') +# .env 파일 로드 +load_dotenv(env_path) + +db_config = { + 'host' : os.getenv('DB_HOST'), + 'user' : os.getenv('DB_USERNAME'), + 'password' : os.getenv('DB_PASSWD'), + 'database' : os.getenv('DB_DATABASE'), +} + +def insert_stocks(stockData) : + try : + conn = mysql.connector.connect(**db_config) + cursor = conn.cursor() + + insert_query = """INSERT INTO stocks (code, name, market) VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE name = VALUES(name), market = VALUES(market)""" + + records = stockData.to_dict('records') + for record in records: + values = (record['Code'], record['Name'], record['Market']) + cursor.execute(insert_query, values) + conn.commit() + + except Exception as e : + print(e) + conn.rollback() + + finally : + if conn.is_connected() : + cursor.close() + conn.close() + +df_krx = fdr.StockListing('KRX') +df_selected = df_krx[['Code','Name','Market']] + +insert_stocks(df_selected) \ No newline at end of file From 26b9f66aa69c52e800cfc598a254acaaf33d2591 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 21:12:16 +0900 Subject: [PATCH 067/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B2=B4=EA=B2=B0?= =?UTF-8?q?=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20stock=20code=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20=EC=9C=84=ED=95=9C=20sto?= =?UTF-8?q?ck=20item=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=96=B4=20=EA=B5=AC=ED=98=84=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/item/enum/market-type.ts | 4 ++++ BE/src/stock/item/stock-item.controller.ts | 9 +++++++++ BE/src/stock/item/stock-item.entity.ts | 18 ++++++++++++++++++ BE/src/stock/item/stock-item.module.ts | 14 ++++++++++++++ BE/src/stock/item/stock-item.repository.ts | 11 +++++++++++ BE/src/stock/item/stock-item.service.ts | 13 +++++++++++++ 6 files changed, 69 insertions(+) create mode 100644 BE/src/stock/item/enum/market-type.ts create mode 100644 BE/src/stock/item/stock-item.controller.ts create mode 100644 BE/src/stock/item/stock-item.entity.ts create mode 100644 BE/src/stock/item/stock-item.module.ts create mode 100644 BE/src/stock/item/stock-item.repository.ts create mode 100644 BE/src/stock/item/stock-item.service.ts diff --git a/BE/src/stock/item/enum/market-type.ts b/BE/src/stock/item/enum/market-type.ts new file mode 100644 index 00000000..691fdee3 --- /dev/null +++ b/BE/src/stock/item/enum/market-type.ts @@ -0,0 +1,4 @@ +export enum MarketType { + KOSPI = 'KOSPI', + KOSDAQ = 'KOSDAQ', +} diff --git a/BE/src/stock/item/stock-item.controller.ts b/BE/src/stock/item/stock-item.controller.ts new file mode 100644 index 00000000..5eef4530 --- /dev/null +++ b/BE/src/stock/item/stock-item.controller.ts @@ -0,0 +1,9 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { StockItemService } from './stock-item.service'; + +@Controller('/api/stocks/item') +@ApiTags('주식 개별 주 API') +export class StockItemController { + constructor(private readonly stockItemService: StockItemService) {} +} diff --git a/BE/src/stock/item/stock-item.entity.ts b/BE/src/stock/item/stock-item.entity.ts new file mode 100644 index 00000000..78c1e731 --- /dev/null +++ b/BE/src/stock/item/stock-item.entity.ts @@ -0,0 +1,18 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; +import { MarketType } from './enum/market-type'; + +@Entity('stocks') +export class Stock { + @PrimaryColumn() + code: string; + + @Column({ nullable: false }) + name: string; + + @Column({ + type: 'enum', + enum: MarketType, + nullable: false, + }) + market: MarketType; +} diff --git a/BE/src/stock/item/stock-item.module.ts b/BE/src/stock/item/stock-item.module.ts new file mode 100644 index 00000000..ad915d14 --- /dev/null +++ b/BE/src/stock/item/stock-item.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StockItemController } from './stock-item.controller'; +import { StockItemService } from './stock-item.service'; +import { Stock } from './stock-item.entity'; +import { StockItemRepository } from './stock-item.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([Stock])], + controllers: [StockItemController], + providers: [StockItemService, StockItemRepository], + exports: [StockItemService], +}) +export class StockItemModule {} diff --git a/BE/src/stock/item/stock-item.repository.ts b/BE/src/stock/item/stock-item.repository.ts new file mode 100644 index 00000000..e0af08b8 --- /dev/null +++ b/BE/src/stock/item/stock-item.repository.ts @@ -0,0 +1,11 @@ +import { DataSource, Repository } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; +import { Stock } from './stock-item.entity'; + +@Injectable() +export class StockItemRepository extends Repository { + constructor(@InjectDataSource() dataSource: DataSource) { + super(Stock, dataSource.createEntityManager()); + } +} diff --git a/BE/src/stock/item/stock-item.service.ts b/BE/src/stock/item/stock-item.service.ts new file mode 100644 index 00000000..ef8a0389 --- /dev/null +++ b/BE/src/stock/item/stock-item.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { StockItemRepository } from './stock-item.repository'; + +@Injectable() +export class StockItemService { + constructor(private readonly stockItemRepository: StockItemRepository) {} + + getAllStockItems() { + return this.stockItemRepository.find({ + select: ['code'], + }); + } +} From 1a4d658916083e974c5a0a785e8d22783fae44bd Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 21:14:10 +0900 Subject: [PATCH 068/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20stock=5Fid?= =?UTF-8?q?=EB=A5=BC=20stock=5Fcode=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/order/stock-order.entity.ts b/BE/src/stock/order/stock-order.entity.ts index a88de180..7498b72a 100644 --- a/BE/src/stock/order/stock-order.entity.ts +++ b/BE/src/stock/order/stock-order.entity.ts @@ -16,7 +16,7 @@ export class Order { user_id: number; @Column({ nullable: false }) - stock_id: number; + stock_code: string; @Column({ type: 'enum', From c8cc1c581fc827936eaf13fd885d599d8ab2c348 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 21:15:18 +0900 Subject: [PATCH 069/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B2=B4=EA=B2=B0?= =?UTF-8?q?=20=EA=B0=80=EB=8A=A5=ED=95=9C=20=EC=A3=BC=EB=AC=B8=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.service.ts | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 997de306..94096a12 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -4,6 +4,7 @@ import { Injectable, } from '@nestjs/common'; import { NotFoundError } from 'rxjs'; +import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; @@ -52,4 +53,39 @@ export class StockOrderService { await this.stockOrderRepository.remove(order); } + + async checkExecutableOrder(stockCode, value) { + const buyOrders = await this.stockOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.BUY, + status: StatusType.PENDING, + price: MoreThanOrEqual(value), + }, + }); + + const sellOrders = await this.stockOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.SELL, + status: StatusType.PENDING, + price: LessThanOrEqual(value), + }, + }); + + await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); + await Promise.all( + sellOrders.map((sellOrder) => this.executeSell(sellOrder)), + ); + } + + private executeBuy(order) { + // TODO: 매수 체결 로직 필요... + console.log(`${order.id}번 매수 예약이 체결되었습니다.`); + } + + private executeSell(order) { + // TODO: 매도 체결 로직 필요... + console.log(`${order.id}번 매도 예약이 체결되었습니다.`); + } } From 3ca62eaa33eef402eae8d6504f4567f2e4daf816 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 21:16:13 +0900 Subject: [PATCH 070/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EB=AA=A8=EB=93=88=20import=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.module.ts | 1 + BE/src/websocket/socket.module.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index b5c54337..0b1d480a 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -9,5 +9,6 @@ import { StockOrderRepository } from './stock-order.repository'; imports: [TypeOrmModule.forFeature([Order])], controllers: [StockOrderController], providers: [StockOrderService, StockOrderRepository], + exports: [StockOrderService], }) export class StockOrderModule {} diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts index 21bbbf2e..e059658a 100644 --- a/BE/src/websocket/socket.module.ts +++ b/BE/src/websocket/socket.module.ts @@ -2,15 +2,19 @@ import { Module } from '@nestjs/common'; import { StockIndexSocketService } from './stock-index-socket.service'; import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; +import { StockPriceSocketService } from './stock/price/stock-price-socket.service'; +import { StockItemModule } from '../stock/item/stock-item.module'; import { BaseSocketService } from './base-socket.service'; +import { StockOrderModule } from '../stock/order/stock-order.module'; @Module({ - imports: [], + imports: [StockItemModule, StockOrderModule], controllers: [], providers: [ SocketTokenService, StockIndexSocketService, SocketGateway, + StockPriceSocketService, BaseSocketService, ], exports: [SocketGateway], From 4ae4780cdb01bb228facd2dced9f9fb0d801f8cd Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 21:17:00 +0900 Subject: [PATCH 071/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=B2=B4=EA=B2=B0?= =?UTF-8?q?=EA=B0=80=20=EC=9B=B9=EC=86=8C=EC=BC=93=EA=B3=BC=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=B2=B4=EA=B2=B0=20=EC=97=B0=EB=8F=99=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/price/stock-price-socket.service.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 BE/src/websocket/stock/price/stock-price-socket.service.ts diff --git a/BE/src/websocket/stock/price/stock-price-socket.service.ts b/BE/src/websocket/stock/price/stock-price-socket.service.ts new file mode 100644 index 00000000..feefe457 --- /dev/null +++ b/BE/src/websocket/stock/price/stock-price-socket.service.ts @@ -0,0 +1,38 @@ +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { BaseSocketService } from '../../base-socket.service'; +import { SocketGateway } from '../../socket.gateway'; +import { StockItemService } from '../../../stock/item/stock-item.service'; +import { StockOrderService } from '../../../stock/order/stock-order.service'; + +@Injectable() +export class StockPriceSocketService { + private TR_ID = 'H0STCNT0'; + + constructor( + private readonly socketGateway: SocketGateway, + private readonly baseSocketService: BaseSocketService, + private readonly stockItemService: StockItemService, + private readonly stockOrderService: StockOrderService, + ) { + baseSocketService.registerSocketOpenHandler(async () => { + const stockList = await this.stockItemService.getAllStockItems(); + stockList.forEach((stock) => { + this.baseSocketService.registerCode(this.TR_ID, stock.code); + }); + }); + + baseSocketService.registerSocketDataHandler( + this.TR_ID, + (data: string[]) => { + stockOrderService + .checkExecutableOrder( + data[0], // 주식 코드 + data[2], // 주식 체결가 + ) + .catch(() => { + throw new InternalServerErrorException(); + }); + }, + ); + } +} From 426b7523da1c4eb3e674a657d11449943caa5820 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Tue, 12 Nov 2024 21:41:20 +0900 Subject: [PATCH 072/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=A7=A4?= =?UTF-8?q?=EC=88=98/=EB=A7=A4=EB=8F=84=20=EB=A9=94=EC=86=8C=EB=93=9C?= =?UTF-8?q?=EC=9D=98=20stock=5Fid=EB=A5=BC=20stock=5Fcode=EB=A1=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 --- BE/src/stock/order/dto/stock-order-request.dto.ts | 6 ++---- BE/src/stock/order/stock-order.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/BE/src/stock/order/dto/stock-order-request.dto.ts b/BE/src/stock/order/dto/stock-order-request.dto.ts index b06a3dcb..407d05ad 100644 --- a/BE/src/stock/order/dto/stock-order-request.dto.ts +++ b/BE/src/stock/order/dto/stock-order-request.dto.ts @@ -2,10 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsNumber, IsPositive } from 'class-validator'; export class StockOrderRequestDto { - @ApiProperty({ description: '주식 id' }) - @IsInt() - @IsPositive() - stock_id: number; + @ApiProperty({ description: '주식 id', example: '005930' }) + stock_code: string; @ApiProperty({ description: '매수/매도 희망 가격' }) @IsNumber() diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 94096a12..3221e13b 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -17,7 +17,7 @@ export class StockOrderService { async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { const order = this.stockOrderRepository.create({ user_id: userId, - stock_id: stockOrderRequest.stock_id, + stock_code: stockOrderRequest.stock_code, trade_type: TradeType.BUY, amount: stockOrderRequest.amount, price: stockOrderRequest.price, @@ -30,7 +30,7 @@ export class StockOrderService { async sell(userId: number, stockOrderRequest: StockOrderRequestDto) { const order = this.stockOrderRepository.create({ user_id: userId, - stock_id: stockOrderRequest.stock_id, + stock_code: stockOrderRequest.stock_code, trade_type: TradeType.SELL, amount: stockOrderRequest.amount, price: stockOrderRequest.price, From 4ac4733501dadab2af09d8b7121a3fe16ad8cfc8 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 12 Nov 2024 22:07:28 +0900 Subject: [PATCH 073/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B2=B4=EA=B2=B0=20=EB=82=B4=EC=97=AD=20API?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20DTO,=20interface=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/stock-trade-history-data.dto.ts | 18 ++++++++++++++ .../dto/stock-trade-history-output.dto.ts | 24 +++++++++++++++++++ ...stock-trade-history-query-parameter.dto.ts | 16 +++++++++++++ .../dto/stock-trade-history-response.dto.ts | 10 ++++++++ .../interface/Inquire-ccnl.interface.ts | 16 +++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts create mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts create mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts create mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts create mode 100644 BE/src/stock/trade/history/interface/Inquire-ccnl.interface.ts diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts new file mode 100644 index 00000000..3ead1198 --- /dev/null +++ b/BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockTradeHistoryDataDto { + @ApiProperty({ description: '주식 체결 시간' }) + stck_cntg_hour: string; + + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '체결 거래량' }) + cntg_vol: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; +} diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts new file mode 100644 index 00000000..05415c64 --- /dev/null +++ b/BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockTradeHistoryOutputDto { + @ApiProperty({ description: '주식 체결 시간' }) + stck_cntg_hour: string; + + @ApiProperty({ description: '주식 현재가' }) + stck_prpr: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '체결 거래량' }) + cntg_vol: string; + + @ApiProperty({ description: '당일 체결강도' }) + tday_rltv: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; +} diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts new file mode 100644 index 00000000..6d9ccfa6 --- /dev/null +++ b/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts @@ -0,0 +1,16 @@ +/** + * 주식현재가 체결 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO + */ +export class StockTradeHistoryQueryParameterDto { + /** + * 조건 시장 분류 코드 + * 'J' 주식 + */ + fid_cond_mrkt_div_code: string; + + /** + * 주식 종목 코드 + * (ex) 005930 + */ + fid_input_iscd: string; +} diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts new file mode 100644 index 00000000..265c1d5e --- /dev/null +++ b/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { StockTradeHistoryOutputDto } from './stock-trade-history-output.dto'; + +/** + * 주식현재가 체결 API 응답값 정제 후 FE에 보낼 DTO + */ +export class StockTradeHistoryResponseDto { + @ApiProperty({ type: StockTradeHistoryOutputDto, description: '상승률 순위' }) + output: StockTradeHistoryOutputDto[]; +} diff --git a/BE/src/stock/trade/history/interface/Inquire-ccnl.interface.ts b/BE/src/stock/trade/history/interface/Inquire-ccnl.interface.ts new file mode 100644 index 00000000..94a19e5e --- /dev/null +++ b/BE/src/stock/trade/history/interface/Inquire-ccnl.interface.ts @@ -0,0 +1,16 @@ +export interface InquireCCNLOutputData { + stck_cntg_hour: string; + stck_prpr: string; + prdy_vrss: string; + prdy_vrss_sign: string; + cntg_vol: string; + tday_rltv: string; + prdy_ctrt: string; +} + +export interface InquireCCNLApiResponse { + output: InquireCCNLOutputData[]; + rt_cd: string; + msg_cd: string; + msg1: string; +} From afe6c79d7cbf1e184854f26d5c915792d05a0a15 Mon Sep 17 00:00:00 2001 From: JIN Date: Tue, 12 Nov 2024 22:08:22 +0900 Subject: [PATCH 074/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=8B=A4=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B2=B4=EA=B2=B0=20=EB=82=B4=EC=97=AD=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 + .../history/stock-trade-history.controller.ts | 29 +++++ .../history/stock-trade-history.module.ts | 11 ++ .../history/stock-trade-history.service.ts | 110 ++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 BE/src/stock/trade/history/stock-trade-history.controller.ts create mode 100644 BE/src/stock/trade/history/stock-trade-history.module.ts create mode 100644 BE/src/stock/trade/history/stock-trade-history.service.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 93e45a4e..4b138032 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -12,6 +12,7 @@ import { SocketModule } from './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 { StockTradeHistoryModule } from './stock/trade/history/stock-trade-history.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { typeOrmConfig } from './configs/typeorm.config'; SocketModule, StockDetailModule, StockOrderModule, + StockTradeHistoryModule, ], controllers: [AppController], providers: [AppService], diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts new file mode 100644 index 00000000..11efa7fa --- /dev/null +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -0,0 +1,29 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { StockTradeHistoryService } from './stock-trade-history.service'; +import { StockTradeHistoryResponseDto } from './dto/stock-trade-history-response.dto'; + +@Controller('/api/stocks') +export class StockTradeHistoryController { + constructor( + private readonly stockTradeHistoryService: StockTradeHistoryService, + ) {} + + @Get(':stockCode/trade-history') + @ApiOperation({ summary: '단일 주식 종목에 대한 주식현재가 체결 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: '단일 주식 종목에 대한 주식현재가 체결값 조회 성공', + type: StockTradeHistoryResponseDto, + }) + getStockDetail(@Param('stockCode') stockCode: string) { + return this.stockTradeHistoryService.getStockTradeHistory(stockCode); + } +} diff --git a/BE/src/stock/trade/history/stock-trade-history.module.ts b/BE/src/stock/trade/history/stock-trade-history.module.ts new file mode 100644 index 00000000..965889a1 --- /dev/null +++ b/BE/src/stock/trade/history/stock-trade-history.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { KoreaInvestmentModule } from '../../../koreaInvestment/korea-investment.module'; +import { StockTradeHistoryController } from './stock-trade-history.controller'; +import { StockTradeHistoryService } from './stock-trade-history.service'; + +@Module({ + imports: [KoreaInvestmentModule], + controllers: [StockTradeHistoryController], + providers: [StockTradeHistoryService], +}) +export class StockTradeHistoryModule {} diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts new file mode 100644 index 00000000..dc9b24e4 --- /dev/null +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -0,0 +1,110 @@ +import axios from 'axios'; +import { Injectable, Logger } from '@nestjs/common'; +import { KoreaInvestmentService } from '../../../koreaInvestment/korea-investment.service'; +import { getHeader } from '../../../util/get-header'; +import { getFullURL } from '../../../util/get-full-URL'; +import { StockTradeHistoryQueryParameterDto } from './dto/stock-trade-history-query-parameter.dto'; +import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; +import { StockTradeHistoryOutputDto } from './dto/stock-trade-history-output.dto'; +import { StockTradeHistoryDataDto } from './dto/stock-trade-history-data.dto'; + +@Injectable() +export class StockTradeHistoryService { + private readonly logger = new Logger(); + + constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + + /** + * 특정 주식의 현재가 체결 데이터를 반환하는 함수 + * @param {string} stockCode - 종목코드 + * @returns - 특정 주식의 현재가 체결 데이터 객체 반환 + * + * @author uuuo3o + */ + async getStockTradeHistory(stockCode: string) { + try { + const queryParams = new StockTradeHistoryQueryParameterDto(); + queryParams.fid_cond_mrkt_div_code = 'J'; + queryParams.fid_input_iscd = stockCode; + + const response = await this.requestApi(queryParams); + + return this.formatTradeHistoryData(response.output); + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + + /** + * @private 한국투자 Open API - [국내주식] 기본시세 - 주식현재가 체결 호출 함수 + * @param {StockTradeHistoryQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - 주식현재가 체결 데이터 + * + * @author uuuo3o + */ + private async requestApi(queryParams: StockTradeHistoryQueryParameterDto) { + try { + const accessToken = await this.koreaInvetmentService.getAccessToken(); + const headers = getHeader(accessToken, 'FHKST01010300'); + const url = getFullURL('/uapi/domestic-stock/v1/quotations/inquire-ccnl'); + const params = this.getTradeHistoryParams(queryParams); + + const response = await axios.get(url, { + headers, + params, + }); + + return response.data; + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, + message: error.message, + }); + throw error; + } + } + + /** + * @private API에서 받은 주식현재가 체결 데이터를 필요한 정보로 정제하는 함수 + * @param {StockTradeHistoryOutputDto} infos - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatTradeHistoryData(infos: StockTradeHistoryOutputDto[]) { + return infos.map((info) => { + const infoData = new StockTradeHistoryDataDto(); + infoData.stck_cntg_hour = info.stck_cntg_hour; + infoData.stck_prpr = info.stck_prpr; + infoData.prdy_vrss_sign = info.prdy_vrss_sign; + infoData.cntg_vol = info.cntg_vol; + infoData.prdy_ctrt = info.prdy_ctrt; + + return infoData; + }); + } + + /** + * @private 주식현재가 체결 요청을 위한 쿼리 파라미터 객체 생성 함수 + * @param {StockTradeHistoryQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO + * @returns - API 요청에 필요한 쿼리 파라미터 객체 + * + * @author uuuo3o + */ + private getTradeHistoryParams(params: StockTradeHistoryQueryParameterDto) { + return { + fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, + fid_input_iscd: params.fid_input_iscd, + }; + } +} From c5db15399300d36683d1746573b3e15146835d2c Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 21:10:17 +0900 Subject: [PATCH 075/158] =?UTF-8?q?=E2=9E=95=20add=20:=20=20stock=20Entity?= =?UTF-8?q?=20=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=9E=91=EC=84=B1(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/stock-list.entity.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 BE/src/stock/list/stock-list.entity.ts diff --git a/BE/src/stock/list/stock-list.entity.ts b/BE/src/stock/list/stock-list.entity.ts new file mode 100644 index 00000000..a8a1cdba --- /dev/null +++ b/BE/src/stock/list/stock-list.entity.ts @@ -0,0 +1,13 @@ +import { BaseEntity, Column, Entity } from 'typeorm'; + +@Entity() +export class Stock extends BaseEntity { + @Column({ primary: true }) + code: string; + + @Column() + name: string; + + @Column() + market: string; +} From 9e32903eeefc76c93e74724f586e975d33904496 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 21:14:14 +0900 Subject: [PATCH 076/158] =?UTF-8?q?=E2=9E=95=20add=20:=20stockListReposito?= =?UTF-8?q?ry=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=91=EC=84=B1(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/stock-list.repostiory.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 BE/src/stock/list/stock-list.repostiory.ts diff --git a/BE/src/stock/list/stock-list.repostiory.ts b/BE/src/stock/list/stock-list.repostiory.ts new file mode 100644 index 00000000..31292d03 --- /dev/null +++ b/BE/src/stock/list/stock-list.repostiory.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { Stock } from './stock-list.entity'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource, Repository } from 'typeorm'; + +@Injectable() +export class StockListRepository extends Repository { + constructor(@InjectDataSource() dataSource: DataSource) { + super(Stock, dataSource.createEntityManager()); + } +} From f545e676cfe626b66d139d2139335e97035d3c79 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 22:54:20 +0900 Subject: [PATCH 077/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=EC=A3=BC?= =?UTF-8?q?=EC=8B=9D=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=20=EA=B0=80=EC=A0=B8?= =?UTF-8?q?=EC=98=A4=EA=B8=B0=20API=20=EA=B5=AC=ED=98=84(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 2 + BE/src/stock/list/dto/stock-response.dto.ts | 12 +++++ .../list/interface/search-params.interface.ts | 5 ++ BE/src/stock/list/stock-list.controller.ts | 53 +++++++++++++++++++ BE/src/stock/list/stock-list.entity.ts | 6 +-- BE/src/stock/list/stock-list.module.ts | 14 +++++ BE/src/stock/list/stock-list.repostiory.ts | 31 +++++++++-- BE/src/stock/list/stock-list.service.ts | 33 ++++++++++++ 8 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 BE/src/stock/list/dto/stock-response.dto.ts create mode 100644 BE/src/stock/list/interface/search-params.interface.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 93e45a4e..c62056ef 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -12,6 +12,7 @@ import { SocketModule } from './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'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { typeOrmConfig } from './configs/typeorm.config'; SocketModule, StockDetailModule, StockOrderModule, + StockListModule, ], controllers: [AppController], providers: [AppService], diff --git a/BE/src/stock/list/dto/stock-response.dto.ts b/BE/src/stock/list/dto/stock-response.dto.ts new file mode 100644 index 00000000..9bf7c3bf --- /dev/null +++ b/BE/src/stock/list/dto/stock-response.dto.ts @@ -0,0 +1,12 @@ +import { Stocks } from '../stock-list.entity'; +export class StockResponseDto { + code: string; + name: string; + market: string; + + constructor(code: string, name: string, market: string) { + this.code = code; + this.name = name; + this.market = market; + } +} diff --git a/BE/src/stock/list/interface/search-params.interface.ts b/BE/src/stock/list/interface/search-params.interface.ts new file mode 100644 index 00000000..88080d0a --- /dev/null +++ b/BE/src/stock/list/interface/search-params.interface.ts @@ -0,0 +1,5 @@ +export interface SearchParams { + name?: string; + market?: string; + code?: string; +} diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index e69de29b..293b0b7c 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -0,0 +1,53 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { StockListService } from './stock-list.service'; +import { StockResponseDto } from './dto/stock-response.dto'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; + +@ApiTags('주식 리스트 API') +@Controller('/api/stocks/list') +export class StockListController { + constructor(private readonly stockListService: StockListService) {} + + @ApiOperation({ + summary: '전체 주식 종목 조회 API', + description: '모든 주식 종목 리스트를 조회한다.', + }) + @Get() + async findAll(): Promise { + return await this.stockListService.findAll(); + } + + @ApiOperation({ + summary: '주식 목록 검색 API', + description: + '주식 종목을 검색한다. name, market, code로 검색을 진행할 수 있다.', + }) + @ApiResponse({ + status: 200, + description: '주식 검색 성공', + type: StockResponseDto, + isArray: true, + }) + @Get('/search') + async searchWithQuery( + @Query('name') name?: string, + @Query('market') market?: string, + @Query('code') code?: string, + ): Promise { + return await this.stockListService.search({ name, market, code }); + } + + @ApiOperation({ + summary: '특정 주식 종목 조회 API', + description: 'code를 이용해 특정 주식 정보를 조회한다.', + }) + @ApiResponse({ + status: 200, + description: 'code를 이용한 주식 조회 성공', + type: StockResponseDto, + }) + @Get('/:code') + async findOne(@Query('code') code: string): Promise { + return await this.stockListService.findOne(code); + } +} diff --git a/BE/src/stock/list/stock-list.entity.ts b/BE/src/stock/list/stock-list.entity.ts index a8a1cdba..37182a41 100644 --- a/BE/src/stock/list/stock-list.entity.ts +++ b/BE/src/stock/list/stock-list.entity.ts @@ -1,8 +1,8 @@ -import { BaseEntity, Column, Entity } from 'typeorm'; +import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; @Entity() -export class Stock extends BaseEntity { - @Column({ primary: true }) +export class Stocks extends BaseEntity { + @PrimaryColumn() code: string; @Column() diff --git a/BE/src/stock/list/stock-list.module.ts b/BE/src/stock/list/stock-list.module.ts index e69de29b..0e71e2d9 100644 --- a/BE/src/stock/list/stock-list.module.ts +++ b/BE/src/stock/list/stock-list.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { StockListRepository } from './stock-list.repostiory'; +import { StockListService } from './stock-list.service'; +import { StockListController } from './stock-list.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Stocks } from './stock-list.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Stocks])], + controllers: [StockListController], + providers: [StockListRepository, StockListService], + exports: [], +}) +export class StockListModule {} diff --git a/BE/src/stock/list/stock-list.repostiory.ts b/BE/src/stock/list/stock-list.repostiory.ts index 31292d03..09435b72 100644 --- a/BE/src/stock/list/stock-list.repostiory.ts +++ b/BE/src/stock/list/stock-list.repostiory.ts @@ -1,11 +1,36 @@ import { Injectable } from '@nestjs/common'; -import { Stock } from './stock-list.entity'; +import { Stocks } from './stock-list.entity'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; +import { SearchParams } from './interface/search-params.interface'; @Injectable() -export class StockListRepository extends Repository { +export class StockListRepository extends Repository { constructor(@InjectDataSource() dataSource: DataSource) { - super(Stock, dataSource.createEntityManager()); + super(Stocks, dataSource.createEntityManager()); + } + + async findAllStocks() { + return await this.find(); + } + + async findOneStock(code: string): Promise { + return await this.findOne({ where: { code } }); + } + + async search(params: SearchParams): Promise { + const queryBuilder = this.createQueryBuilder(); + if (params.name) { + queryBuilder.where('name LIKE :name', { name: `%${params.name}%` }); + } + if (params.market) { + queryBuilder.andWhere('market LIKE :market', { + market: `%${params.market}%`, + }); + } + if (params.code) { + queryBuilder.andWhere('code LIKE :code', { code: `%${params.code}%` }); + } + return await queryBuilder.getMany(); } } diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index e69de29b..06e519af 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -0,0 +1,33 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { StockListRepository } from './stock-list.repostiory'; +import { Stocks } from './stock-list.entity'; +import { StockResponseDto } from './dto/stock-response.dto'; +import { SearchParams } from './interface/search-params.interface'; + +@Injectable() +export class StockListService { + constructor(private readonly stockListRepository: StockListRepository) {} + + private toResponseDto(stock: Stocks): StockResponseDto { + return new StockResponseDto(stock.code, stock.name, stock.market); + } + + async findAll() { + const stocks = await this.stockListRepository.findAllStocks(); + return stocks.map((stock) => this.toResponseDto(stock)); + } + + async findOne(code: string) { + const stock = await this.stockListRepository.findOneStock(code); + + if (!stock) { + throw new NotFoundException(`Stock with code ${code} not found`); + } + return this.toResponseDto(stock); + } + + async search(params: SearchParams): Promise { + const stocks = await this.stockListRepository.search(params); + return stocks.map((stock) => this.toResponseDto(stock)); + } +} From 57f6b6ffa7603d9356f47e7991dc89a18cd9d297 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 23:10:29 +0900 Subject: [PATCH 078/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20lin?= =?UTF-8?q?t=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/dto/stock-response.dto.ts | 1 - BE/src/stock/list/stock-list.controller.ts | 8 ++++---- BE/src/stock/list/stock-list.module.ts | 2 +- BE/src/stock/list/stock-list.repostiory.ts | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/BE/src/stock/list/dto/stock-response.dto.ts b/BE/src/stock/list/dto/stock-response.dto.ts index 9bf7c3bf..6ab35866 100644 --- a/BE/src/stock/list/dto/stock-response.dto.ts +++ b/BE/src/stock/list/dto/stock-response.dto.ts @@ -1,4 +1,3 @@ -import { Stocks } from '../stock-list.entity'; export class StockResponseDto { code: string; name: string; diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index 293b0b7c..62818791 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockListService } from './stock-list.service'; import { StockResponseDto } from './dto/stock-response.dto'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; @ApiTags('주식 리스트 API') @Controller('/api/stocks/list') @@ -14,7 +14,7 @@ export class StockListController { }) @Get() async findAll(): Promise { - return await this.stockListService.findAll(); + return this.stockListService.findAll(); } @ApiOperation({ @@ -34,7 +34,7 @@ export class StockListController { @Query('market') market?: string, @Query('code') code?: string, ): Promise { - return await this.stockListService.search({ name, market, code }); + return this.stockListService.search({ name, market, code }); } @ApiOperation({ @@ -48,6 +48,6 @@ export class StockListController { }) @Get('/:code') async findOne(@Query('code') code: string): Promise { - return await this.stockListService.findOne(code); + return this.stockListService.findOne(code); } } diff --git a/BE/src/stock/list/stock-list.module.ts b/BE/src/stock/list/stock-list.module.ts index 0e71e2d9..9087c692 100644 --- a/BE/src/stock/list/stock-list.module.ts +++ b/BE/src/stock/list/stock-list.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { StockListRepository } from './stock-list.repostiory'; import { StockListService } from './stock-list.service'; import { StockListController } from './stock-list.controller'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { Stocks } from './stock-list.entity'; @Module({ diff --git a/BE/src/stock/list/stock-list.repostiory.ts b/BE/src/stock/list/stock-list.repostiory.ts index 09435b72..2bad5fa9 100644 --- a/BE/src/stock/list/stock-list.repostiory.ts +++ b/BE/src/stock/list/stock-list.repostiory.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { Stocks } from './stock-list.entity'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; +import { Stocks } from './stock-list.entity'; import { SearchParams } from './interface/search-params.interface'; @Injectable() @@ -11,11 +11,11 @@ export class StockListRepository extends Repository { } async findAllStocks() { - return await this.find(); + return this.find(); } async findOneStock(code: string): Promise { - return await this.findOne({ where: { code } }); + return this.findOne({ where: { code } }); } async search(params: SearchParams): Promise { @@ -31,6 +31,6 @@ export class StockListRepository extends Repository { if (params.code) { queryBuilder.andWhere('code LIKE :code', { code: `%${params.code}%` }); } - return await queryBuilder.getMany(); + return queryBuilder.getMany(); } } From 92a6402b99b6b3d32e2f268d111675b7dc31d6b7 Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 23:17:48 +0900 Subject: [PATCH 079/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20sto?= =?UTF-8?q?ckResponseDto=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20swagger=20doc?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20API=20Property=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/list/dto/stock-list-response.dto.ts | 18 ++++++++++++++++++ BE/src/stock/list/dto/stock-response.dto.ts | 11 ----------- BE/src/stock/list/stock-list.controller.ts | 12 ++++++------ BE/src/stock/list/stock-list.service.ts | 8 ++++---- 4 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 BE/src/stock/list/dto/stock-list-response.dto.ts delete mode 100644 BE/src/stock/list/dto/stock-response.dto.ts diff --git a/BE/src/stock/list/dto/stock-list-response.dto.ts b/BE/src/stock/list/dto/stock-list-response.dto.ts new file mode 100644 index 00000000..b1dcb7b9 --- /dev/null +++ b/BE/src/stock/list/dto/stock-list-response.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class StockListResponseDto { + @ApiProperty({ example: '005930', description: '종목 코드' }) + code: string; + + @ApiProperty({ example: '삼성전자', description: '종목 이름' }) + name: string; + + @ApiProperty({ example: 'KOSPI', description: '시장' }) + market: string; + + constructor(code: string, name: string, market: string) { + this.code = code; + this.name = name; + this.market = market; + } +} diff --git a/BE/src/stock/list/dto/stock-response.dto.ts b/BE/src/stock/list/dto/stock-response.dto.ts deleted file mode 100644 index 6ab35866..00000000 --- a/BE/src/stock/list/dto/stock-response.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class StockResponseDto { - code: string; - name: string; - market: string; - - constructor(code: string, name: string, market: string) { - this.code = code; - this.name = name; - this.market = market; - } -} diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index 62818791..33f7d2ee 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Query } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockListService } from './stock-list.service'; -import { StockResponseDto } from './dto/stock-response.dto'; +import { StockListResponseDto } from './dto/stock-list-response.dto'; @ApiTags('주식 리스트 API') @Controller('/api/stocks/list') @@ -13,7 +13,7 @@ export class StockListController { description: '모든 주식 종목 리스트를 조회한다.', }) @Get() - async findAll(): Promise { + async findAll(): Promise { return this.stockListService.findAll(); } @@ -25,7 +25,7 @@ export class StockListController { @ApiResponse({ status: 200, description: '주식 검색 성공', - type: StockResponseDto, + type: StockListResponseDto, isArray: true, }) @Get('/search') @@ -33,7 +33,7 @@ export class StockListController { @Query('name') name?: string, @Query('market') market?: string, @Query('code') code?: string, - ): Promise { + ): Promise { return this.stockListService.search({ name, market, code }); } @@ -44,10 +44,10 @@ export class StockListController { @ApiResponse({ status: 200, description: 'code를 이용한 주식 조회 성공', - type: StockResponseDto, + type: StockListResponseDto, }) @Get('/:code') - async findOne(@Query('code') code: string): Promise { + async findOne(@Query('code') code: string): Promise { return this.stockListService.findOne(code); } } diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index 06e519af..1c46c5bb 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -1,15 +1,15 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { StockListRepository } from './stock-list.repostiory'; import { Stocks } from './stock-list.entity'; -import { StockResponseDto } from './dto/stock-response.dto'; +import { StockListResponseDto } from './dto/stock-list-response.dto'; import { SearchParams } from './interface/search-params.interface'; @Injectable() export class StockListService { constructor(private readonly stockListRepository: StockListRepository) {} - private toResponseDto(stock: Stocks): StockResponseDto { - return new StockResponseDto(stock.code, stock.name, stock.market); + private toResponseDto(stock: Stocks): StockListResponseDto { + return new StockListResponseDto(stock.code, stock.name, stock.market); } async findAll() { @@ -26,7 +26,7 @@ export class StockListService { return this.toResponseDto(stock); } - async search(params: SearchParams): Promise { + async search(params: SearchParams): Promise { const stocks = await this.stockListRepository.search(params); return stocks.map((stock) => this.toResponseDto(stock)); } From 2d1ba002fd238ab4235ea830fb94c068641de84e Mon Sep 17 00:00:00 2001 From: jinddings Date: Tue, 12 Nov 2024 23:43:33 +0900 Subject: [PATCH 080/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20swa?= =?UTF-8?q?gger=20docs=EC=97=90=20=ED=95=84=EC=88=98=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20=EA=B0=92=20=ED=91=9C=EC=8B=9C=ED=95=98=EA=B2=8C=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 --- BE/src/auth/dto/auth-credentials.dto.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BE/src/auth/dto/auth-credentials.dto.ts b/BE/src/auth/dto/auth-credentials.dto.ts index 31bc69b6..90b81254 100644 --- a/BE/src/auth/dto/auth-credentials.dto.ts +++ b/BE/src/auth/dto/auth-credentials.dto.ts @@ -27,6 +27,7 @@ export class AuthCredentialsDto { @ApiProperty({ description: '카카오 ID', + required: false, }) @IsString() @IsOptional() @@ -34,6 +35,7 @@ export class AuthCredentialsDto { @ApiProperty({ description: '카카오 액세스 토큰', + required: false, }) @IsString() @IsOptional() From 69c0ba53dbbba7839a9d3bf769c46034313163c8 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 11:36:58 +0900 Subject: [PATCH 081/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20cors=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/main.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BE/src/main.ts b/BE/src/main.ts index f64342a0..ed080e3c 100644 --- a/BE/src/main.ts +++ b/BE/src/main.ts @@ -12,6 +12,8 @@ async function bootstrap() { origin: [ 'http://localhost:5173', 'http://223.130.151.42:5173', + 'http://juga.kro.kr:5173', + 'http://juga.kro.kr:3000', 'http://223.130.151.42:3000', ], methods: 'GET, HEAD, PUT, PATH, POST, DELETE', From c570788925dda60cee284976d3af036392e82b4b Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 13 Nov 2024 11:56:38 +0900 Subject: [PATCH 082/158] =?UTF-8?q?=F0=9F=93=9D=20docs=20:=20swagger=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20Parameter=20=ED=95=84=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/dto/stock-list-response.dto.ts | 10 +++++++--- BE/src/stock/list/stock-list.controller.ts | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/BE/src/stock/list/dto/stock-list-response.dto.ts b/BE/src/stock/list/dto/stock-list-response.dto.ts index b1dcb7b9..d7df39b4 100644 --- a/BE/src/stock/list/dto/stock-list-response.dto.ts +++ b/BE/src/stock/list/dto/stock-list-response.dto.ts @@ -1,13 +1,17 @@ import { ApiProperty } from '@nestjs/swagger'; export class StockListResponseDto { - @ApiProperty({ example: '005930', description: '종목 코드' }) + @ApiProperty({ example: '005930', description: '종목 코드', required: false }) code: string; - @ApiProperty({ example: '삼성전자', description: '종목 이름' }) + @ApiProperty({ + example: '삼성전자', + description: '종목 이름', + required: false, + }) name: string; - @ApiProperty({ example: 'KOSPI', description: '시장' }) + @ApiProperty({ example: 'KOSPI', description: '시장', required: false }) market: string; constructor(code: string, name: string, market: string) { diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index 33f7d2ee..427f9ff8 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Query } from '@nestjs/common'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockListService } from './stock-list.service'; import { StockListResponseDto } from './dto/stock-list-response.dto'; @@ -28,6 +28,9 @@ export class StockListController { type: StockListResponseDto, isArray: true, }) + @ApiQuery({ name: 'name', required: false }) + @ApiQuery({ name: 'market', required: false }) + @ApiQuery({ name: 'code', required: false }) @Get('/search') async searchWithQuery( @Query('name') name?: string, From dc4184997d418d2595ad9b5765c31f10c97961b2 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 12:17:31 +0900 Subject: [PATCH 083/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=86=8C=EC=BC=93?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/base-socket.service.ts | 17 ++++++++++++++++- BE/src/websocket/socket.gateway.ts | 7 +++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 9c39d431..848999e9 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -2,6 +2,7 @@ import { WebSocket } from 'ws'; import { Injectable, InternalServerErrorException, + Logger, OnModuleInit, } from '@nestjs/common'; import { SocketTokenService } from './socket-token.service'; @@ -15,6 +16,8 @@ export class BaseSocketService implements OnModuleInit { [key: string]: (data) => void; } = {}; + private readonly logger = new Logger(); + constructor(private readonly socketTokenService: SocketTokenService) {} async onModuleInit() { @@ -37,9 +40,21 @@ export class BaseSocketService implements OnModuleInit { typeof event.data === 'string' ? event.data.split('|') : JSON.stringify(event.data); - if (data.length < 2) return; + + if (data.length < 2) { + const json = JSON.parse(data[0]); + this.logger.log( + `한국투자증권 웹소켓 연결: ${json.body.msg1}`, + json.header.tr_id, + ); + return; + } const dataList = data[3].split('^'); + + if (Number(dataList[1]) % 500 === 0) + this.logger.log(`한국투자증권 데이터 수신 성공 (5분 단위)`, data[1]); + this.socketDataHandlers[data[1]](dataList); }; } diff --git a/BE/src/websocket/socket.gateway.ts b/BE/src/websocket/socket.gateway.ts index e65eac69..d533c3f0 100644 --- a/BE/src/websocket/socket.gateway.ts +++ b/BE/src/websocket/socket.gateway.ts @@ -1,16 +1,23 @@ import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; import { Server } from 'socket.io'; +import { Logger } from '@nestjs/common'; @WebSocketGateway({ namespace: 'socket', cors: { origin: '*' } }) export class SocketGateway { @WebSocketServer() private server: Server; + private readonly logger = new Logger(); + sendStockIndexListToClient(stockChart) { this.server.emit('chart', stockChart); } sendStockIndexValueToClient(event, stockIndexValue) { + const now = new Date(); + if (now.getMinutes() % 5 === 0 && now.getSeconds() === 0) + this.logger.log('한국투자증권 데이터 발신 성공 (5분 단위)', event); + this.server.emit(event, stockIndexValue); } } From 36356bfa2c04299702a40efa362aead54cc86e78 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 13 Nov 2024 13:01:12 +0900 Subject: [PATCH 084/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20ssh=20?= =?UTF-8?q?=ED=84=B0=EB=84=90=EB=A7=81=EC=9D=84=20=EC=9D=B4=EC=9A=A9?= =?UTF-8?q?=ED=95=B4=20NCP=20mysql=EC=97=90=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/stock-list.script.py | 62 +++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/scripts/stock-list.script.py b/scripts/stock-list.script.py index df5290aa..c33e1366 100644 --- a/scripts/stock-list.script.py +++ b/scripts/stock-list.script.py @@ -1,9 +1,9 @@ import FinanceDataReader as fdr -import json import os -import mysql.connector +import pymysql from dotenv import load_dotenv from pathlib import Path +from sshtunnel import SSHTunnelForwarder root_dir = Path(__file__).parent env_path = os.path.join(root_dir, '.env') @@ -17,30 +17,40 @@ 'database' : os.getenv('DB_DATABASE'), } -def insert_stocks(stockData) : - try : - conn = mysql.connector.connect(**db_config) - cursor = conn.cursor() - - insert_query = """INSERT INTO stocks (code, name, market) VALUES (%s, %s, %s) - ON DUPLICATE KEY UPDATE name = VALUES(name), market = VALUES(market)""" +if __name__ == '__main__': + + with SSHTunnelForwarder( + (os.getenv('SSH_HOST'), 22), + ssh_username=os.getenv('SSH_USERNAME'), + ssh_password=os.getenv('SSH_PASSWD'), + remote_bind_address=(os.getenv('DB_HOST'), 3306) + ) as tunnel: - records = stockData.to_dict('records') - for record in records: - values = (record['Code'], record['Name'], record['Market']) - cursor.execute(insert_query, values) - conn.commit() + with pymysql.connect( + host='127.0.0.1', + user=os.getenv('DB_USERNAME'), + password=os.getenv('DB_PASSWD'), + db=os.getenv('DB_DATABASE'), + charset='utf8', + port=tunnel.local_bind_port, + cursorclass=pymysql.cursors.DictCursor) as conn: - except Exception as e : - print(e) - conn.rollback() - - finally : - if conn.is_connected() : - cursor.close() - conn.close() - -df_krx = fdr.StockListing('KRX') -df_selected = df_krx[['Code','Name','Market']] + with conn.cursor() as cursor: + try : + df_krx = fdr.StockListing('KRX') + df_selected = df_krx[['Code','Name','Market']] + stockData = df_selected + insert_query = """INSERT INTO stocks (code, name, market) VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE name = VALUES(name), market = VALUES(market)""" + + records = stockData.to_dict('records') + for record in records: + values = (record['Code'], record['Name'], record['Market']) + cursor.execute(insert_query, values) + conn.commit() + + except Exception as e : + print(e) + conn.rollback() -insert_stocks(df_selected) \ No newline at end of file + \ No newline at end of file From 01e32939655a73777c93dd1e74c67fef81efc56c Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 14:19:15 +0900 Subject: [PATCH 085/158] =?UTF-8?q?=F0=9F=94=A5=20remove:=20stock-item=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/item/enum/market-type.ts | 4 ---- BE/src/stock/item/stock-item.controller.ts | 9 --------- BE/src/stock/item/stock-item.entity.ts | 18 ------------------ BE/src/stock/item/stock-item.module.ts | 14 -------------- BE/src/stock/item/stock-item.repository.ts | 11 ----------- BE/src/stock/item/stock-item.service.ts | 13 ------------- 6 files changed, 69 deletions(-) delete mode 100644 BE/src/stock/item/enum/market-type.ts delete mode 100644 BE/src/stock/item/stock-item.controller.ts delete mode 100644 BE/src/stock/item/stock-item.entity.ts delete mode 100644 BE/src/stock/item/stock-item.module.ts delete mode 100644 BE/src/stock/item/stock-item.repository.ts delete mode 100644 BE/src/stock/item/stock-item.service.ts diff --git a/BE/src/stock/item/enum/market-type.ts b/BE/src/stock/item/enum/market-type.ts deleted file mode 100644 index 691fdee3..00000000 --- a/BE/src/stock/item/enum/market-type.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MarketType { - KOSPI = 'KOSPI', - KOSDAQ = 'KOSDAQ', -} diff --git a/BE/src/stock/item/stock-item.controller.ts b/BE/src/stock/item/stock-item.controller.ts deleted file mode 100644 index 5eef4530..00000000 --- a/BE/src/stock/item/stock-item.controller.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { StockItemService } from './stock-item.service'; - -@Controller('/api/stocks/item') -@ApiTags('주식 개별 주 API') -export class StockItemController { - constructor(private readonly stockItemService: StockItemService) {} -} diff --git a/BE/src/stock/item/stock-item.entity.ts b/BE/src/stock/item/stock-item.entity.ts deleted file mode 100644 index 78c1e731..00000000 --- a/BE/src/stock/item/stock-item.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; -import { MarketType } from './enum/market-type'; - -@Entity('stocks') -export class Stock { - @PrimaryColumn() - code: string; - - @Column({ nullable: false }) - name: string; - - @Column({ - type: 'enum', - enum: MarketType, - nullable: false, - }) - market: MarketType; -} diff --git a/BE/src/stock/item/stock-item.module.ts b/BE/src/stock/item/stock-item.module.ts deleted file mode 100644 index ad915d14..00000000 --- a/BE/src/stock/item/stock-item.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { StockItemController } from './stock-item.controller'; -import { StockItemService } from './stock-item.service'; -import { Stock } from './stock-item.entity'; -import { StockItemRepository } from './stock-item.repository'; - -@Module({ - imports: [TypeOrmModule.forFeature([Stock])], - controllers: [StockItemController], - providers: [StockItemService, StockItemRepository], - exports: [StockItemService], -}) -export class StockItemModule {} diff --git a/BE/src/stock/item/stock-item.repository.ts b/BE/src/stock/item/stock-item.repository.ts deleted file mode 100644 index e0af08b8..00000000 --- a/BE/src/stock/item/stock-item.repository.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { InjectDataSource } from '@nestjs/typeorm'; -import { Injectable } from '@nestjs/common'; -import { Stock } from './stock-item.entity'; - -@Injectable() -export class StockItemRepository extends Repository { - constructor(@InjectDataSource() dataSource: DataSource) { - super(Stock, dataSource.createEntityManager()); - } -} diff --git a/BE/src/stock/item/stock-item.service.ts b/BE/src/stock/item/stock-item.service.ts deleted file mode 100644 index ef8a0389..00000000 --- a/BE/src/stock/item/stock-item.service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { StockItemRepository } from './stock-item.repository'; - -@Injectable() -export class StockItemService { - constructor(private readonly stockItemRepository: StockItemRepository) {} - - getAllStockItems() { - return this.stockItemRepository.find({ - select: ['code'], - }); - } -} From eb204fd10e0f6c2816564566585a6ef285fb450c Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 14:20:58 +0900 Subject: [PATCH 086/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20=EC=98=88=EC=95=BD=20=EC=8B=9C=EC=97=90=EB=A7=8C=20?= =?UTF-8?q?=EC=9B=B9=EC=86=8C=EC=BC=93=20=EA=B5=AC=EB=8F=85=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - forwardRef로 circular dependency 문제 해결 --- BE/src/stock/order/stock-order.module.ts | 5 ++-- BE/src/stock/order/stock-order.service.ts | 11 +++++++- BE/src/websocket/base-socket.service.ts | 19 ++++++++++++++ BE/src/websocket/socket.module.ts | 7 +++--- .../stock/price/stock-price-socket.service.ts | 25 +++++++++++-------- 5 files changed, 50 insertions(+), 17 deletions(-) diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index 0b1d480a..a31eee97 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -1,12 +1,13 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { StockOrderController } from './stock-order.controller'; import { StockOrderService } from './stock-order.service'; import { Order } from './stock-order.entity'; import { StockOrderRepository } from './stock-order.repository'; +import { SocketModule } from '../../websocket/socket.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order])], + imports: [TypeOrmModule.forFeature([Order]), forwardRef(() => SocketModule)], controllers: [StockOrderController], providers: [StockOrderService, StockOrderRepository], exports: [StockOrderService], diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 3221e13b..83bca461 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -1,6 +1,8 @@ import { ConflictException, ForbiddenException, + forwardRef, + Inject, Injectable, } from '@nestjs/common'; import { NotFoundError } from 'rxjs'; @@ -9,10 +11,15 @@ import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; import { StatusType } from './enum/status-type'; +import { StockPriceSocketService } from '../../websocket/stock/price/stock-price-socket.service'; @Injectable() export class StockOrderService { - constructor(private readonly stockOrderRepository: StockOrderRepository) {} + constructor( + private readonly stockOrderRepository: StockOrderRepository, + @Inject(forwardRef(() => StockPriceSocketService)) + private readonly stockPriceSocketService: StockPriceSocketService, + ) {} async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { const order = this.stockOrderRepository.create({ @@ -25,6 +32,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); + this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); } async sell(userId: number, stockOrderRequest: StockOrderRequestDto) { @@ -38,6 +46,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); + this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); } async cancel(userId: number, orderId: number) { diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 848999e9..a29808b3 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -78,6 +78,25 @@ export class BaseSocketService implements OnModuleInit { ); } + unregisterCode(trId: string, trKey: string) { + this.socket.send( + JSON.stringify({ + header: { + approval_key: this.socketConnectionKey, + custtype: 'P', + tr_type: '2', + 'content-type': 'utf-8', + }, + body: { + input: { + tr_id: trId, + tr_key: trKey, + }, + }, + }), + ); + } + registerSocketOpenHandler(handler: () => void | Promise) { this.socketOpenHandlers.push(handler); } diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts index e059658a..ca662e5b 100644 --- a/BE/src/websocket/socket.module.ts +++ b/BE/src/websocket/socket.module.ts @@ -1,14 +1,13 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { StockIndexSocketService } from './stock-index-socket.service'; import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; import { StockPriceSocketService } from './stock/price/stock-price-socket.service'; -import { StockItemModule } from '../stock/item/stock-item.module'; import { BaseSocketService } from './base-socket.service'; import { StockOrderModule } from '../stock/order/stock-order.module'; @Module({ - imports: [StockItemModule, StockOrderModule], + imports: [forwardRef(() => StockOrderModule)], controllers: [], providers: [ SocketTokenService, @@ -17,6 +16,6 @@ import { StockOrderModule } from '../stock/order/stock-order.module'; StockPriceSocketService, BaseSocketService, ], - exports: [SocketGateway], + exports: [SocketGateway, StockPriceSocketService], }) export class SocketModule {} diff --git a/BE/src/websocket/stock/price/stock-price-socket.service.ts b/BE/src/websocket/stock/price/stock-price-socket.service.ts index feefe457..b05fb7a9 100644 --- a/BE/src/websocket/stock/price/stock-price-socket.service.ts +++ b/BE/src/websocket/stock/price/stock-price-socket.service.ts @@ -1,7 +1,11 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { + forwardRef, + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; import { BaseSocketService } from '../../base-socket.service'; import { SocketGateway } from '../../socket.gateway'; -import { StockItemService } from '../../../stock/item/stock-item.service'; import { StockOrderService } from '../../../stock/order/stock-order.service'; @Injectable() @@ -11,16 +15,9 @@ export class StockPriceSocketService { constructor( private readonly socketGateway: SocketGateway, private readonly baseSocketService: BaseSocketService, - private readonly stockItemService: StockItemService, + @Inject(forwardRef(() => StockOrderService)) private readonly stockOrderService: StockOrderService, ) { - baseSocketService.registerSocketOpenHandler(async () => { - const stockList = await this.stockItemService.getAllStockItems(); - stockList.forEach((stock) => { - this.baseSocketService.registerCode(this.TR_ID, stock.code); - }); - }); - baseSocketService.registerSocketDataHandler( this.TR_ID, (data: string[]) => { @@ -35,4 +32,12 @@ export class StockPriceSocketService { }, ); } + + subscribeByCode(trKey: string) { + this.baseSocketService.registerCode(this.TR_ID, trKey); + } + + unsubscribeByCode(trKey: string) { + this.baseSocketService.unregisterCode(this.TR_ID, trKey); + } } From f1f0c0bbab0613abafba045425461f4975382d95 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 14:30:25 +0900 Subject: [PATCH 087/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20stock?= =?UTF-8?q?=20index=20socket=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/socket.module.ts | 2 +- .../{ => stock/index}/stock-index-socket.service.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename BE/src/websocket/{ => stock/index}/stock-index-socket.service.ts (84%) diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts index ca662e5b..5dd0e042 100644 --- a/BE/src/websocket/socket.module.ts +++ b/BE/src/websocket/socket.module.ts @@ -1,5 +1,5 @@ import { forwardRef, Module } from '@nestjs/common'; -import { StockIndexSocketService } from './stock-index-socket.service'; +import { StockIndexSocketService } from './stock/index/stock-index-socket.service'; import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; import { StockPriceSocketService } from './stock/price/stock-price-socket.service'; diff --git a/BE/src/websocket/stock-index-socket.service.ts b/BE/src/websocket/stock/index/stock-index-socket.service.ts similarity index 84% rename from BE/src/websocket/stock-index-socket.service.ts rename to BE/src/websocket/stock/index/stock-index-socket.service.ts index 751807f6..76ac9eb8 100644 --- a/BE/src/websocket/stock-index-socket.service.ts +++ b/BE/src/websocket/stock/index/stock-index-socket.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { StockIndexValueElementDto } from '../stock/index/dto/stock-index-value-element.dto'; -import { BaseSocketService } from './base-socket.service'; -import { SocketGateway } from './socket.gateway'; +import { StockIndexValueElementDto } from '../../../stock/index/dto/stock-index-value-element.dto'; +import { BaseSocketService } from '../../base-socket.service'; +import { SocketGateway } from '../../socket.gateway'; @Injectable() export class StockIndexSocketService { From 0530c30b15ac63354d6ffe3b6447a4f8b2bcbc3f Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 14:45:35 +0900 Subject: [PATCH 088/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=EC=8B=9C=20=EB=93=B1=EB=A1=9D=EB=90=9C?= =?UTF-8?q?=20=EC=A3=BC=EB=AC=B8=EC=97=90=20=EB=8C=80=ED=95=B4=20=EC=9B=B9?= =?UTF-8?q?=EC=86=8C=EC=BC=93=20=EA=B5=AC=EB=8F=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.service.ts | 6 ++++++ BE/src/websocket/stock/price/stock-price-socket.service.ts | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 83bca461..c940dff8 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -21,6 +21,12 @@ export class StockOrderService { private readonly stockPriceSocketService: StockPriceSocketService, ) {} + async findAllPendingOrder() { + return this.stockOrderRepository.findBy({ + status: StatusType.PENDING, + }); + } + async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { const order = this.stockOrderRepository.create({ user_id: userId, diff --git a/BE/src/websocket/stock/price/stock-price-socket.service.ts b/BE/src/websocket/stock/price/stock-price-socket.service.ts index b05fb7a9..93f0625c 100644 --- a/BE/src/websocket/stock/price/stock-price-socket.service.ts +++ b/BE/src/websocket/stock/price/stock-price-socket.service.ts @@ -18,6 +18,13 @@ export class StockPriceSocketService { @Inject(forwardRef(() => StockOrderService)) private readonly stockOrderService: StockOrderService, ) { + baseSocketService.registerSocketOpenHandler(async () => { + const orders = await stockOrderService.findAllPendingOrder(); + orders.forEach((order) => { + baseSocketService.registerCode(this.TR_ID, order.stock_code); + }); + }); + baseSocketService.registerSocketDataHandler( this.TR_ID, (data: string[]) => { From caf6a1a6803fbd166235d09b02d87eb71d883675 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 14:59:42 +0900 Subject: [PATCH 089/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=B2=B4=EA=B2=B0?= =?UTF-8?q?=20=EC=99=84=EB=A3=8C=20=EB=90=98=EA=B1=B0=EB=82=98=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=EB=90=9C=20=EC=A3=BC=EB=AC=B8=20=EC=A2=85=EB=AA=A9?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=B4=20=EA=B5=AC=EB=8F=85=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.service.ts | 30 ++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index c940dff8..29536678 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -67,9 +67,17 @@ export class StockOrderService { throw new ConflictException('이미 체결된 주문은 취소할 수 없습니다.'); await this.stockOrderRepository.remove(order); + + if ( + !(await this.stockOrderRepository.existsBy({ + stock_code: order.stock_code, + status: StatusType.PENDING, + })) + ) + this.stockPriceSocketService.unsubscribeByCode(order.stock_code); } - async checkExecutableOrder(stockCode, value) { + async checkExecutableOrder(stockCode: string, value) { const buyOrders = await this.stockOrderRepository.find({ where: { stock_code: stockCode, @@ -92,15 +100,31 @@ export class StockOrderService { await Promise.all( sellOrders.map((sellOrder) => this.executeSell(sellOrder)), ); + + if ( + !(await this.stockOrderRepository.existsBy({ + stock_code: stockCode, + status: StatusType.PENDING, + })) + ) + this.stockPriceSocketService.unsubscribeByCode(stockCode); } - private executeBuy(order) { + private async executeBuy(order) { // TODO: 매수 체결 로직 필요... console.log(`${order.id}번 매수 예약이 체결되었습니다.`); + await this.stockOrderRepository.update( + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); } - private executeSell(order) { + private async executeSell(order) { // TODO: 매도 체결 로직 필요... console.log(`${order.id}번 매도 예약이 체결되었습니다.`); + await this.stockOrderRepository.update( + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); } } From 7061ec1bb6f8f4538d8d7b6bdcba92673009632f Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 15:31:11 +0900 Subject: [PATCH 090/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=20=EC=B2=B4=EA=B2=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=B6=94=EA=B0=80=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 29536678..1aba3347 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -4,6 +4,7 @@ import { forwardRef, Inject, Injectable, + Logger, } from '@nestjs/common'; import { NotFoundError } from 'rxjs'; import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; @@ -21,6 +22,8 @@ export class StockOrderService { private readonly stockPriceSocketService: StockPriceSocketService, ) {} + private readonly logger = new Logger(); + async findAllPendingOrder() { return this.stockOrderRepository.findBy({ status: StatusType.PENDING, @@ -112,7 +115,7 @@ export class StockOrderService { private async executeBuy(order) { // TODO: 매수 체결 로직 필요... - console.log(`${order.id}번 매수 예약이 체결되었습니다.`); + this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); await this.stockOrderRepository.update( { id: order.id }, { status: StatusType.COMPLETE, completed_at: new Date() }, @@ -121,7 +124,7 @@ export class StockOrderService { private async executeSell(order) { // TODO: 매도 체결 로직 필요... - console.log(`${order.id}번 매도 예약이 체결되었습니다.`); + this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); await this.stockOrderRepository.update( { id: order.id }, { status: StatusType.COMPLETE, completed_at: new Date() }, From 8a4ec3871c8967e939b7a7b79bc64aff4ab08e4c Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 15:31:43 +0900 Subject: [PATCH 091/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20eslint=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/.eslintrc.js b/BE/.eslintrc.js index b54cc5a8..4807b847 100644 --- a/BE/.eslintrc.js +++ b/BE/.eslintrc.js @@ -35,5 +35,6 @@ module.exports = { '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/naming-convention': 'off', + 'import/no-cycle': 'off', }, }; From e42261d2f2ef785b21920d9f80265eee1c1baf7e Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 16:21:22 +0900 Subject: [PATCH 092/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX:=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/base-socket.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 848999e9..e2d137ba 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -43,10 +43,11 @@ export class BaseSocketService implements OnModuleInit { if (data.length < 2) { const json = JSON.parse(data[0]); - this.logger.log( - `한국투자증권 웹소켓 연결: ${json.body.msg1}`, - json.header.tr_id, - ); + if (json.body) + this.logger.log( + `한국투자증권 웹소켓 연결: ${json.body.msg1}`, + json.header.tr_id, + ); return; } From a6b6ea690d91091167bb0bb39b2c33fd9775b154 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 17:15:41 +0900 Subject: [PATCH 093/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=8B=9C=20assets=20table=EC=97=90?= =?UTF-8?q?=EB=8F=84=20row=20=EC=B6=94=EA=B0=80=EB=90=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.controller.ts | 6 ++++ BE/src/asset/asset.entity.ts | 30 +++++++++++++++++ BE/src/asset/asset.module.ts | 14 ++++++++ BE/src/asset/asset.repository.ts | 11 +++++++ BE/src/asset/asset.service.ts | 4 +++ BE/src/auth/auth.module.ts | 2 ++ BE/src/auth/user.repository.ts | 55 +++++++++++++++++++++++++------- 7 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 BE/src/asset/asset.controller.ts create mode 100644 BE/src/asset/asset.entity.ts create mode 100644 BE/src/asset/asset.module.ts create mode 100644 BE/src/asset/asset.repository.ts create mode 100644 BE/src/asset/asset.service.ts diff --git a/BE/src/asset/asset.controller.ts b/BE/src/asset/asset.controller.ts new file mode 100644 index 00000000..3bca562e --- /dev/null +++ b/BE/src/asset/asset.controller.ts @@ -0,0 +1,6 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@Controller('/api/assets') +@ApiTags('자산 API') +export class AssetController {} diff --git a/BE/src/asset/asset.entity.ts b/BE/src/asset/asset.entity.ts new file mode 100644 index 00000000..6abc123a --- /dev/null +++ b/BE/src/asset/asset.entity.ts @@ -0,0 +1,30 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +const INIT_ASSET = 10000000; + +@Entity('assets') +export class Asset { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: false }) + user_id: number; + + @Column({ nullable: false, default: INIT_ASSET }) + cash_balance: number; + + @Column({ nullable: false, default: INIT_ASSET }) + stock_balance: number; + + @Column({ nullable: false, default: INIT_ASSET }) + total_asset: number; + + @Column({ nullable: false, default: 0 }) + total_profit: number; + + @Column({ nullable: false, default: 0 }) + total_profit_rate: number; + + @Column({ nullable: true }) + last_updated?: Date; +} diff --git a/BE/src/asset/asset.module.ts b/BE/src/asset/asset.module.ts new file mode 100644 index 00000000..5ddf8874 --- /dev/null +++ b/BE/src/asset/asset.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AssetController } from './asset.controller'; +import { AssetService } from './asset.service'; +import { AssetRepository } from './asset.repository'; +import { Asset } from './asset.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Asset])], + controllers: [AssetController], + providers: [AssetService, AssetRepository], + exports: [AssetRepository], +}) +export class AssetModule {} diff --git a/BE/src/asset/asset.repository.ts b/BE/src/asset/asset.repository.ts new file mode 100644 index 00000000..cfc05c92 --- /dev/null +++ b/BE/src/asset/asset.repository.ts @@ -0,0 +1,11 @@ +import { DataSource, Repository } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; +import { Asset } from './asset.entity'; + +@Injectable() +export class AssetRepository extends Repository { + constructor(@InjectDataSource() dataSource: DataSource) { + super(Asset, dataSource.createEntityManager()); + } +} diff --git a/BE/src/asset/asset.service.ts b/BE/src/asset/asset.service.ts new file mode 100644 index 00000000..f424a0d8 --- /dev/null +++ b/BE/src/asset/asset.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AssetService {} diff --git a/BE/src/auth/auth.module.ts b/BE/src/auth/auth.module.ts index 522e5199..581b401c 100644 --- a/BE/src/auth/auth.module.ts +++ b/BE/src/auth/auth.module.ts @@ -9,6 +9,7 @@ import { User } from './user.entity'; import { UserRepository } from './user.repository'; import { JwtStrategy } from './strategy/jwt.strategy'; import { KakaoStrategy } from './strategy/kakao.strategy'; +import { AssetModule } from '../asset/asset.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { KakaoStrategy } from './strategy/kakao.strategy'; }), inject: [ConfigService], }), + AssetModule, ], controllers: [AuthController], providers: [AuthService, UserRepository, JwtStrategy, KakaoStrategy], diff --git a/BE/src/auth/user.repository.ts b/BE/src/auth/user.repository.ts index b7a06fa4..c17ce75f 100644 --- a/BE/src/auth/user.repository.ts +++ b/BE/src/auth/user.repository.ts @@ -1,30 +1,63 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource, Repository } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { User } from './user.entity'; import { AuthCredentialsDto } from './dto/auth-credentials.dto'; +import { AssetRepository } from '../asset/asset.repository'; @Injectable() export class UserRepository extends Repository { - constructor(@InjectDataSource() dataSource: DataSource) { + constructor( + @InjectDataSource() private dataSource: DataSource, + private readonly assetRepository: AssetRepository, + ) { super(User, dataSource.createEntityManager()); } async registerUser(authCredentialsDto: AuthCredentialsDto) { const { email, password } = authCredentialsDto; - const salt: string = await bcrypt.genSalt(); - const hashedPassword: string = await bcrypt.hash(password, salt); - const user = this.create({ email, password: hashedPassword }); - await this.save(user); + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + const salt: string = await bcrypt.genSalt(); + const hashedPassword: string = await bcrypt.hash(password, salt); + const user = this.create({ email, password: hashedPassword }); + await queryRunner.manager.save(user); + const asset = this.assetRepository.create({ user_id: user.id }); + await queryRunner.manager.save(asset); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } } async registerKakaoUser(authCredentialsDto: AuthCredentialsDto) { - const { kakaoId, email } = authCredentialsDto; - const salt: string = await bcrypt.genSalt(); - const hashedPassword: string = await bcrypt.hash(String(kakaoId), salt); - const user = this.create({ email, kakaoId, password: hashedPassword }); - await this.save(user); + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + const { kakaoId, email } = authCredentialsDto; + const salt: string = await bcrypt.genSalt(); + const hashedPassword: string = await bcrypt.hash(String(kakaoId), salt); + const user = this.create({ email, kakaoId, password: hashedPassword }); + await this.save(user); + const asset = this.assetRepository.create({ user_id: user.id }); + await queryRunner.manager.save(asset); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } } async updateUserWithRefreshToken( From e7ae190050948b421279c63148babb736ed134c4 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:25:05 +0900 Subject: [PATCH 094/158] =?UTF-8?q?=E2=9C=A8=20feat:=20API=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=B4=20chart=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ....dto.ts => stock-detail-chart-data.dto.ts} | 2 +- .../dto/stock-detail-chart-response.dto.ts | 5 + .../detail/dto/stock-detail-response.dto.ts | 14 --- .../stock/detail/stock-detail.controller.ts | 6 +- BE/src/stock/detail/stock-detail.service.ts | 98 +++++++------------ 5 files changed, 42 insertions(+), 83 deletions(-) rename BE/src/stock/detail/dto/{stock-detail-output2.dto.ts => stock-detail-chart-data.dto.ts} (96%) create mode 100644 BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts delete mode 100644 BE/src/stock/detail/dto/stock-detail-response.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-output2.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts similarity index 96% rename from BE/src/stock/detail/dto/stock-detail-output2.dto.ts rename to BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts index c6df900f..fd108090 100644 --- a/BE/src/stock/detail/dto/stock-detail-output2.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class InquirePriceOutput2Dto { +export class InquirePriceChartDataDto { @ApiProperty({ description: '주식 영업 일자' }) stck_bsop_date: string; diff --git a/BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts new file mode 100644 index 00000000..3c580851 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts @@ -0,0 +1,5 @@ +import { InquirePriceChartDataDto } from './stock-detail-chart-data.dto'; + +export class InquirePriceChartResponseDto { + output: InquirePriceChartDataDto[]; +} diff --git a/BE/src/stock/detail/dto/stock-detail-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts deleted file mode 100644 index a86f9e04..00000000 --- a/BE/src/stock/detail/dto/stock-detail-response.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { InquirePriceOutput1Dto } from './stock-detail-output1.dto'; -import { InquirePriceOutput2Dto } from './stock-detail-output2.dto'; - -/** - * 국내주식기간별시세(일/주/월/년) API 응답값 정제 후 FE에 보낼 DTO - */ -export class InquirePriceResponseDto { - @ApiProperty({ type: InquirePriceOutput1Dto, description: '상승률 순위' }) - output1: InquirePriceOutput1Dto; - - @ApiProperty({ type: [InquirePriceOutput2Dto], description: '하락률 순위' }) - output2: InquirePriceOutput2Dto[]; -} diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 5c4cfa85..64509746 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Param, Post } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; import { StockDetailRequestDto } from './dto/stock-detail-request.dto'; -import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; +import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @Controller('/api/stocks') export class StockDetailController { @@ -28,14 +28,14 @@ export class StockDetailController { @ApiResponse({ status: 201, description: '단일 주식 종목 기본값 조회 성공', - type: InquirePriceResponseDto, + type: InquirePriceChartResponseDto, }) getStockDetail( @Param('stockCode') stockCode: string, @Body() body: StockDetailRequestDto, ) { const { fid_input_date_1, fid_input_date_2, fid_period_div_code } = body; - return this.stockDetailService.getInquirePrice( + return this.stockDetailService.getInquirePriceChart( stockCode, fid_input_date_1, fid_input_date_2, diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index c0629406..a454beec 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -4,8 +4,7 @@ import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.s import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; import { InquirePriceApiResponse } from './interface/stock-detail.interface'; -import { StockDetailQueryParameterDto } from './dto/stock-detail-query-parameter.dto'; -import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; +import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @Injectable() export class StockDetailService { @@ -23,23 +22,29 @@ export class StockDetailService { * * @author uuuo3o */ - async getInquirePrice( + async getInquirePriceChart( stockCode: string, date1: string, date2: string, periodDivCode: string, ) { try { - const queryParams = new StockDetailQueryParameterDto(); - queryParams.fid_cond_mrkt_div_code = 'J'; - queryParams.fid_input_iscd = stockCode; - queryParams.fid_input_date_1 = date1; - queryParams.fid_input_date_2 = date2; - queryParams.fid_period_div_code = periodDivCode; + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + fid_input_date_1: date1, + fid_input_date_2: date2, + fid_period_div_code: periodDivCode, + fid_org_adj_prc: '0', + }; - const response = await this.requestApi(queryParams); + const response = await this.requestApi( + 'FHKST03010100', + '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', + queryParams, + ); - return this.formatStockData(response); + return this.formatStockInquirePriceData(response); } catch (error) { this.logger.error('API Error Details:', { status: error.response?.status, @@ -53,22 +58,25 @@ export class StockDetailService { } /** - * @private 한국투자 Open API - [국내주식] 기본시세 - 국내주식기간별시세(일/주/월/년) 호출 함수 - * @param {StockDetailQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO - * @returns - 국내주식기간별시세(일/주/월/년) 데이터 + * @private 한국투자 Open API - API 호출용 공통 함수 + * @param {string} trId - API 호출에 사용할 tr_id + * @param {string} apiURL - API 호출에 사용할 URL + * @param {Record} params - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - API 호출에 대한 응답 데이터 * * @author uuuo3o */ - private async requestApi(queryParams: StockDetailQueryParameterDto) { + private async requestApi( + trId: string, + apiURL: string, + params: Record, + ): Promise { try { const accessToken = await this.koreaInvetmentService.getAccessToken(); - const headers = getHeader(accessToken, 'FHKST03010100'); - const url = getFullURL( - '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', - ); - const params = this.getInquirePriceParams(queryParams); + const headers = getHeader(accessToken, trId); + const url = getFullURL(apiURL); - const response = await axios.get(url, { + const response = await axios.get(url, { headers, params, }); @@ -93,52 +101,12 @@ export class StockDetailService { * * @author uuuo3o */ - private formatStockData(response: InquirePriceApiResponse) { - const stockData = new InquirePriceResponseDto(); - const { output1, output2 } = response; - - const { - hts_kor_isnm, - stck_shrn_iscd, - stck_prpr, - prdy_vrss, - prdy_vrss_sign, - prdy_ctrt, - hts_avls, - per, - } = output1; - - stockData.output1 = { - hts_kor_isnm, - stck_shrn_iscd, - stck_prpr, - prdy_vrss, - prdy_vrss_sign, - prdy_ctrt, - hts_avls, - per, - }; + private formatStockInquirePriceData(response: InquirePriceApiResponse) { + const stockData = new InquirePriceChartResponseDto(); + const { output2 } = response; - stockData.output2 = output2; + stockData.output = output2; return stockData; } - - /** - * @private 국내주식기간별시세(일/주/월/년) 요청을 위한 쿼리 파라미터 객체 생성 함수 - * @param {StockDetailQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO - * @returns - API 요청에 필요한 쿼리 파라미터 객체 - * - * @author uuuo3o - */ - private getInquirePriceParams(params: StockDetailQueryParameterDto) { - return { - fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, - fid_input_iscd: params.fid_input_iscd, - fid_input_date_1: params.fid_input_date_1, - fid_input_date_2: params.fid_input_date_2, - fid_period_div_code: params.fid_period_div_code, - fid_org_adj_prc: 0, - }; - } } From 5989e081812c065013a154854bd0f07def6f5a2e Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:34:13 +0900 Subject: [PATCH 095/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EA=B0=92=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20DTO=20=EC=88=98=EC=A0=95#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/dto/stock-detail-chart-data.dto.ts | 21 ---------------- BE/src/stock/detail/stock-detail.service.ts | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts index fd108090..4574d3a7 100644 --- a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts @@ -18,25 +18,4 @@ export class InquirePriceChartDataDto { @ApiProperty({ description: '누적 거래량' }) acml_vol: string; - - @ApiProperty({ description: '누적 거래 대금' }) - acml_tr_pbmn: string; - - @ApiProperty({ description: '락 구분 코드' }) - flng_cls_code: string; - - @ApiProperty({ description: '분할 비율' }) - prtt_rate: string; - - @ApiProperty({ description: '분할변경여부' }) - mod_yn: string; - - @ApiProperty({ description: '전일 대비 부호' }) - prdy_vrss_sign: string; - - @ApiProperty({ description: '전일 대비' }) - prdy_vrss: string; - - @ApiProperty({ description: '재평가사유코드' }) - revl_issu_reas: string; } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index a454beec..b47a6606 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -4,7 +4,7 @@ import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.s import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; import { InquirePriceApiResponse } from './interface/stock-detail.interface'; -import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; +import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; @Injectable() export class StockDetailService { @@ -102,11 +102,27 @@ export class StockDetailService { * @author uuuo3o */ private formatStockInquirePriceData(response: InquirePriceApiResponse) { - const stockData = new InquirePriceChartResponseDto(); const { output2 } = response; - stockData.output = output2; + return output2.map((info) => { + const stockData = new InquirePriceChartDataDto(); + const { + stck_bsop_date, + stck_clpr, + stck_oprc, + stck_hgpr, + stck_lwpr, + acml_vol, + } = info; - return stockData; + stockData.stck_bsop_date = stck_bsop_date; + stockData.stck_clpr = stck_clpr; + stockData.stck_oprc = stck_oprc; + stockData.stck_hgpr = stck_hgpr; + stockData.stck_lwpr = stck_lwpr; + stockData.acml_vol = acml_vol; + + return stockData; + }); } } From b0b6d8d115308fd7bdf0b8273346f9793bffd142 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:36:22 +0900 Subject: [PATCH 096/158] =?UTF-8?q?=F0=9F=8E=A8=20style:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 64 ++++++++++----------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index b47a6606..ef463dc7 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -57,6 +57,38 @@ export class StockDetailService { } } + /** + * @private API에서 받은 국내주식기간별시세(일/주/월/년) 데이터를 필요한 정보로 정제하는 함수 + * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatStockInquirePriceData(response: InquirePriceApiResponse) { + const { output2 } = response; + + return output2.map((info) => { + const stockData = new InquirePriceChartDataDto(); + const { + stck_bsop_date, + stck_clpr, + stck_oprc, + stck_hgpr, + stck_lwpr, + acml_vol, + } = info; + + stockData.stck_bsop_date = stck_bsop_date; + stockData.stck_clpr = stck_clpr; + stockData.stck_oprc = stck_oprc; + stockData.stck_hgpr = stck_hgpr; + stockData.stck_lwpr = stck_lwpr; + stockData.acml_vol = acml_vol; + + return stockData; + }); + } + /** * @private 한국투자 Open API - API 호출용 공통 함수 * @param {string} trId - API 호출에 사용할 tr_id @@ -93,36 +125,4 @@ export class StockDetailService { throw error; } } - - /** - * @private API에서 받은 국내주식기간별시세(일/주/월/년) 데이터를 필요한 정보로 정제하는 함수 - * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 - * @returns - 필요한 정보만 추출한 데이터 배열 - * - * @author uuuo3o - */ - private formatStockInquirePriceData(response: InquirePriceApiResponse) { - const { output2 } = response; - - return output2.map((info) => { - const stockData = new InquirePriceChartDataDto(); - const { - stck_bsop_date, - stck_clpr, - stck_oprc, - stck_hgpr, - stck_lwpr, - acml_vol, - } = info; - - stockData.stck_bsop_date = stck_bsop_date; - stockData.stck_clpr = stck_clpr; - stockData.stck_oprc = stck_oprc; - stockData.stck_hgpr = stck_hgpr; - stockData.stck_lwpr = stck_lwpr; - stockData.acml_vol = acml_vol; - - return stockData; - }); - } } From 153e023e3b45696baf3d63d6a142704a20967e58 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:36:33 +0900 Subject: [PATCH 097/158] =?UTF-8?q?=F0=9F=94=A5=20remove:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/stock-detail-query-parameter.dto.ts | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts b/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts deleted file mode 100644 index feb9ca0f..00000000 --- a/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 주식 현재가 시세 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO - */ -export class StockDetailQueryParameterDto { - /** - * 조건 시장 분류 코드 - * 'J' 주식 - */ - fid_cond_mrkt_div_code: string; - - /** - * 주식 종목 코드 - * (ex) 005930 - */ - fid_input_iscd: string; - - /** - * 조회 시작일자 - * (ex) 20220501 - */ - fid_input_date_1: string; - - /** - * 조회 종료일자 - * (ex) 20220530 - */ - fid_input_date_2: string; - - /** - * 기간 분류 코드 - * D:일봉, W:주봉, M:월봉, Y:년봉 - */ - fid_period_div_code: string; -} From c7561727ed5fdffd302fad16733eab9fec970c66 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:41:07 +0900 Subject: [PATCH 098/158] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20swagger?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EC=88=98=EC=A0=95#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...equest.dto.ts => stock-detail-chart-request.dto.ts} | 2 +- BE/src/stock/detail/stock-detail.controller.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename BE/src/stock/detail/dto/{stock-detail-request.dto.ts => stock-detail-chart-request.dto.ts} (92%) diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-request.dto.ts similarity index 92% rename from BE/src/stock/detail/dto/stock-detail-request.dto.ts rename to BE/src/stock/detail/dto/stock-detail-chart-request.dto.ts index 7bd1b1d8..4a21bbc3 100644 --- a/BE/src/stock/detail/dto/stock-detail-request.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-request.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; /** * 국내주식기간별시세(일/주/월/년) API를 이용할 때 필요한 요청 데이터를 담고 있는 DTO */ -export class StockDetailRequestDto { +export class StockDetailChartRequestDto { @ApiProperty({ description: '조회 시작일자 (ex) 20220501' }) fid_input_date_1: string; diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 64509746..86149af2 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Param, Post } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; -import { StockDetailRequestDto } from './dto/stock-detail-request.dto'; +import { StockDetailChartRequestDto } from './dto/stock-detail-chart-request.dto'; import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @Controller('/api/stocks') @@ -9,7 +9,7 @@ export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} @Post(':stockCode') - @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) + @ApiOperation({ summary: '국내주식기간별시세(일/주/월/년) 조회 API' }) @ApiParam({ name: 'stockCode', required: true, @@ -23,16 +23,16 @@ export class StockDetailController { 'fid_input_date_1: 조회 시작일자 (ex) 20240505\n\n' + 'fid_input_date_2: 조회 종료일자 (ex) 20241111\n\n' + 'fid_period_div_code: 기간 분류 코드 (ex) D(일봉), W(주봉), M(월봉), Y(년봉)', - type: StockDetailRequestDto, + type: StockDetailChartRequestDto, }) @ApiResponse({ status: 201, - description: '단일 주식 종목 기본값 조회 성공', + description: '국내주식기간별시세(일/주/월/년) 조회 성공', type: InquirePriceChartResponseDto, }) getStockDetail( @Param('stockCode') stockCode: string, - @Body() body: StockDetailRequestDto, + @Body() body: StockDetailChartRequestDto, ) { const { fid_input_date_1, fid_input_date_2, fid_period_div_code } = body; return this.stockDetailService.getInquirePriceChart( From a6eeef40be6484c5f8479a4bd4295022cf7b6bcc Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:46:25 +0900 Subject: [PATCH 099/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=B0=A8=ED=8A=B8?= =?UTF-8?q?=20=EA=B7=B8=EB=A6=AC=EA=B8=B0=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B0=92=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts | 3 +++ ...ck-detail.interface.ts => stock-detail-chart.interface.ts} | 0 BE/src/stock/detail/stock-detail.service.ts | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) rename BE/src/stock/detail/interface/{stock-detail.interface.ts => stock-detail-chart.interface.ts} (100%) diff --git a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts index 4574d3a7..de68279f 100644 --- a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts @@ -18,4 +18,7 @@ export class InquirePriceChartDataDto { @ApiProperty({ description: '누적 거래량' }) acml_vol: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; } diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail-chart.interface.ts similarity index 100% rename from BE/src/stock/detail/interface/stock-detail.interface.ts rename to BE/src/stock/detail/interface/stock-detail-chart.interface.ts diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index ef463dc7..e66b0736 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; -import { InquirePriceApiResponse } from './interface/stock-detail.interface'; +import { InquirePriceApiResponse } from './interface/stock-detail-chart.interface'; import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; @Injectable() @@ -76,6 +76,7 @@ export class StockDetailService { stck_hgpr, stck_lwpr, acml_vol, + prdy_vrss_sign, } = info; stockData.stck_bsop_date = stck_bsop_date; @@ -84,6 +85,7 @@ export class StockDetailService { stockData.stck_hgpr = stck_hgpr; stockData.stck_lwpr = stck_lwpr; stockData.acml_vol = acml_vol; + stockData.prdy_vrss_sign = prdy_vrss_sign; return stockData; }); From 99a94fac8e13b6191e4c7ff740eafa11ca792ed9 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:59:50 +0900 Subject: [PATCH 100/158] =?UTF-8?q?=F0=9F=9A=9A=20rename:=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EB=90=98=EB=8A=94=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/interface/stock-detail-chart.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/detail/interface/stock-detail-chart.interface.ts b/BE/src/stock/detail/interface/stock-detail-chart.interface.ts index 1169eb90..e49746e9 100644 --- a/BE/src/stock/detail/interface/stock-detail-chart.interface.ts +++ b/BE/src/stock/detail/interface/stock-detail-chart.interface.ts @@ -47,7 +47,7 @@ export interface InquirePriceOutput2Data { revl_issu_reas: string; } -export interface InquirePriceApiResponse { +export interface InquirePriceChartApiResponse { output1: InquirePriceOutput1Data; output2: InquirePriceOutput2Data[]; rt_cd: string; From fc9a9170ced449e3a18e8883a210fff71f63fb76 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 18:01:38 +0900 Subject: [PATCH 101/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=ED=98=84=EC=9E=AC=EA=B0=80=20API=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20interface,=20dto=20=EA=B5=AC=ED=98=84#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...t1.dto.ts => stock-detail-response.dto.ts} | 2 +- .../interface/stock-detail.interface.ts | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) rename BE/src/stock/detail/dto/{stock-detail-output1.dto.ts => stock-detail-response.dto.ts} (94%) create mode 100644 BE/src/stock/detail/interface/stock-detail.interface.ts diff --git a/BE/src/stock/detail/dto/stock-detail-output1.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts similarity index 94% rename from BE/src/stock/detail/dto/stock-detail-output1.dto.ts rename to BE/src/stock/detail/dto/stock-detail-response.dto.ts index aa911bd1..96e4ba52 100644 --- a/BE/src/stock/detail/dto/stock-detail-output1.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-response.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class InquirePriceOutput1Dto { +export class InquirePriceResponseDto { @ApiProperty({ description: 'HTS 한글 종목명' }) hts_kor_isnm: string; diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail.interface.ts new file mode 100644 index 00000000..1cef7848 --- /dev/null +++ b/BE/src/stock/detail/interface/stock-detail.interface.ts @@ -0,0 +1,84 @@ +export interface InquirePriceOutputData { + iscd_stat_cls_code: string; + marg_rate: string; + rprs_mrkt_kor_name: string; + new_hgpr_lwpr_cls_code: string; + btsp_kor_isnm: string; + temp_stop_yn: string; + oprc_rang_cont_yn: string; + clpr_rang_cont_yn: string; + crdt_able_yn: string; + grmn_rate_cls_code: string; + elw_pblc_yn: string; + stck_prpr: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + acml_tr_pbmn: string; + acml_vol: string; + prdy_vrss_vol_rate: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + stck_mxpr: string; + stck_llam: string; + stck_sdpr: string; + wghn_avrg_stck_prc: string; + hts_frgn_ehrt: string; + frgn_ntby_qty: string; + pgtr_ntby_qty: string; + dmrs_val: string; + dmsp_val: string; + cpfn: string; + rstc_wdth_prc: string; + stck_fcam: string; + stck_sspr: string; + aspr_unit: string; + hts_deal_qty_unit_val: string; + lstn_stcn: string; + hts_avls: string; + per: string; + pbr: string; + stac_month: string; + vol_tnrt: string; + eps: string; + bps: string; + d250_hgpr: string; + d250_hgpr_date: string; + d250_hgpr_vrss_prpr_rate: string; + d250_lwpr: string; + d250_lwpr_date: string; + d250_lwpr_vrss_prpr_rate: string; + stck_dryy_hgpr: string; + dryy_hgpr_vrss_prpr_rate: string; + dryy_hgpr_date: string; + stck_dryy_lwpr: string; + dryy_lwpr_vrss_prpr_rate: string; + dryy_lwpr_date: string; + w52_hgpr: string; + w52_hgpr_vrss_prpr_ctrt: string; + w52_hgpr_date: string; + w52_lwpr: string; + w52_lwpr_vrss_prpr_ctrt: string; + w52_lwpr_date: string; + whol_loan_rmnd_rate: string; + ssts_yn: string; + stck_shrn_iscd: string; + fcam_cnnm: string; + cpfn_cnnm: string; + apprch_rate: string; + frgn_hldn_qty: string; + vi_cls_code: string; + ovtm_vi_cls_code: string; + last_ssts_cntg_qty: string; + invt_caful_yn: string; + mrkt_warn_cls_code: string; + short_over_yn: string; + sltr_yn: string; +} +export interface InquirePriceApiResponse { + output: InquirePriceOutputData; + rt_cd: string; + msg_cd: string; + msg1: string; +} From 5f7d33c2842bdabe1027b9d84ac69c8c703852af Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 18:01:53 +0900 Subject: [PATCH 102/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=ED=98=84=EC=9E=AC=EA=B0=80=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/detail/stock-detail.controller.ts | 23 ++++++- BE/src/stock/detail/stock-detail.service.ts | 63 ++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 86149af2..533b67ca 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,6 +1,7 @@ -import { Body, Controller, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; import { StockDetailChartRequestDto } from './dto/stock-detail-chart-request.dto'; import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @@ -8,6 +9,24 @@ import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response. export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} + @Get(':stockCode') + @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: '단일 주식 종목 기본값 조회 성공', + type: InquirePriceResponseDto, + }) + getStockDetail(@Param('stockCode') stockCode: string) { + return this.stockDetailService.getInquirePrice(stockCode); + } + @Post(':stockCode') @ApiOperation({ summary: '국내주식기간별시세(일/주/월/년) 조회 API' }) @ApiParam({ @@ -30,7 +49,7 @@ export class StockDetailController { description: '국내주식기간별시세(일/주/월/년) 조회 성공', type: InquirePriceChartResponseDto, }) - getStockDetail( + getStockDetailChart( @Param('stockCode') stockCode: string, @Body() body: StockDetailChartRequestDto, ) { diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index e66b0736..8031c4d6 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -3,8 +3,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; -import { InquirePriceApiResponse } from './interface/stock-detail-chart.interface'; +import { InquirePriceChartApiResponse } from './interface/stock-detail-chart.interface'; import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; +import { + InquirePriceApiResponse, + InquirePriceOutputData, +} from './interface/stock-detail.interface'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; @Injectable() export class StockDetailService { @@ -12,6 +17,58 @@ export class StockDetailService { constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + /** + * 주식현재가 시세 데이터를 반환하는 함수 + * @param {string} stockCode - 종목코드 + * @returns - 주식현재가 시세 데이터 객체 반환 + * + * @author uuuo3o + */ + async getInquirePrice(stockCode: string) { + try { + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + }; + + const response = await this.requestApi( + 'FHKST01010100', + '/uapi/domestic-stock/v1/quotations/inquire-price', + queryParams, + ); + + return this.formatStockData(response.output); + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + + /** + * @private API에서 받은 주식현재가 시세 데이터를 필요한 정보로 정제하는 함수 + * @param {InquirePriceOutputData} stock - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatStockData(stock: InquirePriceOutputData) { + const stockData = new InquirePriceResponseDto(); + stockData.stck_shrn_iscd = stock.stck_shrn_iscd; + stockData.stck_prpr = stock.stck_prpr; + stockData.prdy_vrss = stock.prdy_vrss; + stockData.prdy_vrss_sign = stock.prdy_vrss_sign; + stockData.prdy_ctrt = stock.prdy_ctrt; + stockData.hts_avls = stock.hts_avls; + stockData.per = stock.per; + return stockData; + } + /** * 특정 주식의 기간별시세 데이터를 반환하는 함수 * @param {string} stockCode - 종목코드 @@ -38,7 +95,7 @@ export class StockDetailService { fid_org_adj_prc: '0', }; - const response = await this.requestApi( + const response = await this.requestApi( 'FHKST03010100', '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', queryParams, @@ -64,7 +121,7 @@ export class StockDetailService { * * @author uuuo3o */ - private formatStockInquirePriceData(response: InquirePriceApiResponse) { + private formatStockInquirePriceData(response: InquirePriceChartApiResponse) { const { output2 } = response; return output2.map((info) => { From 4910002c4658ec533d381167011e0eb823f37195 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 18:03:29 +0900 Subject: [PATCH 103/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=20=EC=B2=B4=EA=B2=B0=20=EC=8B=9C=20=EC=9E=90=EC=82=B0=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.module.ts | 7 +- BE/src/stock/order/stock-order.repository.ts | 74 +++++++++++++++++++- BE/src/stock/order/stock-order.service.ts | 31 +++++--- 3 files changed, 101 insertions(+), 11 deletions(-) diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index a31eee97..944813d5 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -5,9 +5,14 @@ import { StockOrderService } from './stock-order.service'; import { Order } from './stock-order.entity'; import { StockOrderRepository } from './stock-order.repository'; import { SocketModule } from '../../websocket/socket.module'; +import { AssetModule } from '../../asset/asset.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order]), forwardRef(() => SocketModule)], + imports: [ + TypeOrmModule.forFeature([Order]), + forwardRef(() => SocketModule), + AssetModule, + ], controllers: [StockOrderController], providers: [StockOrderService, StockOrderRepository], exports: [StockOrderService], diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index cec0080a..6b0dc184 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -1,11 +1,81 @@ import { DataSource, Repository } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; -import { Injectable } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Order } from './stock-order.entity'; +import { StatusType } from './enum/status-type'; +import { Asset } from '../../asset/asset.entity'; @Injectable() export class StockOrderRepository extends Repository { - constructor(@InjectDataSource() dataSource: DataSource) { + constructor(@InjectDataSource() private dataSource: DataSource) { super(Order, dataSource.createEntityManager()); } + + async updateOrderAndAssetWhenBuy(order, realPrice) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => 'cash_balance - :realPrice', + stock_balance: () => 'stock_balance - :realPrice', + total_asset: () => 'total_asset - :realPrice', + total_profit: () => 'total_profit - :realPrice', + total_profit_rate: () => `total_profit / 10000000`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameter('realPrice', realPrice) + .execute(); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } + } + + async updateOrderAndAssetWhenSell(order, realPrice) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.startTransaction(); + + try { + await queryRunner.manager.update( + Order, + { id: order.id }, + { status: StatusType.COMPLETE, completed_at: new Date() }, + ); + await queryRunner.manager + .createQueryBuilder() + .update(Asset) + .set({ + cash_balance: () => 'cash_balance + :realPrice', + stock_balance: () => 'stock_balance + :realPrice', + total_asset: () => 'total_asset + :realPrice', + total_profit: () => 'total_profit + :realPrice', + total_profit_rate: () => `total_profit / 10000000`, + last_updated: new Date(), + }) + .where({ user_id: order.user_id }) + .setParameter('realPrice', realPrice) + .execute(); + + await queryRunner.commitTransaction(); + } catch (err) { + await queryRunner.rollbackTransaction(); + throw new InternalServerErrorException(); + } finally { + await queryRunner.release(); + } + } } diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 1aba3347..c756e8d2 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -114,20 +114,35 @@ export class StockOrderService { } private async executeBuy(order) { - // TODO: 매수 체결 로직 필요... this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); - await this.stockOrderRepository.update( - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockOrderRepository.updateOrderAndAssetWhenBuy( + order, + totalPrice + fee, ); } private async executeSell(order) { - // TODO: 매도 체결 로직 필요... this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); - await this.stockOrderRepository.update( - { id: order.id }, - { status: StatusType.COMPLETE, completed_at: new Date() }, + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockOrderRepository.updateOrderAndAssetWhenSell( + order, + totalPrice - fee, ); } + + private calculateFee(totalPrice: number) { + if (totalPrice <= 10000000) return totalPrice * 0.16; + if (totalPrice > 10000000 && totalPrice <= 50000000) + return totalPrice * 0.14; + if (totalPrice > 50000000 && totalPrice <= 100000000) + return totalPrice * 0.12; + if (totalPrice > 100000000 && totalPrice <= 300000000) + return totalPrice * 0.1; + return totalPrice * 0.08; + } } From 16c34a5aa540c5863b0e3982a813810fceef35b6 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Wed, 13 Nov 2024 18:21:58 +0900 Subject: [PATCH 104/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EB=9E=A8=20=EC=8B=9C=EC=9E=91=20=EC=8B=9C=20?= =?UTF-8?q?=EC=A2=85=EB=AA=A9=20=EC=A4=91=EB=B3=B5=20=EA=B5=AC=EB=8F=85=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.repository.ts | 6 ++++++ BE/src/stock/order/stock-order.service.ts | 6 ++---- BE/src/websocket/stock/price/stock-price-socket.service.ts | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 6b0dc184..8067a7ff 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -11,6 +11,12 @@ export class StockOrderRepository extends Repository { super(Order, dataSource.createEntityManager()); } + async findAllCodeByStatus() { + return this.createQueryBuilder('orders') + .select('DISTINCT orders.stock_code') + .getRawMany(); + } + async updateOrderAndAssetWhenBuy(order, realPrice) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.startTransaction(); diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index c756e8d2..866a96cc 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -24,10 +24,8 @@ export class StockOrderService { private readonly logger = new Logger(); - async findAllPendingOrder() { - return this.stockOrderRepository.findBy({ - status: StatusType.PENDING, - }); + async findAllPendingOrderCode() { + return this.stockOrderRepository.findAllCodeByStatus(); } async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { diff --git a/BE/src/websocket/stock/price/stock-price-socket.service.ts b/BE/src/websocket/stock/price/stock-price-socket.service.ts index 93f0625c..4f591c81 100644 --- a/BE/src/websocket/stock/price/stock-price-socket.service.ts +++ b/BE/src/websocket/stock/price/stock-price-socket.service.ts @@ -7,6 +7,7 @@ import { import { BaseSocketService } from '../../base-socket.service'; import { SocketGateway } from '../../socket.gateway'; import { StockOrderService } from '../../../stock/order/stock-order.service'; +import { Order } from '../../../stock/order/stock-order.entity'; @Injectable() export class StockPriceSocketService { @@ -19,7 +20,7 @@ export class StockPriceSocketService { private readonly stockOrderService: StockOrderService, ) { baseSocketService.registerSocketOpenHandler(async () => { - const orders = await stockOrderService.findAllPendingOrder(); + const orders: Order[] = await stockOrderService.findAllPendingOrderCode(); orders.forEach((order) => { baseSocketService.registerCode(this.TR_ID, order.stock_code); }); From 3c650ac984e8df627e76a9da63f9491b3b610eac Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 19:46:15 +0900 Subject: [PATCH 105/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20reques?= =?UTF-8?q?tApi=20=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...stock-trade-history-query-parameter.dto.ts | 16 ---- .../history/stock-trade-history.service.ts | 85 +++++++++---------- 2 files changed, 40 insertions(+), 61 deletions(-) delete mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts deleted file mode 100644 index 6d9ccfa6..00000000 --- a/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 주식현재가 체결 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO - */ -export class StockTradeHistoryQueryParameterDto { - /** - * 조건 시장 분류 코드 - * 'J' 주식 - */ - fid_cond_mrkt_div_code: string; - - /** - * 주식 종목 코드 - * (ex) 005930 - */ - fid_input_iscd: string; -} diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index dc9b24e4..7617846e 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -3,7 +3,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { KoreaInvestmentService } from '../../../koreaInvestment/korea-investment.service'; import { getHeader } from '../../../util/get-header'; import { getFullURL } from '../../../util/get-full-URL'; -import { StockTradeHistoryQueryParameterDto } from './dto/stock-trade-history-query-parameter.dto'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; import { StockTradeHistoryOutputDto } from './dto/stock-trade-history-output.dto'; import { StockTradeHistoryDataDto } from './dto/stock-trade-history-data.dto'; @@ -23,11 +22,16 @@ export class StockTradeHistoryService { */ async getStockTradeHistory(stockCode: string) { try { - const queryParams = new StockTradeHistoryQueryParameterDto(); - queryParams.fid_cond_mrkt_div_code = 'J'; - queryParams.fid_input_iscd = stockCode; + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + }; - const response = await this.requestApi(queryParams); + const response = await this.requestApi( + 'FHKST01010300', + '/uapi/domestic-stock/v1/quotations/inquire-ccnl', + queryParams, + ); return this.formatTradeHistoryData(response.output); } catch (error) { @@ -42,38 +46,6 @@ export class StockTradeHistoryService { } } - /** - * @private 한국투자 Open API - [국내주식] 기본시세 - 주식현재가 체결 호출 함수 - * @param {StockTradeHistoryQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO - * @returns - 주식현재가 체결 데이터 - * - * @author uuuo3o - */ - private async requestApi(queryParams: StockTradeHistoryQueryParameterDto) { - try { - const accessToken = await this.koreaInvetmentService.getAccessToken(); - const headers = getHeader(accessToken, 'FHKST01010300'); - const url = getFullURL('/uapi/domestic-stock/v1/quotations/inquire-ccnl'); - const params = this.getTradeHistoryParams(queryParams); - - const response = await axios.get(url, { - headers, - params, - }); - - return response.data; - } catch (error) { - this.logger.error('API Error Details:', { - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - headers: error.response?.config?.headers, - message: error.message, - }); - throw error; - } - } - /** * @private API에서 받은 주식현재가 체결 데이터를 필요한 정보로 정제하는 함수 * @param {StockTradeHistoryOutputDto} infos - API 응답에서 받은 원시 데이터 @@ -95,16 +67,39 @@ export class StockTradeHistoryService { } /** - * @private 주식현재가 체결 요청을 위한 쿼리 파라미터 객체 생성 함수 - * @param {StockTradeHistoryQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO - * @returns - API 요청에 필요한 쿼리 파라미터 객체 + * @private 한국투자 Open API - API 호출용 공통 함수 + * @param {string} trId - API 호출에 사용할 tr_id + * @param {string} apiURL - API 호출에 사용할 URL + * @param {Record} params - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - API 호출에 대한 응답 데이터 * * @author uuuo3o */ - private getTradeHistoryParams(params: StockTradeHistoryQueryParameterDto) { - return { - fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, - fid_input_iscd: params.fid_input_iscd, - }; + private async requestApi( + trId: string, + apiURL: string, + params: Record, + ): Promise { + try { + const accessToken = await this.koreaInvetmentService.getAccessToken(); + const headers = getHeader(accessToken, trId); + const url = getFullURL(apiURL); + + const response = await axios.get(url, { + headers, + params, + }); + + return response.data; + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, + message: error.message, + }); + throw error; + } } } From 2fb460e6b4cb9ee82875fb2810059fcdcc72bd1f Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 19:53:16 +0900 Subject: [PATCH 106/158] =?UTF-8?q?=F0=9F=9A=9A=20rename:=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/stock-trade-history-response.dto.ts | 10 ---------- ....ts => today-stock-trade-history-data.dto.ts} | 2 +- ...s => today-stock-trade-history-output.dto.ts} | 2 +- .../today-stock-trade-history-response.dto.ts | 13 +++++++++++++ .../history/stock-trade-history.controller.ts | 8 ++++---- .../trade/history/stock-trade-history.service.ts | 16 +++++++++------- 6 files changed, 28 insertions(+), 23 deletions(-) delete mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts rename BE/src/stock/trade/history/dto/{stock-trade-history-data.dto.ts => today-stock-trade-history-data.dto.ts} (90%) rename BE/src/stock/trade/history/dto/{stock-trade-history-output.dto.ts => today-stock-trade-history-output.dto.ts} (92%) create mode 100644 BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts deleted file mode 100644 index 265c1d5e..00000000 --- a/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { StockTradeHistoryOutputDto } from './stock-trade-history-output.dto'; - -/** - * 주식현재가 체결 API 응답값 정제 후 FE에 보낼 DTO - */ -export class StockTradeHistoryResponseDto { - @ApiProperty({ type: StockTradeHistoryOutputDto, description: '상승률 순위' }) - output: StockTradeHistoryOutputDto[]; -} diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts b/BE/src/stock/trade/history/dto/today-stock-trade-history-data.dto.ts similarity index 90% rename from BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts rename to BE/src/stock/trade/history/dto/today-stock-trade-history-data.dto.ts index 3ead1198..7842a29b 100644 --- a/BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts +++ b/BE/src/stock/trade/history/dto/today-stock-trade-history-data.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class StockTradeHistoryDataDto { +export class TodayStockTradeHistoryDataDto { @ApiProperty({ description: '주식 체결 시간' }) stck_cntg_hour: string; diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts b/BE/src/stock/trade/history/dto/today-stock-trade-history-output.dto.ts similarity index 92% rename from BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts rename to BE/src/stock/trade/history/dto/today-stock-trade-history-output.dto.ts index 05415c64..ab41d377 100644 --- a/BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts +++ b/BE/src/stock/trade/history/dto/today-stock-trade-history-output.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class StockTradeHistoryOutputDto { +export class TodayStockTradeHistoryOutputDto { @ApiProperty({ description: '주식 체결 시간' }) stck_cntg_hour: string; diff --git a/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts new file mode 100644 index 00000000..e758dfba --- /dev/null +++ b/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { TodayStockTradeHistoryOutputDto } from './today-stock-trade-history-output.dto'; + +/** + * 주식현재가 체결 API 응답값 정제 후 FE에 보낼 DTO + */ +export class TodayStockTradeHistoryResponseDto { + @ApiProperty({ + type: TodayStockTradeHistoryOutputDto, + description: '상승률 순위', + }) + output: TodayStockTradeHistoryOutputDto[]; +} diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index 11efa7fa..8a2cf430 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; -import { StockTradeHistoryResponseDto } from './dto/stock-trade-history-response.dto'; +import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-history-response.dto'; @Controller('/api/stocks') export class StockTradeHistoryController { @@ -9,7 +9,7 @@ export class StockTradeHistoryController { private readonly stockTradeHistoryService: StockTradeHistoryService, ) {} - @Get(':stockCode/trade-history') + @Get(':stockCode/today-trade-history') @ApiOperation({ summary: '단일 주식 종목에 대한 주식현재가 체결 API' }) @ApiParam({ name: 'stockCode', @@ -21,9 +21,9 @@ export class StockTradeHistoryController { @ApiResponse({ status: 200, description: '단일 주식 종목에 대한 주식현재가 체결값 조회 성공', - type: StockTradeHistoryResponseDto, + type: TodayStockTradeHistoryResponseDto, }) getStockDetail(@Param('stockCode') stockCode: string) { - return this.stockTradeHistoryService.getStockTradeHistory(stockCode); + return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } } diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index 7617846e..c5e0dacf 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -4,8 +4,8 @@ import { KoreaInvestmentService } from '../../../koreaInvestment/korea-investmen import { getHeader } from '../../../util/get-header'; import { getFullURL } from '../../../util/get-full-URL'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; -import { StockTradeHistoryOutputDto } from './dto/stock-trade-history-output.dto'; -import { StockTradeHistoryDataDto } from './dto/stock-trade-history-data.dto'; +import { TodayStockTradeHistoryOutputDto } from './dto/today-stock-trade-history-output.dto'; +import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; @Injectable() export class StockTradeHistoryService { @@ -20,7 +20,7 @@ export class StockTradeHistoryService { * * @author uuuo3o */ - async getStockTradeHistory(stockCode: string) { + async getTodayStockTradeHistory(stockCode: string) { try { const queryParams = { fid_cond_mrkt_div_code: 'J', @@ -33,7 +33,7 @@ export class StockTradeHistoryService { queryParams, ); - return this.formatTradeHistoryData(response.output); + return this.formatTodayStockTradeHistoryData(response.output); } catch (error) { this.logger.error('API Error Details:', { status: error.response?.status, @@ -48,14 +48,16 @@ export class StockTradeHistoryService { /** * @private API에서 받은 주식현재가 체결 데이터를 필요한 정보로 정제하는 함수 - * @param {StockTradeHistoryOutputDto} infos - API 응답에서 받은 원시 데이터 + * @param {TodayStockTradeHistoryOutputDto} infos - API 응답에서 받은 원시 데이터 * @returns - 필요한 정보만 추출한 데이터 배열 * * @author uuuo3o */ - private formatTradeHistoryData(infos: StockTradeHistoryOutputDto[]) { + private formatTodayStockTradeHistoryData( + infos: TodayStockTradeHistoryOutputDto[], + ) { return infos.map((info) => { - const infoData = new StockTradeHistoryDataDto(); + const infoData = new TodayStockTradeHistoryDataDto(); infoData.stck_cntg_hour = info.stck_cntg_hour; infoData.stck_prpr = info.stck_prpr; infoData.prdy_vrss_sign = info.prdy_vrss_sign; From 7012e9ab188a53ea0040b5ea8d49d83a05d1f5ff Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 20:16:18 +0900 Subject: [PATCH 107/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20=EC=9D=BC=EC=9E=90=EB=B3=84=20API?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20interface,=20dto=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/daily-stock-trade-history-data.dto.ts | 27 +++++++++++ .../daily-stock-trade-history-ouput.dto.ts | 45 +++++++++++++++++++ .../daily-stock-trade-history-response.dto.ts | 8 ++++ .../inquire-daily-price.interface.ts | 23 ++++++++++ 4 files changed, 103 insertions(+) create mode 100644 BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts create mode 100644 BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts create mode 100644 BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts create mode 100644 BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts diff --git a/BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts b/BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts new file mode 100644 index 00000000..5cd38693 --- /dev/null +++ b/BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DailyStockTradeHistoryDataDto { + @ApiProperty({ description: '주식 영업 일자' }) + stck_bsop_date: string; + + @ApiProperty({ description: '주식 시가' }) + stck_oprc: string; + + @ApiProperty({ description: '주식 최고가' }) + stck_hgpr: string; + + @ApiProperty({ description: '주식 최저가' }) + stck_lwpr: string; + + @ApiProperty({ description: '주식 종가' }) + stck_clpr: string; + + @ApiProperty({ description: '누적 거래량' }) + acml_vol: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; +} diff --git a/BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts b/BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts new file mode 100644 index 00000000..99427dd6 --- /dev/null +++ b/BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DailyStockTradeHistoryOutputDto { + @ApiProperty({ description: '주식 영업 일자' }) + stck_bsop_date: string; + + @ApiProperty({ description: '주식 시가' }) + stck_oprc: string; + + @ApiProperty({ description: '주식 최고가' }) + stck_hgpr: string; + + @ApiProperty({ description: '주식 최저가' }) + stck_lwpr: string; + + @ApiProperty({ description: '주식 종가' }) + stck_clpr: string; + + @ApiProperty({ description: '누적 거래량' }) + acml_vol: string; + + @ApiProperty({ description: '전일 대비 거래량 비율' }) + prdy_vrss_vol_rate: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; + + @ApiProperty({ description: 'HTS 외국인 소진율' }) + hts_frgn_ehrt: string; + + @ApiProperty({ description: '외국인 순매수 수량' }) + frgn_ntby_qty: string; + + @ApiProperty({ description: '락 구분 코드' }) + flng_cls_code: string; + + @ApiProperty({ description: '누적 분할 비율' }) + acml_prtt_rate: string; +} diff --git a/BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts new file mode 100644 index 00000000..dd9bc0e1 --- /dev/null +++ b/BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts @@ -0,0 +1,8 @@ +import { DailyStockTradeHistoryOutputDto } from './daily-stock-trade-history-ouput.dto'; + +/** + * 주식현재가 일자별 API 응답값 정제 후 FE에 보낼 DTO + */ +export class DailyStockTradeHistoryResponseDto { + output: DailyStockTradeHistoryOutputDto[]; +} diff --git a/BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts b/BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts new file mode 100644 index 00000000..2546ade6 --- /dev/null +++ b/BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts @@ -0,0 +1,23 @@ +export interface InquireDailyPriceOutputData { + stck_bsop_date: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + stck_clpr: string; + acml_vol: string; + prdy_vrss_vol_rate: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + hts_frgn_ehrt: string; + frgn_ntby_qty: string; + flng_cls_code: string; + acml_prtt_rate: string; +} + +export interface InquireDailyPriceApiResponse { + output: InquireDailyPriceOutputData[]; + rt_cd: string; + msg_cd: string; + msg1: string; +} From 4a686ac957f9ed315da4d225e75dd99cfb412a33 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 20:16:30 +0900 Subject: [PATCH 108/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20=EC=9D=BC=EC=9E=90=EB=B3=84=20API?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../history/stock-trade-history.controller.ts | 21 ++++++- .../history/stock-trade-history.service.ts | 63 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index 8a2cf430..e9de0f81 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-history-response.dto'; +import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; @Controller('/api/stocks') export class StockTradeHistoryController { @@ -23,7 +24,25 @@ export class StockTradeHistoryController { description: '단일 주식 종목에 대한 주식현재가 체결값 조회 성공', type: TodayStockTradeHistoryResponseDto, }) - getStockDetail(@Param('stockCode') stockCode: string) { + getTodayStockTradeHistory(@Param('stockCode') stockCode: string) { return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } + + @Get(':stockCode/daily-trade-history') + @ApiOperation({ summary: '단일 주식 종목에 대한 일자별 주식현재가 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: '단일 주식 종목에 대한 일자별 주식현재가 조회 성공', + type: DailyStockTradeHistoryDataDto, + }) + getDailyStockTradeHistory(@Param('stockCode') stockCode: string) { + return this.stockTradeHistoryService.getDailyStockTradeHistory(stockCode); + } } diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index c5e0dacf..50204aff 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -6,6 +6,9 @@ import { getFullURL } from '../../../util/get-full-URL'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; import { TodayStockTradeHistoryOutputDto } from './dto/today-stock-trade-history-output.dto'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; +import { InquireDailyPriceApiResponse } from './interface/inquire-daily-price.interface'; +import { DailyStockTradeHistoryOutputDto } from './dto/daily-stock-trade-history-ouput.dto'; +import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; @Injectable() export class StockTradeHistoryService { @@ -68,6 +71,66 @@ export class StockTradeHistoryService { }); } + /** + * 특정 주식의 일자별 체결 데이터를 반환하는 함수 + * @param {string} stockCode - 종목코드 + * @returns - 특정 주식의 현재가 체결 데이터 객체 반환 + * + * @author uuuo3o + */ + async getDailyStockTradeHistory(stockCode: string) { + try { + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + fid_period_div_code: 'D', + fid_org_adj_prc: '0', + }; + + const response = await this.requestApi( + 'FHKST01010400', + '/uapi/domestic-stock/v1/quotations/inquire-daily-price', + queryParams, + ); + + return this.formatDailyStockTradeHistoryData(response.output); + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + + /** + * @private API에서 받은 주식현재가 일자별 데이터를 필요한 정보로 정제하는 함수 + * @param {DailyStockTradeHistoryOutputDto} datas - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatDailyStockTradeHistoryData( + datas: DailyStockTradeHistoryOutputDto[], + ) { + return datas.map((data) => { + const historyData = new DailyStockTradeHistoryDataDto(); + historyData.stck_bsop_date = data.stck_bsop_date; + historyData.stck_oprc = data.stck_oprc; + historyData.stck_hgpr = data.stck_hgpr; + historyData.stck_lwpr = data.stck_lwpr; + historyData.stck_clpr = data.stck_clpr; + historyData.acml_vol = data.acml_vol; + historyData.prdy_vrss_sign = data.prdy_vrss_sign; + historyData.prdy_ctrt = data.prdy_ctrt; + + return historyData; + }); + } + /** * @private 한국투자 Open API - API 호출용 공통 함수 * @param {string} trId - API 호출에 사용할 tr_id From 0afe5b19f40f0e5e9391031027cc8b43215f47c3 Mon Sep 17 00:00:00 2001 From: jinddings Date: Wed, 13 Nov 2024 19:01:22 +0900 Subject: [PATCH 109/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20redis=EB=A5=BC?= =?UTF-8?q?=20=EC=9D=B4=EC=9A=A9=ED=95=B4=20=EC=B5=9C=EA=B7=BC=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=8B=A8=EC=96=B4=20=EC=A0=80=EC=9E=A5(#57):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/package-lock.json | 79 +++++++++++++++++++ BE/package.json | 1 + BE/src/app.module.ts | 2 + BE/src/auth/strategy/jwt.strategy.ts | 7 +- BE/src/common/redis/redis.module.ts | 22 ++++++ BE/src/common/redis/redis.provider.ts | 14 ++++ BE/src/common/redis/redis.ts | 45 +++++++++++ .../list/interface/search-params.interface.ts | 1 + BE/src/stock/list/stock-list.controller.ts | 9 ++- BE/src/stock/list/stock-list.module.ts | 6 +- BE/src/stock/list/stock-list.service.ts | 10 ++- 11 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 BE/src/common/redis/redis.module.ts create mode 100644 BE/src/common/redis/redis.provider.ts create mode 100644 BE/src/common/redis/redis.ts diff --git a/BE/package-lock.json b/BE/package-lock.json index f9b6300f..b443ce76 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -32,6 +32,7 @@ "dotenv": "^16.4.5", "express": "^4.21.1", "fastify-swagger": "^5.1.1", + "ioredis": "^5.4.1", "mysql2": "^3.11.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -822,6 +823,12 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "license": "ISC", @@ -3630,6 +3637,15 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "devOptional": true, @@ -6407,6 +6423,30 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -7813,11 +7853,23 @@ "version": "4.17.21", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -9084,6 +9136,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "license": "Apache-2.0" @@ -9911,6 +9984,12 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "license": "MIT", diff --git a/BE/package.json b/BE/package.json index 86c3538b..9bff210b 100644 --- a/BE/package.json +++ b/BE/package.json @@ -43,6 +43,7 @@ "dotenv": "^16.4.5", "express": "^4.21.1", "fastify-swagger": "^5.1.1", + "ioredis": "^5.4.1", "mysql2": "^3.11.3", "passport": "^0.7.0", "passport-jwt": "^4.0.1", diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index ca5abc62..27220f4b 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -14,6 +14,7 @@ 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'; @Module({ imports: [ @@ -29,6 +30,7 @@ import { StockTradeHistoryModule } from './stock/trade/history/stock-trade-histo StockOrderModule, StockListModule, StockTradeHistoryModule, + RedisModule, ], controllers: [AppController], providers: [AppService], diff --git a/BE/src/auth/strategy/jwt.strategy.ts b/BE/src/auth/strategy/jwt.strategy.ts index e6e85d05..7f8f5d2a 100644 --- a/BE/src/auth/strategy/jwt.strategy.ts +++ b/BE/src/auth/strategy/jwt.strategy.ts @@ -23,6 +23,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) { const user: User = await this.userRepository.findOne({ where: { email } }); if (!user) throw new UnauthorizedException(); - return user; + return { + userId: user.id, + email: user.email, + tutorial: user.tutorial, + kakaoId: user.kakaoId, + }; } } diff --git a/BE/src/common/redis/redis.module.ts b/BE/src/common/redis/redis.module.ts new file mode 100644 index 00000000..d8aa958c --- /dev/null +++ b/BE/src/common/redis/redis.module.ts @@ -0,0 +1,22 @@ +// src/common/redis/redis.module.ts +import { Global, Module } from '@nestjs/common'; +import Redis from 'ioredis'; +import { RedisUtil } from './redis'; + +@Global() +@Module({ + providers: [ + { + provide: 'REDIS_CLIENT', + useFactory: () => { + return new Redis({ + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }); + }, + }, + RedisUtil, + ], + exports: [RedisUtil, 'REDIS_CLIENT'], +}) +export class RedisModule {} diff --git a/BE/src/common/redis/redis.provider.ts b/BE/src/common/redis/redis.provider.ts new file mode 100644 index 00000000..28e1471d --- /dev/null +++ b/BE/src/common/redis/redis.provider.ts @@ -0,0 +1,14 @@ +import { Provider } from '@nestjs/common'; +import Redis from 'ioredis'; +import dotenv from 'dotenv'; + +dotenv.config(); +export const RedisProvider: Provider = { + provide: 'REDIS_CLIENT', + useFactory: () => { + return new Redis({ + host: process.env.REDIS_HOST, + port: Number(process.env.REDIS_PORT), + }); + }, +}; diff --git a/BE/src/common/redis/redis.ts b/BE/src/common/redis/redis.ts new file mode 100644 index 00000000..e2bee153 --- /dev/null +++ b/BE/src/common/redis/redis.ts @@ -0,0 +1,45 @@ +import { Injectable, Inject } from '@nestjs/common'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisUtil { + constructor( + @Inject('REDIS_CLIENT') + private readonly redis: Redis, + ) {} + + async get(key: string): Promise { + return this.redis.get(key); + } + + async set(key: string, value: string, expires?: number): Promise<'OK'> { + if (expires) { + return this.redis.set(key, value, 'EX', expires); + } + return this.redis.set(key, value); + } + + async del(key: string): Promise { + return this.redis.del(key); + } + + async zadd(key: string, score: number, member: string): Promise { + return this.redis.zadd(key, score, member); + } + + async zrange(key: string, start: number, stop: number): Promise { + return this.redis.zrange(key, start, stop); + } + + async zrevrange(key: string, start: number, stop: number): Promise { + return this.redis.zrevrange(key, start, stop); + } + + async zrem(key: string, member: string): Promise { + return this.redis.zrem(key, member); + } + + async expire(key: string, seconds: number): Promise { + return this.redis.expire(key, seconds); + } +} diff --git a/BE/src/stock/list/interface/search-params.interface.ts b/BE/src/stock/list/interface/search-params.interface.ts index 88080d0a..879f55b9 100644 --- a/BE/src/stock/list/interface/search-params.interface.ts +++ b/BE/src/stock/list/interface/search-params.interface.ts @@ -2,4 +2,5 @@ export interface SearchParams { name?: string; market?: string; code?: string; + userId: number; } diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index 427f9ff8..d7272784 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -1,5 +1,7 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Request } from 'express'; +import { JwtAuthGuard } from 'src/auth/jwt-auth-guard'; import { StockListService } from './stock-list.service'; import { StockListResponseDto } from './dto/stock-list-response.dto'; @@ -32,12 +34,15 @@ export class StockListController { @ApiQuery({ name: 'market', required: false }) @ApiQuery({ name: 'code', required: false }) @Get('/search') + @UseGuards(JwtAuthGuard) async searchWithQuery( + @Req() req: Request, @Query('name') name?: string, @Query('market') market?: string, @Query('code') code?: string, ): Promise { - return this.stockListService.search({ name, market, code }); + const userId = parseInt(req.user.userId, 10); + return this.stockListService.search({ name, market, code, userId }); } @ApiOperation({ diff --git a/BE/src/stock/list/stock-list.module.ts b/BE/src/stock/list/stock-list.module.ts index 9087c692..9c2c79e0 100644 --- a/BE/src/stock/list/stock-list.module.ts +++ b/BE/src/stock/list/stock-list.module.ts @@ -1,14 +1,16 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { RedisUtil } from 'src/common/redis/redis'; +import { RedisModule } from 'src/common/redis/redis.module'; import { StockListRepository } from './stock-list.repostiory'; import { StockListService } from './stock-list.service'; import { StockListController } from './stock-list.controller'; import { Stocks } from './stock-list.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Stocks])], + imports: [TypeOrmModule.forFeature([Stocks]), RedisModule], controllers: [StockListController], - providers: [StockListRepository, StockListService], + providers: [StockListRepository, StockListService, RedisUtil], exports: [], }) export class StockListModule {} diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index 1c46c5bb..5868d9a8 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -1,4 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { RedisUtil } from 'src/common/redis/redis'; import { StockListRepository } from './stock-list.repostiory'; import { Stocks } from './stock-list.entity'; import { StockListResponseDto } from './dto/stock-list-response.dto'; @@ -6,7 +7,10 @@ import { SearchParams } from './interface/search-params.interface'; @Injectable() export class StockListService { - constructor(private readonly stockListRepository: StockListRepository) {} + constructor( + private readonly stockListRepository: StockListRepository, + private readonly redisUtil: RedisUtil, + ) {} private toResponseDto(stock: Stocks): StockListResponseDto { return new StockListResponseDto(stock.code, stock.name, stock.market); @@ -27,6 +31,10 @@ export class StockListService { } async search(params: SearchParams): Promise { + const key = `search:${params.userId}`; + const score = Date.now(); + + await this.redisUtil.zadd(key, score, JSON.stringify(params)); const stocks = await this.stockListRepository.search(params); return stocks.map((stock) => this.toResponseDto(stock)); } From 4ae58a8624509715698cc344e0d2263c6a8bd895 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:25:28 +0900 Subject: [PATCH 110/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index e2cdb2a2..04bfd32b 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -113,7 +113,8 @@ jobs: --name ${{ matrix.app.container }} \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ --env-file .env \ - ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} \ + -v /etc/localtime:/etc/localtime:ro -e TZ=Asia/Seoul - name: Remove Github Action Ip to Security group run: | From 9c0f9806cf96374ff7d255240f50808d5e5b98d1 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:38:36 +0900 Subject: [PATCH 111/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20docker=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 04bfd32b..e0cbe310 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -113,8 +113,9 @@ jobs: --name ${{ matrix.app.container }} \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ --env-file .env \ + -v /etc/localtime:/etc/localtime:ro \ + -e TZ=Asia/Seoul \ ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} \ - -v /etc/localtime:/etc/localtime:ro -e TZ=Asia/Seoul - name: Remove Github Action Ip to Security group run: | From 128ddb1b71d320c173644ea476079e40544530e7 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:43:26 +0900 Subject: [PATCH 112/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20docker=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index e0cbe310..c2cde28f 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -115,7 +115,7 @@ jobs: --env-file .env \ -v /etc/localtime:/etc/localtime:ro \ -e TZ=Asia/Seoul \ - ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} \ + ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} - name: Remove Github Action Ip to Security group run: | From 7547bcb28a5343c43bec52ccad6e132b001a51fb Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:51:50 +0900 Subject: [PATCH 113/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20auth=20api=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 98c3d7ad..8fcdf300 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -16,7 +16,7 @@ import { ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; import { AuthCredentialsDto } from './dto/auth-credentials.dto'; -@Controller('auth') +@Controller('/api/auth') export class AuthController { constructor( private authService: AuthService, From 8276c039f0505ee2382d3c303740817a1e2b8996 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 12:05:51 +0900 Subject: [PATCH 114/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20stock=5Fbalance?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=EA=B0=92=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20rate=20=EC=86=8C=EC=88=98=EC=A0=90=205=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EB=82=98=ED=83=80=EB=82=B4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/asset/asset.entity.ts | 4 ++-- BE/src/stock/order/stock-order.repository.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BE/src/asset/asset.entity.ts b/BE/src/asset/asset.entity.ts index 6abc123a..b3504463 100644 --- a/BE/src/asset/asset.entity.ts +++ b/BE/src/asset/asset.entity.ts @@ -13,7 +13,7 @@ export class Asset { @Column({ nullable: false, default: INIT_ASSET }) cash_balance: number; - @Column({ nullable: false, default: INIT_ASSET }) + @Column({ nullable: false, default: 0 }) stock_balance: number; @Column({ nullable: false, default: INIT_ASSET }) @@ -22,7 +22,7 @@ export class Asset { @Column({ nullable: false, default: 0 }) total_profit: number; - @Column({ nullable: false, default: 0 }) + @Column('decimal', { nullable: false, default: 0, precision: 10, scale: 5 }) total_profit_rate: number; @Column({ nullable: true }) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 8067a7ff..88344cf2 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -27,12 +27,12 @@ export class StockOrderRepository extends Repository { { id: order.id }, { status: StatusType.COMPLETE, completed_at: new Date() }, ); + // TODO: stock_balance와 total_asset은 실시간 주가에 따라 변동하도록 따로 구현해야 함 await queryRunner.manager .createQueryBuilder() .update(Asset) .set({ cash_balance: () => 'cash_balance - :realPrice', - stock_balance: () => 'stock_balance - :realPrice', total_asset: () => 'total_asset - :realPrice', total_profit: () => 'total_profit - :realPrice', total_profit_rate: () => `total_profit / 10000000`, @@ -61,12 +61,12 @@ export class StockOrderRepository extends Repository { { id: order.id }, { status: StatusType.COMPLETE, completed_at: new Date() }, ); + // TODO: stock_balance와 total_asset은 실시간 주가에 따라 변동하도록 따로 구현해야 함 await queryRunner.manager .createQueryBuilder() .update(Asset) .set({ cash_balance: () => 'cash_balance + :realPrice', - stock_balance: () => 'stock_balance + :realPrice', total_asset: () => 'total_asset + :realPrice', total_profit: () => 'total_profit + :realPrice', total_profit_rate: () => `total_profit / 10000000`, From b0b13e64cb6977da7606e6d37a164ae6104b4c13 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 12:27:32 +0900 Subject: [PATCH 115/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20stock-order-socke?= =?UTF-8?q?t=EA=B3=BC=20stock-order=20=EC=84=9C=EB=B9=84=EC=8A=A4=EA=B0=80?= =?UTF-8?q?=20=EC=88=9C=ED=99=98=EC=B0=B8=EC=A1=B0=20=ED=95=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EA=B2=8C=20=EC=88=98=EC=A0=95=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 각 socket service를 각 기능 모듈에 종속되도록 수정함. --- .../stock/index/stock-index-socket.service.ts | 6 +- BE/src/stock/index/stock-index.module.ts | 3 +- .../stock/order/stock-order-socket.service.ts | 119 ++++++++++++++++++ BE/src/stock/order/stock-order.module.ts | 12 +- BE/src/stock/order/stock-order.service.ts | 85 +------------ BE/src/websocket/socket.module.ts | 17 +-- .../stock/price/stock-price-socket.service.ts | 51 -------- 7 files changed, 136 insertions(+), 157 deletions(-) rename BE/src/{websocket => }/stock/index/stock-index-socket.service.ts (84%) create mode 100644 BE/src/stock/order/stock-order-socket.service.ts delete mode 100644 BE/src/websocket/stock/price/stock-price-socket.service.ts diff --git a/BE/src/websocket/stock/index/stock-index-socket.service.ts b/BE/src/stock/index/stock-index-socket.service.ts similarity index 84% rename from BE/src/websocket/stock/index/stock-index-socket.service.ts rename to BE/src/stock/index/stock-index-socket.service.ts index 76ac9eb8..f82ba9a2 100644 --- a/BE/src/websocket/stock/index/stock-index-socket.service.ts +++ b/BE/src/stock/index/stock-index-socket.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { StockIndexValueElementDto } from '../../../stock/index/dto/stock-index-value-element.dto'; -import { BaseSocketService } from '../../base-socket.service'; -import { SocketGateway } from '../../socket.gateway'; +import { StockIndexValueElementDto } from './dto/stock-index-value-element.dto'; +import { BaseSocketService } from '../../websocket/base-socket.service'; +import { SocketGateway } from '../../websocket/socket.gateway'; @Injectable() export class StockIndexSocketService { diff --git a/BE/src/stock/index/stock-index.module.ts b/BE/src/stock/index/stock-index.module.ts index 498ddd8b..d763d972 100644 --- a/BE/src/stock/index/stock-index.module.ts +++ b/BE/src/stock/index/stock-index.module.ts @@ -3,10 +3,11 @@ import { StockIndexController } from './stock-index.controller'; import { StockIndexService } from './stock-index.service'; import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; import { SocketModule } from '../../websocket/socket.module'; +import { StockIndexSocketService } from './stock-index-socket.service'; @Module({ imports: [KoreaInvestmentModule, SocketModule], controllers: [StockIndexController], - providers: [StockIndexService], + providers: [StockIndexService, StockIndexSocketService], }) export class StockIndexModule {} diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts new file mode 100644 index 00000000..0f56140f --- /dev/null +++ b/BE/src/stock/order/stock-order-socket.service.ts @@ -0,0 +1,119 @@ +import { + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; +import { BaseSocketService } from '../../websocket/base-socket.service'; +import { SocketGateway } from '../../websocket/socket.gateway'; +import { Order } from './stock-order.entity'; +import { TradeType } from './enum/trade-type'; +import { StatusType } from './enum/status-type'; +import { StockOrderRepository } from './stock-order.repository'; + +@Injectable() +export class StockOrderSocketService { + private TR_ID = 'H0STCNT0'; + + private readonly logger = new Logger(); + + constructor( + private readonly socketGateway: SocketGateway, + private readonly baseSocketService: BaseSocketService, + private readonly stockOrderRepository: StockOrderRepository, + ) { + baseSocketService.registerSocketOpenHandler(async () => { + const orders: Order[] = + await this.stockOrderRepository.findAllCodeByStatus(); + orders.forEach((order) => { + baseSocketService.registerCode(this.TR_ID, order.stock_code); + }); + }); + + baseSocketService.registerSocketDataHandler( + this.TR_ID, + (data: string[]) => { + this.checkExecutableOrder( + data[0], // 주식 코드 + data[2], // 주식 체결가 + ).catch(() => { + throw new InternalServerErrorException(); + }); + }, + ); + } + + subscribeByCode(trKey: string) { + this.baseSocketService.registerCode(this.TR_ID, trKey); + } + + unsubscribeByCode(trKey: string) { + this.baseSocketService.unregisterCode(this.TR_ID, trKey); + } + + private async checkExecutableOrder(stockCode: string, value) { + const buyOrders = await this.stockOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.BUY, + status: StatusType.PENDING, + price: MoreThanOrEqual(value), + }, + }); + + const sellOrders = await this.stockOrderRepository.find({ + where: { + stock_code: stockCode, + trade_type: TradeType.SELL, + status: StatusType.PENDING, + price: LessThanOrEqual(value), + }, + }); + + await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); + await Promise.all( + sellOrders.map((sellOrder) => this.executeSell(sellOrder)), + ); + + if ( + !(await this.stockOrderRepository.existsBy({ + stock_code: stockCode, + status: StatusType.PENDING, + })) + ) + this.unsubscribeByCode(stockCode); + } + + private async executeBuy(order) { + this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockOrderRepository.updateOrderAndAssetWhenBuy( + order, + totalPrice + fee, + ); + } + + private async executeSell(order) { + this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); + + const totalPrice = order.price * order.amount; + const fee = this.calculateFee(totalPrice); + await this.stockOrderRepository.updateOrderAndAssetWhenSell( + order, + totalPrice - fee, + ); + } + + private calculateFee(totalPrice: number) { + if (totalPrice <= 10000000) return totalPrice * 0.16; + if (totalPrice > 10000000 && totalPrice <= 50000000) + return totalPrice * 0.14; + if (totalPrice > 50000000 && totalPrice <= 100000000) + return totalPrice * 0.12; + if (totalPrice > 100000000 && totalPrice <= 300000000) + return totalPrice * 0.1; + return totalPrice * 0.08; + } +} diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index 944813d5..454ca252 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -1,4 +1,4 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { StockOrderController } from './stock-order.controller'; import { StockOrderService } from './stock-order.service'; @@ -6,15 +6,11 @@ import { Order } from './stock-order.entity'; import { StockOrderRepository } from './stock-order.repository'; import { SocketModule } from '../../websocket/socket.module'; import { AssetModule } from '../../asset/asset.module'; +import { StockOrderSocketService } from './stock-order-socket.service'; @Module({ - imports: [ - TypeOrmModule.forFeature([Order]), - forwardRef(() => SocketModule), - AssetModule, - ], + imports: [TypeOrmModule.forFeature([Order]), SocketModule, AssetModule], controllers: [StockOrderController], - providers: [StockOrderService, StockOrderRepository], - exports: [StockOrderService], + providers: [StockOrderService, StockOrderRepository, StockOrderSocketService], }) export class StockOrderModule {} diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index 866a96cc..f0266010 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -1,8 +1,6 @@ import { ConflictException, ForbiddenException, - forwardRef, - Inject, Injectable, Logger, } from '@nestjs/common'; @@ -12,22 +10,15 @@ import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; import { StatusType } from './enum/status-type'; -import { StockPriceSocketService } from '../../websocket/stock/price/stock-price-socket.service'; +import { StockOrderSocketService } from './stock-order-socket.service'; @Injectable() export class StockOrderService { constructor( private readonly stockOrderRepository: StockOrderRepository, - @Inject(forwardRef(() => StockPriceSocketService)) - private readonly stockPriceSocketService: StockPriceSocketService, + private readonly stockOrderSocketService: StockOrderSocketService, ) {} - private readonly logger = new Logger(); - - async findAllPendingOrderCode() { - return this.stockOrderRepository.findAllCodeByStatus(); - } - async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { const order = this.stockOrderRepository.create({ user_id: userId, @@ -39,7 +30,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); - this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); + this.stockOrderSocketService.subscribeByCode(stockOrderRequest.stock_code); } async sell(userId: number, stockOrderRequest: StockOrderRequestDto) { @@ -53,7 +44,7 @@ export class StockOrderService { }); await this.stockOrderRepository.save(order); - this.stockPriceSocketService.subscribeByCode(stockOrderRequest.stock_code); + this.stockOrderSocketService.subscribeByCode(stockOrderRequest.stock_code); } async cancel(userId: number, orderId: number) { @@ -75,72 +66,6 @@ export class StockOrderService { status: StatusType.PENDING, })) ) - this.stockPriceSocketService.unsubscribeByCode(order.stock_code); - } - - async checkExecutableOrder(stockCode: string, value) { - const buyOrders = await this.stockOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.BUY, - status: StatusType.PENDING, - price: MoreThanOrEqual(value), - }, - }); - - const sellOrders = await this.stockOrderRepository.find({ - where: { - stock_code: stockCode, - trade_type: TradeType.SELL, - status: StatusType.PENDING, - price: LessThanOrEqual(value), - }, - }); - - await Promise.all(buyOrders.map((buyOrder) => this.executeBuy(buyOrder))); - await Promise.all( - sellOrders.map((sellOrder) => this.executeSell(sellOrder)), - ); - - if ( - !(await this.stockOrderRepository.existsBy({ - stock_code: stockCode, - status: StatusType.PENDING, - })) - ) - this.stockPriceSocketService.unsubscribeByCode(stockCode); - } - - private async executeBuy(order) { - this.logger.log(`${order.id}번 매수 예약이 체결되었습니다.`, 'BUY'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetWhenBuy( - order, - totalPrice + fee, - ); - } - - private async executeSell(order) { - this.logger.log(`${order.id}번 매도 예약이 체결되었습니다.`, 'SELL'); - - const totalPrice = order.price * order.amount; - const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetWhenSell( - order, - totalPrice - fee, - ); - } - - private calculateFee(totalPrice: number) { - if (totalPrice <= 10000000) return totalPrice * 0.16; - if (totalPrice > 10000000 && totalPrice <= 50000000) - return totalPrice * 0.14; - if (totalPrice > 50000000 && totalPrice <= 100000000) - return totalPrice * 0.12; - if (totalPrice > 100000000 && totalPrice <= 300000000) - return totalPrice * 0.1; - return totalPrice * 0.08; + this.stockOrderSocketService.unsubscribeByCode(order.stock_code); } } diff --git a/BE/src/websocket/socket.module.ts b/BE/src/websocket/socket.module.ts index 5dd0e042..e7a2dcc7 100644 --- a/BE/src/websocket/socket.module.ts +++ b/BE/src/websocket/socket.module.ts @@ -1,21 +1,10 @@ -import { forwardRef, Module } from '@nestjs/common'; -import { StockIndexSocketService } from './stock/index/stock-index-socket.service'; +import { Module } from '@nestjs/common'; import { SocketGateway } from './socket.gateway'; import { SocketTokenService } from './socket-token.service'; -import { StockPriceSocketService } from './stock/price/stock-price-socket.service'; import { BaseSocketService } from './base-socket.service'; -import { StockOrderModule } from '../stock/order/stock-order.module'; @Module({ - imports: [forwardRef(() => StockOrderModule)], - controllers: [], - providers: [ - SocketTokenService, - StockIndexSocketService, - SocketGateway, - StockPriceSocketService, - BaseSocketService, - ], - exports: [SocketGateway, StockPriceSocketService], + providers: [SocketTokenService, SocketGateway, BaseSocketService], + exports: [SocketGateway, BaseSocketService], }) export class SocketModule {} diff --git a/BE/src/websocket/stock/price/stock-price-socket.service.ts b/BE/src/websocket/stock/price/stock-price-socket.service.ts deleted file mode 100644 index 4f591c81..00000000 --- a/BE/src/websocket/stock/price/stock-price-socket.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - forwardRef, - Inject, - Injectable, - InternalServerErrorException, -} from '@nestjs/common'; -import { BaseSocketService } from '../../base-socket.service'; -import { SocketGateway } from '../../socket.gateway'; -import { StockOrderService } from '../../../stock/order/stock-order.service'; -import { Order } from '../../../stock/order/stock-order.entity'; - -@Injectable() -export class StockPriceSocketService { - private TR_ID = 'H0STCNT0'; - - constructor( - private readonly socketGateway: SocketGateway, - private readonly baseSocketService: BaseSocketService, - @Inject(forwardRef(() => StockOrderService)) - private readonly stockOrderService: StockOrderService, - ) { - baseSocketService.registerSocketOpenHandler(async () => { - const orders: Order[] = await stockOrderService.findAllPendingOrderCode(); - orders.forEach((order) => { - baseSocketService.registerCode(this.TR_ID, order.stock_code); - }); - }); - - baseSocketService.registerSocketDataHandler( - this.TR_ID, - (data: string[]) => { - stockOrderService - .checkExecutableOrder( - data[0], // 주식 코드 - data[2], // 주식 체결가 - ) - .catch(() => { - throw new InternalServerErrorException(); - }); - }, - ); - } - - subscribeByCode(trKey: string) { - this.baseSocketService.registerCode(this.TR_ID, trKey); - } - - unsubscribeByCode(trKey: string) { - this.baseSocketService.unregisterCode(this.TR_ID, trKey); - } -} From 690d05aaf04d1865e6721f15691410521a860570 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 12:31:20 +0900 Subject: [PATCH 116/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20lint=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20import/no?= =?UTF-8?q?-cycle=20=EB=8B=A4=EC=8B=9C=20on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/.eslintrc.js | 1 - BE/src/stock/order/stock-order.service.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/BE/.eslintrc.js b/BE/.eslintrc.js index 4807b847..b54cc5a8 100644 --- a/BE/.eslintrc.js +++ b/BE/.eslintrc.js @@ -35,6 +35,5 @@ module.exports = { '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/naming-convention': 'off', - 'import/no-cycle': 'off', }, }; diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index f0266010..b558dc5e 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -2,10 +2,8 @@ import { ConflictException, ForbiddenException, Injectable, - Logger, } from '@nestjs/common'; import { NotFoundError } from 'rxjs'; -import { LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; import { StockOrderRequestDto } from './dto/stock-order-request.dto'; import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; From a6170383e2d1a691fc87f3a1144fe7f133496bbb Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:25:05 +0900 Subject: [PATCH 117/158] =?UTF-8?q?=E2=9C=A8=20feat:=20API=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=B4=20chart=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ....dto.ts => stock-detail-chart-data.dto.ts} | 2 +- .../dto/stock-detail-chart-response.dto.ts | 5 + .../detail/dto/stock-detail-response.dto.ts | 14 --- .../stock/detail/stock-detail.controller.ts | 6 +- BE/src/stock/detail/stock-detail.service.ts | 98 +++++++------------ 5 files changed, 42 insertions(+), 83 deletions(-) rename BE/src/stock/detail/dto/{stock-detail-output2.dto.ts => stock-detail-chart-data.dto.ts} (96%) create mode 100644 BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts delete mode 100644 BE/src/stock/detail/dto/stock-detail-response.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-output2.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts similarity index 96% rename from BE/src/stock/detail/dto/stock-detail-output2.dto.ts rename to BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts index c6df900f..fd108090 100644 --- a/BE/src/stock/detail/dto/stock-detail-output2.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class InquirePriceOutput2Dto { +export class InquirePriceChartDataDto { @ApiProperty({ description: '주식 영업 일자' }) stck_bsop_date: string; diff --git a/BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts new file mode 100644 index 00000000..3c580851 --- /dev/null +++ b/BE/src/stock/detail/dto/stock-detail-chart-response.dto.ts @@ -0,0 +1,5 @@ +import { InquirePriceChartDataDto } from './stock-detail-chart-data.dto'; + +export class InquirePriceChartResponseDto { + output: InquirePriceChartDataDto[]; +} diff --git a/BE/src/stock/detail/dto/stock-detail-response.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts deleted file mode 100644 index a86f9e04..00000000 --- a/BE/src/stock/detail/dto/stock-detail-response.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { InquirePriceOutput1Dto } from './stock-detail-output1.dto'; -import { InquirePriceOutput2Dto } from './stock-detail-output2.dto'; - -/** - * 국내주식기간별시세(일/주/월/년) API 응답값 정제 후 FE에 보낼 DTO - */ -export class InquirePriceResponseDto { - @ApiProperty({ type: InquirePriceOutput1Dto, description: '상승률 순위' }) - output1: InquirePriceOutput1Dto; - - @ApiProperty({ type: [InquirePriceOutput2Dto], description: '하락률 순위' }) - output2: InquirePriceOutput2Dto[]; -} diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 5c4cfa85..64509746 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Param, Post } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; import { StockDetailRequestDto } from './dto/stock-detail-request.dto'; -import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; +import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @Controller('/api/stocks') export class StockDetailController { @@ -28,14 +28,14 @@ export class StockDetailController { @ApiResponse({ status: 201, description: '단일 주식 종목 기본값 조회 성공', - type: InquirePriceResponseDto, + type: InquirePriceChartResponseDto, }) getStockDetail( @Param('stockCode') stockCode: string, @Body() body: StockDetailRequestDto, ) { const { fid_input_date_1, fid_input_date_2, fid_period_div_code } = body; - return this.stockDetailService.getInquirePrice( + return this.stockDetailService.getInquirePriceChart( stockCode, fid_input_date_1, fid_input_date_2, diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index c0629406..a454beec 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -4,8 +4,7 @@ import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.s import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; import { InquirePriceApiResponse } from './interface/stock-detail.interface'; -import { StockDetailQueryParameterDto } from './dto/stock-detail-query-parameter.dto'; -import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; +import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @Injectable() export class StockDetailService { @@ -23,23 +22,29 @@ export class StockDetailService { * * @author uuuo3o */ - async getInquirePrice( + async getInquirePriceChart( stockCode: string, date1: string, date2: string, periodDivCode: string, ) { try { - const queryParams = new StockDetailQueryParameterDto(); - queryParams.fid_cond_mrkt_div_code = 'J'; - queryParams.fid_input_iscd = stockCode; - queryParams.fid_input_date_1 = date1; - queryParams.fid_input_date_2 = date2; - queryParams.fid_period_div_code = periodDivCode; + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + fid_input_date_1: date1, + fid_input_date_2: date2, + fid_period_div_code: periodDivCode, + fid_org_adj_prc: '0', + }; - const response = await this.requestApi(queryParams); + const response = await this.requestApi( + 'FHKST03010100', + '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', + queryParams, + ); - return this.formatStockData(response); + return this.formatStockInquirePriceData(response); } catch (error) { this.logger.error('API Error Details:', { status: error.response?.status, @@ -53,22 +58,25 @@ export class StockDetailService { } /** - * @private 한국투자 Open API - [국내주식] 기본시세 - 국내주식기간별시세(일/주/월/년) 호출 함수 - * @param {StockDetailQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO - * @returns - 국내주식기간별시세(일/주/월/년) 데이터 + * @private 한국투자 Open API - API 호출용 공통 함수 + * @param {string} trId - API 호출에 사용할 tr_id + * @param {string} apiURL - API 호출에 사용할 URL + * @param {Record} params - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - API 호출에 대한 응답 데이터 * * @author uuuo3o */ - private async requestApi(queryParams: StockDetailQueryParameterDto) { + private async requestApi( + trId: string, + apiURL: string, + params: Record, + ): Promise { try { const accessToken = await this.koreaInvetmentService.getAccessToken(); - const headers = getHeader(accessToken, 'FHKST03010100'); - const url = getFullURL( - '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', - ); - const params = this.getInquirePriceParams(queryParams); + const headers = getHeader(accessToken, trId); + const url = getFullURL(apiURL); - const response = await axios.get(url, { + const response = await axios.get(url, { headers, params, }); @@ -93,52 +101,12 @@ export class StockDetailService { * * @author uuuo3o */ - private formatStockData(response: InquirePriceApiResponse) { - const stockData = new InquirePriceResponseDto(); - const { output1, output2 } = response; - - const { - hts_kor_isnm, - stck_shrn_iscd, - stck_prpr, - prdy_vrss, - prdy_vrss_sign, - prdy_ctrt, - hts_avls, - per, - } = output1; - - stockData.output1 = { - hts_kor_isnm, - stck_shrn_iscd, - stck_prpr, - prdy_vrss, - prdy_vrss_sign, - prdy_ctrt, - hts_avls, - per, - }; + private formatStockInquirePriceData(response: InquirePriceApiResponse) { + const stockData = new InquirePriceChartResponseDto(); + const { output2 } = response; - stockData.output2 = output2; + stockData.output = output2; return stockData; } - - /** - * @private 국내주식기간별시세(일/주/월/년) 요청을 위한 쿼리 파라미터 객체 생성 함수 - * @param {StockDetailQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO - * @returns - API 요청에 필요한 쿼리 파라미터 객체 - * - * @author uuuo3o - */ - private getInquirePriceParams(params: StockDetailQueryParameterDto) { - return { - fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, - fid_input_iscd: params.fid_input_iscd, - fid_input_date_1: params.fid_input_date_1, - fid_input_date_2: params.fid_input_date_2, - fid_period_div_code: params.fid_period_div_code, - fid_org_adj_prc: 0, - }; - } } From e23424b4e51d676a5b95f0509fb0367e98b0054d Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:34:13 +0900 Subject: [PATCH 118/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=ED=94=84=EB=A1=A0?= =?UTF-8?q?=ED=8A=B8=EC=97=90=EC=84=9C=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EA=B0=92=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20DTO=20=EC=88=98=EC=A0=95#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/dto/stock-detail-chart-data.dto.ts | 21 ---------------- BE/src/stock/detail/stock-detail.service.ts | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts index fd108090..4574d3a7 100644 --- a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts @@ -18,25 +18,4 @@ export class InquirePriceChartDataDto { @ApiProperty({ description: '누적 거래량' }) acml_vol: string; - - @ApiProperty({ description: '누적 거래 대금' }) - acml_tr_pbmn: string; - - @ApiProperty({ description: '락 구분 코드' }) - flng_cls_code: string; - - @ApiProperty({ description: '분할 비율' }) - prtt_rate: string; - - @ApiProperty({ description: '분할변경여부' }) - mod_yn: string; - - @ApiProperty({ description: '전일 대비 부호' }) - prdy_vrss_sign: string; - - @ApiProperty({ description: '전일 대비' }) - prdy_vrss: string; - - @ApiProperty({ description: '재평가사유코드' }) - revl_issu_reas: string; } diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index a454beec..b47a6606 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -4,7 +4,7 @@ import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.s import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; import { InquirePriceApiResponse } from './interface/stock-detail.interface'; -import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; +import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; @Injectable() export class StockDetailService { @@ -102,11 +102,27 @@ export class StockDetailService { * @author uuuo3o */ private formatStockInquirePriceData(response: InquirePriceApiResponse) { - const stockData = new InquirePriceChartResponseDto(); const { output2 } = response; - stockData.output = output2; + return output2.map((info) => { + const stockData = new InquirePriceChartDataDto(); + const { + stck_bsop_date, + stck_clpr, + stck_oprc, + stck_hgpr, + stck_lwpr, + acml_vol, + } = info; - return stockData; + stockData.stck_bsop_date = stck_bsop_date; + stockData.stck_clpr = stck_clpr; + stockData.stck_oprc = stck_oprc; + stockData.stck_hgpr = stck_hgpr; + stockData.stck_lwpr = stck_lwpr; + stockData.acml_vol = acml_vol; + + return stockData; + }); } } From 0203e65fc95bcda5467421dccf4f220b3088cf61 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:36:22 +0900 Subject: [PATCH 119/158] =?UTF-8?q?=F0=9F=8E=A8=20style:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 64 ++++++++++----------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index b47a6606..ef463dc7 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -57,6 +57,38 @@ export class StockDetailService { } } + /** + * @private API에서 받은 국내주식기간별시세(일/주/월/년) 데이터를 필요한 정보로 정제하는 함수 + * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatStockInquirePriceData(response: InquirePriceApiResponse) { + const { output2 } = response; + + return output2.map((info) => { + const stockData = new InquirePriceChartDataDto(); + const { + stck_bsop_date, + stck_clpr, + stck_oprc, + stck_hgpr, + stck_lwpr, + acml_vol, + } = info; + + stockData.stck_bsop_date = stck_bsop_date; + stockData.stck_clpr = stck_clpr; + stockData.stck_oprc = stck_oprc; + stockData.stck_hgpr = stck_hgpr; + stockData.stck_lwpr = stck_lwpr; + stockData.acml_vol = acml_vol; + + return stockData; + }); + } + /** * @private 한국투자 Open API - API 호출용 공통 함수 * @param {string} trId - API 호출에 사용할 tr_id @@ -93,36 +125,4 @@ export class StockDetailService { throw error; } } - - /** - * @private API에서 받은 국내주식기간별시세(일/주/월/년) 데이터를 필요한 정보로 정제하는 함수 - * @param {InquirePriceApiResponse} response - API 응답에서 받은 원시 데이터 - * @returns - 필요한 정보만 추출한 데이터 배열 - * - * @author uuuo3o - */ - private formatStockInquirePriceData(response: InquirePriceApiResponse) { - const { output2 } = response; - - return output2.map((info) => { - const stockData = new InquirePriceChartDataDto(); - const { - stck_bsop_date, - stck_clpr, - stck_oprc, - stck_hgpr, - stck_lwpr, - acml_vol, - } = info; - - stockData.stck_bsop_date = stck_bsop_date; - stockData.stck_clpr = stck_clpr; - stockData.stck_oprc = stck_oprc; - stockData.stck_hgpr = stck_hgpr; - stockData.stck_lwpr = stck_lwpr; - stockData.acml_vol = acml_vol; - - return stockData; - }); - } } From 00677bac48f023e5f6faebfc85c1b9469338a0fd Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:36:33 +0900 Subject: [PATCH 120/158] =?UTF-8?q?=F0=9F=94=A5=20remove:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/stock-detail-query-parameter.dto.ts | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts diff --git a/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts b/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts deleted file mode 100644 index feb9ca0f..00000000 --- a/BE/src/stock/detail/dto/stock-detail-query-parameter.dto.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 주식 현재가 시세 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO - */ -export class StockDetailQueryParameterDto { - /** - * 조건 시장 분류 코드 - * 'J' 주식 - */ - fid_cond_mrkt_div_code: string; - - /** - * 주식 종목 코드 - * (ex) 005930 - */ - fid_input_iscd: string; - - /** - * 조회 시작일자 - * (ex) 20220501 - */ - fid_input_date_1: string; - - /** - * 조회 종료일자 - * (ex) 20220530 - */ - fid_input_date_2: string; - - /** - * 기간 분류 코드 - * D:일봉, W:주봉, M:월봉, Y:년봉 - */ - fid_period_div_code: string; -} From 383a166626cfd0ff1b73c92282d644cb58e78cdf Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:41:07 +0900 Subject: [PATCH 121/158] =?UTF-8?q?=F0=9F=93=9D=20docs:=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20swagger?= =?UTF-8?q?=20=EB=AC=B8=EC=84=9C=20=EC=88=98=EC=A0=95#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...equest.dto.ts => stock-detail-chart-request.dto.ts} | 2 +- BE/src/stock/detail/stock-detail.controller.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename BE/src/stock/detail/dto/{stock-detail-request.dto.ts => stock-detail-chart-request.dto.ts} (92%) diff --git a/BE/src/stock/detail/dto/stock-detail-request.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-request.dto.ts similarity index 92% rename from BE/src/stock/detail/dto/stock-detail-request.dto.ts rename to BE/src/stock/detail/dto/stock-detail-chart-request.dto.ts index 7bd1b1d8..4a21bbc3 100644 --- a/BE/src/stock/detail/dto/stock-detail-request.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-request.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; /** * 국내주식기간별시세(일/주/월/년) API를 이용할 때 필요한 요청 데이터를 담고 있는 DTO */ -export class StockDetailRequestDto { +export class StockDetailChartRequestDto { @ApiProperty({ description: '조회 시작일자 (ex) 20220501' }) fid_input_date_1: string; diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 64509746..86149af2 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Param, Post } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; -import { StockDetailRequestDto } from './dto/stock-detail-request.dto'; +import { StockDetailChartRequestDto } from './dto/stock-detail-chart-request.dto'; import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @Controller('/api/stocks') @@ -9,7 +9,7 @@ export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} @Post(':stockCode') - @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) + @ApiOperation({ summary: '국내주식기간별시세(일/주/월/년) 조회 API' }) @ApiParam({ name: 'stockCode', required: true, @@ -23,16 +23,16 @@ export class StockDetailController { 'fid_input_date_1: 조회 시작일자 (ex) 20240505\n\n' + 'fid_input_date_2: 조회 종료일자 (ex) 20241111\n\n' + 'fid_period_div_code: 기간 분류 코드 (ex) D(일봉), W(주봉), M(월봉), Y(년봉)', - type: StockDetailRequestDto, + type: StockDetailChartRequestDto, }) @ApiResponse({ status: 201, - description: '단일 주식 종목 기본값 조회 성공', + description: '국내주식기간별시세(일/주/월/년) 조회 성공', type: InquirePriceChartResponseDto, }) getStockDetail( @Param('stockCode') stockCode: string, - @Body() body: StockDetailRequestDto, + @Body() body: StockDetailChartRequestDto, ) { const { fid_input_date_1, fid_input_date_2, fid_period_div_code } = body; return this.stockDetailService.getInquirePriceChart( From 463c4cd3f7c1833bc87ae6140bf20d3652a30bd8 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:46:25 +0900 Subject: [PATCH 122/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=B0=A8=ED=8A=B8?= =?UTF-8?q?=20=EA=B7=B8=EB=A6=AC=EA=B8=B0=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EA=B0=92=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts | 3 +++ ...ck-detail.interface.ts => stock-detail-chart.interface.ts} | 0 BE/src/stock/detail/stock-detail.service.ts | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) rename BE/src/stock/detail/interface/{stock-detail.interface.ts => stock-detail-chart.interface.ts} (100%) diff --git a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts index 4574d3a7..de68279f 100644 --- a/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-chart-data.dto.ts @@ -18,4 +18,7 @@ export class InquirePriceChartDataDto { @ApiProperty({ description: '누적 거래량' }) acml_vol: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; } diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail-chart.interface.ts similarity index 100% rename from BE/src/stock/detail/interface/stock-detail.interface.ts rename to BE/src/stock/detail/interface/stock-detail-chart.interface.ts diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index ef463dc7..e66b0736 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -3,7 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; -import { InquirePriceApiResponse } from './interface/stock-detail.interface'; +import { InquirePriceApiResponse } from './interface/stock-detail-chart.interface'; import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; @Injectable() @@ -76,6 +76,7 @@ export class StockDetailService { stck_hgpr, stck_lwpr, acml_vol, + prdy_vrss_sign, } = info; stockData.stck_bsop_date = stck_bsop_date; @@ -84,6 +85,7 @@ export class StockDetailService { stockData.stck_hgpr = stck_hgpr; stockData.stck_lwpr = stck_lwpr; stockData.acml_vol = acml_vol; + stockData.prdy_vrss_sign = prdy_vrss_sign; return stockData; }); From 764de7b4a95a8b8c8b365a26c9d2f9ceda6fda51 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 17:59:50 +0900 Subject: [PATCH 123/158] =?UTF-8?q?=F0=9F=9A=9A=20rename:=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EB=90=98=EB=8A=94=20=EC=9D=B4=EB=A6=84=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/interface/stock-detail-chart.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/detail/interface/stock-detail-chart.interface.ts b/BE/src/stock/detail/interface/stock-detail-chart.interface.ts index 1169eb90..e49746e9 100644 --- a/BE/src/stock/detail/interface/stock-detail-chart.interface.ts +++ b/BE/src/stock/detail/interface/stock-detail-chart.interface.ts @@ -47,7 +47,7 @@ export interface InquirePriceOutput2Data { revl_issu_reas: string; } -export interface InquirePriceApiResponse { +export interface InquirePriceChartApiResponse { output1: InquirePriceOutput1Data; output2: InquirePriceOutput2Data[]; rt_cd: string; From 98b8b5793dc760e059ca68b37d94b6480f3a5460 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 18:01:38 +0900 Subject: [PATCH 124/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=ED=98=84=EC=9E=AC=EA=B0=80=20API=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20interface,=20dto=20=EA=B5=AC=ED=98=84#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...t1.dto.ts => stock-detail-response.dto.ts} | 2 +- .../interface/stock-detail.interface.ts | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) rename BE/src/stock/detail/dto/{stock-detail-output1.dto.ts => stock-detail-response.dto.ts} (94%) create mode 100644 BE/src/stock/detail/interface/stock-detail.interface.ts diff --git a/BE/src/stock/detail/dto/stock-detail-output1.dto.ts b/BE/src/stock/detail/dto/stock-detail-response.dto.ts similarity index 94% rename from BE/src/stock/detail/dto/stock-detail-output1.dto.ts rename to BE/src/stock/detail/dto/stock-detail-response.dto.ts index aa911bd1..96e4ba52 100644 --- a/BE/src/stock/detail/dto/stock-detail-output1.dto.ts +++ b/BE/src/stock/detail/dto/stock-detail-response.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class InquirePriceOutput1Dto { +export class InquirePriceResponseDto { @ApiProperty({ description: 'HTS 한글 종목명' }) hts_kor_isnm: string; diff --git a/BE/src/stock/detail/interface/stock-detail.interface.ts b/BE/src/stock/detail/interface/stock-detail.interface.ts new file mode 100644 index 00000000..1cef7848 --- /dev/null +++ b/BE/src/stock/detail/interface/stock-detail.interface.ts @@ -0,0 +1,84 @@ +export interface InquirePriceOutputData { + iscd_stat_cls_code: string; + marg_rate: string; + rprs_mrkt_kor_name: string; + new_hgpr_lwpr_cls_code: string; + btsp_kor_isnm: string; + temp_stop_yn: string; + oprc_rang_cont_yn: string; + clpr_rang_cont_yn: string; + crdt_able_yn: string; + grmn_rate_cls_code: string; + elw_pblc_yn: string; + stck_prpr: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + acml_tr_pbmn: string; + acml_vol: string; + prdy_vrss_vol_rate: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + stck_mxpr: string; + stck_llam: string; + stck_sdpr: string; + wghn_avrg_stck_prc: string; + hts_frgn_ehrt: string; + frgn_ntby_qty: string; + pgtr_ntby_qty: string; + dmrs_val: string; + dmsp_val: string; + cpfn: string; + rstc_wdth_prc: string; + stck_fcam: string; + stck_sspr: string; + aspr_unit: string; + hts_deal_qty_unit_val: string; + lstn_stcn: string; + hts_avls: string; + per: string; + pbr: string; + stac_month: string; + vol_tnrt: string; + eps: string; + bps: string; + d250_hgpr: string; + d250_hgpr_date: string; + d250_hgpr_vrss_prpr_rate: string; + d250_lwpr: string; + d250_lwpr_date: string; + d250_lwpr_vrss_prpr_rate: string; + stck_dryy_hgpr: string; + dryy_hgpr_vrss_prpr_rate: string; + dryy_hgpr_date: string; + stck_dryy_lwpr: string; + dryy_lwpr_vrss_prpr_rate: string; + dryy_lwpr_date: string; + w52_hgpr: string; + w52_hgpr_vrss_prpr_ctrt: string; + w52_hgpr_date: string; + w52_lwpr: string; + w52_lwpr_vrss_prpr_ctrt: string; + w52_lwpr_date: string; + whol_loan_rmnd_rate: string; + ssts_yn: string; + stck_shrn_iscd: string; + fcam_cnnm: string; + cpfn_cnnm: string; + apprch_rate: string; + frgn_hldn_qty: string; + vi_cls_code: string; + ovtm_vi_cls_code: string; + last_ssts_cntg_qty: string; + invt_caful_yn: string; + mrkt_warn_cls_code: string; + short_over_yn: string; + sltr_yn: string; +} +export interface InquirePriceApiResponse { + output: InquirePriceOutputData; + rt_cd: string; + msg_cd: string; + msg1: string; +} From 4fe008b7d9e0ffa08e4665c02567e8a2953edca0 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 18:01:53 +0900 Subject: [PATCH 125/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=ED=98=84=EC=9E=AC=EA=B0=80=20API=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/detail/stock-detail.controller.ts | 23 ++++++- BE/src/stock/detail/stock-detail.service.ts | 63 ++++++++++++++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 86149af2..533b67ca 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,6 +1,7 @@ -import { Body, Controller, Param, Post } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; import { StockDetailChartRequestDto } from './dto/stock-detail-chart-request.dto'; import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @@ -8,6 +9,24 @@ import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response. export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} + @Get(':stockCode') + @ApiOperation({ summary: '단일 주식 종목 detail 페이지 상단부 조회 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: '단일 주식 종목 기본값 조회 성공', + type: InquirePriceResponseDto, + }) + getStockDetail(@Param('stockCode') stockCode: string) { + return this.stockDetailService.getInquirePrice(stockCode); + } + @Post(':stockCode') @ApiOperation({ summary: '국내주식기간별시세(일/주/월/년) 조회 API' }) @ApiParam({ @@ -30,7 +49,7 @@ export class StockDetailController { description: '국내주식기간별시세(일/주/월/년) 조회 성공', type: InquirePriceChartResponseDto, }) - getStockDetail( + getStockDetailChart( @Param('stockCode') stockCode: string, @Body() body: StockDetailChartRequestDto, ) { diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index e66b0736..8031c4d6 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -3,8 +3,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { KoreaInvestmentService } from '../../koreaInvestment/korea-investment.service'; import { getHeader } from '../../util/get-header'; import { getFullURL } from '../../util/get-full-URL'; -import { InquirePriceApiResponse } from './interface/stock-detail-chart.interface'; +import { InquirePriceChartApiResponse } from './interface/stock-detail-chart.interface'; import { InquirePriceChartDataDto } from './dto/stock-detail-chart-data.dto'; +import { + InquirePriceApiResponse, + InquirePriceOutputData, +} from './interface/stock-detail.interface'; +import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; @Injectable() export class StockDetailService { @@ -12,6 +17,58 @@ export class StockDetailService { constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + /** + * 주식현재가 시세 데이터를 반환하는 함수 + * @param {string} stockCode - 종목코드 + * @returns - 주식현재가 시세 데이터 객체 반환 + * + * @author uuuo3o + */ + async getInquirePrice(stockCode: string) { + try { + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + }; + + const response = await this.requestApi( + 'FHKST01010100', + '/uapi/domestic-stock/v1/quotations/inquire-price', + queryParams, + ); + + return this.formatStockData(response.output); + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + + /** + * @private API에서 받은 주식현재가 시세 데이터를 필요한 정보로 정제하는 함수 + * @param {InquirePriceOutputData} stock - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatStockData(stock: InquirePriceOutputData) { + const stockData = new InquirePriceResponseDto(); + stockData.stck_shrn_iscd = stock.stck_shrn_iscd; + stockData.stck_prpr = stock.stck_prpr; + stockData.prdy_vrss = stock.prdy_vrss; + stockData.prdy_vrss_sign = stock.prdy_vrss_sign; + stockData.prdy_ctrt = stock.prdy_ctrt; + stockData.hts_avls = stock.hts_avls; + stockData.per = stock.per; + return stockData; + } + /** * 특정 주식의 기간별시세 데이터를 반환하는 함수 * @param {string} stockCode - 종목코드 @@ -38,7 +95,7 @@ export class StockDetailService { fid_org_adj_prc: '0', }; - const response = await this.requestApi( + const response = await this.requestApi( 'FHKST03010100', '/uapi/domestic-stock/v1/quotations/inquire-daily-itemchartprice', queryParams, @@ -64,7 +121,7 @@ export class StockDetailService { * * @author uuuo3o */ - private formatStockInquirePriceData(response: InquirePriceApiResponse) { + private formatStockInquirePriceData(response: InquirePriceChartApiResponse) { const { output2 } = response; return output2.map((info) => { From 7dd9da8594338e0b727387eca4db1d28533a613f Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 19:46:15 +0900 Subject: [PATCH 126/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20reques?= =?UTF-8?q?tApi=20=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...stock-trade-history-query-parameter.dto.ts | 16 ---- .../history/stock-trade-history.service.ts | 85 +++++++++---------- 2 files changed, 40 insertions(+), 61 deletions(-) delete mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts deleted file mode 100644 index 6d9ccfa6..00000000 --- a/BE/src/stock/trade/history/dto/stock-trade-history-query-parameter.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * 주식현재가 체결 API를 사용할 때 쿼리 파라미터로 사용할 요청값 DTO - */ -export class StockTradeHistoryQueryParameterDto { - /** - * 조건 시장 분류 코드 - * 'J' 주식 - */ - fid_cond_mrkt_div_code: string; - - /** - * 주식 종목 코드 - * (ex) 005930 - */ - fid_input_iscd: string; -} diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index dc9b24e4..7617846e 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -3,7 +3,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { KoreaInvestmentService } from '../../../koreaInvestment/korea-investment.service'; import { getHeader } from '../../../util/get-header'; import { getFullURL } from '../../../util/get-full-URL'; -import { StockTradeHistoryQueryParameterDto } from './dto/stock-trade-history-query-parameter.dto'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; import { StockTradeHistoryOutputDto } from './dto/stock-trade-history-output.dto'; import { StockTradeHistoryDataDto } from './dto/stock-trade-history-data.dto'; @@ -23,11 +22,16 @@ export class StockTradeHistoryService { */ async getStockTradeHistory(stockCode: string) { try { - const queryParams = new StockTradeHistoryQueryParameterDto(); - queryParams.fid_cond_mrkt_div_code = 'J'; - queryParams.fid_input_iscd = stockCode; + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + }; - const response = await this.requestApi(queryParams); + const response = await this.requestApi( + 'FHKST01010300', + '/uapi/domestic-stock/v1/quotations/inquire-ccnl', + queryParams, + ); return this.formatTradeHistoryData(response.output); } catch (error) { @@ -42,38 +46,6 @@ export class StockTradeHistoryService { } } - /** - * @private 한국투자 Open API - [국내주식] 기본시세 - 주식현재가 체결 호출 함수 - * @param {StockTradeHistoryQueryParameterDto} queryParams - API 요청 시 필요한 쿼리 파라미터 DTO - * @returns - 주식현재가 체결 데이터 - * - * @author uuuo3o - */ - private async requestApi(queryParams: StockTradeHistoryQueryParameterDto) { - try { - const accessToken = await this.koreaInvetmentService.getAccessToken(); - const headers = getHeader(accessToken, 'FHKST01010300'); - const url = getFullURL('/uapi/domestic-stock/v1/quotations/inquire-ccnl'); - const params = this.getTradeHistoryParams(queryParams); - - const response = await axios.get(url, { - headers, - params, - }); - - return response.data; - } catch (error) { - this.logger.error('API Error Details:', { - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - headers: error.response?.config?.headers, - message: error.message, - }); - throw error; - } - } - /** * @private API에서 받은 주식현재가 체결 데이터를 필요한 정보로 정제하는 함수 * @param {StockTradeHistoryOutputDto} infos - API 응답에서 받은 원시 데이터 @@ -95,16 +67,39 @@ export class StockTradeHistoryService { } /** - * @private 주식현재가 체결 요청을 위한 쿼리 파라미터 객체 생성 함수 - * @param {StockTradeHistoryQueryParameterDto} params - API 요청에 필요한 쿼리 파라미터 DTO - * @returns - API 요청에 필요한 쿼리 파라미터 객체 + * @private 한국투자 Open API - API 호출용 공통 함수 + * @param {string} trId - API 호출에 사용할 tr_id + * @param {string} apiURL - API 호출에 사용할 URL + * @param {Record} params - API 요청 시 필요한 쿼리 파라미터 DTO + * @returns - API 호출에 대한 응답 데이터 * * @author uuuo3o */ - private getTradeHistoryParams(params: StockTradeHistoryQueryParameterDto) { - return { - fid_cond_mrkt_div_code: params.fid_cond_mrkt_div_code, - fid_input_iscd: params.fid_input_iscd, - }; + private async requestApi( + trId: string, + apiURL: string, + params: Record, + ): Promise { + try { + const accessToken = await this.koreaInvetmentService.getAccessToken(); + const headers = getHeader(accessToken, trId); + const url = getFullURL(apiURL); + + const response = await axios.get(url, { + headers, + params, + }); + + return response.data; + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, + message: error.message, + }); + throw error; + } } } From 6df873c831d0a691212bc2943cd5b917b52fe8a8 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 19:53:16 +0900 Subject: [PATCH 127/158] =?UTF-8?q?=F0=9F=9A=9A=20rename:=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20=EB=AA=85=ED=99=95=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/stock-trade-history-response.dto.ts | 10 ---------- ....ts => today-stock-trade-history-data.dto.ts} | 2 +- ...s => today-stock-trade-history-output.dto.ts} | 2 +- .../today-stock-trade-history-response.dto.ts | 13 +++++++++++++ .../history/stock-trade-history.controller.ts | 8 ++++---- .../trade/history/stock-trade-history.service.ts | 16 +++++++++------- 6 files changed, 28 insertions(+), 23 deletions(-) delete mode 100644 BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts rename BE/src/stock/trade/history/dto/{stock-trade-history-data.dto.ts => today-stock-trade-history-data.dto.ts} (90%) rename BE/src/stock/trade/history/dto/{stock-trade-history-output.dto.ts => today-stock-trade-history-output.dto.ts} (92%) create mode 100644 BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts deleted file mode 100644 index 265c1d5e..00000000 --- a/BE/src/stock/trade/history/dto/stock-trade-history-response.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { StockTradeHistoryOutputDto } from './stock-trade-history-output.dto'; - -/** - * 주식현재가 체결 API 응답값 정제 후 FE에 보낼 DTO - */ -export class StockTradeHistoryResponseDto { - @ApiProperty({ type: StockTradeHistoryOutputDto, description: '상승률 순위' }) - output: StockTradeHistoryOutputDto[]; -} diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts b/BE/src/stock/trade/history/dto/today-stock-trade-history-data.dto.ts similarity index 90% rename from BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts rename to BE/src/stock/trade/history/dto/today-stock-trade-history-data.dto.ts index 3ead1198..7842a29b 100644 --- a/BE/src/stock/trade/history/dto/stock-trade-history-data.dto.ts +++ b/BE/src/stock/trade/history/dto/today-stock-trade-history-data.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class StockTradeHistoryDataDto { +export class TodayStockTradeHistoryDataDto { @ApiProperty({ description: '주식 체결 시간' }) stck_cntg_hour: string; diff --git a/BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts b/BE/src/stock/trade/history/dto/today-stock-trade-history-output.dto.ts similarity index 92% rename from BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts rename to BE/src/stock/trade/history/dto/today-stock-trade-history-output.dto.ts index 05415c64..ab41d377 100644 --- a/BE/src/stock/trade/history/dto/stock-trade-history-output.dto.ts +++ b/BE/src/stock/trade/history/dto/today-stock-trade-history-output.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -export class StockTradeHistoryOutputDto { +export class TodayStockTradeHistoryOutputDto { @ApiProperty({ description: '주식 체결 시간' }) stck_cntg_hour: string; diff --git a/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts new file mode 100644 index 00000000..e758dfba --- /dev/null +++ b/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { TodayStockTradeHistoryOutputDto } from './today-stock-trade-history-output.dto'; + +/** + * 주식현재가 체결 API 응답값 정제 후 FE에 보낼 DTO + */ +export class TodayStockTradeHistoryResponseDto { + @ApiProperty({ + type: TodayStockTradeHistoryOutputDto, + description: '상승률 순위', + }) + output: TodayStockTradeHistoryOutputDto[]; +} diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index 11efa7fa..8a2cf430 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; -import { StockTradeHistoryResponseDto } from './dto/stock-trade-history-response.dto'; +import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-history-response.dto'; @Controller('/api/stocks') export class StockTradeHistoryController { @@ -9,7 +9,7 @@ export class StockTradeHistoryController { private readonly stockTradeHistoryService: StockTradeHistoryService, ) {} - @Get(':stockCode/trade-history') + @Get(':stockCode/today-trade-history') @ApiOperation({ summary: '단일 주식 종목에 대한 주식현재가 체결 API' }) @ApiParam({ name: 'stockCode', @@ -21,9 +21,9 @@ export class StockTradeHistoryController { @ApiResponse({ status: 200, description: '단일 주식 종목에 대한 주식현재가 체결값 조회 성공', - type: StockTradeHistoryResponseDto, + type: TodayStockTradeHistoryResponseDto, }) getStockDetail(@Param('stockCode') stockCode: string) { - return this.stockTradeHistoryService.getStockTradeHistory(stockCode); + return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } } diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index 7617846e..c5e0dacf 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -4,8 +4,8 @@ import { KoreaInvestmentService } from '../../../koreaInvestment/korea-investmen import { getHeader } from '../../../util/get-header'; import { getFullURL } from '../../../util/get-full-URL'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; -import { StockTradeHistoryOutputDto } from './dto/stock-trade-history-output.dto'; -import { StockTradeHistoryDataDto } from './dto/stock-trade-history-data.dto'; +import { TodayStockTradeHistoryOutputDto } from './dto/today-stock-trade-history-output.dto'; +import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; @Injectable() export class StockTradeHistoryService { @@ -20,7 +20,7 @@ export class StockTradeHistoryService { * * @author uuuo3o */ - async getStockTradeHistory(stockCode: string) { + async getTodayStockTradeHistory(stockCode: string) { try { const queryParams = { fid_cond_mrkt_div_code: 'J', @@ -33,7 +33,7 @@ export class StockTradeHistoryService { queryParams, ); - return this.formatTradeHistoryData(response.output); + return this.formatTodayStockTradeHistoryData(response.output); } catch (error) { this.logger.error('API Error Details:', { status: error.response?.status, @@ -48,14 +48,16 @@ export class StockTradeHistoryService { /** * @private API에서 받은 주식현재가 체결 데이터를 필요한 정보로 정제하는 함수 - * @param {StockTradeHistoryOutputDto} infos - API 응답에서 받은 원시 데이터 + * @param {TodayStockTradeHistoryOutputDto} infos - API 응답에서 받은 원시 데이터 * @returns - 필요한 정보만 추출한 데이터 배열 * * @author uuuo3o */ - private formatTradeHistoryData(infos: StockTradeHistoryOutputDto[]) { + private formatTodayStockTradeHistoryData( + infos: TodayStockTradeHistoryOutputDto[], + ) { return infos.map((info) => { - const infoData = new StockTradeHistoryDataDto(); + const infoData = new TodayStockTradeHistoryDataDto(); infoData.stck_cntg_hour = info.stck_cntg_hour; infoData.stck_prpr = info.stck_prpr; infoData.prdy_vrss_sign = info.prdy_vrss_sign; From 875f2a7230513545441a8244500703fa21523c75 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 20:16:18 +0900 Subject: [PATCH 128/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20=EC=9D=BC=EC=9E=90=EB=B3=84=20API?= =?UTF-8?q?=EC=97=90=20=EC=82=AC=EC=9A=A9=ED=95=A0=20interface,=20dto=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/daily-stock-trade-history-data.dto.ts | 27 +++++++++++ .../daily-stock-trade-history-ouput.dto.ts | 45 +++++++++++++++++++ .../daily-stock-trade-history-response.dto.ts | 8 ++++ .../inquire-daily-price.interface.ts | 23 ++++++++++ 4 files changed, 103 insertions(+) create mode 100644 BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts create mode 100644 BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts create mode 100644 BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts create mode 100644 BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts diff --git a/BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts b/BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts new file mode 100644 index 00000000..5cd38693 --- /dev/null +++ b/BE/src/stock/trade/history/dto/daily-stock-trade-history-data.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DailyStockTradeHistoryDataDto { + @ApiProperty({ description: '주식 영업 일자' }) + stck_bsop_date: string; + + @ApiProperty({ description: '주식 시가' }) + stck_oprc: string; + + @ApiProperty({ description: '주식 최고가' }) + stck_hgpr: string; + + @ApiProperty({ description: '주식 최저가' }) + stck_lwpr: string; + + @ApiProperty({ description: '주식 종가' }) + stck_clpr: string; + + @ApiProperty({ description: '누적 거래량' }) + acml_vol: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; +} diff --git a/BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts b/BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts new file mode 100644 index 00000000..99427dd6 --- /dev/null +++ b/BE/src/stock/trade/history/dto/daily-stock-trade-history-ouput.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DailyStockTradeHistoryOutputDto { + @ApiProperty({ description: '주식 영업 일자' }) + stck_bsop_date: string; + + @ApiProperty({ description: '주식 시가' }) + stck_oprc: string; + + @ApiProperty({ description: '주식 최고가' }) + stck_hgpr: string; + + @ApiProperty({ description: '주식 최저가' }) + stck_lwpr: string; + + @ApiProperty({ description: '주식 종가' }) + stck_clpr: string; + + @ApiProperty({ description: '누적 거래량' }) + acml_vol: string; + + @ApiProperty({ description: '전일 대비 거래량 비율' }) + prdy_vrss_vol_rate: string; + + @ApiProperty({ description: '전일 대비' }) + prdy_vrss: string; + + @ApiProperty({ description: '전일 대비 부호' }) + prdy_vrss_sign: string; + + @ApiProperty({ description: '전일 대비율' }) + prdy_ctrt: string; + + @ApiProperty({ description: 'HTS 외국인 소진율' }) + hts_frgn_ehrt: string; + + @ApiProperty({ description: '외국인 순매수 수량' }) + frgn_ntby_qty: string; + + @ApiProperty({ description: '락 구분 코드' }) + flng_cls_code: string; + + @ApiProperty({ description: '누적 분할 비율' }) + acml_prtt_rate: string; +} diff --git a/BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts new file mode 100644 index 00000000..dd9bc0e1 --- /dev/null +++ b/BE/src/stock/trade/history/dto/daily-stock-trade-history-response.dto.ts @@ -0,0 +1,8 @@ +import { DailyStockTradeHistoryOutputDto } from './daily-stock-trade-history-ouput.dto'; + +/** + * 주식현재가 일자별 API 응답값 정제 후 FE에 보낼 DTO + */ +export class DailyStockTradeHistoryResponseDto { + output: DailyStockTradeHistoryOutputDto[]; +} diff --git a/BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts b/BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts new file mode 100644 index 00000000..2546ade6 --- /dev/null +++ b/BE/src/stock/trade/history/interface/inquire-daily-price.interface.ts @@ -0,0 +1,23 @@ +export interface InquireDailyPriceOutputData { + stck_bsop_date: string; + stck_oprc: string; + stck_hgpr: string; + stck_lwpr: string; + stck_clpr: string; + acml_vol: string; + prdy_vrss_vol_rate: string; + prdy_vrss: string; + prdy_vrss_sign: string; + prdy_ctrt: string; + hts_frgn_ehrt: string; + frgn_ntby_qty: string; + flng_cls_code: string; + acml_prtt_rate: string; +} + +export interface InquireDailyPriceApiResponse { + output: InquireDailyPriceOutputData[]; + rt_cd: string; + msg_cd: string; + msg1: string; +} From e56921ab51c6dbe06a0a319f3ae03d030e25d383 Mon Sep 17 00:00:00 2001 From: JIN Date: Wed, 13 Nov 2024 20:16:30 +0900 Subject: [PATCH 129/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EA=B0=80=20=EC=9D=BC=EC=9E=90=EB=B3=84=20API?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?#55?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../history/stock-trade-history.controller.ts | 21 ++++++- .../history/stock-trade-history.service.ts | 63 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index 8a2cf430..e9de0f81 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-history-response.dto'; +import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; @Controller('/api/stocks') export class StockTradeHistoryController { @@ -23,7 +24,25 @@ export class StockTradeHistoryController { description: '단일 주식 종목에 대한 주식현재가 체결값 조회 성공', type: TodayStockTradeHistoryResponseDto, }) - getStockDetail(@Param('stockCode') stockCode: string) { + getTodayStockTradeHistory(@Param('stockCode') stockCode: string) { return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } + + @Get(':stockCode/daily-trade-history') + @ApiOperation({ summary: '단일 주식 종목에 대한 일자별 주식현재가 API' }) + @ApiParam({ + name: 'stockCode', + required: true, + description: + '종목 코드\n\n' + + '(ex) 005930 삼성전자 / 005380 현대차 / 001500 현대차증권', + }) + @ApiResponse({ + status: 200, + description: '단일 주식 종목에 대한 일자별 주식현재가 조회 성공', + type: DailyStockTradeHistoryDataDto, + }) + getDailyStockTradeHistory(@Param('stockCode') stockCode: string) { + return this.stockTradeHistoryService.getDailyStockTradeHistory(stockCode); + } } diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index c5e0dacf..50204aff 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -6,6 +6,9 @@ import { getFullURL } from '../../../util/get-full-URL'; import { InquireCCNLApiResponse } from './interface/Inquire-ccnl.interface'; import { TodayStockTradeHistoryOutputDto } from './dto/today-stock-trade-history-output.dto'; import { TodayStockTradeHistoryDataDto } from './dto/today-stock-trade-history-data.dto'; +import { InquireDailyPriceApiResponse } from './interface/inquire-daily-price.interface'; +import { DailyStockTradeHistoryOutputDto } from './dto/daily-stock-trade-history-ouput.dto'; +import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; @Injectable() export class StockTradeHistoryService { @@ -68,6 +71,66 @@ export class StockTradeHistoryService { }); } + /** + * 특정 주식의 일자별 체결 데이터를 반환하는 함수 + * @param {string} stockCode - 종목코드 + * @returns - 특정 주식의 현재가 체결 데이터 객체 반환 + * + * @author uuuo3o + */ + async getDailyStockTradeHistory(stockCode: string) { + try { + const queryParams = { + fid_cond_mrkt_div_code: 'J', + fid_input_iscd: stockCode, + fid_period_div_code: 'D', + fid_org_adj_prc: '0', + }; + + const response = await this.requestApi( + 'FHKST01010400', + '/uapi/domestic-stock/v1/quotations/inquire-daily-price', + queryParams, + ); + + return this.formatDailyStockTradeHistoryData(response.output); + } catch (error) { + this.logger.error('API Error Details:', { + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data, + headers: error.response?.config?.headers, // 실제 요청 헤더 + message: error.message, + }); + throw error; + } + } + + /** + * @private API에서 받은 주식현재가 일자별 데이터를 필요한 정보로 정제하는 함수 + * @param {DailyStockTradeHistoryOutputDto} datas - API 응답에서 받은 원시 데이터 + * @returns - 필요한 정보만 추출한 데이터 배열 + * + * @author uuuo3o + */ + private formatDailyStockTradeHistoryData( + datas: DailyStockTradeHistoryOutputDto[], + ) { + return datas.map((data) => { + const historyData = new DailyStockTradeHistoryDataDto(); + historyData.stck_bsop_date = data.stck_bsop_date; + historyData.stck_oprc = data.stck_oprc; + historyData.stck_hgpr = data.stck_hgpr; + historyData.stck_lwpr = data.stck_lwpr; + historyData.stck_clpr = data.stck_clpr; + historyData.acml_vol = data.acml_vol; + historyData.prdy_vrss_sign = data.prdy_vrss_sign; + historyData.prdy_ctrt = data.prdy_ctrt; + + return historyData; + }); + } + /** * @private 한국투자 Open API - API 호출용 공통 함수 * @param {string} trId - API 호출에 사용할 tr_id From 7a61c8da7779e0f888bf03b5a8bc3810ce8bc899 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:25:28 +0900 Subject: [PATCH 130/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20=EB=8F=84?= =?UTF-8?q?=EC=BB=A4=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index e2cdb2a2..04bfd32b 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -113,7 +113,8 @@ jobs: --name ${{ matrix.app.container }} \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ --env-file .env \ - ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} + ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} \ + -v /etc/localtime:/etc/localtime:ro -e TZ=Asia/Seoul - name: Remove Github Action Ip to Security group run: | From 850e53b3fea2f427e1ed3d8dd34ae544ac6a2d5f Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:38:36 +0900 Subject: [PATCH 131/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20docker=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 04bfd32b..e0cbe310 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -113,8 +113,9 @@ jobs: --name ${{ matrix.app.container }} \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ --env-file .env \ + -v /etc/localtime:/etc/localtime:ro \ + -e TZ=Asia/Seoul \ ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} \ - -v /etc/localtime:/etc/localtime:ro -e TZ=Asia/Seoul - name: Remove Github Action Ip to Security group run: | From 5eb4fc5ee07a8c10f9f4a3a4a9c41d65ecdbbc1c Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:43:26 +0900 Subject: [PATCH 132/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20docker=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index e0cbe310..c2cde28f 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -115,7 +115,7 @@ jobs: --env-file .env \ -v /etc/localtime:/etc/localtime:ro \ -e TZ=Asia/Seoul \ - ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} \ + ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} - name: Remove Github Action Ip to Security group run: | From e028917bacf3552f921fb81b3cbbc4fc8f1ef841 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 11:51:50 +0900 Subject: [PATCH 133/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20auth=20api=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=B3=80=EA=B2=BD(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/auth.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 98c3d7ad..8fcdf300 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -16,7 +16,7 @@ import { ConfigService } from '@nestjs/config'; import { AuthService } from './auth.service'; import { AuthCredentialsDto } from './dto/auth-credentials.dto'; -@Controller('auth') +@Controller('/api/auth') export class AuthController { constructor( private authService: AuthService, From d218da80755ca23a211318b5bee38a9dbba71c1b Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 13:23:49 +0900 Subject: [PATCH 134/158] =?UTF-8?q?=F0=9F=9A=9A=20rename=20:=20redisUtil?= =?UTF-8?q?=20->=20redisDomainService=EB=A1=9C=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EB=B0=8F=20=ED=8C=8C=EC=9D=BC=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../redis/{redis.ts => redis.domain-service.ts} | 14 +++++++++++++- BE/src/common/redis/redis.module.ts | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) rename BE/src/common/redis/{redis.ts => redis.domain-service.ts} (80%) diff --git a/BE/src/common/redis/redis.ts b/BE/src/common/redis/redis.domain-service.ts similarity index 80% rename from BE/src/common/redis/redis.ts rename to BE/src/common/redis/redis.domain-service.ts index e2bee153..ce636d6e 100644 --- a/BE/src/common/redis/redis.ts +++ b/BE/src/common/redis/redis.domain-service.ts @@ -2,7 +2,7 @@ import { Injectable, Inject } from '@nestjs/common'; import Redis from 'ioredis'; @Injectable() -export class RedisUtil { +export class RedisDomainService { constructor( @Inject('REDIS_CLIENT') private readonly redis: Redis, @@ -27,10 +27,22 @@ export class RedisUtil { return this.redis.zadd(key, score, member); } + async zcard(key: string): Promise { + return this.redis.zcard(key); + } + async zrange(key: string, start: number, stop: number): Promise { return this.redis.zrange(key, start, stop); } + async zremrangebyrank( + key: string, + start: number, + stop: number, + ): Promise { + return this.redis.zremrangebyrank(key, start, stop); + } + async zrevrange(key: string, start: number, stop: number): Promise { return this.redis.zrevrange(key, start, stop); } diff --git a/BE/src/common/redis/redis.module.ts b/BE/src/common/redis/redis.module.ts index d8aa958c..a0cf55bc 100644 --- a/BE/src/common/redis/redis.module.ts +++ b/BE/src/common/redis/redis.module.ts @@ -1,7 +1,7 @@ // src/common/redis/redis.module.ts import { Global, Module } from '@nestjs/common'; import Redis from 'ioredis'; -import { RedisUtil } from './redis'; +import { RedisDomainService } from './redis.domain-service'; @Global() @Module({ @@ -15,8 +15,8 @@ import { RedisUtil } from './redis'; }); }, }, - RedisUtil, + RedisDomainService, ], - exports: [RedisUtil, 'REDIS_CLIENT'], + exports: [RedisDomainService, 'REDIS_CLIENT'], }) export class RedisModule {} From 3bb03650d2b07a6f011585ac3970fa7ca0e6214f Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 13:24:18 +0900 Subject: [PATCH 135/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=EC=B5=9C?= =?UTF-8?q?=EA=B7=BC=20=EA=B2=80=EC=83=89=EC=96=B4=2010=20=EA=B0=9C?= =?UTF-8?q?=EB=A7=8C=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/stock-list.module.ts | 4 ++-- BE/src/stock/list/stock-list.service.ts | 27 +++++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/BE/src/stock/list/stock-list.module.ts b/BE/src/stock/list/stock-list.module.ts index 9c2c79e0..73243bb9 100644 --- a/BE/src/stock/list/stock-list.module.ts +++ b/BE/src/stock/list/stock-list.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { RedisUtil } from 'src/common/redis/redis'; +import { RedisDomainService } from 'src/common/redis/redis.domain-service'; import { RedisModule } from 'src/common/redis/redis.module'; import { StockListRepository } from './stock-list.repostiory'; import { StockListService } from './stock-list.service'; @@ -10,7 +10,7 @@ import { Stocks } from './stock-list.entity'; @Module({ imports: [TypeOrmModule.forFeature([Stocks]), RedisModule], controllers: [StockListController], - providers: [StockListRepository, StockListService, RedisUtil], + providers: [StockListRepository, StockListService, RedisDomainService], exports: [], }) export class StockListModule {} diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index 5868d9a8..f254576c 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -1,5 +1,5 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { RedisUtil } from 'src/common/redis/redis'; +import { RedisDomainService } from 'src/common/redis/redis.domain-service'; import { StockListRepository } from './stock-list.repostiory'; import { Stocks } from './stock-list.entity'; import { StockListResponseDto } from './dto/stock-list-response.dto'; @@ -7,9 +7,11 @@ import { SearchParams } from './interface/search-params.interface'; @Injectable() export class StockListService { + private readonly SearchHistoryLimit = 10; + constructor( private readonly stockListRepository: StockListRepository, - private readonly redisUtil: RedisUtil, + private readonly RedisDomainService: RedisDomainService, ) {} private toResponseDto(stock: Stocks): StockListResponseDto { @@ -31,11 +33,24 @@ export class StockListService { } async search(params: SearchParams): Promise { - const key = `search:${params.userId}`; - const score = Date.now(); - - await this.redisUtil.zadd(key, score, JSON.stringify(params)); + await this.addSearchTermToRedis(params); const stocks = await this.stockListRepository.search(params); return stocks.map((stock) => this.toResponseDto(stock)); } + + async addSearchTermToRedis(params: SearchParams) { + const key = `search:${params.userId}`; + const timeStamp = Date.now(); + + const { name, market, code } = params; + + const searchTerm = name || market || code; + + await this.RedisDomainService.zadd(key, timeStamp, searchTerm); + + const searchHistoryCount = await this.RedisDomainService.zcard(key); + if (searchHistoryCount > this.SearchHistoryLimit) { + await this.RedisDomainService.zremrangebyrank(key, 0, 0); + } + } } From 8c246afb7475320842fb49e363f5b6983993f068 Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 14 Nov 2024 13:36:18 +0900 Subject: [PATCH 136/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A2=85=EB=AA=A9?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EB=A7=9E=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=EC=9D=84=20DB=EC=97=90=EC=84=9C=20=EA=BA=BC=EB=82=B4?= =?UTF-8?q?=EC=99=80=20=EB=B3=B4=EB=82=B4=EC=A3=BC=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84#54?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.entity.ts | 13 ++++++++ BE/src/stock/detail/stock-detail.module.ts | 7 ++-- .../stock/detail/stock-detail.repository.ts | 15 +++++++++ BE/src/stock/detail/stock-detail.service.ts | 33 ++++++++++++------- 4 files changed, 55 insertions(+), 13 deletions(-) create mode 100644 BE/src/stock/detail/stock-detail.entity.ts create mode 100644 BE/src/stock/detail/stock-detail.repository.ts diff --git a/BE/src/stock/detail/stock-detail.entity.ts b/BE/src/stock/detail/stock-detail.entity.ts new file mode 100644 index 00000000..37182a41 --- /dev/null +++ b/BE/src/stock/detail/stock-detail.entity.ts @@ -0,0 +1,13 @@ +import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity() +export class Stocks extends BaseEntity { + @PrimaryColumn() + code: string; + + @Column() + name: string; + + @Column() + market: string; +} diff --git a/BE/src/stock/detail/stock-detail.module.ts b/BE/src/stock/detail/stock-detail.module.ts index cfb2b57b..b6447a76 100644 --- a/BE/src/stock/detail/stock-detail.module.ts +++ b/BE/src/stock/detail/stock-detail.module.ts @@ -1,11 +1,14 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { KoreaInvestmentModule } from '../../koreaInvestment/korea-investment.module'; import { StockDetailController } from './stock-detail.controller'; import { StockDetailService } from './stock-detail.service'; +import { StockDetailRepository } from './stock-detail.repository'; +import { Stocks } from './stock-detail.entity'; @Module({ - imports: [KoreaInvestmentModule], + imports: [KoreaInvestmentModule, TypeOrmModule.forFeature([Stocks])], controllers: [StockDetailController], - providers: [StockDetailService], + providers: [StockDetailService, StockDetailRepository], }) export class StockDetailModule {} diff --git a/BE/src/stock/detail/stock-detail.repository.ts b/BE/src/stock/detail/stock-detail.repository.ts new file mode 100644 index 00000000..d9143719 --- /dev/null +++ b/BE/src/stock/detail/stock-detail.repository.ts @@ -0,0 +1,15 @@ +import { InjectDataSource } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; +import { DataSource, Repository } from 'typeorm'; +import { Stocks } from './stock-detail.entity'; + +@Injectable() +export class StockDetailRepository extends Repository { + constructor(@InjectDataSource() dataSource: DataSource) { + super(Stocks, dataSource.createEntityManager()); + } + + async findOneByCode(code: string) { + return this.findOne({ where: { code } }); + } +} diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 8031c4d6..1c1268bb 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -10,12 +10,16 @@ import { InquirePriceOutputData, } from './interface/stock-detail.interface'; import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; +import { StockDetailRepository } from './stock-detail.repository'; @Injectable() export class StockDetailService { private readonly logger = new Logger(); - constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + constructor( + private readonly koreaInvetmentService: KoreaInvestmentService, + private readonly stockDetailRepository: StockDetailRepository, + ) {} /** * 주식현재가 시세 데이터를 반환하는 함수 @@ -57,16 +61,23 @@ export class StockDetailService { * * @author uuuo3o */ - private formatStockData(stock: InquirePriceOutputData) { - const stockData = new InquirePriceResponseDto(); - stockData.stck_shrn_iscd = stock.stck_shrn_iscd; - stockData.stck_prpr = stock.stck_prpr; - stockData.prdy_vrss = stock.prdy_vrss; - stockData.prdy_vrss_sign = stock.prdy_vrss_sign; - stockData.prdy_ctrt = stock.prdy_ctrt; - stockData.hts_avls = stock.hts_avls; - stockData.per = stock.per; - return stockData; + private async formatStockData( + stock: InquirePriceOutputData, + ): Promise { + const { name } = await this.stockDetailRepository.findOneByCode( + stock.stck_shrn_iscd, + ); + + return { + hts_kor_isnm: name, + stck_shrn_iscd: stock.stck_shrn_iscd, + stck_prpr: stock.stck_prpr, + prdy_vrss: stock.prdy_vrss, + prdy_vrss_sign: stock.prdy_vrss_sign, + prdy_ctrt: stock.prdy_ctrt, + hts_avls: stock.hts_avls, + per: stock.per, + }; } /** From fb58cd8d6948b8b772fbc8f00875c3bca253cc31 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 13:40:43 +0900 Subject: [PATCH 137/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20socket=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=95=88=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EC=98=A4=EB=A9=B4=20=EC=97=B0=EA=B2=B0=20=EB=81=8A=EA=B8=B0?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=EC=83=81=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/base-socket.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 3a7e20bf..02ebd5e2 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -48,6 +48,7 @@ export class BaseSocketService implements OnModuleInit { `한국투자증권 웹소켓 연결: ${json.body.msg1}`, json.header.tr_id, ); + if (json.header.tr_id === 'PINGPONG') this.socket.pong(json); return; } From b19180cb1c8b4e200b3616fadffe70fa727ab6a0 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 13:49:04 +0900 Subject: [PATCH 138/158] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20:=20lin?= =?UTF-8?q?t=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/stock-list.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index f254576c..9d61f15a 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -11,7 +11,7 @@ export class StockListService { constructor( private readonly stockListRepository: StockListRepository, - private readonly RedisDomainService: RedisDomainService, + private readonly redisDomainService: RedisDomainService, ) {} private toResponseDto(stock: Stocks): StockListResponseDto { @@ -46,11 +46,11 @@ export class StockListService { const searchTerm = name || market || code; - await this.RedisDomainService.zadd(key, timeStamp, searchTerm); + await this.redisDomainService.zadd(key, timeStamp, searchTerm); - const searchHistoryCount = await this.RedisDomainService.zcard(key); + const searchHistoryCount = await this.redisDomainService.zcard(key); if (searchHistoryCount > this.SearchHistoryLimit) { - await this.RedisDomainService.zremrangebyrank(key, 0, 0); + await this.redisDomainService.zremrangebyrank(key, 0, 0); } } } From 30ced2f8b89a1a86df7cae2a293d6150a7cee5a3 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 14:29:10 +0900 Subject: [PATCH 139/158] =?UTF-8?q?=E2=9E=95=20add:=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20=EB=81=8A=EA=B2=BC=EC=9D=84=EB=95=8C=20=EB=A1=9C=EA=B7=B8=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 --- BE/src/websocket/base-socket.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index 02ebd5e2..d9af5adf 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -59,6 +59,10 @@ export class BaseSocketService implements OnModuleInit { this.socketDataHandlers[data[1]](dataList); }; + + this.socket.onclose = () => { + this.logger.warn(`한국투자증권 소켓 연결 종료`); + }; } registerCode(trId: string, trKey: string) { From 6054b6956d4c40001e7060bb81784a59e36df602 Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 14 Nov 2024 14:33:11 +0900 Subject: [PATCH 140/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20koreaInvestmentSe?= =?UTF-8?q?rvice=EB=A5=BC=20koreaInvetmentService=EB=A1=9C=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=EB=83=88=EB=8D=98=20=EB=B6=80=EB=B6=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 4 ++-- BE/src/stock/trade/history/stock-trade-history.service.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index 1c1268bb..f3b193a0 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -17,7 +17,7 @@ export class StockDetailService { private readonly logger = new Logger(); constructor( - private readonly koreaInvetmentService: KoreaInvestmentService, + private readonly koreaInvestmentService: KoreaInvestmentService, private readonly stockDetailRepository: StockDetailRepository, ) {} @@ -174,7 +174,7 @@ export class StockDetailService { params: Record, ): Promise { try { - const accessToken = await this.koreaInvetmentService.getAccessToken(); + const accessToken = await this.koreaInvestmentService.getAccessToken(); const headers = getHeader(accessToken, trId); const url = getFullURL(apiURL); diff --git a/BE/src/stock/trade/history/stock-trade-history.service.ts b/BE/src/stock/trade/history/stock-trade-history.service.ts index 50204aff..449b9451 100644 --- a/BE/src/stock/trade/history/stock-trade-history.service.ts +++ b/BE/src/stock/trade/history/stock-trade-history.service.ts @@ -14,7 +14,9 @@ import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-d export class StockTradeHistoryService { private readonly logger = new Logger(); - constructor(private readonly koreaInvetmentService: KoreaInvestmentService) {} + constructor( + private readonly koreaInvestmentService: KoreaInvestmentService, + ) {} /** * 특정 주식의 현재가 체결 데이터를 반환하는 함수 @@ -146,7 +148,7 @@ export class StockTradeHistoryService { params: Record, ): Promise { try { - const accessToken = await this.koreaInvetmentService.getAccessToken(); + const accessToken = await this.koreaInvestmentService.getAccessToken(); const headers = getHeader(accessToken, trId); const url = getFullURL(apiURL); From 1c5b99321f7bc2956db3660f91963efac37bc79d Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 14 Nov 2024 14:34:55 +0900 Subject: [PATCH 141/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20=EB=B9=A0?= =?UTF-8?q?=EC=A0=B8=EC=9E=88=EB=8D=98=20await=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/detail/stock-detail.service.ts b/BE/src/stock/detail/stock-detail.service.ts index f3b193a0..8114a394 100644 --- a/BE/src/stock/detail/stock-detail.service.ts +++ b/BE/src/stock/detail/stock-detail.service.ts @@ -41,7 +41,7 @@ export class StockDetailService { queryParams, ); - return this.formatStockData(response.output); + return await this.formatStockData(response.output); } catch (error) { this.logger.error('API Error Details:', { status: error.response?.status, From 98fbae30a46ee909db34aace3639917e23763de3 Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 14 Nov 2024 14:39:24 +0900 Subject: [PATCH 142/158] =?UTF-8?q?=F0=9F=93=9D=20docs:=20ApiTags=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 --- BE/src/stock/detail/stock-detail.controller.ts | 9 ++++++++- BE/src/stock/topfive/stock-topfive.controller.ts | 3 ++- .../trade/history/stock-trade-history.controller.ts | 3 ++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index 533b67ca..c9f74222 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -1,10 +1,17 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; -import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { StockDetailService } from './stock-detail.service'; import { InquirePriceResponseDto } from './dto/stock-detail-response.dto'; import { StockDetailChartRequestDto } from './dto/stock-detail-chart-request.dto'; import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; +@ApiTags('특정 주식 종목에 대한 detail 페이지 조회 API') @Controller('/api/stocks') export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} diff --git a/BE/src/stock/topfive/stock-topfive.controller.ts b/BE/src/stock/topfive/stock-topfive.controller.ts index bac275e3..7da63d4c 100644 --- a/BE/src/stock/topfive/stock-topfive.controller.ts +++ b/BE/src/stock/topfive/stock-topfive.controller.ts @@ -1,9 +1,10 @@ -import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Controller, Get, Query } from '@nestjs/common'; import { StockTopfiveService } from './stock-topfive.service'; import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; import { MarketType } from '../enum/market-type'; +@ApiTags('오늘의 상/하위 종목 조회 API') @Controller('/api/stocks') export class StockTopfiveController { constructor(private readonly topFiveService: StockTopfiveService) {} diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index e9de0f81..4885aed2 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -1,9 +1,10 @@ import { Controller, Get, Param } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { StockTradeHistoryService } from './stock-trade-history.service'; import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-history-response.dto'; import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; +@ApiTags('주식현재가 체결 조회 API') @Controller('/api/stocks') export class StockTradeHistoryController { constructor( From e964e789ee06bcc285fc791d96e1b144f85060dd Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 14 Nov 2024 14:42:14 +0900 Subject: [PATCH 143/158] =?UTF-8?q?=F0=9F=93=9D=20docs:=20description=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 --- .../trade/history/dto/today-stock-trade-history-response.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts b/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts index e758dfba..91981143 100644 --- a/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts +++ b/BE/src/stock/trade/history/dto/today-stock-trade-history-response.dto.ts @@ -7,7 +7,7 @@ import { TodayStockTradeHistoryOutputDto } from './today-stock-trade-history-out export class TodayStockTradeHistoryResponseDto { @ApiProperty({ type: TodayStockTradeHistoryOutputDto, - description: '상승률 순위', + description: '오늘 체결된 거래 내역 배열', }) output: TodayStockTradeHistoryOutputDto[]; } From 639b19c5ee650249d24c0f45b9ac71f5343de37d Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 14:51:19 +0900 Subject: [PATCH 144/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20redis=EC=97=90?= =?UTF-8?q?=20=20=EC=B5=9C=EA=B7=BC=20=EA=B2=80=EC=83=89=EC=96=B4=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20Con?= =?UTF-8?q?troller=20=EB=B6=84=EB=A6=AC(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../list/interface/search-params.interface.ts | 2 +- BE/src/stock/list/stock-list.controller.ts | 51 +++++++++++++++++-- BE/src/stock/list/stock-list.service.ts | 23 ++++++--- 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/BE/src/stock/list/interface/search-params.interface.ts b/BE/src/stock/list/interface/search-params.interface.ts index 879f55b9..2f0b205b 100644 --- a/BE/src/stock/list/interface/search-params.interface.ts +++ b/BE/src/stock/list/interface/search-params.interface.ts @@ -2,5 +2,5 @@ export interface SearchParams { name?: string; market?: string; code?: string; - userId: number; + userId?: string; } diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index d7272784..4c484cb1 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -1,5 +1,12 @@ -import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; -import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { Request } from 'express'; import { JwtAuthGuard } from 'src/auth/jwt-auth-guard'; import { StockListService } from './stock-list.service'; @@ -36,13 +43,11 @@ export class StockListController { @Get('/search') @UseGuards(JwtAuthGuard) async searchWithQuery( - @Req() req: Request, @Query('name') name?: string, @Query('market') market?: string, @Query('code') code?: string, ): Promise { - const userId = parseInt(req.user.userId, 10); - return this.stockListService.search({ name, market, code, userId }); + return this.stockListService.search({ name, market, code }); } @ApiOperation({ @@ -58,4 +63,40 @@ export class StockListController { async findOne(@Query('code') code: string): Promise { return this.stockListService.findOne(code); } + + @Post('/search/addHistory') + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '검색어 히스토리 추가 API', + description: '특정 유저의 검색어 히스토리를 추가한다.', + }) + @ApiBody({ + schema: { + type: 'object', + properties: { + searchTerm: { type: 'string' }, + }, + }, + }) + @ApiBearerAuth() + async addSearchHistory(@Req() req: Request) { + const { searchTerm } = req.body; + const userId = parseInt(req.user.userId, 10); + await this.stockListService.addSearchTermToRedis({ + searchTerm, + userId, + }); + } + + @Get('/search/getHistory') + @UseGuards(JwtAuthGuard) + @ApiOperation({ + summary: '검색어 히스토리 조회 API', + description: '특정 유저의 검색어 히스토리를 조회한다.', + }) + @ApiBearerAuth() + async getSearchHistory(@Req() req: Request) { + const userId = req.user.userId; + return this.stockListService.getSearchTermFromRedis(userId); + } } diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index 9d61f15a..5916b210 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -33,19 +33,18 @@ export class StockListService { } async search(params: SearchParams): Promise { - await this.addSearchTermToRedis(params); const stocks = await this.stockListRepository.search(params); return stocks.map((stock) => this.toResponseDto(stock)); } - async addSearchTermToRedis(params: SearchParams) { - const key = `search:${params.userId}`; + async addSearchTermToRedis(searchInfo: { + userId: Number; + searchTerm: string; + }) { + const { userId, searchTerm } = searchInfo; + const key = `search:${userId}`; const timeStamp = Date.now(); - const { name, market, code } = params; - - const searchTerm = name || market || code; - await this.redisDomainService.zadd(key, timeStamp, searchTerm); const searchHistoryCount = await this.redisDomainService.zcard(key); @@ -53,4 +52,14 @@ export class StockListService { await this.redisDomainService.zremrangebyrank(key, 0, 0); } } + + async getSearchTermFromRedis(userId: string): Promise { + const key = `search:${userId}`; + + return await this.redisDomainService.zrevrange( + key, + 0, + this.SearchHistoryLimit - 1, + ); + } } From 949f3984da63b41b525e258602823d0d43f15e08 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 15:09:31 +0900 Subject: [PATCH 145/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=8B=9C=EC=97=90=20=20redis=20container=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EB=B0=8F=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deply-alpha.yml | 10 ++++++++++ BE/docker-compose.yml | 27 +++++++++++++++++++++++++++ BE/src/common/redis/redis.module.ts | 4 ++-- 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 BE/docker-compose.yml diff --git a/.github/workflows/deply-alpha.yml b/.github/workflows/deply-alpha.yml index ec6ec9ce..496c289a 100644 --- a/.github/workflows/deply-alpha.yml +++ b/.github/workflows/deply-alpha.yml @@ -106,11 +106,21 @@ jobs: docker system prune -af echo "${{ secrets.ENV }}" > .env + docker network create juga-network || true + + docker run -d \ + --name redis \ + --network juga-network \ + -p 6379:6379 \ + -v redis_data:/data \ + redis:latest redis-server --appendonly yes + docker pull ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} docker stop ${{ matrix.app.container }} || true docker rm ${{ matrix.app.container }} || true docker run -d \ --name ${{ matrix.app.container }} \ + --network juga-network \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ --env-file .env \ ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} diff --git a/BE/docker-compose.yml b/BE/docker-compose.yml new file mode 100644 index 00000000..37d586f9 --- /dev/null +++ b/BE/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3' +services: + api: + build: + context: . + dockerfile: Dockerfile + ports: + - '3000:3000' + environment: + - NODE_ENV=production + - REDIS_HOST=redis + - REDIS_PORT=6379 + depends_on: + - redis + restart: unless-stopped + + redis: + image: redis:latest + ports: + - '6379:6379' + volumes: + - redis_data:/data + command: redis-server --appendonly yes + restart: unless-stopped + +volumes: + redis_data: diff --git a/BE/src/common/redis/redis.module.ts b/BE/src/common/redis/redis.module.ts index a0cf55bc..863812b5 100644 --- a/BE/src/common/redis/redis.module.ts +++ b/BE/src/common/redis/redis.module.ts @@ -10,8 +10,8 @@ import { RedisDomainService } from './redis.domain-service'; provide: 'REDIS_CLIENT', useFactory: () => { return new Redis({ - host: process.env.REDIS_HOST, - port: Number(process.env.REDIS_PORT), + host: process.env.REDIS_HOST || 'redis', + port: Number(process.env.REDIS_PORT || 6379), }); }, }, From 6e73abe00b3e00601d8f594230ba94a51bdbf7f9 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 15:12:38 +0900 Subject: [PATCH 146/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20:Production=20?= =?UTF-8?q?Yml=20=ED=8C=8C=EC=9D=BC=EB=8F=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-production.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index c2cde28f..fea0130d 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -106,15 +106,23 @@ jobs: docker system prune -af echo "${{ secrets.ENV }}" > .env + docker network create juga-network || true + + docker run -d \ + --name redis \ + --network juga-network \ + -p 6379:6379 \ + -v redis_data:/data \ + redis:latest redis-server --appendonly yes + docker pull ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} docker stop ${{ matrix.app.container }} || true docker rm ${{ matrix.app.container }} || true docker run -d \ --name ${{ matrix.app.container }} \ + --network juga-network \ -p ${{ matrix.app.port }}:${{ matrix.app.port }} \ --env-file .env \ - -v /etc/localtime:/etc/localtime:ro \ - -e TZ=Asia/Seoul \ ${{ env.DOCKER_IMAGE }}-${{ matrix.app.name }}:${{ env.DOCKER_TAG }} - name: Remove Github Action Ip to Security group From 7c17c5782d8e7c7052f94d4ef1717c75f753685f Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 15:35:22 +0900 Subject: [PATCH 147/158] =?UTF-8?q?=E2=9C=A8=20feat:=20userStock=20domain?= =?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 --- BE/src/userStock/user-stock.controller.ts | 6 +++++ BE/src/userStock/user-stock.entity.ts | 27 +++++++++++++++++++++++ BE/src/userStock/user-stock.module.ts | 13 +++++++++++ BE/src/userStock/user-stock.repository.ts | 11 +++++++++ BE/src/userStock/user-stock.service.ts | 4 ++++ 5 files changed, 61 insertions(+) create mode 100644 BE/src/userStock/user-stock.controller.ts create mode 100644 BE/src/userStock/user-stock.entity.ts create mode 100644 BE/src/userStock/user-stock.module.ts create mode 100644 BE/src/userStock/user-stock.repository.ts create mode 100644 BE/src/userStock/user-stock.service.ts diff --git a/BE/src/userStock/user-stock.controller.ts b/BE/src/userStock/user-stock.controller.ts new file mode 100644 index 00000000..e1e4c209 --- /dev/null +++ b/BE/src/userStock/user-stock.controller.ts @@ -0,0 +1,6 @@ +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +@Controller('/api/userStock') +@ApiTags('사용자 보유 주식 API') +export class UserStockController {} diff --git a/BE/src/userStock/user-stock.entity.ts b/BE/src/userStock/user-stock.entity.ts new file mode 100644 index 00000000..60a1b169 --- /dev/null +++ b/BE/src/userStock/user-stock.entity.ts @@ -0,0 +1,27 @@ +import { + Column, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +@Entity('user_stocks') +export class UserStock { + @PrimaryGeneratedColumn() + id: number; + + @Column({ nullable: false }) + user_id: number; + + @Column({ nullable: false }) + stock_code: string; + + @Column({ nullable: false }) + quantity: number; + + @Column('decimal', { nullable: false, precision: 10, scale: 5 }) + avg_price: number; + + @UpdateDateColumn() + last_updated: Date; +} diff --git a/BE/src/userStock/user-stock.module.ts b/BE/src/userStock/user-stock.module.ts new file mode 100644 index 00000000..c7584fc0 --- /dev/null +++ b/BE/src/userStock/user-stock.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UserStock } from './user-stock.entity'; +import { UserStockController } from './user-stock.controller'; +import { UserStockRepository } from './user-stock.repository'; +import { UserStockService } from './user-stock.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([UserStock])], + controllers: [UserStockController], + providers: [UserStockRepository, UserStockService], +}) +export class StockOrderModule {} diff --git a/BE/src/userStock/user-stock.repository.ts b/BE/src/userStock/user-stock.repository.ts new file mode 100644 index 00000000..e945a667 --- /dev/null +++ b/BE/src/userStock/user-stock.repository.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource, Repository } from 'typeorm'; +import { UserStock } from './user-stock.entity'; + +@Injectable() +export class UserStockRepository extends Repository { + constructor(@InjectDataSource() private dataSource: DataSource) { + super(UserStock, dataSource.createEntityManager()); + } +} diff --git a/BE/src/userStock/user-stock.service.ts b/BE/src/userStock/user-stock.service.ts new file mode 100644 index 00000000..1c69dcaf --- /dev/null +++ b/BE/src/userStock/user-stock.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UserStockService {} From e71c34874a300fb57cf04a2cd3a590b192b55020 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 15:52:06 +0900 Subject: [PATCH 148/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EA=B5=AC=EB=A7=A4=20=EC=B2=B4=EA=B2=B0=20=EC=8B=9C=20user?= =?UTF-8?q?=5Fstocks=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock/order/stock-order-socket.service.ts | 2 +- BE/src/stock/order/stock-order.repository.ts | 38 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts index 0f56140f..4c2a0bfc 100644 --- a/BE/src/stock/order/stock-order-socket.service.ts +++ b/BE/src/stock/order/stock-order-socket.service.ts @@ -89,7 +89,7 @@ export class StockOrderSocketService { const totalPrice = order.price * order.amount; const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetWhenBuy( + await this.stockOrderRepository.updateOrderAndAssetAndUserStockWhenBuy( order, totalPrice + fee, ); diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 88344cf2..c84c2f59 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -4,6 +4,9 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Order } from './stock-order.entity'; import { StatusType } from './enum/status-type'; import { Asset } from '../../asset/asset.entity'; +import { StockOrderRequestDto } from './dto/stock-order-request.dto'; +import { TradeType } from './enum/trade-type'; +import { UserStock } from '../../userStock/user-stock.entity'; @Injectable() export class StockOrderRepository extends Repository { @@ -17,7 +20,7 @@ export class StockOrderRepository extends Repository { .getRawMany(); } - async updateOrderAndAssetWhenBuy(order, realPrice) { + async updateOrderAndAssetAndUserStockWhenBuy(order, realPrice) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.startTransaction(); @@ -32,14 +35,32 @@ export class StockOrderRepository extends Repository { .createQueryBuilder() .update(Asset) .set({ - cash_balance: () => 'cash_balance - :realPrice', - total_asset: () => 'total_asset - :realPrice', - total_profit: () => 'total_profit - :realPrice', + cash_balance: () => `cash_balance - ${realPrice}`, + total_asset: () => `total_asset - ${realPrice}`, + total_profit: () => `total_profit - ${realPrice}`, total_profit_rate: () => `total_profit / 10000000`, last_updated: new Date(), }) .where({ user_id: order.user_id }) - .setParameter('realPrice', realPrice) + .execute(); + + await queryRunner.manager + .createQueryBuilder() + .insert() + .into(UserStock) + .values({ + user_id: order.user_id, + stock_code: order.stock_code, + quantity: order.amount, + avg_price: order.price, + }) + .orUpdate( + [ + `quantity = quantity + ${order.amount}`, + `avg_price = ((avg_price * quantity + ${order.price} * ${order.amount}) / (quantity + ${order.amount}))`, + ], + ['user_id', 'stock_code'], + ) .execute(); await queryRunner.commitTransaction(); @@ -66,14 +87,13 @@ export class StockOrderRepository extends Repository { .createQueryBuilder() .update(Asset) .set({ - cash_balance: () => 'cash_balance + :realPrice', - total_asset: () => 'total_asset + :realPrice', - total_profit: () => 'total_profit + :realPrice', + cash_balance: () => `cash_balance + ${realPrice}`, + total_asset: () => `total_asset + ${realPrice}`, + total_profit: () => `total_profit + ${realPrice}`, total_profit_rate: () => `total_profit / 10000000`, last_updated: new Date(), }) .where({ user_id: order.user_id }) - .setParameter('realPrice', realPrice) .execute(); await queryRunner.commitTransaction(); From a727145edc2a5bfb7f6322f76238bc0156b7130a Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 16:00:54 +0900 Subject: [PATCH 149/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20=20HttpExeption?= =?UTF-8?q?=20=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 10 ++++- .../common/filters/http-exception.filter.ts | 44 +++++++++++++++++++ BE/src/stock/list/stock-list.controller.ts | 2 +- BE/src/stock/list/stock-list.service.ts | 4 +- 4 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 BE/src/common/filters/http-exception.filter.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 27220f4b..2b2a14d5 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; +import { APP_FILTER } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; @@ -15,6 +16,7 @@ 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'; @Module({ imports: [ @@ -33,6 +35,12 @@ import { RedisModule } from './common/redis/redis.module'; RedisModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_FILTER, + useClass: HTTPExceptionFilter, + }, + ], }) export class AppModule {} diff --git a/BE/src/common/filters/http-exception.filter.ts b/BE/src/common/filters/http-exception.filter.ts new file mode 100644 index 00000000..a9007911 --- /dev/null +++ b/BE/src/common/filters/http-exception.filter.ts @@ -0,0 +1,44 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; + +import { Request, Response } from 'express'; + +@Catch() +export class HTTPExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(HTTPExceptionFilter.name); + + catch(exception: Error, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.message + : 'Internal Server Error'; + + const errorResponse = { + statusCode: status, + message, + timestamp: new Date().toISOString(), + path: request.url, + }; + + this.logger.error( + `[${request.method}] ${request.url} - Status: ${status} - Error: ${exception.message}`, + ); + + response.status(status).json(errorResponse); + } +} diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index 4c484cb1..dd0278eb 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -96,7 +96,7 @@ export class StockListController { }) @ApiBearerAuth() async getSearchHistory(@Req() req: Request) { - const userId = req.user.userId; + const { userId } = req.user; return this.stockListService.getSearchTermFromRedis(userId); } } diff --git a/BE/src/stock/list/stock-list.service.ts b/BE/src/stock/list/stock-list.service.ts index 5916b210..7e4c4956 100644 --- a/BE/src/stock/list/stock-list.service.ts +++ b/BE/src/stock/list/stock-list.service.ts @@ -38,7 +38,7 @@ export class StockListService { } async addSearchTermToRedis(searchInfo: { - userId: Number; + userId: number; searchTerm: string; }) { const { userId, searchTerm } = searchInfo; @@ -56,7 +56,7 @@ export class StockListService { async getSearchTermFromRedis(userId: string): Promise { const key = `search:${userId}`; - return await this.redisDomainService.zrevrange( + return this.redisDomainService.zrevrange( key, 0, this.SearchHistoryLimit - 1, From e78d88e19bfb9c910da481ac0061e082bd700795 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 16:01:56 +0900 Subject: [PATCH 150/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20=EC=A3=BC?= =?UTF-8?q?=EC=8B=9D=20=EA=B2=80=EC=83=89=EC=97=90=20=EB=8C=80=ED=95=9C=20?= =?UTF-8?q?JWT=20guard=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/list/stock-list.controller.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/BE/src/stock/list/stock-list.controller.ts b/BE/src/stock/list/stock-list.controller.ts index dd0278eb..4db69192 100644 --- a/BE/src/stock/list/stock-list.controller.ts +++ b/BE/src/stock/list/stock-list.controller.ts @@ -41,7 +41,6 @@ export class StockListController { @ApiQuery({ name: 'market', required: false }) @ApiQuery({ name: 'code', required: false }) @Get('/search') - @UseGuards(JwtAuthGuard) async searchWithQuery( @Query('name') name?: string, @Query('market') market?: string, From 30a57db519ad9625530a4a2154bedcaf1618dc46 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 16:11:15 +0900 Subject: [PATCH 151/158] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A3=BC=EC=8B=9D?= =?UTF-8?q?=20=EB=A7=A4=EB=8F=84=20=EC=B2=B4=EA=B2=B0=20=EC=8B=9C=20user?= =?UTF-8?q?=5Fstocks=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20#53?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 매도할 개수만큼 매수하지 않은 주식에 대해 매도 막음 --- BE/src/stock/order/stock-order-socket.service.ts | 2 +- BE/src/stock/order/stock-order.module.ts | 8 +++++++- BE/src/stock/order/stock-order.repository.ts | 11 ++++++++++- BE/src/stock/order/stock-order.service.ts | 11 +++++++++++ BE/src/userStock/user-stock.module.ts | 3 ++- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/BE/src/stock/order/stock-order-socket.service.ts b/BE/src/stock/order/stock-order-socket.service.ts index 4c2a0bfc..6e2bec3b 100644 --- a/BE/src/stock/order/stock-order-socket.service.ts +++ b/BE/src/stock/order/stock-order-socket.service.ts @@ -100,7 +100,7 @@ export class StockOrderSocketService { const totalPrice = order.price * order.amount; const fee = this.calculateFee(totalPrice); - await this.stockOrderRepository.updateOrderAndAssetWhenSell( + await this.stockOrderRepository.updateOrderAndAssetAndUserStockWhenSell( order, totalPrice - fee, ); diff --git a/BE/src/stock/order/stock-order.module.ts b/BE/src/stock/order/stock-order.module.ts index 454ca252..155c0855 100644 --- a/BE/src/stock/order/stock-order.module.ts +++ b/BE/src/stock/order/stock-order.module.ts @@ -7,9 +7,15 @@ import { StockOrderRepository } from './stock-order.repository'; import { SocketModule } from '../../websocket/socket.module'; import { AssetModule } from '../../asset/asset.module'; import { StockOrderSocketService } from './stock-order-socket.service'; +import { UserStockModule } from '../../userStock/user-stock.module'; @Module({ - imports: [TypeOrmModule.forFeature([Order]), SocketModule, AssetModule], + imports: [ + TypeOrmModule.forFeature([Order]), + SocketModule, + AssetModule, + UserStockModule, + ], controllers: [StockOrderController], providers: [StockOrderService, StockOrderRepository, StockOrderSocketService], }) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index c84c2f59..4ae1b58a 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -72,7 +72,7 @@ export class StockOrderRepository extends Repository { } } - async updateOrderAndAssetWhenSell(order, realPrice) { + async updateOrderAndAssetAndUserStockWhenSell(order, realPrice) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.startTransaction(); @@ -96,6 +96,15 @@ export class StockOrderRepository extends Repository { .where({ user_id: order.user_id }) .execute(); + await queryRunner.manager + .createQueryBuilder() + .update(UserStock) + .set({ + quantity: () => `quantity - ${order.amount}`, + }) + .where({ user_id: order.user_id, stock_code: order.stock_code }) + .execute(); + await queryRunner.commitTransaction(); } catch (err) { await queryRunner.rollbackTransaction(); diff --git a/BE/src/stock/order/stock-order.service.ts b/BE/src/stock/order/stock-order.service.ts index b558dc5e..bdbfe8ce 100644 --- a/BE/src/stock/order/stock-order.service.ts +++ b/BE/src/stock/order/stock-order.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, ConflictException, ForbiddenException, Injectable, @@ -9,12 +10,14 @@ import { StockOrderRepository } from './stock-order.repository'; import { TradeType } from './enum/trade-type'; import { StatusType } from './enum/status-type'; import { StockOrderSocketService } from './stock-order-socket.service'; +import { UserStockRepository } from '../../userStock/user-stock.repository'; @Injectable() export class StockOrderService { constructor( private readonly stockOrderRepository: StockOrderRepository, private readonly stockOrderSocketService: StockOrderSocketService, + private readonly userStockRepository: UserStockRepository, ) {} async buy(userId: number, stockOrderRequest: StockOrderRequestDto) { @@ -32,6 +35,14 @@ export class StockOrderService { } async sell(userId: number, stockOrderRequest: StockOrderRequestDto) { + const userStock = await this.userStockRepository.findOneBy({ + user_id: userId, + stock_code: stockOrderRequest.stock_code, + }); + + if (!userStock || userStock.quantity === 0) + throw new BadRequestException('주식을 매도 수만큼 가지고 있지 않습니다.'); + const order = this.stockOrderRepository.create({ user_id: userId, stock_code: stockOrderRequest.stock_code, diff --git a/BE/src/userStock/user-stock.module.ts b/BE/src/userStock/user-stock.module.ts index c7584fc0..a6466976 100644 --- a/BE/src/userStock/user-stock.module.ts +++ b/BE/src/userStock/user-stock.module.ts @@ -9,5 +9,6 @@ import { UserStockService } from './user-stock.service'; imports: [TypeOrmModule.forFeature([UserStock])], controllers: [UserStockController], providers: [UserStockRepository, UserStockService], + exports: [UserStockRepository], }) -export class StockOrderModule {} +export class UserStockModule {} From ae0e854a85e34ca236fd658f99ba0f9731f730ad Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 16:11:57 +0900 Subject: [PATCH 152/158] =?UTF-8?q?=F0=9F=94=A5=20remove=20:=20filter=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/filters/http-exception.filter.ts | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 BE/src/common/filters/http-exception.filter.ts diff --git a/BE/src/common/filters/http-exception.filter.ts b/BE/src/common/filters/http-exception.filter.ts deleted file mode 100644 index a9007911..00000000 --- a/BE/src/common/filters/http-exception.filter.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, - HttpStatus, - Logger, -} from '@nestjs/common'; - -import { Request, Response } from 'express'; - -@Catch() -export class HTTPExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(HTTPExceptionFilter.name); - - catch(exception: Error, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const request = ctx.getRequest(); - const response = ctx.getResponse(); - - const status = - exception instanceof HttpException - ? exception.getStatus() - : HttpStatus.INTERNAL_SERVER_ERROR; - - const message = - exception instanceof HttpException - ? exception.message - : 'Internal Server Error'; - - const errorResponse = { - statusCode: status, - message, - timestamp: new Date().toISOString(), - path: request.url, - }; - - this.logger.error( - `[${request.method}] ${request.url} - Status: ${status} - Error: ${exception.message}`, - ); - - response.status(status).json(errorResponse); - } -} From 4802fc36d8fe36de1462973b31922d00b637e040 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 16:14:25 +0900 Subject: [PATCH 153/158] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20app.moudle?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=95=84=ED=84=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 2b2a14d5..ca5abc62 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; -import { APP_FILTER } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; @@ -15,8 +14,6 @@ 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'; @Module({ imports: [ @@ -32,15 +29,8 @@ import { HTTPExceptionFilter } from './common/filters/http-exception.filter'; StockOrderModule, StockListModule, StockTradeHistoryModule, - RedisModule, ], controllers: [AppController], - providers: [ - AppService, - { - provide: APP_FILTER, - useClass: HTTPExceptionFilter, - }, - ], + providers: [AppService], }) export class AppModule {} From d0d294371309177cdb08c41a0befdcf0b786dcc8 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 16:20:07 +0900 Subject: [PATCH 154/158] =?UTF-8?q?=E2=9C=A8=20feat=20:=20HttpException=20?= =?UTF-8?q?Filter=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/app.module.ts | 12 ++++- .../common/filters/http-exception.filter.ts | 44 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 BE/src/common/filters/http-exception.filter.ts diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index ca5abc62..2b2a14d5 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; +import { APP_FILTER } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; @@ -14,6 +15,8 @@ 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'; @Module({ imports: [ @@ -29,8 +32,15 @@ import { StockTradeHistoryModule } from './stock/trade/history/stock-trade-histo StockOrderModule, StockListModule, StockTradeHistoryModule, + RedisModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_FILTER, + useClass: HTTPExceptionFilter, + }, + ], }) export class AppModule {} diff --git a/BE/src/common/filters/http-exception.filter.ts b/BE/src/common/filters/http-exception.filter.ts new file mode 100644 index 00000000..a9007911 --- /dev/null +++ b/BE/src/common/filters/http-exception.filter.ts @@ -0,0 +1,44 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; + +import { Request, Response } from 'express'; + +@Catch() +export class HTTPExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(HTTPExceptionFilter.name); + + catch(exception: Error, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.message + : 'Internal Server Error'; + + const errorResponse = { + statusCode: status, + message, + timestamp: new Date().toISOString(), + path: request.url, + }; + + this.logger.error( + `[${request.method}] ${request.url} - Status: ${status} - Error: ${exception.message}`, + ); + + response.status(status).json(errorResponse); + } +} From 712cabd9da56d1cd59c16ac0810399c40062a9b9 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 16:20:59 +0900 Subject: [PATCH 155/158] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20chore:=20=EB=A6=B0?= =?UTF-8?q?=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/order/stock-order.repository.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/BE/src/stock/order/stock-order.repository.ts b/BE/src/stock/order/stock-order.repository.ts index 4ae1b58a..6cd70c39 100644 --- a/BE/src/stock/order/stock-order.repository.ts +++ b/BE/src/stock/order/stock-order.repository.ts @@ -4,8 +4,6 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Order } from './stock-order.entity'; import { StatusType } from './enum/status-type'; import { Asset } from '../../asset/asset.entity'; -import { StockOrderRequestDto } from './dto/stock-order-request.dto'; -import { TradeType } from './enum/trade-type'; import { UserStock } from '../../userStock/user-stock.entity'; @Injectable() From 2af524ceaed1bb7caa9b9bc20c8422c36215ac37 Mon Sep 17 00:00:00 2001 From: anjdydhody Date: Thu, 14 Nov 2024 17:11:28 +0900 Subject: [PATCH 156/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX:=20=ED=95=91?= =?UTF-8?q?=ED=90=81=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/websocket/base-socket.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BE/src/websocket/base-socket.service.ts b/BE/src/websocket/base-socket.service.ts index d9af5adf..66bc6a1d 100644 --- a/BE/src/websocket/base-socket.service.ts +++ b/BE/src/websocket/base-socket.service.ts @@ -48,7 +48,8 @@ export class BaseSocketService implements OnModuleInit { `한국투자증권 웹소켓 연결: ${json.body.msg1}`, json.header.tr_id, ); - if (json.header.tr_id === 'PINGPONG') this.socket.pong(json); + if (json.header.tr_id === 'PINGPONG') + this.socket.pong(JSON.stringify(json)); return; } From f07eceac4850ec77cdf53232a5d01c48efdba84b Mon Sep 17 00:00:00 2001 From: JIN Date: Thu, 14 Nov 2024 17:40:24 +0900 Subject: [PATCH 157/158] =?UTF-8?q?=F0=9F=94=A7=20fix:=20controller=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/stock/detail/stock-detail.controller.ts | 2 +- BE/src/stock/topfive/stock-topfive.controller.ts | 4 ++-- .../stock/trade/history/stock-trade-history.controller.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BE/src/stock/detail/stock-detail.controller.ts b/BE/src/stock/detail/stock-detail.controller.ts index c9f74222..5df8d6c7 100644 --- a/BE/src/stock/detail/stock-detail.controller.ts +++ b/BE/src/stock/detail/stock-detail.controller.ts @@ -12,7 +12,7 @@ import { StockDetailChartRequestDto } from './dto/stock-detail-chart-request.dto import { InquirePriceChartResponseDto } from './dto/stock-detail-chart-response.dto'; @ApiTags('특정 주식 종목에 대한 detail 페이지 조회 API') -@Controller('/api/stocks') +@Controller('/api/stocks/detail') export class StockDetailController { constructor(private readonly stockDetailService: StockDetailService) {} diff --git a/BE/src/stock/topfive/stock-topfive.controller.ts b/BE/src/stock/topfive/stock-topfive.controller.ts index 7da63d4c..3d6258e0 100644 --- a/BE/src/stock/topfive/stock-topfive.controller.ts +++ b/BE/src/stock/topfive/stock-topfive.controller.ts @@ -5,11 +5,11 @@ import { StockRankingResponseDto } from './dto/stock-ranking-response.dto'; import { MarketType } from '../enum/market-type'; @ApiTags('오늘의 상/하위 종목 조회 API') -@Controller('/api/stocks') +@Controller('/api/stocks/topfive') export class StockTopfiveController { constructor(private readonly topFiveService: StockTopfiveService) {} - @Get('topfive') + @Get() @ApiOperation({ summary: '오늘의 상/하위 종목 조회 API' }) @ApiQuery({ name: 'market', diff --git a/BE/src/stock/trade/history/stock-trade-history.controller.ts b/BE/src/stock/trade/history/stock-trade-history.controller.ts index 4885aed2..1b4c1533 100644 --- a/BE/src/stock/trade/history/stock-trade-history.controller.ts +++ b/BE/src/stock/trade/history/stock-trade-history.controller.ts @@ -5,13 +5,13 @@ import { TodayStockTradeHistoryResponseDto } from './dto/today-stock-trade-histo import { DailyStockTradeHistoryDataDto } from './dto/daily-stock-trade-history-data.dto'; @ApiTags('주식현재가 체결 조회 API') -@Controller('/api/stocks') +@Controller('/api/stocks/trade-history') export class StockTradeHistoryController { constructor( private readonly stockTradeHistoryService: StockTradeHistoryService, ) {} - @Get(':stockCode/today-trade-history') + @Get(':stockCode/today') @ApiOperation({ summary: '단일 주식 종목에 대한 주식현재가 체결 API' }) @ApiParam({ name: 'stockCode', @@ -29,7 +29,7 @@ export class StockTradeHistoryController { return this.stockTradeHistoryService.getTodayStockTradeHistory(stockCode); } - @Get(':stockCode/daily-trade-history') + @Get(':stockCode/daily') @ApiOperation({ summary: '단일 주식 종목에 대한 일자별 주식현재가 API' }) @ApiParam({ name: 'stockCode', From 6efbd2caed30c2b011af93470f094ecf5808baaa Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 14 Nov 2024 18:38:03 +0900 Subject: [PATCH 158/158] =?UTF-8?q?=F0=9F=9A=91=20!HOTFIX=20:=20kakao=20ca?= =?UTF-8?q?llbackURL=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/auth/strategy/kakao.strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BE/src/auth/strategy/kakao.strategy.ts b/BE/src/auth/strategy/kakao.strategy.ts index 00d6d96f..1e4e0c85 100644 --- a/BE/src/auth/strategy/kakao.strategy.ts +++ b/BE/src/auth/strategy/kakao.strategy.ts @@ -32,7 +32,7 @@ export class KakaoStrategy extends PassportStrategy( const options: KakaoStrategyOptions = { clientID: configService.get('KAKAO_CLIENT_ID') || '', clientSecret: '', - callbackURL: `${configService.get('BACKEND_URL') || ''}/auth/kakao`, + callbackURL: `${configService.get('BACKEND_URL') || ''}/api/auth/kakao`, }; super(options);