From e5a3de41edf3aa3a3828dee0979d70beb283bc99 Mon Sep 17 00:00:00 2001 From: sunghwki <52474291+swkim12345@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:29:14 +0900 Subject: [PATCH] =?UTF-8?q?deploy=20v0.1=20=EC=B6=94=EA=B0=80=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat(module openapi-scrapper 추가): module로 개발 시작 * 🚚 chore: backend packege 의존성 업데이트 * ✨ feat: backend/stock-price 추가 * 🚚 chore: yarn.lock 업데이트 * ✨ feat(korea-stock-info.*.ts): 한국 주식 데이터를 파싱하고, 가져오는 것을 제작함 * 🚚 chore: deploy 추가, front, backend dockerfile 추가 * commit after merge * 🚚 chore: yarn.lock, package.json ISSUES CLOSED: unzipper, axios 설치 * 🚚 chore: yarn.lock, workflows 업데이트 - dev,fe,be 모두 작동 * 🚚 chore: type unzipper 추가 * 🚚 chore: path 문제 해결 - build -\> dist, packages/fronted/dist =\> dist * 🚚 chore: server password * 🚚 chore: tsconfig.buildjson output dir 추가 * 📦️ ci: frontend dockerfile 수정 * ✨ feat: 구글 로그인 페이지 이동 관련 swagger 설정 * ✨ feat: 구글 로그인 성공시 리다이렉트 엔드포인트 구현 * ✨ feat: 구글 oauth 서버로부터 access token 획득 * ♻️ refactor: request 관련 타입 수정 * ✨ feat: 구글 oauth 로그인 성공 후 사용자 정보를 획득 * ✨ feat(module openapi-scrapper 추가): module로 개발 시작 * ✨ feat: backend/stock-price 추가 * ✨ feat(korea-stock-info.*.ts): 한국 주식 데이터를 파싱하고, 가져오는 것을 제작함 * 🐛 fix: 리뷰 반영 - env, 중복코드 제거, .vscode 삭제 / dto 사용 확대 * 🚚 chore: backend package prod 위치 변경 * 📦️ ci: package.json 수정 - ..좋은 방법은 아니지만.. * 📦️ ci: packages.json 중복 삭제 * 🐛 fix: 충돌로 발생한 문제 해결 * 🚚 chore: deploy.yml backend 부터 만들게 설정 * 🚚 chore: deploy docker-down 추가 * 📦️ ci: 편-안하게 ssh로 변경, pull request 작동 삭제 * 📦️ ci: front, backend action 다르게 설정. * 📦️ ci: 이름 변경 --------- Co-authored-by: kimminsu --- .github/workflows/deploy-backend.yml | 65 ++++++++ .github/workflows/deploy-frontend.yml | 65 ++++++++ .gitignore | 5 +- packages/backend/Dockerfile | 7 + packages/backend/package.json | 7 +- packages/backend/src/app.controller.spec.ts | 22 --- packages/backend/src/app.controller.ts | 16 -- packages/backend/src/app.module.ts | 8 +- packages/backend/src/app.service.ts | 8 - .../src/auth/passport/google.strategy.ts | 5 +- .../dto/master-download.dto.ts | 14 ++ .../korea-stock-info/dto/master-split.dto.ts | 32 ++++ .../korea-stock-info/entities/stock.entity.ts | 23 +++ .../korea-stock-info.controller.spec.ts | 18 +++ .../korea-stock-info.controller.ts | 4 + .../korea-stock-info.service.spec.ts | 18 +++ .../korea-stock-info.service.ts | 139 ++++++++++++++++++ .../openapi-scraper.controller.ts | 4 + .../openapi-scraper/openapi-scraper.module.ts | 9 ++ .../openapi-scraper.service.ts | 35 +++++ .../src/openapi-scraper/openapi-scraper.ts | 4 + .../src/openapi-scraper/openapi.config.ts | 12 ++ .../stock-price.controller.spec.ts | 18 +++ .../src/stock-price/stock-price.controller.ts | 5 + .../src/stock-price/stock-price.module.ts | 4 + packages/backend/tsconfig.build.json | 2 +- packages/frontend/Dockerfile | 11 ++ yarn.lock | 89 ++++++++++- 28 files changed, 590 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/deploy-backend.yml create mode 100644 .github/workflows/deploy-frontend.yml create mode 100644 packages/backend/Dockerfile delete mode 100644 packages/backend/src/app.controller.spec.ts delete mode 100644 packages/backend/src/app.controller.ts delete mode 100644 packages/backend/src/app.service.ts create mode 100644 packages/backend/src/openapi-scraper/korea-stock-info/dto/master-download.dto.ts create mode 100644 packages/backend/src/openapi-scraper/korea-stock-info/dto/master-split.dto.ts create mode 100644 packages/backend/src/openapi-scraper/korea-stock-info/entities/stock.entity.ts create mode 100644 packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.spec.ts create mode 100644 packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.ts create mode 100644 packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.spec.ts create mode 100644 packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.ts create mode 100644 packages/backend/src/openapi-scraper/openapi-scraper.controller.ts create mode 100644 packages/backend/src/openapi-scraper/openapi-scraper.module.ts create mode 100644 packages/backend/src/openapi-scraper/openapi-scraper.service.ts create mode 100644 packages/backend/src/openapi-scraper/openapi-scraper.ts create mode 100644 packages/backend/src/openapi-scraper/openapi.config.ts create mode 100644 packages/backend/src/stock-price/stock-price.controller.spec.ts create mode 100644 packages/backend/src/stock-price/stock-price.controller.ts create mode 100644 packages/backend/src/stock-price/stock-price.module.ts create mode 100644 packages/frontend/Dockerfile diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml new file mode 100644 index 00000000..fee8078e --- /dev/null +++ b/.github/workflows/deploy-backend.yml @@ -0,0 +1,65 @@ +name: Deploy Backend in Monorepo + +on: + push: + branches: + - dev-be + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + services: + docker: + image: docker:20.10.7 + options: --privileged + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Yarn dependencies + uses: actions/cache@v3 + with: + path: | + **/node_modules + ~/.yarn-cache + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build backend + run: | + yarn workspace backend build + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push backend Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/backend:latest -f packages/backend/Dockerfile . + docker push ${{ secrets.DOCKER_USERNAME }}/backend:latest + + - name: Deploy to server + uses: appleboy/ssh-action@v1.1.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY}} + port: ${{ secrets.SERVER_PORT }} + script: | + docker pull ${{ secrets.DOCKER_USERNAME }}/backend:latest + docker-compose down + docker-compose up -d diff --git a/.github/workflows/deploy-frontend.yml b/.github/workflows/deploy-frontend.yml new file mode 100644 index 00000000..79cd98e2 --- /dev/null +++ b/.github/workflows/deploy-frontend.yml @@ -0,0 +1,65 @@ +name: Deploy Frontend in Monorepo + +on: + push: + branches: + - dev-fe + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + services: + docker: + image: docker:20.10.7 + options: --privileged + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Yarn dependencies + uses: actions/cache@v3 + with: + path: | + **/node_modules + ~/.yarn-cache + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build frontend + run: | + yarn workspace frontend build + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push backend Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/backend:latest -f packages/backend/Dockerfile . + docker push ${{ secrets.DOCKER_USERNAME }}/backend:latest + + - name: Deploy to server + uses: appleboy/ssh-action@v1.1.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY}} + port: ${{ secrets.SERVER_PORT }} + script: | + docker pull ${{ secrets.DOCKER_USERNAME }}/frontend:latest + docker-compose down + docker-compose up -d diff --git a/.gitignore b/.gitignore index e3881704..66b03b45 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,7 @@ pids *.pid.lock # Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json \ No newline at end of file +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# vscode setting +.vscode diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile new file mode 100644 index 00000000..41ae4118 --- /dev/null +++ b/packages/backend/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-alpine +WORKDIR /packages +COPY . . +RUN yarn install --frozen-lockfile +RUN yarn workspace backend build +EXPOSE 3000 +CMD ["yarn", "workspace", "backend", "start:prod"] diff --git a/packages/backend/package.json b/packages/backend/package.json index f70cac51..fd67f9dc 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -11,7 +11,7 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "node ../../dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", @@ -21,6 +21,7 @@ }, "dependencies": { "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", @@ -28,6 +29,7 @@ "@nestjs/swagger": "^8.0.5", "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.4.7", + "axios": "^1.7.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.5", @@ -40,6 +42,7 @@ "rxjs": "^7.8.1", "socket.io": "^4.8.1", "typeorm": "^0.3.20", + "unzipper": "^0.12.3", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0" }, @@ -53,8 +56,10 @@ "@types/node": "^20.3.1", "@types/passport-google-oauth20": "^2.0.16", "@types/supertest": "^6.0.0", + "@types/unzipper": "^0.10.10", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", + "cz-emoji-conventional": "^1.1.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", diff --git a/packages/backend/src/app.controller.spec.ts b/packages/backend/src/app.controller.spec.ts deleted file mode 100644 index d22f3890..00000000 --- a/packages/backend/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/packages/backend/src/app.controller.ts b/packages/backend/src/app.controller.ts deleted file mode 100644 index 8de39f5b..00000000 --- a/packages/backend/src/app.controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } - - getHi(a: any): string { - return 'hi'; - } -} diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index c7c4b36b..bf8288a9 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -1,8 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WinstonModule } from 'nest-winston'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { AuthModule } from '@/auth/auth.module'; import { logger } from '@/configs/logger.config'; import { @@ -14,6 +12,8 @@ import { UserModule } from '@/user/user.module'; @Module({ imports: [ + //OpenapiScraperModule, + //StockPriceModule, StockModule, UserModule, TypeOrmModule.forRoot( @@ -24,7 +24,7 @@ import { UserModule } from '@/user/user.module'; WinstonModule.forRoot(logger), AuthModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/packages/backend/src/app.service.ts b/packages/backend/src/app.service.ts deleted file mode 100644 index 927d7cca..00000000 --- a/packages/backend/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/packages/backend/src/auth/passport/google.strategy.ts b/packages/backend/src/auth/passport/google.strategy.ts index 230ece55..3851330c 100644 --- a/packages/backend/src/auth/passport/google.strategy.ts +++ b/packages/backend/src/auth/passport/google.strategy.ts @@ -1,8 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Profile, Strategy, VerifyCallback } from 'passport-google-oauth20'; import { GoogleAuthService } from '@/auth/googleAuth.service'; import { OauthType } from '@/user/domain/ouathType'; +import { Logger } from 'winston'; export interface OauthUserInfo { type: OauthType; @@ -14,7 +15,7 @@ export interface OauthUserInfo { @Injectable() export class GoogleStrategy extends PassportStrategy(Strategy) { - constructor(private readonly googleAuthService: GoogleAuthService) { + constructor(private readonly googleAuthService: GoogleAuthService, @Inject('winston') private readonly logger: Logger) { super({ clientID: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, diff --git a/packages/backend/src/openapi-scraper/korea-stock-info/dto/master-download.dto.ts b/packages/backend/src/openapi-scraper/korea-stock-info/dto/master-download.dto.ts new file mode 100644 index 00000000..d21c9a80 --- /dev/null +++ b/packages/backend/src/openapi-scraper/korea-stock-info/dto/master-download.dto.ts @@ -0,0 +1,14 @@ +import { IsString } from "class-validator"; + +export class MasterDownloadDto { + @IsString() + baseDir!: string; + + @IsString() + target: string; + + constructor(baseDir: string, target: string) { + this.baseDir = baseDir; + this.target = target; + } +} diff --git a/packages/backend/src/openapi-scraper/korea-stock-info/dto/master-split.dto.ts b/packages/backend/src/openapi-scraper/korea-stock-info/dto/master-split.dto.ts new file mode 100644 index 00000000..d58a1147 --- /dev/null +++ b/packages/backend/src/openapi-scraper/korea-stock-info/dto/master-split.dto.ts @@ -0,0 +1,32 @@ +import { IsArray } from 'class-validator'; + +export class MasterSplit { + @IsArray() + shortCode: [number, number]; + + @IsArray() + standardCode: [number, number]; + + @IsArray() + koreanName: [number, number]; + + @IsArray() + groupCode: [number, number]; + + @IsArray() + marketCapSize: [number, number]; + + constructor( + shortCode: [number, number], + standardCode: [number, number], + koreanName: [number, number], + groupCode: [number, number], + marketCapSize: [number, number], + ) { + this.shortCode = shortCode; + this.standardCode = standardCode; + this.koreanName = koreanName; + this.groupCode = groupCode; + this.marketCapSize = marketCapSize; + } +} diff --git a/packages/backend/src/openapi-scraper/korea-stock-info/entities/stock.entity.ts b/packages/backend/src/openapi-scraper/korea-stock-info/entities/stock.entity.ts new file mode 100644 index 00000000..ad721147 --- /dev/null +++ b/packages/backend/src/openapi-scraper/korea-stock-info/entities/stock.entity.ts @@ -0,0 +1,23 @@ +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +//TODO : entity update require +@Entity() +export class Master { + @PrimaryGeneratedColumn({ type: 'int', unsigned: true }) + id?: number; + + @Column() + shortCode?: string; + + @Column() + standardCode?: string; + + @Column() + koreanName?: string; + + @Column() + groupCode?: string; + + @Column() + marketCapSize?: string; +} diff --git a/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.spec.ts b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.spec.ts new file mode 100644 index 00000000..6818fba0 --- /dev/null +++ b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { KoreaStockInfoController } from './korea-stock-info.controller'; + +describe('KoreaStockInfoController', () => { + let controller: KoreaStockInfoController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [KoreaStockInfoController], + }).compile(); + + controller = module.get(KoreaStockInfoController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.ts b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.ts new file mode 100644 index 00000000..0171b0d6 --- /dev/null +++ b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('korea-stock-info') +export class KoreaStockInfoController {} diff --git a/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.spec.ts b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.spec.ts new file mode 100644 index 00000000..c8197ee4 --- /dev/null +++ b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { KoreaStockInfoService } from './korea-stock-info.service'; + +describe('KoreaStockInfoService', () => { + let service: KoreaStockInfoService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [KoreaStockInfoService], + }).compile(); + + service = module.get(KoreaStockInfoService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.ts b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.ts new file mode 100644 index 00000000..dd9aad35 --- /dev/null +++ b/packages/backend/src/openapi-scraper/korea-stock-info/korea-stock-info.service.ts @@ -0,0 +1,139 @@ +import { Injectable } from '@nestjs/common'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as https from 'https'; +import * as readline from 'readline'; +import * as iconv from 'iconv-lite'; +import * as unzipper from 'unzipper'; +import { MasterDownloadDto } from './dto/master-download.dto'; +import { Master } from './entities/stock.entity'; +import { config as dotenvConfig } from 'dotenv'; + +dotenvConfig(); + +@Injectable() +export class KoreaStockInfoService { + constructor() { + this.downloadMaster({ baseDir: './', target: 'kosdaq_code' }); + this.downloadMaster({ baseDir: './', target: 'kosdaq_code' }); + + this.getKospiMasterData({ baseDir: './', target: 'kosdaq_code' }); + this.getKosdaqMasterData({ baseDir: './', target: 'kosdaq_code' }); + } + + private async downloadFile(url: string, filePath: string): Promise { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + https + .get(url, (response) => { + response.pipe(file); + file.on('finish', () => { + file.close(); + resolve(); + }); + }) + .on('error', (error) => { + fs.unlinkSync(filePath); + reject(error); + }); + }); + } + + private async extractZip(filePath: string, extractTo: string): Promise { + await fs + .createReadStream(filePath) + .pipe(unzipper.Extract({ path: extractTo })) + .promise(); + } + + private getValueFromMst(row: string, start: number, end: number) { + return row.slice(start, end).trim(); + } + + public async downloadMaster(downloadDto: MasterDownloadDto): Promise { + const { baseDir, target } = downloadDto; + const fileName = target + '.zip'; + const targetFileName = target + '.mst'; + const url = + process.env.MST_URL + + target + + '.mst.zip'; + const filePath = path.join(baseDir, fileName); + const extractedFile = path.join(baseDir, targetFileName); + + await this.downloadFile(url, filePath); + await this.extractZip(filePath, baseDir); + + fs.unlink(filePath, (err) => { + if (err) throw err; + }); + + return extractedFile; + } + + public async getKospiMasterData( + downloadDto: MasterDownloadDto, + ): Promise { + const targetFileName = downloadDto.target + '.mst'; + const fileName = path.join(downloadDto.baseDir, targetFileName); + const encoding = 'cp949'; + const kospiMasters: Master[] = []; + + const rl = readline.createInterface({ + input: fs.createReadStream(fileName).pipe(iconv.decodeStream(encoding)), + crlfDelay: Infinity, + }); + + for await (const row of rl) { + const shortCode = this.getValueFromMst(row, 0, 9); + const standardCode = this.getValueFromMst(row, 9, 21); + const koreanName = this.getValueFromMst(row, 21, row.length - 228); + const groupCode = this.getValueFromMst(row, row.length - 228, row.length - 226); + const marketCapSize = this.getValueFromMst(row, row.length - 226, row.length - 225); + + kospiMasters.push({ + shortCode, + standardCode, + koreanName, + groupCode, + marketCapSize, + }); + } + + console.log('Done'); + return kospiMasters; + } + + public async getKosdaqMasterData( + downloadDto: MasterDownloadDto, + ): Promise { + const targetFileName = downloadDto.target + '.mst'; + const fileName = path.join(downloadDto.baseDir, targetFileName); + const encoding = 'cp949'; + const kosdaqMasters: Master[] = []; + + const rl = readline.createInterface({ + input: fs.createReadStream(fileName).pipe(iconv.decodeStream(encoding)), + crlfDelay: Infinity, + }); + + for await (const row of rl) { + const shortCode = this.getValueFromMst(row, 0, 9); + const standardCode = this.getValueFromMst(row, 9, 21); + const koreanName = this.getValueFromMst(row, 21, row.length - 222); + const groupCode = this.getValueFromMst(row, row.length - 222, row.length - 220); + const marketCapSize = this.getValueFromMst(row, row.length - 220, row.length - 219); + + kosdaqMasters.push({ + shortCode, + standardCode, + koreanName, + groupCode, + marketCapSize, + }); + } + + console.log('Done'); + return kosdaqMasters; + } +} diff --git a/packages/backend/src/openapi-scraper/openapi-scraper.controller.ts b/packages/backend/src/openapi-scraper/openapi-scraper.controller.ts new file mode 100644 index 00000000..56e60188 --- /dev/null +++ b/packages/backend/src/openapi-scraper/openapi-scraper.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('openapi-scraper') +export class OpenapiScraperController {} diff --git a/packages/backend/src/openapi-scraper/openapi-scraper.module.ts b/packages/backend/src/openapi-scraper/openapi-scraper.module.ts new file mode 100644 index 00000000..7f272bd9 --- /dev/null +++ b/packages/backend/src/openapi-scraper/openapi-scraper.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { OpenapiScraperService } from './openapi-scraper.service'; + +@Module({ + imports: [], + controllers: [], + providers: [OpenapiScraperService], +}) +export class OpenapiScraperModule {} diff --git a/packages/backend/src/openapi-scraper/openapi-scraper.service.ts b/packages/backend/src/openapi-scraper/openapi-scraper.service.ts new file mode 100644 index 00000000..a4b14527 --- /dev/null +++ b/packages/backend/src/openapi-scraper/openapi-scraper.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import axios from 'axios'; +import { openApiConfig } from './openapi.config'; + +@Injectable() +export class OpenapiScraperService { + private readonly config: typeof openApiConfig; + public constructor() { + this.config = openApiConfig; + this.getToken(); + } + + private async fetchOpenApi( + url: string, + query: {} | undefined, + body: {}, + ): Promise { + try { + const response = await axios.post(url + "/oauth2/tokenP", body); + return response.data; // 응답에서 data 부분만 추출 + } catch (error) { + throw new Error(`Request failed: ${error}`); + } + } + + private async getToken() { + const body = { + grant_type: 'client_credentials', + appkey: this.config.APPKEY, + appsecret: this.config.APPSECRET, + }; + const tmp = await this.fetchOpenApi(this.config.PROD!, undefined, body); + console.log(tmp.access_token); + } +} diff --git a/packages/backend/src/openapi-scraper/openapi-scraper.ts b/packages/backend/src/openapi-scraper/openapi-scraper.ts new file mode 100644 index 00000000..621c0988 --- /dev/null +++ b/packages/backend/src/openapi-scraper/openapi-scraper.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class OpenapiScraper {} diff --git a/packages/backend/src/openapi-scraper/openapi.config.ts b/packages/backend/src/openapi-scraper/openapi.config.ts new file mode 100644 index 00000000..3f3410ed --- /dev/null +++ b/packages/backend/src/openapi-scraper/openapi.config.ts @@ -0,0 +1,12 @@ +import * as dotenv from 'dotenv'; + +dotenv.config({ + path: './.env', +}); + +export const openApiConfig = { + PROD: process.env.PROD, + CANO_REAL: process.env.CANO_REAL, + APPKEY: process.env.PROD_APPKEY, + APPSECRET: process.env.PROD_APPSECRET, +}; diff --git a/packages/backend/src/stock-price/stock-price.controller.spec.ts b/packages/backend/src/stock-price/stock-price.controller.spec.ts new file mode 100644 index 00000000..a6fe3fb6 --- /dev/null +++ b/packages/backend/src/stock-price/stock-price.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { StockPriceController } from './stock-price.controller'; + +describe('StockPriceController', () => { + let controller: StockPriceController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [StockPriceController], + }).compile(); + + controller = module.get(StockPriceController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/backend/src/stock-price/stock-price.controller.ts b/packages/backend/src/stock-price/stock-price.controller.ts new file mode 100644 index 00000000..1436c6a1 --- /dev/null +++ b/packages/backend/src/stock-price/stock-price.controller.ts @@ -0,0 +1,5 @@ +import { Controller, HttpCode, Post } from '@nestjs/common'; + +@Controller('stock-price') +export class StockPriceController { +} diff --git a/packages/backend/src/stock-price/stock-price.module.ts b/packages/backend/src/stock-price/stock-price.module.ts new file mode 100644 index 00000000..c6df3a29 --- /dev/null +++ b/packages/backend/src/stock-price/stock-price.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class StockPriceModule {} diff --git a/packages/backend/tsconfig.build.json b/packages/backend/tsconfig.build.json index c4fad51c..8aea2aaa 100644 --- a/packages/backend/tsconfig.build.json +++ b/packages/backend/tsconfig.build.json @@ -5,6 +5,6 @@ "baseUrl": ".", "paths": { "@/*": ["src/*"] - } + }, } } diff --git a/packages/frontend/Dockerfile b/packages/frontend/Dockerfile new file mode 100644 index 00000000..cbeb22e3 --- /dev/null +++ b/packages/frontend/Dockerfile @@ -0,0 +1,11 @@ +FROM node:20-alpine AS builder +WORKDIR /packages +COPY . . +RUN yarn install --frozen-lockfile +RUN yarn workspace frontend build + +# Nginx 서버로 빌드된 파일 서빙 +FROM nginx:alpine +COPY --from=builder /packages/packages/frontend/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/yarn.lock b/yarn.lock index 2495faeb..557adde2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1085,6 +1085,15 @@ iterare "1.2.1" tslib "2.7.0" +"@nestjs/config@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@nestjs/config/-/config-3.3.0.tgz#ddc520ba26a8453ee5e690e18fb7b35e9bac7974" + integrity sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA== + dependencies: + dotenv "16.4.5" + dotenv-expand "10.0.0" + lodash "4.17.21" + "@nestjs/core@^10.0.0": version "10.4.6" resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.6.tgz#797b381f12bd62d2e425897058fa219da4c3689d" @@ -1664,6 +1673,13 @@ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c" integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== +"@types/unzipper@^0.10.10": + version "0.10.10" + resolved "https://registry.yarnpkg.com/@types/unzipper/-/unzipper-0.10.10.tgz#4407f7f633db0c5cf20f05257352cb8197fb9e5a" + integrity sha512-jKJdNxhmCHTZsaKW5x0qjn6rB+gHk0w5VFbEKsw84i+RJqXZyfTmGnpjDcKqzMpjz7VVLsUBMtO5T3mVidpt0g== + dependencies: + "@types/node" "*" + "@types/validator@^13.11.8": version "13.12.2" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.12.2.tgz#760329e756e18a4aab82fc502b51ebdfebbe49f5" @@ -2220,6 +2236,15 @@ aws-ssl-profiles@^1.1.1: resolved "https://registry.yarnpkg.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz#157dd77e9f19b1d123678e93f120e6f193022641" integrity sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g== +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -2317,6 +2342,11 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bluebird@~3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + body-parser@1.20.3: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" @@ -2695,7 +2725,7 @@ comment-json@4.2.5: has-own-prop "^2.0.0" repeat-string "^1.6.1" -commitizen@^4.0.3, commitizen@^4.3.1: +commitizen@^4.0.3, commitizen@^4.3.0, commitizen@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/commitizen/-/commitizen-4.3.1.tgz#f0e0e4b7ae3fafc92e444bbb78f2ded5a1d4311a" integrity sha512-gwAPAVTy/j5YcOOebcCRIijn+mSjWJC+IYKivTu6aG8Ei/scoXgfsMRnuAk6b0GRste2J4NGxVdMN3ZpfNaVaw== @@ -2922,6 +2952,15 @@ cz-customizable@^7.2.1: temp "^0.9.0" word-wrap "^1.2.3" +cz-emoji-conventional@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cz-emoji-conventional/-/cz-emoji-conventional-1.1.0.tgz#c42c34828619a5c2c7337afd4798c7da4366c753" + integrity sha512-W0EHK/Ek9ZHwJ+UaClF/xBHp6lkLuJgkiREiCVnwGuIz8Jhp/khoQzLcRI9AAF6vfT2slfz7E+RgGelmsVfzoQ== + dependencies: + chalk "4.1.2" + commitizen "^4.3.0" + word-wrap "^1.2.3" + dargs@^8.0.0: version "8.1.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-8.1.0.tgz#a34859ea509cbce45485e5aa356fef70bfcc7272" @@ -3109,11 +3148,23 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" -dotenv@^16.0.3, dotenv@^16.4.5: +dotenv-expand@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@16.4.5, dotenv@^16.0.3, dotenv@^16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -3914,6 +3965,11 @@ fn.name@1.x.x: resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -3999,6 +4055,15 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-monkey@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" @@ -4209,7 +4274,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -6193,6 +6258,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -6274,7 +6344,7 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -readable-stream@^2.2.2: +readable-stream@^2.0.2, readable-stream@^2.2.2: version "2.3.8" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -7402,6 +7472,17 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unzipper@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.12.3.tgz#31958f5eed7368ed8f57deae547e5a673e984f87" + integrity sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA== + dependencies: + bluebird "~3.7.2" + duplexer2 "~0.1.4" + fs-extra "^11.2.0" + graceful-fs "^4.2.2" + node-int64 "^0.4.0" + update-browserslist-db@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5"