├── .env.exam ├── .gitattributes ├── .github └── workflows │ └── hello.yml ├── .gitignore ├── .husky ├── commit-msg ├── commit-msg-check.js └── pre-commit ├── README.md ├── bot ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── DockerFile ├── package-lock.json ├── package.json ├── src │ ├── boobit │ │ ├── boobit.bot.ts │ │ ├── boobit.error.ts │ │ ├── bot.config.ts │ │ ├── currency.code.ts │ │ └── order.limit.request.dto.ts │ ├── index.ts │ ├── upbit │ │ ├── upbit.index.ts │ │ └── upbit.response.dto.ts │ └── utils │ │ ├── config.setting.argv.ts │ │ └── config.setting.client.ts └── tsconfig.json ├── docker-compose.yml ├── docker_init ├── init_mongo.sh ├── mongo_init.js └── mysql_init.sql ├── kubernetes ├── README.md ├── bot │ ├── auto.yaml │ ├── normal.yaml │ └── test.yaml ├── common │ ├── configmap.yaml │ ├── kustomization.yaml │ └── secret.exam.yaml ├── database │ ├── auth │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ ├── statefulset.yaml │ │ └── storage.yaml │ ├── balance │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ ├── statefulset.yaml │ │ └── storage.yaml │ └── transaction │ │ ├── config.yaml │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ ├── statefulset.yaml │ │ └── storage.yaml ├── ingress │ ├── alb.yaml │ └── devonly │ │ ├── kustomization.yaml │ │ ├── loadbalancer.yaml │ │ ├── nginx-configmap.yaml │ │ └── nginx-deployment.yaml ├── redis │ ├── session.yaml │ └── trade.yaml └── service │ ├── auth │ ├── config.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml │ ├── balance │ ├── config.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml │ ├── interval │ ├── config.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml │ ├── static │ └── deployment.yaml │ ├── trade │ ├── config.yaml │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml │ └── transaction │ ├── config.yaml │ ├── deployment.yaml │ ├── hpa.yaml │ ├── kustomization.yaml │ └── service.yaml ├── microservice ├── .dockerignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── apps │ ├── auth │ │ ├── DockerFile │ │ ├── prisma │ │ │ ├── migrations │ │ │ │ ├── 20241121051919_ │ │ │ │ │ └── migration.sql │ │ │ │ └── migration_lock.toml │ │ │ └── schema.prisma │ │ ├── src │ │ │ ├── auth.controller.ts │ │ │ ├── auth.module.ts │ │ │ ├── auth.service.ts │ │ │ ├── dto │ │ │ │ ├── login.dto.ts │ │ │ │ └── signup.dto.ts │ │ │ ├── main.ts │ │ │ └── passport │ │ │ │ ├── local.auth.guard.ts │ │ │ │ └── local.strategy.ts │ │ ├── test │ │ │ ├── auth.controller.spec.ts │ │ │ ├── auth.service.spec.ts │ │ │ └── signup.dto.spec.ts │ │ └── tsconfig.app.json │ ├── balance │ │ ├── DockerFile │ │ ├── prisma │ │ │ ├── migrations │ │ │ │ ├── 20241121052601_ │ │ │ │ │ └── migration.sql │ │ │ │ └── migration_lock.toml │ │ │ └── schema.prisma │ │ ├── src │ │ │ ├── balance.controller.spec.ts │ │ │ ├── balance.controller.ts │ │ │ ├── balance.module.ts │ │ │ ├── balance.repository.spec.ts │ │ │ ├── balance.repository.ts │ │ │ ├── balance.service.spec.ts │ │ │ ├── balance.service.ts │ │ │ ├── dto │ │ │ │ ├── asset.dto.ts │ │ │ │ ├── available.balance.response.dto.ts │ │ │ │ ├── create.transaction.dto.ts │ │ │ │ ├── get.transactions.request.dto.ts │ │ │ │ └── get.transactions.response.dto.ts │ │ │ ├── exception │ │ │ │ ├── balance.exception.ts │ │ │ │ ├── balance.exceptions.ts │ │ │ │ ├── custom.exception.ts │ │ │ │ └── exception.type.ts │ │ │ └── main.ts │ │ ├── test │ │ │ ├── e2e │ │ │ │ └── balance.e2e-spec.ts │ │ │ └── jest-e2e.json │ │ └── tsconfig.app.json │ ├── interval │ │ ├── DockerFile │ │ ├── prisma │ │ │ └── schema.prisma │ │ ├── src │ │ │ ├── dto │ │ │ │ └── order.book.dto.ts │ │ │ ├── interval.candle.repository.ts │ │ │ ├── interval.make.service.ts │ │ │ ├── interval.module.ts │ │ │ ├── interval.order.book.repository.ts │ │ │ ├── interval.order.book.service.ts │ │ │ ├── interval.service.ts │ │ │ └── main.ts │ │ └── tsconfig.app.json │ ├── trade │ │ ├── DockerFile │ │ ├── prisma │ │ │ └── schema.prisma │ │ ├── src │ │ │ ├── dto │ │ │ │ ├── trade.buy.order.type.ts │ │ │ │ └── trade.sell.order.type.ts │ │ │ ├── main.ts │ │ │ ├── trade.balance.service.ts │ │ │ ├── trade.module.ts │ │ │ ├── trade.processor.ts │ │ │ ├── trade.repository.ts │ │ │ ├── trade.service.spec.ts │ │ │ └── trade.service.ts │ │ ├── test │ │ │ ├── app.e2e-spec.ts │ │ │ └── jest-e2e.json │ │ └── tsconfig.app.json │ └── transaction │ │ ├── DockerFile │ │ ├── prisma │ │ └── schema.prisma │ │ ├── src │ │ ├── dto │ │ │ ├── order.limit.request.dto.ts │ │ │ ├── order.pending.dto.ts │ │ │ ├── order.pending.response.dto.ts │ │ │ ├── pending.buy.order.type.ts │ │ │ ├── pending.sell.order.type.ts │ │ │ └── trade.get.response.dto.ts │ │ ├── gateway │ │ │ ├── transaction.ws.gateway.ts │ │ │ └── transaction.ws.service.ts │ │ ├── main.ts │ │ ├── transaction.controller.ts │ │ ├── transaction.module.ts │ │ ├── transaction.order.service.ts │ │ ├── transaction.queue.service.ts │ │ ├── transaction.repository.ts │ │ └── transaction.service.ts │ │ ├── test │ │ ├── app.e2e-spec.ts │ │ ├── jest-e2e.json │ │ └── transaction.controller.spec.ts │ │ └── tsconfig.app.json ├── libs │ ├── bull │ │ ├── src │ │ │ ├── bull.module.ts │ │ │ └── index.ts │ │ └── tsconfig.lib.json │ ├── common │ │ ├── src │ │ │ ├── common.controller.ts │ │ │ ├── common.module.ts │ │ │ ├── common.service.spec.ts │ │ │ ├── common.service.ts │ │ │ ├── cors.ts │ │ │ ├── enums │ │ │ │ ├── chart-timescale.enum.ts │ │ │ │ ├── currency-code.enum.ts │ │ │ │ ├── grpc-status.enum.ts │ │ │ │ ├── index.ts │ │ │ │ ├── order-status.enum.ts │ │ │ │ ├── order-type.enum.ts │ │ │ │ ├── redis-channel.enum.ts │ │ │ │ ├── trade.gradient.enum.ts │ │ │ │ └── ws-event.enum.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ │ └── number.format.util.ts │ │ └── tsconfig.lib.json │ ├── grpc │ │ ├── proto │ │ │ ├── account.proto │ │ │ ├── order.proto │ │ │ └── trade.proto │ │ ├── src │ │ │ ├── account.interface.ts │ │ │ ├── dto │ │ │ │ ├── account.create.request.dto.ts │ │ │ │ ├── account.create.response.dto.ts │ │ │ │ ├── order.request.dto.ts │ │ │ │ ├── order.response.dto.ts │ │ │ │ ├── trade.buyer.request.dto.ts │ │ │ │ ├── trade.cancel.request.dto.ts │ │ │ │ ├── trade.history.request.dto.ts │ │ │ │ ├── trade.order.dto.ts │ │ │ │ ├── trade.reponse.dto.ts │ │ │ │ ├── trade.request.dto.ts │ │ │ │ └── trade.seller.request.dto.ts │ │ │ ├── order.interface.ts │ │ │ └── trade.interface.ts │ │ └── tsconfig.lib.json │ ├── prisma │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── prisma.module.ts │ │ │ └── prisma.service.ts │ │ └── tsconfig.lib.json │ ├── session │ │ ├── src │ │ │ ├── guard │ │ │ │ ├── authenticated.guard.spec.ts │ │ │ │ └── authenticated.guard.ts │ │ │ ├── index.ts │ │ │ ├── passport │ │ │ │ ├── session.serializer.spec.ts │ │ │ │ └── session.serializer.ts │ │ │ ├── session.middleware.ts │ │ │ └── session.module.ts │ │ └── tsconfig.lib.json │ └── ws │ │ ├── src │ │ ├── decorators │ │ │ ├── ws.event.decorator.ts │ │ │ └── ws.validate.message.decorator.ts │ │ ├── dto │ │ │ ├── candle.data.dto.ts │ │ │ ├── chart.response.dto.ts │ │ │ ├── subscribe.request.dto.ts │ │ │ ├── trade.data.dto.ts │ │ │ ├── trade.response.dto.ts │ │ │ └── ws.base.dto.ts │ │ ├── ws.base.gateway.ts │ │ ├── ws.error.ts │ │ ├── ws.module.ts │ │ └── ws.service.ts │ │ └── tsconfig.lib.json ├── nest-cli.json ├── package-lock.json ├── package.json ├── tsconfig.build.json └── tsconfig.json ├── static ├── .gitignore ├── DockerFile ├── app.js ├── package-lock.json └── package.json └── view ├── .env.exam ├── .env.production ├── .gitignore ├── .prettierrc ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── BuBu.png └── mockServiceWorker.js ├── src ├── __mocks │ ├── browser.ts │ └── handlers │ │ ├── balanceHandlers.ts │ │ ├── index.ts │ │ ├── transactionHandlers.ts │ │ └── websocketHandlers.ts ├── app │ ├── App.tsx │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts ├── entities │ ├── Chart │ │ ├── index.tsx │ │ ├── lib │ │ │ ├── addText.ts │ │ │ ├── createBarYAsixScale.ts │ │ │ ├── createXAxisScale.ts │ │ │ ├── createYAxisScale.ts │ │ │ └── updateMarketText.ts │ │ └── model │ │ │ └── candleDataType.ts │ ├── MyAssetInfo │ │ ├── UI │ │ │ ├── BoxContainer.tsx │ │ │ ├── Title.tsx │ │ │ ├── TransactionForm.tsx │ │ │ ├── TransactionLogItem.tsx │ │ │ └── TransactionLogs.tsx │ │ ├── api │ │ │ ├── depositApi.ts │ │ │ ├── getTransactionsApi.ts │ │ │ └── withdrawApi.ts │ │ ├── consts │ │ │ └── category.ts │ │ ├── index.tsx │ │ └── model │ │ │ ├── AssetType.ts │ │ │ ├── TransactionType.ts │ │ │ ├── TransactionsRequestType.ts │ │ │ ├── useDeposit.ts │ │ │ ├── useGetTransactions.ts │ │ │ └── useWithdraw.ts │ ├── MyAssetList │ │ ├── UI │ │ │ ├── AssetItem.tsx │ │ │ ├── BoxContainer.tsx │ │ │ └── InfoBar.tsx │ │ ├── consts │ │ │ └── mockImg.png │ │ └── index.tsx │ ├── MyInfo │ │ ├── UI │ │ │ └── InfoItem.tsx │ │ ├── api │ │ │ └── getProfileApi.ts │ │ ├── index.tsx │ │ └── model │ │ │ └── useGetProfile.ts │ ├── MyOpenOrders │ │ ├── api │ │ │ ├── deleteOrderApi.ts │ │ │ └── getPendingApi.ts │ │ ├── index.tsx │ │ └── model │ │ │ ├── OrderType.ts │ │ │ ├── useDeleteOrder.ts │ │ │ └── useGetPending.ts │ ├── MyOrderHistory │ │ ├── api │ │ │ └── getOrdersApi.ts │ │ ├── const │ │ │ └── status.ts │ │ ├── index.tsx │ │ └── model │ │ │ ├── OrderType.ts │ │ │ └── useGetOrders.ts │ ├── MyTradeHistory │ │ ├── api │ │ │ └── getTradesApi.ts │ │ ├── index.tsx │ │ └── model │ │ │ ├── TradeType.ts │ │ │ └── useGetTrades.ts │ ├── OrderBook │ │ ├── UI │ │ │ └── OrderItem.tsx │ │ └── index.tsx │ ├── OrderPanel │ │ ├── UI │ │ │ ├── Button.tsx │ │ │ ├── InputNumber.tsx │ │ │ └── SectionBlock.tsx │ │ ├── api │ │ │ ├── getAvailableAssetApi.ts │ │ │ ├── postBuyApi.ts │ │ │ └── postSellApi.ts │ │ ├── const │ │ │ └── orderCategory.ts │ │ ├── index.tsx │ │ └── model │ │ │ ├── RequestDataType.ts │ │ │ ├── useGetAvailableAsset.ts │ │ │ ├── useOrderAmount.ts │ │ │ ├── usePostBuy.ts │ │ │ └── usePostSell.ts │ └── TradeRecords │ │ ├── UI │ │ ├── TradeRecordRow.tsx │ │ └── TradeRecordsCell.tsx │ │ └── index.tsx ├── pages │ ├── Home │ │ ├── UI │ │ │ ├── TimeScaleItem.tsx │ │ │ ├── TimeScaleSelector.tsx │ │ │ └── Title.tsx │ │ └── index.tsx │ ├── MyPage │ │ ├── UI │ │ │ ├── CategoryItem.tsx │ │ │ ├── MainviewLayout.tsx │ │ │ ├── SubviewLayout.tsx │ │ │ └── Title.tsx │ │ ├── consts │ │ │ └── category.ts │ │ └── index.tsx │ ├── SignIn │ │ ├── api │ │ │ └── signinApi.ts │ │ ├── index.tsx │ │ └── model │ │ │ ├── formDataType.ts │ │ │ ├── useSignin.ts │ │ │ └── useSigninForm.ts │ └── SignUp │ │ ├── UI │ │ ├── Guideline.tsx │ │ └── LabeledInput.tsx │ │ ├── api │ │ └── signupApi.ts │ │ ├── index.tsx │ │ └── model │ │ ├── formDataType.ts │ │ ├── formValidationType.ts │ │ ├── useSignup.ts │ │ └── useSignupForm.ts ├── shared │ ├── UI │ │ ├── InputFeild.tsx │ │ ├── SubmitButton.tsx │ │ ├── Tab.tsx │ │ ├── TabItem.tsx │ │ ├── TableCell.tsx │ │ ├── TableRow.tsx │ │ └── Toast.tsx │ ├── api │ │ └── getAssetsApi.ts │ ├── consts │ │ ├── errorMessages.ts │ │ └── successMessage.ts │ ├── images │ │ ├── BuBu.png │ │ ├── BuBuWithLogo.png │ │ ├── down.svg │ │ ├── error.svg │ │ ├── info.svg │ │ ├── success.svg │ │ └── up.svg │ ├── model │ │ ├── calculateChangeRate.ts │ │ ├── formatDate.ts │ │ ├── formatPrice.ts │ │ ├── useGetAssets.ts │ │ └── useWebSocket.ts │ ├── store │ │ ├── ToastContext.tsx │ │ └── auth │ │ │ ├── authActions.ts │ │ │ └── authContext.tsx │ └── types │ │ ├── ChartTimeScaleType.ts │ │ ├── LayoutProps.ts │ │ ├── MyAsset.ts │ │ ├── RecordType.ts │ │ └── socket │ │ ├── BuyAndSellType.ts │ │ ├── CandleChartType.ts │ │ ├── CandleSocketType.ts │ │ ├── OrderType.ts │ │ ├── SocketEventType.ts │ │ ├── TradeType.ts │ │ └── WebSocketMessageType.ts └── widgets │ ├── AuthLayout │ └── index.tsx │ ├── CashTransaction │ └── index.tsx │ ├── Header │ ├── UI │ │ └── Logo.tsx │ ├── api │ │ └── signoutApi.ts │ ├── index.tsx │ └── model │ │ └── useSignout.ts │ └── Layout │ └── index.tsx ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.env.exam: -------------------------------------------------------------------------------- 1 | #----------------------------------- 2 | 3 | # docker compose를 사용하여 실행 시 필요한 환경변수 4 | # MySQL 공통 설정 5 | MYSQL_USER=boobit 6 | MYSQL_PASSWORD=password 7 | MYSQL_ROOT_PASSWORD=rootpassword 8 | MYSQL_DATABASE=boobit 9 | MYSQL_PORT=3306 10 | 11 | # Auth 서비스 설정 12 | AUTH_DB=auth-db 13 | AUTH_HOST_SERVICE_PORT=3000 14 | AUTH_HOST_DB_PORT=30000 15 | 16 | # Balance 서비스 설정 17 | BALANCE_DB=balance-db 18 | BALANCE_HOST_SERVICE_PORT=3100 19 | BALANCE_HOST_DB_PORT=31000 20 | BALANCE_HOST_GRPC_PORT=50051 21 | 22 | # Transaction 서비스 설정 23 | TRANSACTION_DB=transaction-db 24 | TRANSACTION_HOST_SERVICE_PORT=3200 25 | TRANSACTION_HOST_DB_PORT=32000 26 | TRANSACTION_HOST_DB_PORT_SECONDARY1=32001 27 | TRANSACTION_HOST_DB_PORT_SECONDARY2=32002 28 | 29 | # Trade 서비스 설정 30 | TRADE_HOST_SERVICE_PORT=3300 31 | 32 | # Redis 설정 33 | SESSION_DB=session-db 34 | REDIS_PORT=6379 35 | REDIS_HOST_PORT=36379 36 | SESSION_SECRET=SECRET 37 | 38 | # Redis 설정 39 | TRADE_REDIS=trade-redis 40 | TRADE_REDIS_HOST_PORT=36380 41 | 42 | # Interval 서비스 설정 43 | INTERVAL_HOST_SERVICE_PORT=3400 44 | 45 | # Trading Bot 설정 46 | OPTIONS="--count=10" 47 | 48 | #----------------------------------- -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | // .gitattributes 2 | # 모든 텍스트 파일에 대해 LF 사용 3 | * text=auto eol=lf 4 | 5 | # 소스 코드 6 | *.ts text eol=lf 7 | *.js text eol=lf 8 | *.jsx text eol=lf 9 | *.tsx text eol=lf 10 | *.json text eol=lf 11 | *.yml text eol=lf 12 | *.yaml text eol=lf 13 | *.md text eol=lf 14 | *.html text eol=lf 15 | *.css text eol=lf 16 | *.scss text eol=lf 17 | *.less text eol=lf 18 | 19 | # 설정 파일 20 | .env* text eol=lf 21 | .gitignore text eol=lf 22 | .gitattributes text eol=lf 23 | .eslintrc text eol=lf 24 | .prettierrc text eol=lf 25 | *.config.js text eol=lf 26 | *.config.ts text eol=lf 27 | 28 | # 스크립트 29 | *.sh text eol=lf 30 | *.bash text eol=lf 31 | 32 | # 바이너리 파일 (변환하지 않음) 33 | *.png binary 34 | *.jpg binary 35 | *.gif binary 36 | *.ico binary 37 | *.svg binary 38 | *.woff binary 39 | *.woff2 binary 40 | *.eot binary 41 | *.ttf binary 42 | *.pdf binary -------------------------------------------------------------------------------- /.github/workflows/hello.yml: -------------------------------------------------------------------------------- 1 | name: Webhook Hello World 2 | 3 | # repository_dispatch 이벤트로 트리거 4 | on: 5 | repository_dispatch: 6 | types: [webhook-trigger] 7 | 8 | jobs: 9 | say-hello: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # webhook으로 전달받은 데이터 출력 13 | - name: Print webhook payload 14 | run: | 15 | echo "Hello World!" 16 | echo "Webhook data received:" 17 | echo "Event Type: ${{ github.event.action }}" 18 | echo "Client Payload: ${{ toJSON(github.event.client_payload) }}" 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | 58 | #testDB 59 | /data 60 | /db_data_auth 61 | /db_data_balance 62 | /db_data_transaction 63 | /db_data_redis 64 | 65 | #kubernetes secret 66 | /kubernetes/common/secret.yaml -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | node .husky/commit-msg-check.js 2 | -------------------------------------------------------------------------------- /.husky/commit-msg-check.js: -------------------------------------------------------------------------------- 1 | // .husky/commit-msg-check.js 2 | const fs = require('fs'); 3 | 4 | const msgPath = process.env.HUSKY_GIT_PARAMS || '.git/COMMIT_EDITMSG'; 5 | const message = fs.readFileSync(msgPath, 'utf-8').trim(); 6 | 7 | const commitMessageRegex = /^(📝Docs\.|♻️Refactor\.|✨Feat\.|🐛Fix\.|⚡️Improve\.|🛠️Update\.|⚙️Config\.)/; 8 | 9 | if (!commitMessageRegex.test(message)) { 10 | console.error(` 11 | ⛔️ 커밋 메시지 규칙을 지켜주세요! 12 | 포멧 : type. titleName #issue 13 | - 사용 가능한 이모지와 텍스트 매핑: 14 | 📝Docs: 문서 추가, 삭제, 수정 15 | ♻️Refactor: 코드 리팩토링 16 | ✨Feat: 새로운 기능 추가 17 | 🐛Fix: 예상치 못한 버그 수정 18 | ⚡️Improve: 기존 기능 향상 (기능적) 19 | 🛠️Update: 기존 기능 개선 (코드 품질) 20 | ⚙️Config : 개발환경 관련 설정 21 | - 예시: "📝 Docs: 문서 추가" 22 | `); 23 | process.exit(1); 24 | } -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged -------------------------------------------------------------------------------- /bot/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | parserOptions: { 4 | project: "tsconfig.json", 5 | tsconfigRootDir: __dirname, 6 | sourceType: "module", 7 | }, 8 | plugins: ["@typescript-eslint/eslint-plugin"], 9 | extends: [ 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:prettier/recommended", 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: [".eslintrc.js"], 19 | rules: { 20 | "@typescript-eslint/interface-name-prefix": "off", 21 | "@typescript-eslint/explicit-function-return-type": "off", 22 | "@typescript-eslint/explicit-module-boundary-types": "off", 23 | "@typescript-eslint/no-explicit-any": "off", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /bot/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /bot/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "endOfLine": "lf", 10 | "useTabs": false, 11 | "overrides": [ 12 | { 13 | "files": "*.{ts,tsx}", 14 | "options": { 15 | "parser": "typescript" 16 | } 17 | }, 18 | { 19 | "files": "*.decorator.ts", 20 | "options": { 21 | "printWidth": 120 22 | } 23 | }, 24 | { 25 | "files": "*.dto.ts", 26 | "options": { 27 | "printWidth": 80 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /bot/DockerFile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy only necessary files for this service 7 | COPY bot ./ 8 | 9 | # Install and build 10 | RUN npm ci 11 | RUN npm run build 12 | 13 | # Production stage 14 | FROM node:20-alpine 15 | 16 | WORKDIR /app 17 | COPY --from=builder /app ./ 18 | 19 | # Run application 20 | CMD node dist/index.js ${OPTIONS} 21 | 22 | -------------------------------------------------------------------------------- /bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boobit-bot", 3 | "version": "1.0.0", 4 | "description": "boobit bot", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "start": "ts-node src/index.ts --client true", 8 | "build": "tsc", 9 | "dev": "ts-node-dev --respawn src/index.ts", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@types/yargs": "^17.0.33", 17 | "axios": "^1.7.8", 18 | "http": "^0.0.1-security", 19 | "yargs": "^17.7.2" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20.10.0", 23 | "@typescript-eslint/eslint-plugin": "^8.16.0", 24 | "@typescript-eslint/parser": "^8.16.0", 25 | "eslint": "^9.15.0", 26 | "ts-node": "^10.9.1", 27 | "ts-node-dev": "^2.0.0", 28 | "typescript": "^5.3.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /bot/src/boobit/boobit.error.ts: -------------------------------------------------------------------------------- 1 | export class LoginError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = 'LoginError'; 5 | } 6 | } 7 | 8 | export class TradingError extends Error { 9 | constructor(message: string) { 10 | super(message); 11 | this.name = 'TradingError'; 12 | } 13 | } 14 | 15 | export class LogoutError extends Error { 16 | constructor(message: string) { 17 | super(message); 18 | this.name = 'LogoutError'; 19 | } 20 | } 21 | 22 | export class SessionError extends Error { 23 | constructor(message: string) { 24 | super(message); 25 | this.name = 'SessionError'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bot/src/boobit/currency.code.ts: -------------------------------------------------------------------------------- 1 | export enum CurrencyCode { 2 | KRW = 'KRW', 3 | BTC = 'BTC', 4 | } 5 | 6 | export enum CurrencyCodeName { 7 | KRW = '원화', 8 | BTC = '비트코인', 9 | } 10 | -------------------------------------------------------------------------------- /bot/src/boobit/order.limit.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class OrderLimitRequestDto { 2 | constructor(coinCode: string, amount: number, price: number) { 3 | this.coinCode = coinCode; 4 | this.amount = amount; 5 | this.price = price; 6 | } 7 | coinCode: string; 8 | amount: number; 9 | price: number; 10 | 11 | toString() { 12 | return `OrderLimitRequestDto { coinCode: ${this.coinCode}, amount: ${ 13 | this.amount 14 | }, price: ${this.price.toLocaleString('ko-KR', { 15 | style: 'currency', 16 | currency: 'KRW', 17 | })} }`; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /bot/src/upbit/upbit.index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { UpbitResponse } from './upbit.response.dto'; 3 | 4 | export async function getUpbitPrice(): Promise { 5 | try { 6 | const response = await axios.get( 7 | 'https://api.upbit.com/v1/ticker?markets=KRW-BTC', 8 | ); 9 | 10 | const tradePrice = response.data[0].trade_price; 11 | const currentTime = new Date().toLocaleTimeString(); 12 | 13 | console.log( 14 | `[${currentTime}] Current Upbit Bitcoin Price: ${tradePrice.toLocaleString('ko-KR', { 15 | style: 'currency', 16 | currency: 'KRW', 17 | })}`, 18 | ); 19 | return tradePrice; 20 | } catch (error) { 21 | if (error instanceof Error) { 22 | console.error('Error fetching Bitcoin price:', error.message); 23 | } else { 24 | console.error('An unknown error occurred'); 25 | } 26 | throw error; 27 | } 28 | } 29 | export async function getBoobitPrice(): Promise { 30 | try { 31 | const response = await axios.get('http://boobit.xyz/api/orders/price'); 32 | const tradePrice = response.data; 33 | const currentTime = new Date().toLocaleTimeString(); 34 | 35 | console.log( 36 | `[${currentTime}] Current Boobit Bitcoin Price: ${tradePrice.toLocaleString('ko-KR', { 37 | style: 'currency', 38 | currency: 'KRW', 39 | })}`, 40 | ); 41 | return tradePrice; 42 | } catch (error) { 43 | if (error instanceof Error) { 44 | console.error('Error fetching Bitcoin price:', error.message); 45 | } else { 46 | console.error('An unknown error occurred'); 47 | } 48 | throw error; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /bot/src/upbit/upbit.response.dto.ts: -------------------------------------------------------------------------------- 1 | export interface UpbitResponse { 2 | market: string; 3 | trade_date: string; 4 | trade_time: string; 5 | trade_date_kst: string; 6 | trade_time_kst: string; 7 | trade_timestamp: number; 8 | opening_price: number; 9 | high_price: number; 10 | low_price: number; 11 | trade_price: number; 12 | prev_closing_price: number; 13 | change: 'RISE' | 'FALL' | 'EVEN'; 14 | change_price: number; 15 | change_rate: number; 16 | signed_change_price: number; 17 | signed_change_rate: number; 18 | trade_volume: number; 19 | acc_trade_price: number; 20 | acc_trade_price_24h: number; 21 | acc_trade_volume: number; 22 | acc_trade_volume_24h: number; 23 | highest_52_week_price: number; 24 | highest_52_week_date: string; 25 | lowest_52_week_price: number; 26 | lowest_52_week_date: string; 27 | timestamp: number; 28 | } 29 | -------------------------------------------------------------------------------- /bot/src/utils/config.setting.argv.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import { hideBin } from 'yargs/helpers'; 3 | 4 | export async function getArgs() { 5 | return await yargs(hideBin(process.argv)) 6 | .option('client', { 7 | alias: 'c', 8 | default: false, 9 | description: 'client로 실행', 10 | }) 11 | .option('cpi', { 12 | type: 'number', 13 | description: 'check price interval (ms)', 14 | }) 15 | .option('ai', { 16 | type: 'number', 17 | description: 'action interval (ms)', 18 | }) 19 | .option('at', { 20 | type: 'string', 21 | description: 'action type', 22 | }) 23 | .option('minp', { 24 | type: 'number', 25 | description: 'min percent', 26 | }) 27 | .option('maxp', { 28 | type: 'number', 29 | description: 'max percent', 30 | }) 31 | .option('mina', { 32 | type: 'number', 33 | description: 'min amount', 34 | }) 35 | .option('maxa', { 36 | type: 'number', 37 | description: 'max amount', 38 | }) 39 | .option('count', { 40 | type: 'number', 41 | description: 'count', 42 | }) 43 | .parse(); 44 | } 45 | -------------------------------------------------------------------------------- /bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src" 11 | }, 12 | "include": ["src/**/*"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /docker_init/mongo_init.js: -------------------------------------------------------------------------------- 1 | db = db.getSiblingDB("boobit"); 2 | try { 3 | db.createUser({ 4 | user: process.env.MONGO_INITDB_ROOT_USERNAME, 5 | pwd: process.env.MONGO_INITDB_ROOT_PASSWORD, 6 | roles: [ 7 | { role: "dbAdmin", db: "boobit" }, 8 | { role: "readWrite", db: "boobit" }, 9 | ], 10 | }); 11 | 12 | quit(0); 13 | } catch (error) { 14 | quit(1); 15 | } 16 | -------------------------------------------------------------------------------- /docker_init/mysql_init.sql: -------------------------------------------------------------------------------- 1 | GRANT ALL PRIVILEGES ON *.* TO 'boobit'@'%' WITH GRANT OPTION; 2 | 3 | FLUSH PRIVILEGES; -------------------------------------------------------------------------------- /kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # DB Kubernetes Deployment 2 | 3 | 이 프로젝트는 MySQL 데이터베이스를 쿠버네티스에 배포하기 위한 매니페스트 파일들을 포함하고 있습니다. 4 | 5 | ## 사전 준비사항 6 | 7 | 1. 쿠버네티스 클러스터 접근 권한 8 | 2. kubectl 설치 9 | 3. 데이터베이스 시크릿 10 | 11 | ## 환경 변수 설정 12 | 13 | `common/base/secret.yaml` 파일을 작성해주세요. 14 | 15 | ## 배포 방법 16 | 17 | ### 0. common 배포 (configmap, secret) 18 | 19 | ```bash 20 | # 프로젝트 디렉토리로 이동 21 | cd common 22 | 23 | # 모든 리소스 한번에 배포 24 | kubectl apply -k base/ 25 | ``` 26 | 27 | ### 1. balance-db 배포 28 | 29 | ```bash 30 | # 프로젝트 디렉토리로 이동 31 | cd balance-db 32 | 33 | # 모든 리소스 한번에 배포 34 | kubectl apply -k base/ 35 | ``` 36 | 37 | ### 2. 배포 확인 38 | 39 | ```bash 40 | # Pod 상태 확인 41 | kubectl get pods -l app=balance-db 42 | 43 | # Secret 확인 44 | kubectl get secrets -l app=balance-db 45 | 46 | # ConfigMap 확인 47 | kubectl get configmaps -l app=balance-db 48 | 49 | # PVC 상태 확인 50 | kubectl get pvc -l app=balance-db 51 | 52 | # Service 확인 53 | kubectl get svc -l app=balance-db 54 | ``` 55 | 56 | ### 3. 배포 삭제 57 | 58 | ```bash 59 | # 모든 리소스 삭제 60 | kubectl delete -k base/ 61 | ``` 62 | 63 | ## 주의사항 64 | 65 | 1. PersistentVolume은 클러스터에 맞게 설정되어야 합니다. 66 | 2. MySQL 초기화 스크립트는 ConfigMap의 init.sql에 추가해야 합니다. 67 | 68 | ## 문제 해결 69 | 70 | 데이터베이스 Pod의 로그 확인: 71 | 72 | ```bash 73 | kubectl logs -l app=balance-db 74 | ``` 75 | 76 | 데이터베이스 Pod의 이벤트 확인: 77 | 78 | ```bash 79 | kubectl describe pod -l app=balance-db 80 | ``` 81 | -------------------------------------------------------------------------------- /kubernetes/bot/auto.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: trading-bot 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: trading-bot 9 | replicas: 1 10 | template: 11 | metadata: 12 | labels: 13 | app: trading-bot 14 | spec: 15 | containers: 16 | - name: trading-bot 17 | image: boobit-ncr.kr.ncr.ntruss.com/trading-bot:latest 18 | env: 19 | - name: OPTIONS 20 | value: "--count=-1 --at=auto" 21 | -------------------------------------------------------------------------------- /kubernetes/bot/normal.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: trading-bot-normal 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: trading-bot 9 | replicas: 3 10 | template: 11 | metadata: 12 | labels: 13 | app: trading-bot 14 | spec: 15 | containers: 16 | - name: trading-bot 17 | image: boobit-ncr.kr.ncr.ntruss.com/trading-bot:latest 18 | env: 19 | - name: OPTIONS 20 | value: "--count=-1" 21 | -------------------------------------------------------------------------------- /kubernetes/bot/test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: trading-bot-2 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: trading-bot 10 | image: boobit-ncr.kr.ncr.ntruss.com/trading-bot:latest 11 | env: 12 | - name: OPTIONS 13 | value: "--count=1000 --ai=10" 14 | restartPolicy: Never 15 | backoffLimit: 0 # 실패 시 재시도 횟수를 0으로 설정 16 | -------------------------------------------------------------------------------- /kubernetes/common/configmap.yaml: -------------------------------------------------------------------------------- 1 | # configmap.yaml 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: mysql-init-script 6 | data: 7 | init.sql: | 8 | GRANT ALL PRIVILEGES ON *.* TO 'boobit'@'%' WITH GRANT OPTION; 9 | FLUSH PRIVILEGES; 10 | -------------------------------------------------------------------------------- /kubernetes/common/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # base/kustomization.yaml 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | # 리소스 적용 순서 정의 6 | resources: 7 | - configmap.yaml # 1. ConfigMap 생성 8 | - secret.yaml # 2. Secret 생성 9 | 10 | -------------------------------------------------------------------------------- /kubernetes/common/secret.exam.yaml: -------------------------------------------------------------------------------- 1 | # secret.yaml 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: db-credentials 6 | type: Opaque 7 | stringData: 8 | DB_ROOT_PASSWORD: 9 | DB_USER: 10 | DB_PASSWORD: 11 | DB_SCHEMA: 12 | -------------------------------------------------------------------------------- /kubernetes/database/auth/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # base/kustomization.yaml 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | # 리소스 적용 순서 정의 6 | resources: 7 | - storage.yaml # 1. PV/PVC 생성 8 | - service.yaml # 2. Service 생성 9 | - statefulset.yaml # 3. StatefulSet 생성 10 | 11 | # 공통 메타데이터 12 | commonLabels: 13 | app: auth-db 14 | environment: production 15 | -------------------------------------------------------------------------------- /kubernetes/database/auth/service.yaml: -------------------------------------------------------------------------------- 1 | # service.yaml 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: auth-db # 서비스 이름 6 | spec: 7 | type: ClusterIP # 서비스 타입 (ClusterIP, NodePort, LoadBalancer, ExternalName) 8 | ports: 9 | - port: 3306 # 서비스가 노출하는 포트 10 | targetPort: 3306 # Pod의 포트 11 | name: auth-db # 포트 이름 12 | selector: 13 | app: auth-db # 어떤 Pod를 연결할지 선택 14 | -------------------------------------------------------------------------------- /kubernetes/database/auth/storage.yaml: -------------------------------------------------------------------------------- 1 | # storage.yaml 2 | apiVersion: v1 3 | kind: PersistentVolume 4 | metadata: 5 | name: auth-db 6 | spec: 7 | capacity: 8 | storage: 20Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: '/mnt/data/auth-db' 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: auth-db 18 | spec: 19 | accessModes: 20 | - ReadWriteOnce 21 | resources: 22 | requests: 23 | storage: 20Gi 24 | -------------------------------------------------------------------------------- /kubernetes/database/balance/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # base/kustomization.yaml 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | # 리소스 적용 순서 정의 6 | resources: 7 | - storage.yaml # 1. PV/PVC 생성 8 | - service.yaml # 2. Service 생성 9 | - statefulset.yaml # 3. StatefulSet 생성 10 | 11 | # 공통 메타데이터 12 | commonLabels: 13 | app: balance-db 14 | environment: production 15 | -------------------------------------------------------------------------------- /kubernetes/database/balance/service.yaml: -------------------------------------------------------------------------------- 1 | # service.yaml 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: balance-db # 서비스 이름 6 | spec: 7 | type: ClusterIP # 서비스 타입 8 | ports: 9 | - port: 3306 # 서비스가 노출하는 포트 10 | targetPort: 3306 # Pod의 포트 11 | name: balance-db # 포트 이름 12 | selector: 13 | app: balance-db # 어떤 Pod를 연결할지 선택 14 | -------------------------------------------------------------------------------- /kubernetes/database/balance/storage.yaml: -------------------------------------------------------------------------------- 1 | # storage.yaml 2 | apiVersion: v1 3 | kind: PersistentVolume 4 | metadata: 5 | name: balance-db 6 | spec: 7 | capacity: 8 | storage: 20Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: '/mnt/data/balance-db' 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: balance-db 18 | spec: 19 | accessModes: 20 | - ReadWriteOnce 21 | resources: 22 | requests: 23 | storage: 20Gi 24 | -------------------------------------------------------------------------------- /kubernetes/database/transaction/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - storage.yaml 6 | - config.yaml 7 | - service.yaml 8 | - statefulset.yaml 9 | 10 | commonLabels: 11 | app: transaction-db 12 | environment: production 13 | -------------------------------------------------------------------------------- /kubernetes/database/transaction/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: transaction-db 5 | labels: 6 | app: transaction-db 7 | spec: 8 | clusterIP: None 9 | selector: 10 | app: transaction-db 11 | ports: 12 | - port: 27017 13 | targetPort: 27017 14 | -------------------------------------------------------------------------------- /kubernetes/database/transaction/storage.yaml: -------------------------------------------------------------------------------- 1 | # storage.yaml 2 | apiVersion: v1 3 | kind: PersistentVolume 4 | metadata: 5 | name: transaction-db 6 | spec: 7 | capacity: 8 | storage: 20Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: '/mnt/data/transaction-db' 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: transaction-db 18 | spec: 19 | accessModes: 20 | - ReadWriteOnce 21 | resources: 22 | requests: 23 | storage: 20Gi 24 | -------------------------------------------------------------------------------- /kubernetes/ingress/alb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: alb 5 | annotations: 6 | alb.ingress.kubernetes.io/ncloud-load-balancer-retain-public-ip-on-termination: "true" # 공인 IP 유지 7 | alb.ingress.kubernetes.io/public-ip-instance-no: "100522047" 8 | alb.ingress.kubernetes.io/healthcheck-path: /health 9 | spec: 10 | ingressClassName: alb 11 | rules: 12 | - http: 13 | paths: 14 | - path: /ws 15 | pathType: Prefix 16 | backend: 17 | service: 18 | name: transaction-service 19 | port: 20 | number: 80 21 | - path: /api/auth 22 | pathType: Prefix 23 | backend: 24 | service: 25 | name: auth-service 26 | port: 27 | number: 80 28 | - path: /api/users 29 | pathType: Prefix 30 | backend: 31 | service: 32 | name: balance-service 33 | port: 34 | number: 80 35 | - path: /api/orders 36 | pathType: Prefix 37 | backend: 38 | service: 39 | name: transaction-service 40 | port: 41 | number: 80 42 | - path: / 43 | pathType: Prefix 44 | backend: 45 | service: 46 | name: static-server 47 | port: 48 | number: 80 49 | -------------------------------------------------------------------------------- /kubernetes/ingress/devonly/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - nginx-configmap.yaml # nginx 6 | - nginx-deployment.yaml # nginx 7 | - loadbalancer.yaml # loadbalancer 8 | 9 | -------------------------------------------------------------------------------- /kubernetes/ingress/devonly/loadbalancer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: boobit-lb 5 | annotations: 6 | service.beta.kubernetes.io/ncloud-load-balancer-retain-public-ip-on-termination: 'true' 7 | spec: 8 | loadBalancerIP: 223.130.146.186 9 | type: LoadBalancer 10 | selector: 11 | app: boobit-nginx 12 | ports: 13 | - name: auth-db 14 | port: 30000 15 | targetPort: 30000 16 | protocol: TCP 17 | - name: balance-db 18 | port: 31000 19 | targetPort: 31000 20 | protocol: TCP 21 | - name: trasaction-db 22 | port: 32000 23 | targetPort: 32000 24 | protocol: TCP 25 | - name: session-db 26 | port: 32123 27 | targetPort: 32123 28 | protocol: TCP 29 | -------------------------------------------------------------------------------- /kubernetes/ingress/devonly/nginx-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: nginx-config 5 | data: 6 | nginx.conf: | 7 | user nginx; 8 | worker_processes 1; 9 | 10 | events { 11 | worker_connections 1024; 12 | } 13 | 14 | stream { 15 | upstream auth_db { 16 | server auth-db:3306; 17 | } 18 | 19 | upstream balance_db { 20 | server balance-db:3306; 21 | } 22 | 23 | upstream transaction_db { 24 | server transaction-db:27017; 25 | } 26 | 27 | upstream session_db { 28 | server session-db:6379; 29 | } 30 | 31 | server { 32 | listen 30000; 33 | proxy_pass auth_db; 34 | } 35 | 36 | server { 37 | listen 31000; 38 | proxy_pass balance_db; 39 | } 40 | 41 | server { 42 | listen 32000; 43 | proxy_pass transaction_db; 44 | } 45 | 46 | server { 47 | listen 32123; 48 | proxy_pass session_db; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /kubernetes/ingress/devonly/nginx-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: boobit-nginx 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: boobit-nginx 10 | template: 11 | metadata: 12 | labels: 13 | app: boobit-nginx 14 | spec: 15 | containers: 16 | - name: boobit-nginx 17 | image: nginx:latest 18 | ports: 19 | - containerPort: 30000 20 | name: auth-db 21 | - containerPort: 31000 22 | name: balance-db 23 | - containerPort: 32000 24 | name: transaction-db 25 | - containerPort: 32123 26 | name: session-db 27 | volumeMounts: 28 | - name: nginx-config 29 | mountPath: /etc/nginx/nginx.conf 30 | subPath: nginx.conf 31 | volumes: 32 | - name: nginx-config 33 | configMap: 34 | name: nginx-config 35 | -------------------------------------------------------------------------------- /kubernetes/redis/session.yaml: -------------------------------------------------------------------------------- 1 | # deployment.yaml 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: session-db 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: session-db 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: session-db 15 | spec: 16 | containers: 17 | - name: session-db 18 | image: redis:7.2-alpine 19 | ports: 20 | - containerPort: 6379 21 | resources: 22 | requests: 23 | cpu: 250m 24 | memory: 256Mi 25 | limits: 26 | cpu: 250m 27 | memory: 256Mi 28 | --- 29 | # service.yaml 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | name: session-db 34 | spec: 35 | type: ClusterIP 36 | selector: 37 | app: session-db 38 | ports: 39 | - port: 6379 40 | targetPort: 6379 41 | -------------------------------------------------------------------------------- /kubernetes/redis/trade.yaml: -------------------------------------------------------------------------------- 1 | # deployment.yaml 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: trade-redis 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: trade-redis 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: trade-redis 15 | spec: 16 | containers: 17 | - name: trade-redis 18 | image: redis:7.2-alpine 19 | ports: 20 | - containerPort: 6379 21 | resources: 22 | requests: 23 | cpu: 500m 24 | memory: 1Gi 25 | limits: 26 | cpu: 500m 27 | memory: 1Gi 28 | --- 29 | # service.yaml 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | name: trade-redis 34 | spec: 35 | type: ClusterIP 36 | selector: 37 | app: trade-redis 38 | ports: 39 | - port: 6379 40 | targetPort: 6379 41 | -------------------------------------------------------------------------------- /kubernetes/service/auth/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: auth-service-config 5 | data: 6 | DB_HOST: 'auth-db' 7 | REDIS_URL: 'redis://session-db:6379' 8 | #GRPC관련설정 추가필요 9 | -------------------------------------------------------------------------------- /kubernetes/service/auth/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: auth-service 5 | labels: 6 | app: auth-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: auth-service 12 | template: 13 | metadata: 14 | labels: 15 | app: auth-service 16 | spec: 17 | containers: 18 | - name: auth-service 19 | image: boobit-ncr.kr.ncr.ntruss.com/auth-service:latest #todo 20 | ports: 21 | - containerPort: 3000 22 | resources: 23 | requests: 24 | cpu: "500m" 25 | memory: "1Gi" 26 | limits: 27 | cpu: "500m" 28 | memory: "1Gi" 29 | env: 30 | - name: DATABASE_URL 31 | value: "mysql://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):3306/$(DB_SCHEMA)" 32 | - name: BALANCE_GRPC_URL 33 | value: "balance-service:50051" 34 | envFrom: 35 | - configMapRef: 36 | name: auth-service-config 37 | - secretRef: 38 | name: db-credentials 39 | -------------------------------------------------------------------------------- /kubernetes/service/auth/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # base/kustomization.yaml 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | 5 | resources: 6 | - config.yaml 7 | - deployment.yaml 8 | - service.yaml 9 | 10 | commonLabels: 11 | app: auth-service 12 | environment: production 13 | -------------------------------------------------------------------------------- /kubernetes/service/auth/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: auth-service 5 | spec: 6 | selector: 7 | app: auth-service 8 | ports: 9 | - protocol: TCP 10 | port: 80 11 | targetPort: 3000 12 | nodePort: 30080 13 | type: NodePort 14 | -------------------------------------------------------------------------------- /kubernetes/service/balance/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: balance-service-config 5 | data: 6 | DB_HOST: 'balance-db' 7 | REDIS_URL: 'redis://session-db:6379' 8 | #GRPC관련설정 추가필요 9 | -------------------------------------------------------------------------------- /kubernetes/service/balance/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: balance-service 5 | labels: 6 | app: balance-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: balance-service 12 | template: 13 | metadata: 14 | labels: 15 | app: balance-service 16 | spec: 17 | containers: 18 | - name: balance-service 19 | image: boobit-ncr.kr.ncr.ntruss.com/balance-service:latest #todo 20 | ports: 21 | - containerPort: 3000 22 | resources: 23 | requests: 24 | cpu: "500m" 25 | memory: "1Gi" 26 | limits: 27 | cpu: "500m" 28 | memory: "1Gi" 29 | env: 30 | - name: DATABASE_URL 31 | value: "mysql://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):3306/$(DB_SCHEMA)" 32 | envFrom: 33 | - configMapRef: 34 | name: balance-service-config 35 | - secretRef: 36 | name: db-credentials 37 | -------------------------------------------------------------------------------- /kubernetes/service/balance/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - config.yaml 6 | - deployment.yaml 7 | - service.yaml 8 | 9 | commonLabels: 10 | app: balance-service 11 | environment: production 12 | -------------------------------------------------------------------------------- /kubernetes/service/balance/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: balance-service 5 | spec: 6 | selector: 7 | app: balance-service 8 | ports: 9 | - protocol: TCP 10 | port: 80 11 | targetPort: 3000 12 | nodePort: 31080 13 | name: http 14 | - protocol: TCP 15 | port: 50051 16 | targetPort: 50051 17 | name: grpc 18 | type: NodePort 19 | -------------------------------------------------------------------------------- /kubernetes/service/interval/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: interval-service-config 5 | data: 6 | DB_HOST: "transaction-db" 7 | TRADE_REDIS_URL: "redis://trade-redis:6379" 8 | -------------------------------------------------------------------------------- /kubernetes/service/interval/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: interval-service 5 | labels: 6 | app: interval-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: interval-service 12 | template: 13 | metadata: 14 | labels: 15 | app: interval-service 16 | spec: 17 | containers: 18 | - name: interval-service 19 | image: boobit-ncr.kr.ncr.ntruss.com/interval-service:latest #todo 20 | ports: 21 | - containerPort: 3000 22 | resources: 23 | requests: 24 | cpu: "500m" 25 | memory: "1Gi" 26 | limits: 27 | cpu: "500m" 28 | memory: "1Gi" 29 | env: 30 | - name: DATABASE_URL 31 | value: "mongodb://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):27017/$(DB_SCHEMA)?replicaSet=rs0" 32 | envFrom: 33 | - configMapRef: 34 | name: interval-service-config 35 | - secretRef: 36 | name: db-credentials 37 | -------------------------------------------------------------------------------- /kubernetes/service/interval/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - config.yaml 6 | - deployment.yaml 7 | - service.yaml 8 | 9 | commonLabels: 10 | app: interval-service 11 | environment: production 12 | -------------------------------------------------------------------------------- /kubernetes/service/interval/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: interval-service 5 | spec: 6 | selector: 7 | app: interval-service 8 | ports: 9 | - protocol: TCP 10 | port: 80 11 | targetPort: 3000 12 | name: http 13 | type: ClusterIP 14 | -------------------------------------------------------------------------------- /kubernetes/service/static/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: static-server 5 | labels: 6 | app: static-server 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: static-server 12 | template: 13 | metadata: 14 | labels: 15 | app: static-server 16 | spec: 17 | containers: 18 | - name: static-server 19 | image: boobit-ncr.kr.ncr.ntruss.com/static-server:latest #todo 20 | ports: 21 | - containerPort: 8080 22 | resources: 23 | requests: 24 | cpu: "125m" 25 | memory: "256Mi" 26 | limits: 27 | cpu: "125m" 28 | memory: "256Mi" 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | name: static-server 34 | spec: 35 | selector: 36 | app: static-server 37 | ports: 38 | - protocol: TCP 39 | port: 80 40 | targetPort: 8080 41 | nodePort: 32180 42 | type: NodePort 43 | -------------------------------------------------------------------------------- /kubernetes/service/trade/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: trade-service-config 5 | data: 6 | DB_HOST: "transaction-db" 7 | REDIS_URL: "redis://session-db:6379" 8 | BALANCE_GRPC_URL: "balance-service:50051" 9 | TRADE_REDIS_URL: "redis://trade-redis:6379" 10 | -------------------------------------------------------------------------------- /kubernetes/service/trade/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: trade-service 5 | labels: 6 | app: trade-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: trade-service 12 | template: 13 | metadata: 14 | labels: 15 | app: trade-service 16 | spec: 17 | containers: 18 | - name: trade-service 19 | image: boobit-ncr.kr.ncr.ntruss.com/trade-service:latest #todo 20 | ports: 21 | - containerPort: 3000 22 | resources: 23 | requests: 24 | cpu: "500m" 25 | memory: "1Gi" 26 | limits: 27 | cpu: "500m" 28 | memory: "1Gi" 29 | env: 30 | - name: DATABASE_URL 31 | value: "mongodb://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):27017/$(DB_SCHEMA)?replicaSet=rs0" 32 | envFrom: 33 | - configMapRef: 34 | name: trade-service-config 35 | - secretRef: 36 | name: db-credentials 37 | -------------------------------------------------------------------------------- /kubernetes/service/trade/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - config.yaml 6 | - deployment.yaml 7 | - service.yaml 8 | 9 | commonLabels: 10 | app: trade-service 11 | environment: production 12 | -------------------------------------------------------------------------------- /kubernetes/service/trade/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: trade-service 5 | spec: 6 | selector: 7 | app: trade-service 8 | ports: 9 | - protocol: TCP 10 | port: 80 11 | targetPort: 3000 12 | name: http 13 | type: ClusterIP 14 | -------------------------------------------------------------------------------- /kubernetes/service/transaction/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: transaction-service-config 5 | data: 6 | DB_HOST: "transaction-db" 7 | REDIS_URL: "redis://session-db:6379" 8 | BALANCE_GRPC_URL: "balance-service:50051" 9 | TRADE_REDIS_URL: "redis://trade-redis:6379" 10 | -------------------------------------------------------------------------------- /kubernetes/service/transaction/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: transaction-service 5 | labels: 6 | app: transaction-service 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: transaction-service 12 | template: 13 | metadata: 14 | labels: 15 | app: transaction-service 16 | spec: 17 | containers: 18 | - name: transaction-service 19 | image: boobit-ncr.kr.ncr.ntruss.com/transaction-service:latest #todo 20 | ports: 21 | - containerPort: 3000 22 | resources: 23 | requests: 24 | cpu: "500m" 25 | memory: "1Gi" 26 | limits: 27 | cpu: "500m" 28 | memory: "1Gi" 29 | env: 30 | - name: DATABASE_URL 31 | value: "mongodb://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):27017/$(DB_SCHEMA)?replicaSet=rs0" 32 | envFrom: 33 | - configMapRef: 34 | name: transaction-service-config 35 | - secretRef: 36 | name: db-credentials 37 | -------------------------------------------------------------------------------- /kubernetes/service/transaction/hpa.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: autoscaling/v2 2 | kind: HorizontalPodAutoscaler 3 | metadata: 4 | name: transaction-service 5 | spec: 6 | scaleTargetRef: # 어떤 리소스를 스케일링할지 지정 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: transaction-service # Deployment 이름과 일치해야 함 10 | 11 | minReplicas: 1 12 | maxReplicas: 5 13 | metrics: 14 | - type: Resource 15 | resource: 16 | name: cpu 17 | target: 18 | type: Utilization 19 | averageUtilization: 80 20 | behavior: 21 | scaleUp: 22 | stabilizationWindowSeconds: 60 # 스케일 업 결정 전 대기 시간 23 | policies: 24 | - type: Percent 25 | value: 50 # 한 번에 50%까지만 증가 26 | periodSeconds: 60 27 | scaleDown: 28 | stabilizationWindowSeconds: 300 # 스케일 다운 결정 전 대기 시간 29 | policies: 30 | - type: Percent 31 | value: 25 # 한 번에 25%까지만 감소 32 | periodSeconds: 60 33 | -------------------------------------------------------------------------------- /kubernetes/service/transaction/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # base/kustomization.yaml 2 | apiVersion: kustomize.config.k8s.io/v1beta1 3 | kind: Kustomization 4 | resources: 5 | - config.yaml 6 | - deployment.yaml 7 | - service.yaml 8 | - hpa.yaml 9 | 10 | commonLabels: 11 | app: transaction-service 12 | environment: production 13 | -------------------------------------------------------------------------------- /kubernetes/service/transaction/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: transaction-service 5 | spec: 6 | type: NodePort 7 | selector: 8 | app: transaction-service 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | targetPort: 3000 13 | nodePort: 32080 14 | -------------------------------------------------------------------------------- /microservice/.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules 3 | dist 4 | data 5 | *.env* 6 | coverage -------------------------------------------------------------------------------- /microservice/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /microservice/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | 58 | 59 | #testDB 60 | /data 61 | /db_data_auth 62 | /db_data_balance 63 | /db_data_transaction 64 | /db_data_redis 65 | 66 | #kubernetes secret 67 | /kubernetes/common/base/secret.yaml -------------------------------------------------------------------------------- /microservice/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "endOfLine": "lf", 10 | "useTabs": false, 11 | "overrides": [ 12 | { 13 | "files": "*.{ts,tsx}", 14 | "options": { 15 | "parser": "typescript" 16 | } 17 | }, 18 | { 19 | "files": "*.decorator.ts", 20 | "options": { 21 | "printWidth": 120 22 | } 23 | }, 24 | { 25 | "files": "*.dto.ts", 26 | "options": { 27 | "printWidth": 80 28 | } 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /microservice/apps/auth/DockerFile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy only necessary files for this service 7 | COPY microservice/nest-cli.json ./ 8 | COPY microservice/package*.json ./ 9 | COPY microservice/tsconfig*.json ./ 10 | COPY microservice/apps/auth ./apps/auth 11 | COPY microservice/libs ./libs 12 | 13 | # Install and build 14 | RUN npm ci 15 | RUN npm run generate:auth 16 | RUN npm run build auth 17 | 18 | # Production stage 19 | FROM node:20-alpine 20 | 21 | 22 | WORKDIR /app 23 | 24 | # Copy package files and install dependencies 25 | COPY microservice/package*.json ./ 26 | RUN npm ci --only=production 27 | 28 | # Copy built assets and Prisma generated files 29 | COPY --from=builder /app/dist ./dist 30 | COPY --from=builder /app/apps/auth/prisma ./prisma 31 | COPY --from=builder /app/node_modules ./node_modules 32 | COPY --from=builder /app/libs/grpc/proto ./libs/grpc/proto 33 | 34 | # Expose port 35 | EXPOSE 3000 36 | 37 | # Run application 38 | CMD ["npm", "run", "start:prod:auth"] 39 | #CMD ["node", "dist/apps/auth/main"] 40 | 41 | -------------------------------------------------------------------------------- /microservice/apps/auth/prisma/migrations/20241121051919_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `users` ( 3 | `user_id` BIGINT NOT NULL AUTO_INCREMENT, 4 | `email` VARCHAR(100) NOT NULL, 5 | `password_hash` VARCHAR(200) NOT NULL, 6 | `name` VARCHAR(50) NOT NULL, 7 | `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | UNIQUE INDEX `users_email_key`(`email`), 10 | PRIMARY KEY (`user_id`) 11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 12 | -------------------------------------------------------------------------------- /microservice/apps/auth/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /microservice/apps/auth/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model User { 11 | userId BigInt @id @default(autoincrement()) @map("user_id") 12 | email String @unique @db.VarChar(100) 13 | passwordHash String @db.VarChar(200) @map("password_hash") 14 | name String @db.VarChar(50) 15 | created_at DateTime @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp() 16 | 17 | @@map("users") 18 | } -------------------------------------------------------------------------------- /microservice/apps/auth/src/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Body, 5 | UseGuards, 6 | Request, 7 | Get, 8 | HttpCode, 9 | HttpStatus, 10 | Res, 11 | } from '@nestjs/common'; 12 | import { Response } from 'express'; 13 | import { AuthService } from './auth.service'; 14 | import { LocalAuthGuard } from './passport/local.auth.guard'; 15 | import { AuthenticatedGuard } from '@app/session/guard/authenticated.guard'; 16 | import { SignUpDto } from './dto/signup.dto'; 17 | 18 | @Controller('api/auth') 19 | export class AuthController { 20 | constructor(private authService: AuthService) {} 21 | 22 | @Post('signup') 23 | async signup(@Body() signUpDto: SignUpDto) { 24 | return this.authService.signup(signUpDto); 25 | } 26 | 27 | @UseGuards(LocalAuthGuard) 28 | @Post('login') 29 | @HttpCode(HttpStatus.OK) 30 | async login(@Request() req) { 31 | return { 32 | userId: req.user.userId, 33 | email: req.user.email, 34 | name: req.user.name, 35 | }; 36 | } 37 | 38 | @UseGuards(AuthenticatedGuard) 39 | @Get('profile') 40 | getProfile(@Request() req) { 41 | return req.user; 42 | } 43 | 44 | @Post('logout') 45 | @HttpCode(HttpStatus.OK) 46 | logout(@Request() req, @Res({ passthrough: true }) response: Response) { 47 | req.session.destroy(); 48 | response.clearCookie('sid'); 49 | return { message: 'Logged out successfully' }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /microservice/apps/auth/src/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthController } from './auth.controller'; 3 | import { AuthService } from './auth.service'; 4 | import { LocalStrategy } from './passport/local.strategy'; 5 | import { PrismaModule } from '@app/prisma'; 6 | import { LocalAuthGuard } from './passport/local.auth.guard'; 7 | import { SessionModule } from '@app/session'; 8 | import { ClientsModule, Transport } from '@nestjs/microservices'; 9 | import { CommonModule } from '@app/common'; 10 | import { ConfigModule, ConfigService } from '@nestjs/config'; 11 | 12 | @Module({ 13 | imports: [ 14 | PrismaModule, 15 | SessionModule, 16 | CommonModule, 17 | ClientsModule.registerAsync([ 18 | { 19 | name: 'ACCOUNT_PACKAGE', 20 | imports: [ConfigModule], 21 | useFactory: (configService: ConfigService) => ({ 22 | transport: Transport.GRPC, 23 | options: { 24 | package: 'account', 25 | protoPath: 'libs/grpc/proto/account.proto', 26 | url: `${configService.get('BALANCE_GRPC_URL')}`, 27 | }, 28 | }), 29 | inject: [ConfigService], 30 | }, 31 | ]), 32 | ], 33 | controllers: [AuthController], 34 | providers: [AuthService, LocalStrategy, LocalAuthGuard], 35 | }) 36 | export class AuthModule {} 37 | -------------------------------------------------------------------------------- /microservice/apps/auth/src/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export class LoginDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/auth/src/dto/signup.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, Matches, Length } from 'class-validator'; 2 | 3 | export class SignUpDto { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | @Length(8, 32, { message: '비밀번호는 8-32자 사이여야 합니다.' }) 9 | @Matches(/^(?=.*[A-Za-z])(?=.*\d)/, { 10 | message: '비밀번호는 숫자와 문자를 모두 포함해야 합니다.', 11 | }) 12 | password: string; 13 | 14 | @IsString() 15 | name: string; 16 | } 17 | -------------------------------------------------------------------------------- /microservice/apps/auth/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { ValidationPipe } from '@nestjs/common'; 3 | import { AuthModule } from './auth.module'; 4 | import cors from '@app/common/cors'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AuthModule); 8 | 9 | app.useGlobalPipes(new ValidationPipe()); 10 | 11 | (BigInt.prototype as any).toJSON = function () { 12 | return this.toString(); 13 | }; 14 | 15 | app.enableCors(cors); 16 | await app.listen(3000, '0.0.0.0'); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /microservice/apps/auth/src/passport/local.auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') { 6 | async canActivate(context: ExecutionContext): Promise { 7 | const result = (await super.canActivate(context)) as boolean; 8 | const request = context.switchToHttp().getRequest(); 9 | await super.logIn(request); 10 | return result; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /microservice/apps/auth/src/passport/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 4 | import { AuthService } from '../auth.service'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private authService: AuthService) { 9 | super({ usernameField: 'email' }); 10 | } 11 | 12 | async validate(email: string, password: string): Promise { 13 | const user = await this.authService.validateUser(email, password); 14 | if (!user) { 15 | throw new UnauthorizedException(); 16 | } 17 | return user; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /microservice/apps/auth/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/auth" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/balance/DockerFile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy only necessary files for this service 7 | COPY microservice/nest-cli.json ./ 8 | COPY microservice/package*.json ./ 9 | COPY microservice/tsconfig*.json ./ 10 | COPY microservice/apps/balance ./apps/balance 11 | COPY microservice/libs ./libs 12 | 13 | # Install and build 14 | RUN npm ci 15 | RUN npm run generate:balance 16 | RUN npm run build balance 17 | 18 | # Production stage 19 | FROM node:20-alpine 20 | 21 | WORKDIR /app 22 | 23 | # Copy package files and install dependencies 24 | COPY microservice/package*.json ./ 25 | RUN npm ci --only=production 26 | 27 | # Copy built assets and Prisma generated files 28 | COPY --from=builder /app/dist ./dist 29 | COPY --from=builder /app/apps/balance/prisma ./prisma 30 | COPY --from=builder /app/node_modules ./node_modules 31 | COPY --from=builder /app/libs/grpc/proto ./libs/grpc/proto 32 | 33 | # Expose port 34 | EXPOSE 3000 35 | 36 | # Run application 37 | CMD ["npm", "run", "start:prod:balance"] 38 | #CMD ["node", "dist/apps/balance/main"] 39 | 40 | -------------------------------------------------------------------------------- /microservice/apps/balance/prisma/migrations/20241121052601_/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `asset` ( 3 | `asset_id` BIGINT NOT NULL AUTO_INCREMENT, 4 | `user_id` BIGINT NOT NULL, 5 | `currency_code` VARCHAR(20) NOT NULL, 6 | `available_balance` DECIMAL(24, 8) NOT NULL, 7 | `locked_balance` DECIMAL(24, 8) NOT NULL, 8 | `updated_at` TIMESTAMP(3) NOT NULL, 9 | 10 | UNIQUE INDEX `asset_user_id_currency_code_key`(`user_id`, `currency_code`), 11 | PRIMARY KEY (`asset_id`) 12 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 13 | 14 | -- CreateTable 15 | CREATE TABLE `deposit_withdrawal` ( 16 | `tx_id` BIGINT NOT NULL AUTO_INCREMENT, 17 | `user_id` BIGINT NOT NULL, 18 | `currency_code` VARCHAR(20) NOT NULL, 19 | `tx_type` VARCHAR(20) NOT NULL, 20 | `amount` DECIMAL(24, 8) NOT NULL, 21 | `created_at` TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 22 | 23 | INDEX `deposit_withdrawal_user_id_idx`(`user_id`), 24 | PRIMARY KEY (`tx_id`) 25 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 26 | 27 | -- CreateTable 28 | CREATE TABLE `order_history` ( 29 | `history_id` BIGINT NOT NULL AUTO_INCREMENT, 30 | `order_type` VARCHAR(10) NOT NULL, 31 | `user_id` BIGINT NOT NULL, 32 | `coin_code` VARCHAR(20) NOT NULL, 33 | `price` DECIMAL(24, 8) NOT NULL, 34 | `quantity` DECIMAL(24, 8) NOT NULL, 35 | `status` VARCHAR(20) NOT NULL, 36 | `created_at` TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 37 | 38 | INDEX `order_history_user_id_idx`(`user_id`), 39 | PRIMARY KEY (`history_id`) 40 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 41 | -------------------------------------------------------------------------------- /microservice/apps/balance/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /microservice/apps/balance/src/balance.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BalanceController } from './balance.controller'; 3 | import { BalanceService } from './balance.service'; 4 | import { PrismaModule } from '@app/prisma'; 5 | import { BalanceRepository } from './balance.repository'; 6 | import { SessionModule } from '@app/session'; 7 | import { Transport } from '@nestjs/microservices'; 8 | import { CommonModule } from '@app/common'; 9 | 10 | @Module({ 11 | imports: [PrismaModule, SessionModule, CommonModule], 12 | controllers: [BalanceController], 13 | providers: [BalanceService, BalanceRepository], 14 | }) 15 | export class BalanceModule { 16 | static grpcOptions = { 17 | transport: Transport.GRPC, 18 | options: { 19 | package: ['order', 'account', 'trade'], 20 | protoPath: [ 21 | 'libs/grpc/proto/order.proto', 22 | 'libs/grpc/proto/account.proto', 23 | 'libs/grpc/proto/trade.proto', 24 | ], 25 | url: '0.0.0.0:50051', 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/dto/asset.dto.ts: -------------------------------------------------------------------------------- 1 | import { CurrencyCodeName } from '@app/common'; 2 | import { roundToSix } from '@app/common/utils/number.format.util'; 3 | 4 | export class AssetDto { 5 | currencyCode: string; 6 | name: string; 7 | amount: number; 8 | 9 | constructor(currencyCode: string, amount: number) { 10 | this.currencyCode = currencyCode; 11 | this.name = CurrencyCodeName[currencyCode]; 12 | this.amount = roundToSix(amount); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/dto/available.balance.response.dto.ts: -------------------------------------------------------------------------------- 1 | export class AvailableBalanceResponseDto { 2 | availableBalance: number; 3 | currencyCode: string; 4 | 5 | constructor(availableBalance: number, currencyCode: string) { 6 | this.availableBalance = availableBalance; 7 | this.currencyCode = currencyCode; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/dto/create.transaction.dto.ts: -------------------------------------------------------------------------------- 1 | export class CreateTransactionDto { 2 | currencyCode: string; 3 | amount: number; 4 | } 5 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/dto/get.transactions.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class GetTransactionsDto { 2 | currencyCode: string; 3 | id: number | null; 4 | } 5 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/dto/get.transactions.response.dto.ts: -------------------------------------------------------------------------------- 1 | export class TransactionDto { 2 | tx_type: string; 3 | amount: number; 4 | currency_code: string; 5 | timestamp: string; 6 | } 7 | 8 | export class TransactionResponseDto { 9 | nextId: number | null; 10 | transactions: TransactionDto[]; 11 | } 12 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/exception/balance.exception.ts: -------------------------------------------------------------------------------- 1 | import { CustomException } from './custom.exception'; 2 | import { ExceptionType } from './exception.type'; 3 | 4 | export class BalanceException extends CustomException { 5 | constructor(exceptionType: ExceptionType) { 6 | super(exceptionType); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/exception/balance.exceptions.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '@nestjs/common'; 2 | import { ExceptionType } from './exception.type'; 3 | 4 | export const BALANCE_EXCEPTIONS: Record = { 5 | USER_ASSETS_NOT_FOUND: { 6 | name: 'UserAssetsNotFoundException', 7 | status: HttpStatus.NOT_FOUND, 8 | message: '사용자의 자산을 찾을 수 없습니다.', 9 | }, 10 | INVALID_DEPOSIT_AMOUNT: { 11 | name: 'InvalidDepositAmountException', 12 | status: HttpStatus.BAD_REQUEST, 13 | message: '음수 값을 입금할 수 없습니다.', 14 | }, 15 | INVALID_WITHDRAWAL_AMOUNT: { 16 | name: 'InvalidWithdrawalAmountException', 17 | status: HttpStatus.BAD_REQUEST, 18 | message: '음수 값을 출금할 수 없습니다.', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/exception/custom.exception.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | import { ExceptionType } from './exception.type'; 3 | 4 | export class CustomException extends HttpException { 5 | private readonly exceptionName: string; 6 | 7 | constructor(exceptionType: ExceptionType) { 8 | super(exceptionType.message, exceptionType.status); 9 | this.exceptionName = exceptionType.name; 10 | } 11 | 12 | getName() { 13 | return this.exceptionName; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/exception/exception.type.ts: -------------------------------------------------------------------------------- 1 | export type ExceptionType = { 2 | name: string; 3 | status: number; 4 | message: string; 5 | }; 6 | -------------------------------------------------------------------------------- /microservice/apps/balance/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { BalanceModule } from './balance.module'; 3 | import { PrismaService } from '@app/prisma'; 4 | import cors from '@app/common/cors'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(BalanceModule); 8 | const prismaService = app.get(PrismaService); 9 | 10 | await prismaService.enableShutdownHooks(); 11 | app.enableCors(cors); 12 | await app.listen(3000, '0.0.0.0'); 13 | 14 | const grpcApp = await NestFactory.createMicroservice(BalanceModule, BalanceModule.grpcOptions); 15 | await grpcApp.listen(); 16 | } 17 | bootstrap(); 18 | -------------------------------------------------------------------------------- /microservice/apps/balance/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | }, 9 | "moduleNameMapper": { 10 | "^@app/(.*)$": "/../../../libs/$1/src" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /microservice/apps/balance/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/balance" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/interval/DockerFile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy only necessary files for this service 7 | COPY microservice/nest-cli.json ./ 8 | COPY microservice/package*.json ./ 9 | COPY microservice/tsconfig*.json ./ 10 | COPY microservice/apps/interval ./apps/interval 11 | COPY microservice/libs ./libs 12 | 13 | # Install and build 14 | RUN npm ci 15 | RUN npm run generate:interval 16 | RUN npm run build interval 17 | 18 | # Production stage 19 | FROM node:20-alpine 20 | 21 | WORKDIR /app 22 | 23 | # Copy package files and install dependencies 24 | COPY microservice/package*.json ./ 25 | RUN npm ci --only=production 26 | 27 | # Copy built assets and Prisma generated files 28 | COPY --from=builder /app/dist ./dist 29 | COPY --from=builder /app/apps/interval/prisma ./prisma 30 | COPY --from=builder /app/node_modules ./node_modules 31 | COPY --from=builder /app/libs/grpc/proto ./libs/grpc/proto 32 | 33 | # Expose port 34 | EXPOSE 3000 35 | 36 | # Run application 37 | CMD ["npm", "run", "start:prod:interval"] 38 | 39 | -------------------------------------------------------------------------------- /microservice/apps/interval/src/dto/order.book.dto.ts: -------------------------------------------------------------------------------- 1 | export class OrderItemDto { 2 | price: number; 3 | priceChangeRate: number; 4 | amount: number; 5 | } 6 | 7 | export class OrderBookDto { 8 | sell: OrderItemDto[]; 9 | buy: OrderItemDto[]; 10 | } 11 | -------------------------------------------------------------------------------- /microservice/apps/interval/src/interval.module.ts: -------------------------------------------------------------------------------- 1 | import { ScheduleModule } from '@nestjs/schedule'; 2 | import { Module } from '@nestjs/common'; 3 | import { ConfigModule, ConfigService } from '@nestjs/config'; 4 | import { IntervalService } from './interval.service'; 5 | import { IntervalMakeService } from './interval.make.service'; 6 | import { IntervalRepository } from './interval.candle.repository'; 7 | import { PrismaModule } from '@app/prisma'; 8 | import { Redis } from 'ioredis'; 9 | import { IntervalOrderBookService } from './interval.order.book.service'; 10 | import { IntervalOrderBookRepository } from './interval.order.book.repository'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ 15 | isGlobal: true, 16 | }), 17 | PrismaModule, 18 | ScheduleModule.forRoot(), 19 | ], 20 | providers: [ 21 | IntervalService, 22 | IntervalMakeService, 23 | IntervalRepository, 24 | IntervalOrderBookService, 25 | IntervalOrderBookRepository, 26 | { 27 | provide: 'REDIS_PUBLISHER', 28 | useFactory: (configService: ConfigService) => { 29 | const redis = new Redis(configService.get('TRADE_REDIS_URL'), { 30 | maxRetriesPerRequest: null, 31 | }); 32 | 33 | redis.on('error', (err) => { 34 | console.error('Redis connection error:', err); 35 | }); 36 | 37 | return redis; 38 | }, 39 | inject: [ConfigService], 40 | }, 41 | ], 42 | }) 43 | export class IntervalModule {} 44 | -------------------------------------------------------------------------------- /microservice/apps/interval/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { IntervalModule } from './interval.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(IntervalModule); 6 | await app.listen(process.env.port ?? 3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /microservice/apps/interval/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/interval" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/trade/DockerFile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy only necessary files for this service 7 | COPY microservice/nest-cli.json ./ 8 | COPY microservice/package*.json ./ 9 | COPY microservice/tsconfig*.json ./ 10 | COPY microservice/apps/trade ./apps/trade 11 | COPY microservice/libs ./libs 12 | 13 | # Install and build 14 | RUN npm ci 15 | RUN npm run generate:trade 16 | RUN npm run build trade 17 | 18 | # Production stage 19 | FROM node:20-alpine 20 | 21 | WORKDIR /app 22 | 23 | # Copy package files and install dependencies 24 | COPY microservice/package*.json ./ 25 | RUN npm ci --only=production 26 | 27 | # Copy built assets and Prisma generated files 28 | COPY --from=builder /app/dist ./dist 29 | COPY --from=builder /app/apps/trade/prisma ./prisma 30 | COPY --from=builder /app/node_modules ./node_modules 31 | COPY --from=builder /app/libs/grpc/proto ./libs/grpc/proto 32 | 33 | # Expose port 34 | EXPOSE 3000 35 | 36 | # Run application 37 | CMD ["npm", "run", "start:prod:trade"] 38 | -------------------------------------------------------------------------------- /microservice/apps/trade/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mongodb" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model buyOrder { 11 | historyId String @id @map("_id") 12 | userId String @map("user_id") 13 | coinCode String @map("coin_code") 14 | price String 15 | originalQuote String @map("original_quote") 16 | remainingQuote String @map("remaining_quote") 17 | createdAt DateTime @default(now()) @map("created_at") 18 | 19 | @@index(userId) 20 | @@index([price, createdAt]) 21 | @@map("buy_order") 22 | } 23 | 24 | model sellOrder { 25 | historyId String @id @map("_id") 26 | userId String @map("user_id") 27 | coinCode String @map("coin_code") 28 | price String 29 | originalQuote String @map("original_quote") 30 | remainingBase String @map("remaining_base") 31 | createdAt DateTime @default(now()) @map("created_at") 32 | 33 | @@index(userId) 34 | @@index([price, createdAt]) 35 | @@map("sell_order") 36 | } 37 | 38 | model trade { 39 | tradeId String @id @default(cuid()) @map("_id") 40 | buyerId String @map("buyer_id") 41 | buyOrderId String @map("buy_order_id") 42 | sellerId String @map("seller_id") 43 | sellOrderId String @map("sell_order_id") 44 | coinCode String @map("coin_code") 45 | price String 46 | quantity String 47 | tradedAt DateTime @default(now()) @map("traded_at") 48 | 49 | @@index(buyerId) 50 | @@index(sellerId) 51 | @@index([tradedAt(sort: Desc)]) 52 | } 53 | -------------------------------------------------------------------------------- /microservice/apps/trade/src/dto/trade.buy.order.type.ts: -------------------------------------------------------------------------------- 1 | export type BuyOrder = { 2 | historyId: string; 3 | userId: string; 4 | coinCode: string; 5 | price: string; 6 | remainingQuote: string; 7 | }; 8 | -------------------------------------------------------------------------------- /microservice/apps/trade/src/dto/trade.sell.order.type.ts: -------------------------------------------------------------------------------- 1 | export type SellOrder = { 2 | historyId: string; 3 | userId: string; 4 | coinCode: string; 5 | price: string; 6 | remainingBase: string; 7 | }; 8 | -------------------------------------------------------------------------------- /microservice/apps/trade/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { TradeModule } from './trade.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(TradeModule); 6 | await app.listen(process.env.port ?? 3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /microservice/apps/trade/src/trade.balance.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { ClientGrpc } from '@nestjs/microservices'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { TradeGrpcService } from '@app/grpc/trade.interface'; 5 | import { TradeRequestDto } from '@app/grpc/dto/trade.request.dto'; 6 | import { TradeResponseDto } from '@app/grpc/dto/trade.reponse.dto'; 7 | import { TradeCancelRequestDto } from '@app/grpc/dto/trade.cancel.request.dto'; 8 | 9 | @Injectable() 10 | export class TradeBalanceService implements OnModuleInit, TradeGrpcService { 11 | private tradeService; 12 | 13 | constructor(@Inject('TRADE_PACKAGE') private readonly client: ClientGrpc) {} 14 | 15 | onModuleInit() { 16 | this.tradeService = this.client.getService('TradeService'); 17 | } 18 | 19 | settleTransaction(tradeRequest: TradeRequestDto): Promise { 20 | return firstValueFrom(this.tradeService.settleTransaction(tradeRequest)); 21 | } 22 | 23 | cancelOrder(cancelRequest: TradeCancelRequestDto): Promise { 24 | return firstValueFrom(this.tradeService.cancelOrder(cancelRequest)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /microservice/apps/trade/src/trade.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TradeService } from './trade.service'; 3 | import { BullMQModule } from '@app/bull'; 4 | import { TradeProcessor } from './trade.processor'; 5 | import { ClientsModule, Transport } from '@nestjs/microservices'; 6 | import { ConfigModule, ConfigService } from '@nestjs/config'; 7 | import { TradeRepository } from './trade.repository'; 8 | import { TradeBalanceService } from './trade.balance.service'; 9 | import { PrismaModule } from '@app/prisma'; 10 | 11 | @Module({ 12 | imports: [ 13 | PrismaModule, 14 | BullMQModule, 15 | ClientsModule.registerAsync([ 16 | { 17 | name: 'TRADE_PACKAGE', 18 | imports: [ConfigModule], 19 | useFactory: (configService: ConfigService) => ({ 20 | transport: Transport.GRPC, 21 | options: { 22 | package: 'trade', 23 | protoPath: 'libs/grpc/proto/trade.proto', 24 | url: `${configService.get('BALANCE_GRPC_URL')}`, 25 | }, 26 | }), 27 | inject: [ConfigService], 28 | }, 29 | ]), 30 | ], 31 | providers: [TradeService, TradeProcessor, TradeBalanceService, TradeRepository], 32 | }) 33 | export class TradeModule {} 34 | -------------------------------------------------------------------------------- /microservice/apps/trade/src/trade.processor.ts: -------------------------------------------------------------------------------- 1 | import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq'; 2 | import { OrderType } from '@app/common/enums/order-type.enum'; 3 | import { Logger } from '@nestjs/common'; 4 | import { Job } from 'bullmq'; 5 | import { TradeService } from './trade.service'; 6 | 7 | @Processor('trade', { concurrency: 1 }) 8 | export class TradeProcessor extends WorkerHost { 9 | private readonly logger = new Logger(TradeProcessor.name); 10 | 11 | constructor(private tradeService: TradeService) { 12 | super(); 13 | } 14 | 15 | async process(job: Job): Promise { 16 | this.logger.log(`Processing job: ${job.id}`); 17 | 18 | switch (job.name) { 19 | case OrderType.BUY: 20 | await this.tradeService.processTrade(job.name, job.data.trade); 21 | break; 22 | 23 | case OrderType.SELL: 24 | await this.tradeService.processTrade(job.name, job.data.trade); 25 | break; 26 | 27 | case OrderType.CANCELED: 28 | const { userId, historyId, orderType } = job.data; 29 | await this.tradeService.cancelOrder(userId, historyId, orderType); 30 | break; 31 | 32 | default: 33 | this.logger.error(`Unknown job type: ${job.name}`); 34 | } 35 | } 36 | 37 | @OnWorkerEvent('completed') 38 | onCompleted(job: Job) { 39 | this.logger.log(`Job completed: ${job.id}`); 40 | } 41 | 42 | @OnWorkerEvent('failed') 43 | onFailed(job: Job, error: Error) { 44 | this.logger.error(`Job failed: ${job.id}`, error); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /microservice/apps/trade/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { TradeModule } from './../src/trade.module'; 5 | 6 | describe('TradeController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [TradeModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /microservice/apps/trade/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/trade/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/trade" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/transaction/DockerFile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy only necessary files for this service 7 | COPY microservice/nest-cli.json ./ 8 | COPY microservice/package*.json ./ 9 | COPY microservice/tsconfig*.json ./ 10 | COPY microservice/apps/transaction ./apps/transaction 11 | COPY microservice/libs ./libs 12 | 13 | # Install and build 14 | RUN npm ci 15 | RUN npm run generate:transaction 16 | RUN npm run build transaction 17 | 18 | # Production stage 19 | FROM node:20-alpine 20 | 21 | WORKDIR /app 22 | 23 | # Copy package files and install dependencies 24 | COPY microservice/package*.json ./ 25 | RUN npm ci --only=production 26 | 27 | # Copy built assets and Prisma generated files 28 | COPY --from=builder /app/dist ./dist 29 | COPY --from=builder /app/apps/transaction/prisma ./prisma 30 | COPY --from=builder /app/node_modules ./node_modules 31 | COPY --from=builder /app/libs/grpc/proto ./libs/grpc/proto 32 | 33 | # Expose port 34 | EXPOSE 3000 35 | 36 | # Run application 37 | CMD ["npm", "run", "start:prod:transaction"] 38 | #CMD ["node", "dist/apps/transaction/main"] 39 | 40 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/dto/order.limit.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber, IsEnum } from 'class-validator'; 2 | import { CurrencyCode } from '@app/common'; 3 | 4 | export class OrderLimitRequestDto { 5 | @IsEnum(CurrencyCode) 6 | @IsNotEmpty() 7 | coinCode: string; 8 | @IsNotEmpty() 9 | @IsNumber() 10 | amount: number; 11 | @IsNotEmpty() 12 | @IsNumber() 13 | price: number; 14 | } 15 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/dto/order.pending.dto.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@app/common/enums/order-type.enum'; 2 | import { roundToSix, roundToZero } from '@app/common/utils/number.format.util'; 3 | 4 | export class OrderPendingDto { 5 | historyId: number; 6 | orderType: OrderType; 7 | price: number; 8 | quantity: number; 9 | unfilledAmount: number; 10 | createdAt: string; 11 | 12 | constructor( 13 | historyId: string, 14 | orderType: OrderType, 15 | price: string, 16 | quantity: string, 17 | unfilledAmount: string, 18 | createdAt: string, 19 | ) { 20 | this.historyId = Number(historyId); 21 | this.orderType = orderType; 22 | this.price = roundToZero(Number(price)); 23 | this.quantity = roundToSix(Number(quantity)); 24 | this.unfilledAmount = roundToSix(Number(unfilledAmount)); 25 | this.createdAt = createdAt; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/dto/order.pending.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { OrderPendingDto } from './order.pending.dto'; 2 | 3 | export class OrderPendingResponseDto { 4 | nextId?: number | null; 5 | orders: OrderPendingDto[]; 6 | constructor(nextId: number, orders: OrderPendingDto[]) { 7 | this.nextId = nextId; 8 | this.orders = orders; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/dto/pending.buy.order.type.ts: -------------------------------------------------------------------------------- 1 | export type PendingBuyOrder = { 2 | historyId: string; 3 | coinCode: string; 4 | price: string; 5 | originalQuote: string; 6 | remainingQuote: string; 7 | createdAt: Date; 8 | }; 9 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/dto/pending.sell.order.type.ts: -------------------------------------------------------------------------------- 1 | export type PendingSellOrder = { 2 | historyId: string; 3 | coinCode: string; 4 | price: string; 5 | originalQuote: string; 6 | remainingBase: string; 7 | createdAt: Date; 8 | }; 9 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/dto/trade.get.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from '@app/common/enums/order-type.enum'; 2 | import { roundToSix } from '@app/common/utils/number.format.util'; 3 | 4 | export class TradeGetResponseDto { 5 | tradeId: string; 6 | orderType: OrderType; 7 | coinCode: string; 8 | price: string; 9 | quantity: number; 10 | totalAmount: string; 11 | tradedAt: Date; 12 | 13 | constructor( 14 | tradeId: string, 15 | orderType: OrderType, 16 | coinCode: string, 17 | price: string, 18 | quantity: string, 19 | tradedAt: Date, 20 | ) { 21 | this.tradeId = tradeId; 22 | this.orderType = orderType; 23 | this.coinCode = coinCode; 24 | const priceNum = Number(price); 25 | this.price = priceNum.toFixed(0); 26 | this.quantity = roundToSix(Number(quantity)); 27 | this.totalAmount = (priceNum * this.quantity).toFixed(0); 28 | this.tradedAt = tradedAt; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { TransactionModule } from './transaction.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import { WsAdapter } from '@nestjs/platform-ws'; 5 | import cors from '@app/common/cors'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(TransactionModule); 9 | app.useGlobalPipes(new ValidationPipe()); 10 | app.useWebSocketAdapter(new WsAdapter(app)); 11 | app.enableCors(cors); 12 | await app.listen(process.env.port ?? 3000); 13 | } 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/transaction.order.service.ts: -------------------------------------------------------------------------------- 1 | import { OrderService } from '@app/grpc/order.interface'; 2 | import { OrderRequestDto } from '@app/grpc/dto/order.request.dto'; 3 | import { OrderResponseDto } from '@app/grpc/dto/order.response.dto'; 4 | import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; 5 | import { ClientGrpc } from '@nestjs/microservices'; 6 | import { firstValueFrom } from 'rxjs'; 7 | 8 | @Injectable() 9 | export class TransactionOrderService implements OnModuleInit, OrderService { 10 | private orderService; 11 | constructor(@Inject('ORDER_PACKAGE') private readonly client: ClientGrpc) {} 12 | 13 | onModuleInit() { 14 | this.orderService = this.client.getService('OrderService'); 15 | } 16 | 17 | makeBuyOrder(orderRequest: OrderRequestDto): Promise { 18 | return firstValueFrom(this.orderService.makeBuyOrder(orderRequest)); 19 | } 20 | 21 | makeSellOrder(orderRequest: OrderRequestDto): Promise { 22 | return firstValueFrom(this.orderService.makeSellOrder(orderRequest)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /microservice/apps/transaction/src/transaction.queue.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectQueue } from '@nestjs/bullmq'; 3 | import { Queue } from 'bullmq'; 4 | import { OrderType } from '@app/common/enums/order-type.enum'; 5 | import { TradeOrder } from '@app/grpc/dto/trade.order.dto'; 6 | 7 | @Injectable() 8 | export class TransactionQueueService { 9 | constructor(@InjectQueue('trade') private queue: Queue) {} 10 | 11 | async addQueue(name: string, trade: TradeOrder) { 12 | await this.queue.add(name, { trade }, { jobId: `${name}-${trade.historyId}` }); 13 | } 14 | 15 | async addCancelQueue(userId: bigint, historyId: string, orderType: OrderType) { 16 | await this.queue.add( 17 | OrderType.CANCELED, 18 | { userId, historyId, orderType }, 19 | { jobId: `${OrderType.CANCELED}-${historyId}` }, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /microservice/apps/transaction/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { TransactionModule } from '../src/transaction.module'; 5 | 6 | describe('TransactionController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [TransactionModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /microservice/apps/transaction/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservice/apps/transaction/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../dist/apps/transaction" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/bull/src/bull.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from '@nestjs/config'; 3 | import { BullModule } from '@nestjs/bullmq'; 4 | 5 | @Module({ 6 | imports: [ 7 | ConfigModule, 8 | BullModule.forRootAsync({ 9 | imports: [ConfigModule], 10 | inject: [ConfigService], 11 | useFactory: async (configService: ConfigService) => ({ 12 | connection: { 13 | url: configService.get('TRADE_REDIS_URL'), 14 | }, 15 | defaultJobOptions: { 16 | removeOnComplete: true, 17 | attempts: 3, 18 | }, 19 | }), 20 | }), 21 | BullModule.registerQueue({ 22 | name: 'trade', 23 | }), 24 | ], 25 | exports: [BullModule], 26 | }) 27 | export class BullMQModule {} 28 | -------------------------------------------------------------------------------- /microservice/libs/bull/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bull.module'; 2 | -------------------------------------------------------------------------------- /microservice/libs/bull/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/bull" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/common/src/common.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller('/health') 4 | export class CommonController { 5 | @Get() 6 | async get() { 7 | return; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/common/src/common.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommonService } from './common.service'; 3 | import { CommonController } from './common.controller'; 4 | 5 | @Module({ 6 | providers: [CommonService], 7 | controllers: [CommonController], 8 | exports: [CommonService], 9 | }) 10 | export class CommonModule {} 11 | -------------------------------------------------------------------------------- /microservice/libs/common/src/common.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CommonService } from './common.service'; 3 | 4 | describe('CommonService', () => { 5 | let service: CommonService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [CommonService], 10 | }).compile(); 11 | 12 | service = module.get(CommonService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /microservice/libs/common/src/common.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class CommonService {} 5 | -------------------------------------------------------------------------------- /microservice/libs/common/src/cors.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | origin: (origin, callback) => { 3 | // 특정 도메인의 모든 포트 허용을 위한 정규식 4 | const allowedDomains = [ 5 | /^http:\/\/localhost(:[0-9]+)?$/, // localhost의 모든 포트 6 | /^http:\/\/127.0.0.1(:[0-9]+)?$/, // 127.0.0.1 모든 포트 7 | /^http:\/\/example\.com(:[0-9]+)?$/, // example.com의 모든 포트 8 | 'http://boobit.xyz', 9 | ]; 10 | 11 | // origin이 null인 경우는 같은 출처의 요청 12 | if ( 13 | !origin || 14 | allowedDomains.some((domain) => 15 | domain instanceof RegExp ? domain.test(origin) : domain === origin, 16 | ) 17 | ) { 18 | callback(null, true); 19 | } else { 20 | callback(new Error('Not allowed by CORS')); 21 | } 22 | }, 23 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 24 | credentials: true, 25 | }; 26 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/chart-timescale.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TimeScale { 2 | SEC_01 = '1sec', 3 | MIN_01 = '1min', 4 | MIN_10 = '10min', 5 | MIN_30 = '30min', 6 | HOUR_01 = '1hour', 7 | DAY_01 = '1day', 8 | WEEK_01 = '1week', 9 | MONTH_01 = '1month', 10 | } 11 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/currency-code.enum.ts: -------------------------------------------------------------------------------- 1 | export enum CurrencyCode { 2 | KRW = 'KRW', 3 | BTC = 'BTC', 4 | } 5 | 6 | export enum CurrencyCodeName { 7 | KRW = '원화', 8 | BTC = '비트코인', 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/grpc-status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum GrpcOrderStatusCode { 2 | NO_BALANCE = 'NO_BALANCE', 3 | SUCCESS = 'SUCCESS', 4 | TRANSACTION_ERROR = 'TRANSACTION_ERROR', 5 | } 6 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from './currency-code.enum'; 2 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/order-status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum OrderStatus { 2 | ORDERED = 'ORDERED', 3 | CANCELED = 'CANCELED', 4 | } 5 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/order-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum OrderType { 2 | SELL = 'SELL', 3 | BUY = 'BUY', 4 | CANCELED = 'CANCELED', 5 | } 6 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/redis-channel.enum.ts: -------------------------------------------------------------------------------- 1 | export enum RedisChannel { 2 | CANDLE_CHART = 'CANDLE_CHART', 3 | BUY_AND_SELL = 'BUY_AND_SELL', 4 | TRADE = 'TRADE', 5 | } 6 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/trade.gradient.enum.ts: -------------------------------------------------------------------------------- 1 | export enum TradeGradient { 2 | POSITIVE = 'POSITIVE', 3 | NEGATIVE = 'NEGATIVE', 4 | } 5 | -------------------------------------------------------------------------------- /microservice/libs/common/src/enums/ws-event.enum.ts: -------------------------------------------------------------------------------- 1 | export enum WsEvent { 2 | CANDLE_CHART_INIT = 'CANDLE_CHART_INIT', 3 | CANDLE_CHART = 'CANDLE_CHART', 4 | BUY_AND_SELL = 'BUY_AND_SELL', 5 | TRADE = 'TRADE', 6 | } 7 | -------------------------------------------------------------------------------- /microservice/libs/common/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common.module'; 2 | export * from './common.service'; 3 | export * from './enums'; 4 | -------------------------------------------------------------------------------- /microservice/libs/common/src/utils/number.format.util.ts: -------------------------------------------------------------------------------- 1 | export function formatFixedPoint(value: number, paddingLength = 15): string { 2 | const [integerPart, decimalPart = ''] = value.toString().split('.'); 3 | const paddedInteger = integerPart.padStart(paddingLength, '0'); 4 | return decimalPart ? `${paddedInteger}.${decimalPart}` : paddedInteger; 5 | } 6 | 7 | export const roundToSix = (num: number): number => { 8 | return Number(num.toFixed(6)); 9 | }; 10 | 11 | export const roundToThree = (num: number): number => { 12 | return Number(num.toFixed(3)); 13 | }; 14 | 15 | export const roundToZero = (num: number): number => { 16 | return Number(num.toFixed(0)); 17 | }; 18 | -------------------------------------------------------------------------------- /microservice/libs/common/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/common" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/grpc/proto/account.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package account; 4 | 5 | service AccountService { 6 | rpc CreateAccount(AccountCreateRequest) returns (AccountCreateResponse); 7 | } 8 | 9 | message AccountCreateRequest { 10 | string userId = 1; 11 | } 12 | 13 | message AccountCreateResponse { 14 | string status = 1; 15 | } -------------------------------------------------------------------------------- /microservice/libs/grpc/proto/order.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package order; 4 | 5 | service OrderService { 6 | rpc MakeBuyOrder(OrderRequest) returns (OrderResponse); 7 | rpc MakeSellOrder(OrderRequest) returns (OrderResponse); 8 | } 9 | 10 | message OrderRequest { 11 | string userId = 1; 12 | string coinCode = 2; 13 | double amount = 3; 14 | double price = 4; 15 | } 16 | 17 | message OrderResponse { 18 | string status = 1; 19 | string historyId = 2; 20 | } 21 | -------------------------------------------------------------------------------- /microservice/libs/grpc/proto/trade.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package trade; 4 | 5 | service TradeService { 6 | rpc SettleTransaction(TradeRequest) returns (TradeResponse); 7 | rpc CancelOrder(TradeCancelRequest) returns (TradeResponse); 8 | } 9 | 10 | message TradeRequest { 11 | BuyerRequest buyerRequest = 1; 12 | SellerRequest sellerRequest = 2; 13 | } 14 | 15 | message BuyerRequest { 16 | string userId = 1; 17 | string coinCode = 2; 18 | string buyerPrice = 3; 19 | string tradePrice = 4; 20 | string receivedCoins = 5; 21 | } 22 | 23 | message SellerRequest { 24 | string userId = 1; 25 | string coinCode = 2; 26 | string tradePrice = 3; 27 | string soldCoins = 4; 28 | } 29 | 30 | message TradeResponse { 31 | string status = 1; 32 | } 33 | 34 | message TradeCancelRequest { 35 | string userId = 1; 36 | string historyId = 2; 37 | string coinCode = 3; 38 | string price = 4; 39 | string remain = 5; 40 | string orderType = 6; 41 | string orderStatus = 7; 42 | } 43 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/account.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AccountService { 2 | createAccount(accountRequest: { userId: string }): Promise<{ status: string }>; 3 | } 4 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/account.create.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class AccountCreateRequestDto { 2 | userId: string; 3 | } 4 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/account.create.response.dto.ts: -------------------------------------------------------------------------------- 1 | export class AccountCreateResponseDto { 2 | status: string; 3 | } 4 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/order.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class OrderRequestDto { 2 | userId: string; 3 | coinCode: string; 4 | amount: number; 5 | price: number; 6 | 7 | constructor(userId: string, coinCode: string, amount: number, price: number) { 8 | this.userId = userId; 9 | this.coinCode = coinCode; 10 | this.amount = amount; 11 | this.price = price; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/order.response.dto.ts: -------------------------------------------------------------------------------- 1 | export class OrderResponseDto { 2 | status: string; 3 | historyId: string; 4 | 5 | constructor(status, historyId) { 6 | this.status = status; 7 | this.historyId = historyId; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/trade.buyer.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class TradeBuyerRequestDto { 2 | userId: string; 3 | coinCode: string; 4 | buyerPrice: string; 5 | tradePrice: string; 6 | receivedCoins: string; 7 | 8 | constructor( 9 | userId: string, 10 | coinCode: string, 11 | buyerPrice: string, 12 | tradePrice: string, 13 | quantity: string, 14 | ) { 15 | this.userId = userId; 16 | this.coinCode = coinCode; 17 | this.buyerPrice = buyerPrice; 18 | this.tradePrice = tradePrice; 19 | this.receivedCoins = quantity; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/trade.cancel.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class TradeCancelRequestDto { 2 | userId: string; 3 | coinCode: string; 4 | price: string; 5 | remain: string; 6 | orderType: string; 7 | 8 | constructor( 9 | userId: string, 10 | coinCode: string, 11 | price: string, 12 | remain: string, 13 | orderType: string, 14 | ) { 15 | this.userId = userId; 16 | this.coinCode = coinCode; 17 | this.price = price; 18 | this.remain = remain; 19 | this.orderType = orderType; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/trade.history.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class TradeHistoryRequestDto { 2 | historyId: string; 3 | userId: string; 4 | remain: number; 5 | 6 | constructor(historyId: string, userId: string, remain: number) { 7 | this.historyId = historyId; 8 | this.userId = userId; 9 | this.remain = remain; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/trade.order.dto.ts: -------------------------------------------------------------------------------- 1 | import { formatFixedPoint } from '@app/common/utils/number.format.util'; 2 | import { OrderRequestDto } from './order.request.dto'; 3 | 4 | export class TradeOrder { 5 | historyId: string; 6 | userId: string; 7 | coinCode: string; 8 | price: string; 9 | originalQuote: number; 10 | 11 | constructor(historyId: string, orderRequest: OrderRequestDto) { 12 | this.historyId = historyId; 13 | this.userId = orderRequest.userId; 14 | this.coinCode = orderRequest.coinCode; 15 | this.price = formatFixedPoint(orderRequest.price); 16 | this.originalQuote = orderRequest.amount; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/trade.reponse.dto.ts: -------------------------------------------------------------------------------- 1 | export class TradeResponseDto { 2 | status: string; 3 | 4 | constructor(status: string) { 5 | this.status = status; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/trade.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { TradeBuyerRequestDto } from './trade.buyer.request.dto'; 2 | import { TradeSellerRequestDto } from './trade.seller.request.dto'; 3 | 4 | export class TradeRequestDto { 5 | buyerRequest: TradeBuyerRequestDto; 6 | sellerRequest: TradeSellerRequestDto; 7 | 8 | constructor( 9 | buyerRequest: TradeBuyerRequestDto, 10 | sellerRequest: TradeSellerRequestDto, 11 | ) { 12 | this.buyerRequest = buyerRequest; 13 | this.sellerRequest = sellerRequest; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/dto/trade.seller.request.dto.ts: -------------------------------------------------------------------------------- 1 | export class TradeSellerRequestDto { 2 | userId: string; 3 | coinCode: string; 4 | tradePrice: string; 5 | soldCoins: string; 6 | 7 | constructor( 8 | userId: string, 9 | coinCode: string, 10 | tradePrice: string, 11 | quantity: string, 12 | ) { 13 | this.userId = userId; 14 | this.coinCode = coinCode; 15 | this.tradePrice = tradePrice; 16 | this.soldCoins = quantity; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/order.interface.ts: -------------------------------------------------------------------------------- 1 | import { OrderRequestDto } from '@app/grpc/dto/order.request.dto'; 2 | import { OrderResponseDto } from '@app/grpc/dto/order.response.dto'; 3 | export interface OrderService { 4 | makeBuyOrder(buyOrderRequest: OrderRequestDto): Promise; 5 | makeSellOrder(buyOrderRequest: OrderRequestDto): Promise; 6 | } 7 | -------------------------------------------------------------------------------- /microservice/libs/grpc/src/trade.interface.ts: -------------------------------------------------------------------------------- 1 | import { TradeCancelRequestDto } from './dto/trade.cancel.request.dto'; 2 | import { TradeResponseDto } from './dto/trade.reponse.dto'; 3 | import { TradeRequestDto } from './dto/trade.request.dto'; 4 | 5 | export interface TradeGrpcService { 6 | settleTransaction(tradeRequest: TradeRequestDto): Promise; 7 | cancelOrder(cancelRequest: TradeCancelRequestDto): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /microservice/libs/grpc/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/grpc" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/prisma/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prisma.module'; 2 | export * from './prisma.service'; 3 | -------------------------------------------------------------------------------- /microservice/libs/prisma/src/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | import { ConfigModule } from '@nestjs/config'; 4 | 5 | @Global() 6 | @Module({ 7 | imports: [ 8 | ConfigModule.forRoot({ 9 | isGlobal: true, 10 | envFilePath: '.env', 11 | }), 12 | ], 13 | providers: [PrismaService], 14 | exports: [PrismaService], 15 | }) 16 | export class PrismaModule {} 17 | -------------------------------------------------------------------------------- /microservice/libs/prisma/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/prisma" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/session/src/guard/authenticated.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticatedGuard } from './authenticated.guard'; 2 | 3 | describe('AuthenticatedGuard', () => { 4 | it('should be defined', () => { 5 | expect(new AuthenticatedGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /microservice/libs/session/src/guard/authenticated.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AuthenticatedGuard implements CanActivate { 5 | canActivate(context: ExecutionContext): boolean { 6 | const request = context.switchToHttp().getRequest(); 7 | return request.isAuthenticated(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/session/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './session.module'; 2 | export * from './session.middleware'; 3 | -------------------------------------------------------------------------------- /microservice/libs/session/src/passport/session.serializer.spec.ts: -------------------------------------------------------------------------------- 1 | // test/auth/session.serializer.spec.ts 2 | import { SessionSerializer } from './session.serializer'; 3 | 4 | describe('SessionSerializer', () => { 5 | let serializer: SessionSerializer; 6 | 7 | beforeEach(() => { 8 | serializer = new SessionSerializer(); 9 | }); 10 | 11 | describe('serializeUser', () => { 12 | it('필요한 사용자 정보만 세션에 저장', (done) => { 13 | const user = { 14 | userId: '1', 15 | email: 'test@example.com', 16 | name: 'Test User', 17 | passwordHash: 'hash', 18 | created_at: new Date(), 19 | }; 20 | 21 | serializer.serializeUser(user, (err, serialized) => { 22 | expect(err).toBeNull(); 23 | expect(serialized).toEqual({ 24 | user_id: '1', 25 | email: 'test@example.com', 26 | name: 'Test User', 27 | }); 28 | expect(serialized).not.toHaveProperty('passwordHash'); 29 | expect(serialized).not.toHaveProperty('created_at'); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('deserializeUser', () => { 36 | it('세션에서 사용자 정보 복원', (done) => { 37 | const sessionUser = { 38 | user_id: '1', 39 | email: 'test@example.com', 40 | name: 'Test User', 41 | }; 42 | 43 | serializer.deserializeUser(sessionUser, (err, deserialized) => { 44 | expect(err).toBeNull(); 45 | expect(deserialized).toEqual({ 46 | userId: '1', 47 | email: 'test@example.com', 48 | name: 'Test User', 49 | }); 50 | done(); 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /microservice/libs/session/src/passport/session.serializer.ts: -------------------------------------------------------------------------------- 1 | import { PassportSerializer } from '@nestjs/passport'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class SessionSerializer extends PassportSerializer { 6 | serializeUser(user: any, done: (err: Error, user: any) => void): any { 7 | const sessionUser = { 8 | user_id: user.userId, 9 | email: user.email, 10 | name: user.name, 11 | }; 12 | done(null, sessionUser); 13 | } 14 | 15 | deserializeUser(sessionUser: any, done: (err: Error, payload: any) => void) { 16 | const user = { 17 | userId: sessionUser.user_id, 18 | email: sessionUser.email, 19 | name: sessionUser.name, 20 | }; 21 | done(null, user); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /microservice/libs/session/src/session.module.ts: -------------------------------------------------------------------------------- 1 | // src/session/session.module.ts 2 | import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; 3 | import { SessionMiddleware } from './session.middleware'; 4 | import { SessionSerializer } from './passport/session.serializer'; 5 | import { AuthenticatedGuard } from './guard/authenticated.guard'; 6 | import { PassportModule } from '@nestjs/passport'; 7 | import { ConfigModule } from '@nestjs/config'; 8 | @Module({ 9 | imports: [ConfigModule, PassportModule.register({ session: true })], 10 | providers: [SessionSerializer, AuthenticatedGuard], 11 | exports: [SessionSerializer, AuthenticatedGuard], 12 | }) 13 | export class SessionModule implements NestModule { 14 | configure(consumer: MiddlewareConsumer) { 15 | consumer.apply(SessionMiddleware).forRoutes('*'); // 모든 라우트에 적용 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /microservice/libs/session/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/session" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/decorators/ws.event.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const WS_EVENT_HANDLER = 'WS_EVENT_HANDLER'; 4 | 5 | export const WsEventDecorator = (event: string): MethodDecorator => { 6 | return SetMetadata(WS_EVENT_HANDLER, event); 7 | }; 8 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/decorators/ws.validate.message.decorator.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { plainToInstance } from 'class-transformer'; 3 | import { validateSync } from 'class-validator'; 4 | 5 | export const ValidateMessage = createParamDecorator((DtoClass: any, context: ExecutionContext) => { 6 | const data = context.switchToWs().getData(); 7 | const dtoInstance = plainToInstance(DtoClass, data); 8 | const errors = validateSync(dtoInstance); 9 | if (errors.length > 0) { 10 | throw new BadRequestException('Invalid message format'); 11 | } 12 | return dtoInstance; 13 | }); 14 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/dto/candle.data.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsNumber } from 'class-validator'; 2 | import { Type } from 'class-transformer'; 3 | 4 | export class CandleDataDto { 5 | constructor(data: Partial) { 6 | Object.assign(this, data); 7 | } 8 | @IsDate() 9 | @Type(() => Date) 10 | date: Date; 11 | 12 | @IsNumber() 13 | open: number; 14 | 15 | @IsNumber() 16 | close: number; 17 | 18 | @IsNumber() 19 | high: number; 20 | 21 | @IsNumber() 22 | low: number; 23 | 24 | @IsNumber() 25 | volume: number; 26 | 27 | toString(): string { 28 | return `date: ${this.date}, open: ${this.open}, close: ${this.close}, high: ${this.high}, low: ${this.low}, volume: ${this.volume}`; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/dto/chart.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { CandleDataDto } from './candle.data.dto'; 2 | import { ValidateNested, IsEnum } from 'class-validator'; 3 | import { Type } from 'class-transformer'; 4 | import { WsBaseDto } from './ws.base.dto'; 5 | import { TimeScale } from '@app/common/enums/chart-timescale.enum'; 6 | 7 | export class ChartResponseDto extends WsBaseDto { 8 | @IsEnum(TimeScale) 9 | timeScale: TimeScale; 10 | 11 | @ValidateNested({ each: true }) 12 | @Type(() => CandleDataDto) 13 | data: CandleDataDto[]; 14 | } 15 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/dto/subscribe.request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum } from 'class-validator'; 2 | import { TimeScale } from '@app/common/enums/chart-timescale.enum'; 3 | import { WsBaseDto } from './ws.base.dto'; 4 | 5 | export class SubscribeRequestDto extends WsBaseDto { 6 | @IsEnum(TimeScale) 7 | timeScale: TimeScale; 8 | } 9 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/dto/trade.data.dto.ts: -------------------------------------------------------------------------------- 1 | import { TradeGradient } from '@app/common/enums/trade.gradient.enum'; 2 | import { IsEnum } from 'class-validator'; 3 | export class TradeDataDto { 4 | date: Date; 5 | price: number; 6 | amount: number; 7 | tradePrice: number; 8 | @IsEnum(TradeGradient) 9 | gradient: TradeGradient; 10 | } 11 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/dto/trade.response.dto.ts: -------------------------------------------------------------------------------- 1 | import { WsBaseDto } from './ws.base.dto'; 2 | import { TradeDataDto } from './trade.data.dto'; 3 | export class TradeResponseDto extends WsBaseDto { 4 | data: TradeDataDto[]; 5 | } 6 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/dto/ws.base.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum } from 'class-validator'; 2 | import { WsEvent } from '@app/common/enums/ws-event.enum'; 3 | 4 | export class WsBaseDto { 5 | @IsEnum(WsEvent) 6 | event: WsEvent; 7 | } 8 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/ws.error.ts: -------------------------------------------------------------------------------- 1 | export class WsError extends Error { 2 | constructor( 3 | public readonly event: string, 4 | public readonly message: string, 5 | ) { 6 | super(message); 7 | this.name = 'WsError'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /microservice/libs/ws/src/ws.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WsService } from './ws.service'; 3 | 4 | @Module({ 5 | providers: [WsService], 6 | exports: [WsService], 7 | }) 8 | export class WsModule {} 9 | -------------------------------------------------------------------------------- /microservice/libs/ws/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/ws" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /microservice/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /microservice/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false, 20 | "paths": { 21 | "@app/common": [ 22 | "libs/common/src" 23 | ], 24 | "@app/common/*": [ 25 | "libs/common/src/*" 26 | ], 27 | "@app/prisma": [ 28 | "libs/prisma/src" 29 | ], 30 | "@app/prisma/*": [ 31 | "libs/prisma/src/*" 32 | ], 33 | "@app/session": [ 34 | "libs/session/src" 35 | ], 36 | "@app/session/*": [ 37 | "libs/session/src/*" 38 | ], 39 | "@app/grpc": [ 40 | "libs/grpc/src" 41 | ], 42 | "@app/grpc/*": [ 43 | "libs/grpc/src/*" 44 | ], 45 | "@app/ws": [ 46 | "libs/ws/src" 47 | ], 48 | "@app/ws/*": [ 49 | "libs/ws/src/*" 50 | ], 51 | "@app/bull": [ 52 | "libs/bull/src" 53 | ], 54 | "@app/bull/*": [ 55 | "libs/bull/src/*" 56 | ] 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /static/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /static/DockerFile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:20-alpine AS builder 3 | 4 | WORKDIR /app/view 5 | 6 | # Copy only necessary files for this service 7 | COPY view ./ 8 | 9 | # Install and build 10 | RUN npm ci 11 | RUN npm run build 12 | 13 | # Production stage 14 | FROM node:20-alpine 15 | 16 | WORKDIR /app 17 | 18 | # Copy package files and install dependencies 19 | COPY static ./ 20 | 21 | RUN npm ci --only=production 22 | COPY --from=builder /app/view/dist ./public 23 | 24 | # Expose port 25 | EXPOSE 8080 26 | 27 | # Run application 28 | CMD ["node", "app.js"] 29 | 30 | -------------------------------------------------------------------------------- /static/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { fileURLToPath } from 'url'; 3 | import { dirname, join } from 'path'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const app = express(); 9 | const port = process.env.PORT || 8080; 10 | 11 | app.use(express.static(join(__dirname, 'public'))); 12 | 13 | app.get('*', (_req, res) => { 14 | res.sendFile(join(__dirname, 'public', 'index.html')); 15 | }); 16 | 17 | app.listen(port, () => { 18 | console.log(`Server running at ${port}`); 19 | }); 20 | -------------------------------------------------------------------------------- /static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boobit-static", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "express": "^4.21.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /view/.env.exam: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=http://localhost:3000 2 | VITE_BALANCE_URL=http://localhost:3100 3 | VITE_TRANSACTION_URL=http://localhost:3200 4 | VITE_SOCKET_URL=ws://localhost:3200/ws -------------------------------------------------------------------------------- /view/.env.production: -------------------------------------------------------------------------------- 1 | VITE_AUTH_URL=http://boobit.xyz 2 | VITE_BALANCE_URL=http://boobit.xyz 3 | VITE_TRANSACTION_URL=http://boobit.xyz 4 | VITE_SOCKET_URL=ws://boobit.xyz/ws -------------------------------------------------------------------------------- /view/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /view/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "semi": true, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /view/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import reactHooks from 'eslint-plugin-react-hooks'; 4 | import reactRefresh from 'eslint-plugin-react-refresh'; 5 | import tseslint from 'typescript-eslint'; 6 | import prettier from 'eslint-plugin-prettier'; 7 | import importPlugin from 'eslint-plugin-import'; 8 | import jsxA11y from 'eslint-plugin-jsx-a11y'; 9 | import react from 'eslint-plugin-react'; 10 | 11 | export default tseslint.config( 12 | { ignores: ['dist'] }, 13 | { 14 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 15 | files: ['**/*.{ts,tsx}'], 16 | languageOptions: { 17 | ecmaVersion: 2020, 18 | globals: globals.browser, 19 | }, 20 | settings: { 21 | 'import/resolver': { 22 | node: { 23 | extensions: ['.js', '.jsx', '.ts', '.tsx'], // 사용 중인 확장자를 모두 추가 24 | }, 25 | }, 26 | }, 27 | plugins: { 28 | 'react-hooks': reactHooks, 29 | 'react-refresh': reactRefresh, 30 | import: importPlugin, 31 | 'jsx-a11y': jsxA11y, 32 | react: react, 33 | prettier, 34 | }, 35 | rules: { 36 | 'react-hooks/rules-of-hooks': 'error', // Hook 사용 규칙 체크 37 | 'react-hooks/exhaustive-deps': 'warn', // Hook의 의존성 배열 체크 38 | 'react-refresh/only-export-components': [ 39 | 'warn', 40 | { allowConstantExport: true }, // React Refresh에서 컴포넌트만 내보내도록 경고 41 | ], 42 | 'import/no-unresolved': 'error', // 해결되지 않은 import 오류 체크 43 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], // Prettier 오류를 ESLint 오류로 처리 44 | 'jsx-a11y/alt-text': 'warn', 45 | }, 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | BooBit 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /view/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /view/public/BuBu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web02-BooBit/7cada7b270145338c430edcf5a5da162b48a91c4/view/public/BuBu.png -------------------------------------------------------------------------------- /view/src/__mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw/browser'; 2 | import { handlers } from './handlers'; 3 | 4 | // websocket 5 | 6 | export const worker = setupWorker(...handlers); 7 | -------------------------------------------------------------------------------- /view/src/__mocks/handlers/index.ts: -------------------------------------------------------------------------------- 1 | import { transactionHandlers } from './transactionHandlers.ts'; 2 | import { balanceHandlers } from './balanceHandlers.ts'; 3 | 4 | export const handlers = [...transactionHandlers, ...balanceHandlers]; 5 | -------------------------------------------------------------------------------- /view/src/__mocks/handlers/transactionHandlers.ts: -------------------------------------------------------------------------------- 1 | //import { http, HttpResponse } from 'msw'; 2 | //import { BASE_URLS } from '../../shared/consts/baseUrl'; 3 | 4 | export const transactionHandlers = [ 5 | // 주문 취소 요청 핸들러 6 | // http.delete(`${BASE_URLS.TRANSACTION}/api/orders/*`, () => { 7 | // 200 OK 응답 반환 8 | //return new HttpResponse(); 9 | // 403 Unauthorized 응답 반환 10 | // return new HttpResponse('Unauthorized', { status: 403 }); 11 | //}), 12 | ]; 13 | -------------------------------------------------------------------------------- /view/src/app/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background-color: #121212; 7 | font-family: 'Pretendard Variable'; 8 | } 9 | 10 | ::-webkit-scrollbar { 11 | width: 10px; 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | height: 30%; 16 | background: #2b2b3d; 17 | border-radius: 10px; 18 | } 19 | 20 | ::-webkit-scrollbar-track { 21 | background: #1e1e2f; 22 | } 23 | -------------------------------------------------------------------------------- /view/src/app/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App'; 5 | import './index.css'; 6 | import { AuthProvider } from '../shared/store/auth/authContext'; 7 | import { ToastProvider } from '../shared/store/ToastContext'; 8 | 9 | async function enableMocking() { 10 | if (import.meta.env.MODE !== 'development') return; 11 | 12 | const { worker } = await import('../__mocks/browser'); 13 | 14 | return worker.start(); 15 | } 16 | 17 | enableMocking().then(() => 18 | ReactDOM.createRoot(document.getElementById('root')!).render( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | ); 30 | -------------------------------------------------------------------------------- /view/src/app/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /view/src/entities/Chart/lib/addText.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | 3 | const addText = ( 4 | group: d3.Selection, 5 | y: number, 6 | texts: string[] 7 | ): void => { 8 | const text = group 9 | .append('text') 10 | .attr('x', 0) 11 | .attr('y', y) 12 | .attr('fill', '#E0E0E0') 13 | .attr('class', 'market-text') 14 | .style('font-size', '12px'); 15 | 16 | texts.forEach((t, i) => { 17 | text 18 | .append('tspan') 19 | .attr('dx', i === 0 ? '0em' : '1em') 20 | .text(t); 21 | }); 22 | }; 23 | 24 | export default addText; 25 | -------------------------------------------------------------------------------- /view/src/entities/Chart/lib/createBarYAsixScale.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | import { CandleData } from '../model/candleDataType'; 3 | 4 | export const createBarAxisScale = (data: CandleData[], volumeHeight: number) => { 5 | const volumeMax = d3.max(data, (d) => d.volume)!; 6 | const yScale = d3 7 | .scaleLinear() 8 | .domain([0, volumeMax * 1.2]) 9 | .range([volumeHeight, 0]); 10 | 11 | return { yScale }; 12 | }; 13 | -------------------------------------------------------------------------------- /view/src/entities/Chart/lib/createYAxisScale.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | import { CandleData } from '../model/candleDataType'; 3 | 4 | export const createYAxisScale = (data: CandleData[], height: number, volumeHeight: number) => { 5 | const yMin = d3.min(data, (d) => d.low)!; 6 | const yMax = d3.max(data, (d) => d.high)!; 7 | const paddingRatio = 0.02; 8 | 9 | const yScale = d3 10 | .scaleLinear() 11 | .domain([yMin - (yMax - yMin) * paddingRatio, yMax + (yMax - yMin) * paddingRatio]) 12 | .nice() 13 | .range([height - volumeHeight, 0]); 14 | 15 | return { yScale }; 16 | }; 17 | -------------------------------------------------------------------------------- /view/src/entities/Chart/lib/updateMarketText.ts: -------------------------------------------------------------------------------- 1 | import { CandleData } from '../model/candleDataType'; 2 | import * as d3 from 'd3'; 3 | import addText from './addText'; 4 | import formatPrice from '../../../shared/model/formatPrice'; 5 | 6 | const updateMarketText = ( 7 | group: d3.Selection, 8 | data: CandleData 9 | ) => { 10 | group.selectAll('.market-text').remove(); // 기존 텍스트 제거 11 | 12 | // 텍스트 추가 13 | addText(group, 10, [ 14 | `시가: ${formatPrice(data.open)}`, 15 | `종가: ${formatPrice(data.close)}`, 16 | `PRICE: ${formatPrice(data.open)}`, 17 | ]); 18 | addText(group, 22, [ 19 | `고가: ${formatPrice(data.high)}`, 20 | `저가: ${formatPrice(data.low)}`, 21 | `VOL: ${formatPrice(data.volume)}`, 22 | ]); 23 | addText(group, 34, [`시간: ${data.date.slice(0, -5).replace(/[T]/, ' ')}`]); 24 | }; 25 | export default updateMarketText; 26 | -------------------------------------------------------------------------------- /view/src/entities/Chart/model/candleDataType.ts: -------------------------------------------------------------------------------- 1 | export interface CandleData { 2 | date: string; 3 | open: number; 4 | high: number; 5 | low: number; 6 | close: number; 7 | volume: number; 8 | } 9 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/UI/BoxContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutProps } from '../../../shared/types/LayoutProps'; 3 | 4 | const BoxContainer: React.FC = ({ children }) => { 5 | return ( 6 |
7 | {children} 8 |
9 | ); 10 | }; 11 | 12 | export default BoxContainer; 13 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/UI/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Title: React.FC<{ 4 | currency_code: string; 5 | amount: number; 6 | }> = ({ currency_code, amount }) => { 7 | return ( 8 |
9 | {currency_code} 10 |
11 |
총 소유
12 |
13 |
{amount.toLocaleString()}
14 |
{currency_code}
15 |
16 |
17 |
18 | ); 19 | }; 20 | 21 | export default Title; 22 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/UI/TransactionLogItem.tsx: -------------------------------------------------------------------------------- 1 | import formatDate from '../../../shared/model/formatDate'; 2 | import formatPrice from '../../../shared/model/formatPrice'; 3 | import { TransactionType } from '../model/TransactionType'; 4 | 5 | interface TransactionLogItemProps { 6 | log: TransactionType; 7 | currency_code: string; 8 | } 9 | 10 | const TransactionLogItem: React.FC = ({ log, currency_code }) => { 11 | return ( 12 |
16 |
17 |
20 | {log.tx_type === 'withdrawal' ? '출금' : '입금'} 완료 21 |
22 |
{formatDate(log.timestamp)}
23 |
24 |
25 | {formatPrice(log.amount)} {currency_code} 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default TransactionLogItem; 32 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/api/depositApi.ts: -------------------------------------------------------------------------------- 1 | import { AssetType } from '../model/AssetType'; 2 | 3 | const apiUrl = import.meta.env.VITE_BALANCE_URL; 4 | const depositApi = async ({ currencyCode, amount }: AssetType) => { 5 | const response = await fetch(`${apiUrl}/api/users/deposit`, { 6 | method: 'POST', 7 | credentials: 'include', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify({ currencyCode, amount }), 12 | }); 13 | 14 | if (!response.ok) { 15 | throw new Error(response.status.toString()); 16 | } 17 | 18 | return response.ok; 19 | }; 20 | 21 | export default depositApi; 22 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/api/getTransactionsApi.ts: -------------------------------------------------------------------------------- 1 | import { TransactionsRequestType } from '../model/TransactionsRequestType'; 2 | 3 | const getTransactionsApi = async (requestData: TransactionsRequestType) => { 4 | const queryParams = new URLSearchParams({ 5 | currencyCode: requestData.currencyCode, 6 | id: requestData.id !== null ? String(requestData.id) : '', 7 | }).toString(); 8 | const apiUrl = import.meta.env.VITE_BALANCE_URL; 9 | 10 | const response = await fetch(`${apiUrl}/api/users/transactions?${queryParams}`, { 11 | method: 'GET', 12 | credentials: 'include', 13 | }); 14 | 15 | if (!response.ok) { 16 | throw new Error(response.status.toString()); 17 | } 18 | 19 | return response.json(); 20 | }; 21 | 22 | export default getTransactionsApi; 23 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/api/withdrawApi.ts: -------------------------------------------------------------------------------- 1 | import { AssetType } from '../model/AssetType'; 2 | 3 | const apiUrl = import.meta.env.VITE_BALANCE_URL; 4 | const withdrawApi = async ({ currencyCode, amount }: AssetType) => { 5 | const response = await fetch(`${apiUrl}/api/users/withdraw`, { 6 | method: 'POST', 7 | credentials: 'include', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify({ currencyCode, amount }), 12 | }); 13 | 14 | if (!response.ok) { 15 | throw new Error(response.status.toString()); 16 | } 17 | 18 | return response.ok; 19 | }; 20 | 21 | export default withdrawApi; 22 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/consts/category.ts: -------------------------------------------------------------------------------- 1 | export default ['내역', '입금', '출금']; 2 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/model/AssetType.ts: -------------------------------------------------------------------------------- 1 | export interface AssetType { 2 | currencyCode: string; 3 | amount: number; 4 | } 5 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/model/TransactionType.ts: -------------------------------------------------------------------------------- 1 | export interface TransactionType { 2 | timestamp: string; 3 | amount: string; 4 | tx_type: string; 5 | } 6 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/model/TransactionsRequestType.ts: -------------------------------------------------------------------------------- 1 | export interface TransactionsRequestType { 2 | currencyCode: string; 3 | id: number | null; 4 | } 5 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/model/useDeposit.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { useToast } from '../../../shared/store/ToastContext'; 3 | import depositApi from '../api/depositApi'; 4 | import successMessages from '../../../shared/consts/successMessage'; 5 | import errorMessages from '../../../shared/consts/errorMessages'; 6 | 7 | const useDeposit = () => { 8 | const { addToast } = useToast(); 9 | const queryClient = useQueryClient(); 10 | return useMutation({ 11 | mutationFn: depositApi, 12 | onSuccess: () => { 13 | queryClient.invalidateQueries({ queryKey: ['assets'] }); 14 | addToast(successMessages.deposit, 'success'); 15 | }, 16 | onError: (error: unknown) => { 17 | if (error instanceof Error) { 18 | addToast(errorMessages.default.deposit, 'error'); 19 | } else { 20 | addToast(errorMessages.default.general, 'error'); 21 | } 22 | }, 23 | }); 24 | }; 25 | 26 | export default useDeposit; 27 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/model/useGetTransactions.ts: -------------------------------------------------------------------------------- 1 | import { useInfiniteQuery } from '@tanstack/react-query'; 2 | import getTransactionsApi from '../api/getTransactionsApi'; 3 | 4 | const useGetTransactions = ({ currencyCode }: { currencyCode: string }) => { 5 | return useInfiniteQuery({ 6 | queryKey: ['transactions', currencyCode], 7 | queryFn: ({ pageParam }) => getTransactionsApi({ currencyCode, id: pageParam }), 8 | initialPageParam: null, 9 | getNextPageParam: (lastPage) => lastPage.nextId, 10 | getPreviousPageParam: () => null, 11 | }); 12 | }; 13 | export default useGetTransactions; 14 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetInfo/model/useWithdraw.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import withdrawApi from '../api/withdrawApi'; 3 | import { useToast } from '../../../shared/store/ToastContext'; 4 | import successMessages from '../../../shared/consts/successMessage'; 5 | import errorMessages from '../../../shared/consts/errorMessages'; 6 | 7 | const useWithdraw = () => { 8 | const { addToast } = useToast(); 9 | const queryClient = useQueryClient(); 10 | return useMutation({ 11 | mutationFn: withdrawApi, 12 | onSuccess: () => { 13 | queryClient.invalidateQueries({ queryKey: ['assets'] }); 14 | addToast(successMessages.withdraw, 'success'); 15 | }, 16 | onError: (error: unknown) => { 17 | if (error instanceof Error) { 18 | addToast(errorMessages.default.withdraw, 'error'); 19 | } else { 20 | addToast(errorMessages.default.general, 'error'); 21 | } 22 | }, 23 | }); 24 | }; 25 | 26 | export default useWithdraw; 27 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetList/UI/AssetItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import image from '../consts/mockImg.png'; 3 | import { MyAsset } from '../../../shared/types/MyAsset'; 4 | import formatPrice from '../../../shared/model/formatPrice'; 5 | 6 | type AssetItemProps = { 7 | asset: MyAsset; 8 | handleClick: () => void; 9 | }; 10 | 11 | const AssetItem: React.FC = ({ asset, handleClick }) => { 12 | return ( 13 |
  • 17 |
    18 | assetImage 19 |
    20 |
    {asset.name}
    21 |
    22 | {asset.currencyCode} 23 |
    24 |
    25 |
    26 | 27 |
    28 |
    {formatPrice(asset.amount)}
    29 |
    {asset.currencyCode}
    30 |
    31 |
  • 32 | ); 33 | }; 34 | 35 | export default AssetItem; 36 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetList/UI/BoxContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutProps } from '../../../shared/types/LayoutProps'; 3 | 4 | const BoxContainer: React.FC = ({ children }) => { 5 | return ( 6 |
    7 | {children} 8 |
    9 | ); 10 | }; 11 | 12 | export default BoxContainer; 13 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetList/UI/InfoBar.tsx: -------------------------------------------------------------------------------- 1 | const InfoBar = () => { 2 | return ( 3 |
    4 |
    자산 명
    5 |
    보유 수량
    6 |
    7 | ); 8 | }; 9 | 10 | export default InfoBar; 11 | -------------------------------------------------------------------------------- /view/src/entities/MyAssetList/consts/mockImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web02-BooBit/7cada7b270145338c430edcf5a5da162b48a91c4/view/src/entities/MyAssetList/consts/mockImg.png -------------------------------------------------------------------------------- /view/src/entities/MyAssetList/index.tsx: -------------------------------------------------------------------------------- 1 | import BoxContainer from './UI/BoxContainer'; 2 | import InfoBar from './UI/InfoBar'; 3 | import AssetItem from './UI/AssetItem'; 4 | import { MyAsset } from '../../shared/types/MyAsset'; 5 | 6 | type AssetListProps = { 7 | assetList: MyAsset[]; 8 | setSelectedAssetIdx: React.Dispatch>; 9 | }; 10 | 11 | const MyAssetList = ({ assetList, setSelectedAssetIdx }: AssetListProps) => { 12 | return ( 13 | 14 | 15 |
      16 | setSelectedAssetIdx(1)} /> 17 | setSelectedAssetIdx(0)} /> 18 |
    19 |
    20 | ); 21 | }; 22 | 23 | export default MyAssetList; 24 | -------------------------------------------------------------------------------- /view/src/entities/MyInfo/UI/InfoItem.tsx: -------------------------------------------------------------------------------- 1 | const InfoItem = ({ label, value }: { label: string; value: string }) => { 2 | return ( 3 |
    4 | {label} 5 |
    {value}
    6 |
    7 | ); 8 | }; 9 | 10 | export default InfoItem; 11 | -------------------------------------------------------------------------------- /view/src/entities/MyInfo/api/getProfileApi.ts: -------------------------------------------------------------------------------- 1 | const apiUrl = import.meta.env.VITE_AUTH_URL; 2 | const getProfileApi = async () => { 3 | const response = await fetch(`${apiUrl}/api/auth/profile`, { 4 | method: 'GET', 5 | credentials: 'include', 6 | }); 7 | 8 | if (!response.ok) { 9 | throw new Error(response.status.toString()); 10 | } 11 | 12 | return response.json(); 13 | }; 14 | 15 | export default getProfileApi; 16 | -------------------------------------------------------------------------------- /view/src/entities/MyInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import useGetProfile from './model/useGetProfile'; 2 | import InfoItem from './UI/InfoItem'; 3 | 4 | const MyInfo = () => { 5 | const { data: myInfo } = useGetProfile(); 6 | return myInfo ? ( 7 |
    8 | 9 | 10 |
    11 | ) : ( 12 |
    13 | ); 14 | }; 15 | 16 | export default MyInfo; 17 | -------------------------------------------------------------------------------- /view/src/entities/MyInfo/model/useGetProfile.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import getProfileApi from '../api/getProfileApi'; 3 | 4 | const useGetProfile = () => { 5 | return useQuery({ 6 | queryKey: ['profile'], 7 | queryFn: getProfileApi, 8 | retry: 1, 9 | }); 10 | }; 11 | 12 | export default useGetProfile; 13 | -------------------------------------------------------------------------------- /view/src/entities/MyOpenOrders/api/deleteOrderApi.ts: -------------------------------------------------------------------------------- 1 | const apiUrl = import.meta.env.VITE_TRANSACTION_URL; 2 | const deleteOrderApi = async ({ 3 | historyId, 4 | orderType, 5 | }: { 6 | historyId: number; 7 | orderType: 'BUY' | 'SELL'; 8 | }) => { 9 | const queryParams = new URLSearchParams({ 10 | orderType: orderType, 11 | }).toString(); 12 | 13 | const response = await fetch(`${apiUrl}/api/orders/${historyId}?${queryParams}`, { 14 | method: 'DELETE', 15 | credentials: 'include', 16 | }); 17 | 18 | if (!response.ok) { 19 | throw new Error(response.status.toString()); 20 | } 21 | 22 | return response.ok; 23 | }; 24 | 25 | export default deleteOrderApi; 26 | -------------------------------------------------------------------------------- /view/src/entities/MyOpenOrders/api/getPendingApi.ts: -------------------------------------------------------------------------------- 1 | const apiUrl = import.meta.env.VITE_TRANSACTION_URL; 2 | 3 | const getPendingApi = async ({ id }: { id: number | null }) => { 4 | const queryParams = new URLSearchParams({ 5 | id: id !== null ? String(id) : '', 6 | }).toString(); 7 | 8 | const response = await fetch(`${apiUrl}/api/orders/pending?${queryParams}`, { 9 | method: 'GET', 10 | credentials: 'include', 11 | }); 12 | 13 | if (!response.ok) { 14 | throw new Error(response.status.toString()); 15 | } 16 | 17 | return response.json(); 18 | }; 19 | 20 | export default getPendingApi; 21 | -------------------------------------------------------------------------------- /view/src/entities/MyOpenOrders/model/OrderType.ts: -------------------------------------------------------------------------------- 1 | export type OrderType = { 2 | historyId: number; 3 | createdAt: string; 4 | orderType: 'BUY' | 'SELL'; 5 | price: number; 6 | quantity: number; 7 | unfilledAmount: number; 8 | }; 9 | -------------------------------------------------------------------------------- /view/src/entities/MyOpenOrders/model/useDeleteOrder.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import deleteOrderApi from '../api/deleteOrderApi'; 3 | import successMessages from '../../../shared/consts/successMessage'; 4 | import errorMessages from '../../../shared/consts/errorMessages'; 5 | import { useToast } from '../../../shared/store/ToastContext'; 6 | import { useAuthActions } from '../../../shared/store/auth/authActions'; 7 | import { useNavigate } from 'react-router-dom'; 8 | 9 | const useDeleteOrder = () => { 10 | const queryClient = useQueryClient(); 11 | const { addToast } = useToast(); 12 | const { logout } = useAuthActions(); 13 | const navigate = useNavigate(); 14 | 15 | return useMutation({ 16 | mutationFn: deleteOrderApi, 17 | onSuccess: () => { 18 | setTimeout(() => { 19 | queryClient.invalidateQueries({ queryKey: ['pending'] }); 20 | queryClient.invalidateQueries({ queryKey: ['orderHistory'] }); 21 | 22 | addToast(successMessages.deleteOrder, 'success'); 23 | }, 1000); 24 | }, 25 | onError: (error: unknown) => { 26 | if (error instanceof Error) { 27 | if (error.message === '403') { 28 | addToast(errorMessages[403], 'error'); 29 | logout(); 30 | navigate('/signin'); 31 | return; 32 | } 33 | addToast(errorMessages.default.deleteOrder, 'error'); 34 | } else { 35 | addToast(errorMessages.default.general, 'error'); 36 | } 37 | }, 38 | }); 39 | }; 40 | 41 | export default useDeleteOrder; 42 | -------------------------------------------------------------------------------- /view/src/entities/MyOpenOrders/model/useGetPending.ts: -------------------------------------------------------------------------------- 1 | import { useInfiniteQuery } from '@tanstack/react-query'; 2 | import getPendingApi from '../api/getPendingApi'; 3 | 4 | const useGetPending = () => { 5 | return useInfiniteQuery({ 6 | queryKey: ['pending'], 7 | queryFn: ({ pageParam }) => getPendingApi({ id: pageParam }), 8 | initialPageParam: null, 9 | getNextPageParam: (lastPage) => lastPage.nextId, 10 | getPreviousPageParam: () => null, 11 | }); 12 | }; 13 | 14 | export default useGetPending; 15 | -------------------------------------------------------------------------------- /view/src/entities/MyOrderHistory/api/getOrdersApi.ts: -------------------------------------------------------------------------------- 1 | const getOrdersApi = async ({ id }: { id: number | null }) => { 2 | const queryParams = new URLSearchParams({ 3 | id: id !== null ? String(id) : '', 4 | }).toString(); 5 | const apiUrl = import.meta.env.VITE_BALANCE_URL; 6 | 7 | const response = await fetch(`${apiUrl}/api/users/orderHistory?${queryParams}`, { 8 | method: 'GET', 9 | credentials: 'include', 10 | }); 11 | 12 | if (!response.ok) { 13 | throw new Error(response.status.toString()); 14 | } 15 | 16 | return response.json(); 17 | }; 18 | 19 | export default getOrdersApi; 20 | -------------------------------------------------------------------------------- /view/src/entities/MyOrderHistory/const/status.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | ORDERED: '주문', 3 | CANCELED: '취소', 4 | }; 5 | -------------------------------------------------------------------------------- /view/src/entities/MyOrderHistory/model/OrderType.ts: -------------------------------------------------------------------------------- 1 | export interface OrderType { 2 | orderType: 'BUY' | 'SELL'; 3 | coinCode: string; 4 | quantity: string; 5 | price: string; 6 | status: 'ORDERED' | 'CANCELED'; 7 | timestamp: string; 8 | } 9 | -------------------------------------------------------------------------------- /view/src/entities/MyOrderHistory/model/useGetOrders.ts: -------------------------------------------------------------------------------- 1 | import { useInfiniteQuery } from '@tanstack/react-query'; 2 | import getOrdersApi from '../api/getOrdersApi'; 3 | 4 | const useGetOrders = () => { 5 | return useInfiniteQuery({ 6 | queryKey: ['orderHistory'], 7 | queryFn: ({ pageParam }) => getOrdersApi({ id: pageParam }), 8 | initialPageParam: null, 9 | getNextPageParam: (lastPage) => lastPage.nextId, 10 | getPreviousPageParam: () => null, 11 | }); 12 | }; 13 | export default useGetOrders; 14 | -------------------------------------------------------------------------------- /view/src/entities/MyTradeHistory/api/getTradesApi.ts: -------------------------------------------------------------------------------- 1 | const getTradesApi = async ({ id }: { id: number | null }) => { 2 | const queryParams = new URLSearchParams({ 3 | id: id !== null ? String(id) : '', 4 | }).toString(); 5 | const apiUrl = import.meta.env.VITE_TRANSACTION_URL; 6 | 7 | const response = await fetch(`${apiUrl}/api/orders?${queryParams}`, { 8 | method: 'GET', 9 | credentials: 'include', 10 | }); 11 | 12 | if (!response.ok) { 13 | throw new Error(response.status.toString()); 14 | } 15 | 16 | return response.json(); 17 | }; 18 | 19 | export default getTradesApi; 20 | -------------------------------------------------------------------------------- /view/src/entities/MyTradeHistory/model/TradeType.ts: -------------------------------------------------------------------------------- 1 | export interface TradeType { 2 | tradeId: string; 3 | orderType: 'BUY' | 'SELL'; 4 | coinCode: string; 5 | quantity: string; 6 | price: string; 7 | totalAmount: string; 8 | tradedAt: string; 9 | } 10 | -------------------------------------------------------------------------------- /view/src/entities/MyTradeHistory/model/useGetTrades.ts: -------------------------------------------------------------------------------- 1 | import { useInfiniteQuery } from '@tanstack/react-query'; 2 | import getTradesApi from '../api/getTradesApi'; 3 | 4 | const useGetTrades = () => { 5 | return useInfiniteQuery({ 6 | queryKey: ['tradeHistory'], 7 | queryFn: ({ pageParam }) => getTradesApi({ id: pageParam }), 8 | initialPageParam: null, 9 | getNextPageParam: (lastPage) => lastPage.nextId, 10 | getPreviousPageParam: () => null, 11 | }); 12 | }; 13 | export default useGetTrades; 14 | -------------------------------------------------------------------------------- /view/src/entities/OrderBook/UI/OrderItem.tsx: -------------------------------------------------------------------------------- 1 | import { OrderType } from '../../../shared/types/socket/OrderType'; 2 | 3 | interface OrderItemProps { 4 | setOrderPrice: React.Dispatch>; 5 | orderInfo: OrderType; 6 | } 7 | 8 | const OrderItem: React.FC = ({ setOrderPrice, orderInfo }) => { 9 | const { price, priceChangeRate, amount } = orderInfo; 10 | const handleClick = () => { 11 | setOrderPrice(price.toLocaleString()); 12 | }; 13 | return ( 14 |
    18 |
    19 |
    {price.toLocaleString()}
    20 |
    {priceChangeRate}%
    21 |
    22 |
    {amount}
    23 |
    24 | ); 25 | }; 26 | 27 | export default OrderItem; 28 | -------------------------------------------------------------------------------- /view/src/entities/OrderBook/index.tsx: -------------------------------------------------------------------------------- 1 | import { OrderType } from '../../shared/types/socket/OrderType'; 2 | import OrderItem from './UI/OrderItem'; 3 | 4 | interface OrderBookProps { 5 | currentPrice?: number; 6 | hasIncreased: boolean; 7 | setOrderPrice: React.Dispatch>; 8 | orderBook?: { 9 | sell: OrderType[]; 10 | buy: OrderType[]; 11 | }; 12 | } 13 | 14 | const OrderBook: React.FC = ({ 15 | currentPrice, 16 | hasIncreased, 17 | setOrderPrice, 18 | orderBook, 19 | }) => { 20 | return ( 21 |
    22 |
    23 | {orderBook && 24 | orderBook.sell.map((o, i) => { 25 | return ( 26 | 27 | ); 28 | })} 29 |
    30 | 31 |
    34 | {currentPrice && currentPrice.toLocaleString()} 35 |
    36 | 37 |
    38 | {orderBook && 39 | orderBook.buy.map((o, i) => { 40 | return ( 41 | 42 | ); 43 | })} 44 |
    45 |
    46 | ); 47 | }; 48 | 49 | export default OrderBook; 50 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/UI/Button.tsx: -------------------------------------------------------------------------------- 1 | interface ButtonProps { 2 | label: string; 3 | onClick: (e: React.MouseEvent) => void; 4 | styles: string; 5 | } 6 | 7 | const Button: React.FC = ({ label, onClick, styles }) => ( 8 | 11 | ); 12 | 13 | export default Button; 14 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/UI/InputNumber.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from 'react'; 2 | 3 | interface InputNumberProps { 4 | coinCode: string; 5 | amount: string; 6 | setAmount: React.Dispatch>; 7 | updateRelatedValues?: (value: number) => void; 8 | } 9 | 10 | const InputNumber: React.FC = ({ 11 | coinCode, 12 | amount, 13 | setAmount, 14 | updateRelatedValues, 15 | }) => { 16 | const handleChange = (e: ChangeEvent) => { 17 | const value = e.target.value.replace(/,/g, ''); 18 | 19 | const [intPart, decimalPart] = value.split('.'); 20 | 21 | if ( 22 | isNaN(Number(value)) || 23 | (coinCode === 'KRW' && value.includes('.')) || 24 | (decimalPart && decimalPart.length === 7) 25 | ) { 26 | return; 27 | } 28 | 29 | if (updateRelatedValues) { 30 | updateRelatedValues(parseFloat(value)); 31 | } 32 | 33 | const newValue = 34 | Number(intPart).toLocaleString() + (decimalPart !== undefined ? `.${decimalPart}` : ''); 35 | 36 | setAmount(newValue); 37 | }; 38 | 39 | return ( 40 | 46 | ); 47 | }; 48 | 49 | export default InputNumber; 50 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/UI/SectionBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | const SectionBlock: React.FC<{ 4 | title: string; 5 | subtitle?: string; 6 | children: ReactNode; 7 | }> = ({ title, subtitle, children }) => { 8 | return ( 9 |
    10 |
    11 |
    {title}
    12 | {subtitle &&
    ({subtitle})
    } 13 |
    14 |
    {children}
    15 |
    16 | ); 17 | }; 18 | export default SectionBlock; 19 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/api/getAvailableAssetApi.ts: -------------------------------------------------------------------------------- 1 | const apiUrl = import.meta.env.VITE_BALANCE_URL; 2 | const getAvailableAssetApi = async ({ currencyCode }: { currencyCode: string }) => { 3 | const response = await fetch(`${apiUrl}/api/users/available/${currencyCode}`, { 4 | method: 'GET', 5 | credentials: 'include', 6 | }); 7 | 8 | if (!response.ok) { 9 | throw new Error(response.status.toString()); 10 | } 11 | 12 | return response.json(); 13 | }; 14 | 15 | export default getAvailableAssetApi; 16 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/api/postBuyApi.ts: -------------------------------------------------------------------------------- 1 | import { RequestDataType } from '../model/RequestDataType'; 2 | 3 | const apiUrl = import.meta.env.VITE_TRANSACTION_URL; 4 | const postBuyApi = async (requestData: RequestDataType) => { 5 | const response = await fetch(`${apiUrl}/api/orders/limit/buy`, { 6 | method: 'POST', 7 | credentials: 'include', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify(requestData), 12 | }); 13 | 14 | if (!response.ok) { 15 | if (response.status === 404) { 16 | const error = await response.json(); 17 | throw new Error(error.message); 18 | } 19 | throw new Error(response.status.toString()); 20 | } 21 | 22 | return response.ok; 23 | }; 24 | 25 | export default postBuyApi; 26 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/api/postSellApi.ts: -------------------------------------------------------------------------------- 1 | import { RequestDataType } from '../model/RequestDataType'; 2 | 3 | const apiUrl = import.meta.env.VITE_TRANSACTION_URL; 4 | const postSellApi = async (requestData: RequestDataType) => { 5 | const response = await fetch(`${apiUrl}/api/orders/limit/sell`, { 6 | method: 'POST', 7 | credentials: 'include', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify(requestData), 12 | }); 13 | 14 | if (!response.ok) { 15 | if (response.status === 404) { 16 | const error = await response.json(); 17 | throw new Error(error.message); 18 | } 19 | throw new Error(response.status.toString()); 20 | } 21 | 22 | return response.ok; 23 | }; 24 | 25 | export default postSellApi; 26 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/const/orderCategory.ts: -------------------------------------------------------------------------------- 1 | export default ['매수', '매도']; 2 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/model/RequestDataType.ts: -------------------------------------------------------------------------------- 1 | export interface RequestDataType { 2 | coinCode: string; 3 | amount: number; 4 | price: number; 5 | } 6 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/model/useGetAvailableAsset.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import getAvailableAssetApi from '../api/getAvailableAssetApi'; 3 | 4 | const useGetAvailableAsset = ({ currencyCode }: { currencyCode: string }) => { 5 | return useQuery({ 6 | queryKey: ['available'], 7 | queryFn: () => getAvailableAssetApi({ currencyCode }), 8 | }); 9 | }; 10 | 11 | export default useGetAvailableAsset; 12 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/model/useOrderAmount.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import formatPrice from '../../../shared/model/formatPrice'; 3 | 4 | interface UseOrderAmountProps { 5 | tradePrice: string; 6 | } 7 | 8 | const useOrderAmount = ({ tradePrice }: UseOrderAmountProps) => { 9 | const [amount, setAmount] = useState(''); 10 | const [price, setPrice] = useState(''); 11 | 12 | const updatePriceWithAmount = (value: number) => { 13 | if (isNaN(value)) { 14 | setPrice(''); 15 | return; 16 | } 17 | const tradePriceToNum = Number(tradePrice.replace(/,/g, '')); 18 | 19 | const result = formatPrice(Math.floor(value * tradePriceToNum)); 20 | setPrice(result); 21 | }; 22 | 23 | const updateAmountWithPrice = (value: number) => { 24 | if (isNaN(value)) { 25 | setAmount(''); 26 | return; 27 | } 28 | const tradePriceToNum = Number(tradePrice.replace(/,/g, '')); 29 | 30 | const result = formatPrice((value / tradePriceToNum).toFixed(6)); 31 | setAmount(result); 32 | }; 33 | 34 | const reset = () => { 35 | setAmount(''); 36 | setPrice(''); 37 | }; 38 | 39 | return { 40 | amount, 41 | setAmount, 42 | price, 43 | setPrice, 44 | updatePriceWithAmount, 45 | updateAmountWithPrice, 46 | reset, 47 | }; 48 | }; 49 | 50 | export default useOrderAmount; 51 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/model/usePostBuy.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import postBuyApi from '../api/postBuyApi'; 3 | import { useToast } from '../../../shared/store/ToastContext'; 4 | import errorMessages from '../../../shared/consts/errorMessages'; 5 | import successMessages from '../../../shared/consts/successMessage'; 6 | 7 | const usePostBuy = () => { 8 | const { addToast } = useToast(); 9 | const queryClient = useQueryClient(); 10 | 11 | return useMutation({ 12 | mutationFn: postBuyApi, 13 | onSuccess: () => { 14 | queryClient.invalidateQueries({ queryKey: ['available'] }); 15 | addToast(successMessages.buy, 'success'); 16 | }, 17 | onError: (error: unknown) => { 18 | if (error instanceof Error) { 19 | if (error.message.startsWith('Not')) 20 | addToast(errorMessages[400].insufficientBalance, 'error'); 21 | else if (error.message.startsWith('coin')) addToast(errorMessages[400].coinCode, 'error'); 22 | else addToast(errorMessages.default.buy, 'error'); 23 | } else { 24 | addToast(errorMessages.default.general, 'error'); 25 | } 26 | }, 27 | }); 28 | }; 29 | 30 | export default usePostBuy; 31 | -------------------------------------------------------------------------------- /view/src/entities/OrderPanel/model/usePostSell.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { useToast } from '../../../shared/store/ToastContext'; 3 | import errorMessages from '../../../shared/consts/errorMessages'; 4 | import successMessages from '../../../shared/consts/successMessage'; 5 | import postSellApi from '../api/postSellApi'; 6 | 7 | const usePostSell = () => { 8 | const { addToast } = useToast(); 9 | const queryClient = useQueryClient(); 10 | 11 | return useMutation({ 12 | mutationFn: postSellApi, 13 | onSuccess: () => { 14 | queryClient.invalidateQueries({ queryKey: ['available'] }); 15 | addToast(successMessages.sell, 'success'); 16 | }, 17 | onError: (error: unknown) => { 18 | if (error instanceof Error) { 19 | if (error.message.startsWith('Not')) 20 | addToast(errorMessages[400].insufficientBalance, 'error'); 21 | else if (error.message.startsWith('coin')) addToast(errorMessages[400].coinCode, 'error'); 22 | else addToast(errorMessages.default.sell, 'error'); 23 | } else { 24 | addToast(errorMessages.default.general, 'error'); 25 | } 26 | }, 27 | }); 28 | }; 29 | 30 | export default usePostSell; 31 | -------------------------------------------------------------------------------- /view/src/entities/TradeRecords/UI/TradeRecordRow.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface TradeRecordsRowProps { 4 | children: ReactNode; 5 | flex?: boolean; 6 | styles?: string; 7 | } 8 | 9 | const TradeRecordsRow: React.FC = ({ 10 | children, 11 | flex = false, 12 | styles = '', 13 | }) => { 14 | const flexStyle = flex ? 'flex justify-between items-center px-[2rem]' : ''; 15 | return {children}; 16 | }; 17 | export default TradeRecordsRow; 18 | -------------------------------------------------------------------------------- /view/src/entities/TradeRecords/UI/TradeRecordsCell.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | const TradeRecordsCell: React.FC<{ 4 | children: ReactNode; 5 | start?: boolean; 6 | end?: boolean; 7 | styles?: string; 8 | }> = ({ children, start = false, end = false, styles = '' }) => { 9 | const startStyle = start ? 'text-start ' : ''; 10 | const endStyle = end ? 'text-end ' : ''; 11 | return {children}; 12 | }; 13 | export default TradeRecordsCell; 14 | -------------------------------------------------------------------------------- /view/src/pages/Home/UI/TimeScaleItem.tsx: -------------------------------------------------------------------------------- 1 | import { ChartTimeScaleType } from '../../../shared/types/ChartTimeScaleType'; 2 | 3 | type TimeScaleType = { 4 | value: ChartTimeScaleType; 5 | label: string; 6 | rightBorder: boolean; 7 | selectedTimeScale: ChartTimeScaleType; 8 | setSelectedTimeScale: React.Dispatch>; 9 | }; 10 | 11 | const TimeScaleItem: React.FC = ({ 12 | value, 13 | label, 14 | rightBorder, 15 | selectedTimeScale, 16 | setSelectedTimeScale, 17 | }) => { 18 | const handelClick = (e: React.MouseEvent) => { 19 | e.preventDefault(); 20 | setSelectedTimeScale(value); 21 | }; 22 | return ( 23 | 30 | ); 31 | }; 32 | 33 | export default TimeScaleItem; 34 | -------------------------------------------------------------------------------- /view/src/pages/Home/UI/TimeScaleSelector.tsx: -------------------------------------------------------------------------------- 1 | import { ChartTimeScaleType } from '../../../shared/types/ChartTimeScaleType'; 2 | import TimeScaleItem from './TimeScaleItem'; 3 | 4 | const timeScaleOptions: Array<{ 5 | value: ChartTimeScaleType; 6 | label: string; 7 | rightBorder: boolean; 8 | }> = [ 9 | { value: '1sec', label: '초', rightBorder: true }, 10 | { value: '1min', label: '1분', rightBorder: false }, 11 | { value: '10min', label: '10분', rightBorder: false }, 12 | { value: '30min', label: '30분', rightBorder: true }, 13 | { value: '1hour', label: '시간', rightBorder: true }, 14 | { value: '1day', label: '일', rightBorder: true }, 15 | { value: '1week', label: '주', rightBorder: true }, 16 | { value: '1month', label: '월', rightBorder: false }, 17 | ]; 18 | 19 | const TimeScaleSelector: React.FC<{ 20 | selectedTimeScale: ChartTimeScaleType; 21 | setSelectedTimeScale: React.Dispatch>; 22 | }> = ({ selectedTimeScale, setSelectedTimeScale }) => { 23 | return ( 24 |
    25 | {timeScaleOptions.map((s) => { 26 | return ( 27 | 35 | ); 36 | })} 37 |
    38 | ); 39 | }; 40 | 41 | export default TimeScaleSelector; 42 | -------------------------------------------------------------------------------- /view/src/pages/Home/UI/Title.tsx: -------------------------------------------------------------------------------- 1 | import up from '../../../shared/images/up.svg'; 2 | import down from '../../../shared/images/down.svg'; 3 | import formatPrice from '../../../shared/model/formatPrice'; 4 | import calculateChangeRate from '../../../shared/model/calculateChangeRate'; 5 | 6 | interface TitleProps { 7 | currentPrice: number; 8 | lastDayClose: number; 9 | } 10 | 11 | const Title: React.FC = ({ currentPrice, lastDayClose }) => { 12 | return ( 13 |
    14 |
    비트코인
    15 |
    lastDayClose ? 'text-positive' : 'text-negative'} `} 17 | > 18 |
    19 | 20 | {currentPrice ? formatPrice(currentPrice) : ''} 21 | 22 | KRW 23 |
    24 | 25 |
    26 | {calculateChangeRate(currentPrice, lastDayClose)} 27 | lastDayClose ? up : down}`} 29 | alt="mark" 30 | height={14} 31 | width={14} 32 | style={{ marginLeft: '0.5rem' }} 33 | /> 34 | {formatPrice(currentPrice - lastDayClose)} 35 |
    36 |
    37 |
    38 | ); 39 | }; 40 | 41 | export default Title; 42 | -------------------------------------------------------------------------------- /view/src/pages/MyPage/UI/CategoryItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface CategoryItemProps { 4 | category: string; 5 | isSelected: boolean; 6 | onClick: () => void; 7 | } 8 | 9 | const CategoryItem: React.FC = ({ 10 | category, 11 | isSelected, 12 | onClick, 13 | }) => ( 14 |
  • 18 | {category} 19 |
  • 20 | ); 21 | 22 | export default CategoryItem; 23 | -------------------------------------------------------------------------------- /view/src/pages/MyPage/UI/MainviewLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutProps } from '../../../shared/types/LayoutProps'; 3 | 4 | const MainviewLayout: React.FC = ({ children }) => { 5 | return ( 6 |
    9 | {children} 10 |
    11 | ); 12 | }; 13 | 14 | export default MainviewLayout; 15 | -------------------------------------------------------------------------------- /view/src/pages/MyPage/UI/SubviewLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutProps } from '../../../shared/types/LayoutProps'; 3 | 4 | const SubviewLayout: React.FC = ({ children }) => { 5 | return ( 6 | 9 | ); 10 | }; 11 | 12 | export default SubviewLayout; 13 | -------------------------------------------------------------------------------- /view/src/pages/MyPage/UI/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface TitleProps { 4 | content: string; 5 | } 6 | 7 | const Title: React.FC = ({ content }) => { 8 | return
    {content}
    ; 9 | }; 10 | 11 | export default Title; 12 | -------------------------------------------------------------------------------- /view/src/pages/MyPage/consts/category.ts: -------------------------------------------------------------------------------- 1 | const CATEGORY = ['내 정보', '입출금', '투자 내역']; 2 | 3 | export default CATEGORY; 4 | -------------------------------------------------------------------------------- /view/src/pages/SignIn/api/signinApi.ts: -------------------------------------------------------------------------------- 1 | import { FormData } from '../model/formDataType'; 2 | 3 | const apiUrl = import.meta.env.VITE_AUTH_URL; 4 | const signinApi = async (formData: FormData) => { 5 | const response = await fetch(`${apiUrl}/api/auth/login`, { 6 | method: 'POST', 7 | credentials: 'include', 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | }, 11 | body: JSON.stringify(formData), 12 | }); 13 | 14 | if (!response.ok) { 15 | throw new Error(response.status.toString()); 16 | } 17 | 18 | return response.json(); 19 | }; 20 | 21 | export default signinApi; 22 | -------------------------------------------------------------------------------- /view/src/pages/SignIn/model/formDataType.ts: -------------------------------------------------------------------------------- 1 | export interface FormData { 2 | email: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /view/src/pages/SignIn/model/useSignin.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | import signinApi from '../api/signinApi'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useAuthActions } from '../../../shared/store/auth/authActions'; 5 | import errorMessages from '../../../shared/consts/errorMessages'; 6 | 7 | const useSignin = (onErrorCallback: (message: string) => void) => { 8 | const navigate = useNavigate(); 9 | const { login } = useAuthActions(); 10 | return useMutation({ 11 | mutationFn: signinApi, 12 | onSuccess: () => { 13 | navigate('/'); 14 | login(); 15 | }, 16 | onError: (error: unknown) => { 17 | if (error instanceof Error) { 18 | if (error.message === '401') { 19 | onErrorCallback(errorMessages[401]); 20 | } else { 21 | onErrorCallback(errorMessages.default.signin); 22 | } 23 | } else { 24 | onErrorCallback(errorMessages.default.general); 25 | } 26 | }, 27 | }); 28 | }; 29 | 30 | export default useSignin; 31 | -------------------------------------------------------------------------------- /view/src/pages/SignIn/model/useSigninForm.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useState } from 'react'; 2 | import { FormData } from './formDataType'; 3 | 4 | interface FormError { 5 | isError: boolean; 6 | errorMessage: string; 7 | } 8 | 9 | const useSigninForm = () => { 10 | const [formData, setFormData] = useState({ email: '', password: '' }); 11 | const [error, setError] = useState({ isError: false, errorMessage: '' }); 12 | 13 | const handleChange = (e: ChangeEvent) => { 14 | const { name, value } = e.target; 15 | setFormData((prevData) => ({ 16 | ...prevData, 17 | [name]: value, 18 | })); 19 | }; 20 | 21 | const validateForm = () => { 22 | if (formData.email === '' || formData.password === '') { 23 | setError({ isError: true, errorMessage: '이메일 또는 비밀번호를 입력해주세요.' }); 24 | return false; 25 | } 26 | return true; 27 | }; 28 | 29 | const updateErrorMessage = (message: string) => { 30 | setError({ isError: true, errorMessage: message }); 31 | }; 32 | return { formData, error, handleChange, validateForm, updateErrorMessage }; 33 | }; 34 | 35 | export default useSigninForm; 36 | -------------------------------------------------------------------------------- /view/src/pages/SignUp/UI/Guideline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface GuidelineProps { 4 | content: string; 5 | } 6 | 7 | const Guideline: React.FC = ({ content }) => { 8 | return
    🗨️ {content}
    ; 9 | }; 10 | 11 | export default Guideline; 12 | -------------------------------------------------------------------------------- /view/src/pages/SignUp/UI/LabeledInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react'; 2 | import InputField from '../../../shared/UI/InputFeild'; 3 | 4 | interface LabeledInputProps { 5 | label: string; 6 | type: string; 7 | placeholder: string; 8 | name: string; 9 | value: string; 10 | onChange: (e: ChangeEvent) => void; 11 | isError?: boolean; 12 | errorMessage?: string; 13 | } 14 | 15 | const LabeledInput: React.FC = ({ 16 | label, 17 | type, 18 | placeholder, 19 | name, 20 | value, 21 | onChange, 22 | isError = false, 23 | errorMessage = '', 24 | }) => ( 25 |
    26 |
    {label}
    27 | 36 |
    37 | ); 38 | 39 | export default LabeledInput; 40 | -------------------------------------------------------------------------------- /view/src/pages/SignUp/api/signupApi.ts: -------------------------------------------------------------------------------- 1 | import { FormData } from '../model/formDataType'; 2 | 3 | const apiUrl = import.meta.env.VITE_AUTH_URL; 4 | const signUpApi = async (formData: FormData) => { 5 | const response = await fetch(`${apiUrl}/api/auth/signup`, { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify(formData), 11 | }); 12 | 13 | if (!response.ok) { 14 | throw new Error(response.status.toString()); 15 | } 16 | 17 | return response.json(); 18 | }; 19 | 20 | export default signUpApi; 21 | -------------------------------------------------------------------------------- /view/src/pages/SignUp/model/formDataType.ts: -------------------------------------------------------------------------------- 1 | export interface FormData { 2 | email: string; 3 | name: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /view/src/pages/SignUp/model/formValidationType.ts: -------------------------------------------------------------------------------- 1 | export default interface FormValidationType { 2 | email: { hasError: boolean; message: string }; 3 | password: { hasError: boolean; message: string }; 4 | passwordCheck: { hasError: boolean; message: string }; 5 | total: { hasError: boolean; message: string }; 6 | } 7 | -------------------------------------------------------------------------------- /view/src/pages/SignUp/model/useSignup.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from '@tanstack/react-query'; 2 | import signUpApi from '../api/signupApi'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import FormValidationType from './formValidationType'; 5 | import { useToast } from '../../../shared/store/ToastContext'; 6 | import successMessages from '../../../shared/consts/successMessage'; 7 | import errorMessages from '../../../shared/consts/errorMessages'; 8 | 9 | const useSignup = ( 10 | onErrorCallback: (field: keyof FormValidationType, isError: boolean, message: string) => void 11 | ) => { 12 | const navigate = useNavigate(); 13 | const { addToast } = useToast(); 14 | 15 | return useMutation({ 16 | mutationFn: signUpApi, 17 | onSuccess: () => { 18 | addToast(successMessages.signup, 'success'); 19 | navigate('/signin'); 20 | }, 21 | onError: (error: unknown) => { 22 | if (error instanceof Error) { 23 | if (error.message === '400') onErrorCallback('total', true, errorMessages[400].general); 24 | else if (error.message === '409') onErrorCallback('email', true, errorMessages[409]); 25 | else onErrorCallback('total', true, errorMessages.default.signup); 26 | } else { 27 | onErrorCallback('total', true, errorMessages.default.general); 28 | } 29 | }, 30 | }); 31 | }; 32 | 33 | export default useSignup; 34 | -------------------------------------------------------------------------------- /view/src/shared/UI/InputFeild.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react'; 2 | 3 | interface InputFieldProps { 4 | type: string; 5 | name: string; 6 | placeholder: string; 7 | value: string; 8 | onChange: (e: ChangeEvent) => void; 9 | isError?: boolean; 10 | errorMessage?: string; 11 | } 12 | 13 | const InputField: React.FC = ({ 14 | type, 15 | name, 16 | placeholder, 17 | value, 18 | onChange, 19 | isError = false, 20 | errorMessage = '', 21 | }) => { 22 | const handleKeyDown = (e: React.KeyboardEvent) => { 23 | if (e.key === 'Enter') { 24 | e.preventDefault(); 25 | } 26 | }; 27 | 28 | return ( 29 |
    30 | 39 | {isError && } 40 |
    41 | ); 42 | }; 43 | 44 | export default InputField; 45 | -------------------------------------------------------------------------------- /view/src/shared/UI/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface SubmitButtonProps { 4 | height: string; 5 | content: string; 6 | amount?: string; 7 | onClick: (e: React.MouseEvent) => void; 8 | } 9 | 10 | const SubmitButton: React.FC = ({ 11 | height, 12 | content, 13 | amount = 'true', 14 | onClick, 15 | }) => { 16 | const disable = amount === '' || amount === '0'; 17 | const disableStyle = disable 18 | ? 'bg-surface-hover-light text-text-dark' 19 | : 'bg-accent text-text-light'; 20 | return ( 21 | 28 | ); 29 | }; 30 | 31 | export default SubmitButton; 32 | -------------------------------------------------------------------------------- /view/src/shared/UI/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TabItem from './TabItem'; 3 | 4 | interface TabItemProps { 5 | selectedCate: string; 6 | setSelectedCate: React.Dispatch>; 7 | categories: string[]; 8 | width?: string; 9 | } 10 | 11 | const Tab: React.FC = ({ 12 | selectedCate, 13 | setSelectedCate, 14 | categories, 15 | width = 'w-1/3', 16 | }) => { 17 | return ( 18 |
    19 | {categories.map((c, i) => { 20 | return ( 21 | 28 | ); 29 | })} 30 |
    31 | ); 32 | }; 33 | 34 | export default Tab; 35 | -------------------------------------------------------------------------------- /view/src/shared/UI/TabItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface TabItemProps { 4 | content: string; 5 | isSeleted: boolean; 6 | handleClick: React.Dispatch>; 7 | width?: string; 8 | } 9 | 10 | const TabItem: React.FC = ({ content, isSeleted, handleClick, width }) => { 11 | const selectedClasses = 'border-b-[2px] border-border-alt text-display-bold-16'; 12 | 13 | return ( 14 |
    handleClick(content)} 17 | > 18 | {content} 19 |
    20 | ); 21 | }; 22 | 23 | export default TabItem; 24 | -------------------------------------------------------------------------------- /view/src/shared/UI/TableCell.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface TableCellProps { 4 | children: ReactNode; 5 | width: string; 6 | styles?: string; 7 | } 8 | 9 | const TableCell: React.FC = ({ children, width, styles = '' }) => { 10 | return {children}; 11 | }; 12 | 13 | export default TableCell; 14 | -------------------------------------------------------------------------------- /view/src/shared/UI/TableRow.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface TableRowProps { 4 | children: ReactNode; 5 | height: string; 6 | styles?: string; 7 | } 8 | 9 | const TableRow: React.FC = ({ children, height, styles = '' }) => { 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export default TableRow; 18 | -------------------------------------------------------------------------------- /view/src/shared/UI/Toast.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useToast } from '../store/ToastContext'; 3 | import success from '../images/success.svg'; 4 | import error from '../images/error.svg'; 5 | import info from '../images/info.svg'; 6 | 7 | const Toast: React.FC = () => { 8 | const { toasts, removeToast } = useToast(); 9 | 10 | return ( 11 |
    12 | {toasts.map((toast) => ( 13 |
    removeToast(toast.id)} 26 | > 27 | icon 33 |
    {toast.message}
    34 |
    35 | ))} 36 |
    37 | ); 38 | }; 39 | 40 | export default Toast; 41 | -------------------------------------------------------------------------------- /view/src/shared/api/getAssetsApi.ts: -------------------------------------------------------------------------------- 1 | const apiUrl = import.meta.env.VITE_BALANCE_URL; 2 | const getAssetsApi = async () => { 3 | const response = await fetch(`${apiUrl}/api/users/assets`, { 4 | method: 'GET', 5 | credentials: 'include', 6 | }); 7 | 8 | if (!response.ok) { 9 | throw new Error(response.status.toString()); 10 | } 11 | 12 | return response.json(); 13 | }; 14 | 15 | export default getAssetsApi; 16 | -------------------------------------------------------------------------------- /view/src/shared/consts/errorMessages.ts: -------------------------------------------------------------------------------- 1 | const errorMessages = { 2 | '400': { 3 | general: '잘못된 요청입니다. 입력값을 확인해주세요.', 4 | coinCode: '코드 값은 KRW 또는 BTC 중 하나여야 합니다.', 5 | insufficientBalance: '잔액이 부족합니다. 잔액을 확인해주세요.', 6 | }, 7 | '401': '이메일 혹은 비밀 번호를 확인해주세요.', 8 | '403': '로그인이 필요한 서비스입니다.', 9 | '409': '이미 사용 중인 이메일 주소입니다. 다른 이메일을 사용해 주세요.', 10 | '500': '서버에 문제가 발생했습니다. 잠시 후 다시 시도해주세요.', 11 | 12 | default: { 13 | signup: '회원가입에 실패했습니다. 다시 시도해 주세요.', 14 | signin: '로그인에 실패했습니다. 다시 시도해주세요.', 15 | signout: '로그아웃에 실패했습니다. 다시 시도해주세요.', 16 | withdraw: '출금이 실패했습니다. 다시 시도해주세요.', 17 | deposit: '입금이 실패했습니다. 다시 시도해주세요.', 18 | buy: '매수 주문을 처리할 수 없습니다. 다시 시도해 주세요.', 19 | sell: '매도 주문을 처리할 수 없습니다. 다시 시도해 주세요.', 20 | deleteOrder: '주문 취소에 실패하였습니다. 다시 시도해 주세요.', 21 | general: '요청 처리 중 문제가 발생했습니다. 다시 시도해주세요.', 22 | }, 23 | }; 24 | 25 | export default errorMessages; 26 | -------------------------------------------------------------------------------- /view/src/shared/consts/successMessage.ts: -------------------------------------------------------------------------------- 1 | const successMessages = { 2 | withdraw: '출금이 성공적으로 완료되었습니다.', 3 | deposit: '입금이 성공적으로 완료되었습니다.', 4 | signup: '회원 가입 되었습니다.', 5 | buy: '매수 주문이 완료되었습니다.', 6 | sell: '매도 주문이 완료되었습니다.', 7 | deleteOrder: '주문 취소가 접수되었습니다.', 8 | }; 9 | 10 | export default successMessages; 11 | -------------------------------------------------------------------------------- /view/src/shared/images/BuBu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web02-BooBit/7cada7b270145338c430edcf5a5da162b48a91c4/view/src/shared/images/BuBu.png -------------------------------------------------------------------------------- /view/src/shared/images/BuBuWithLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2024/web02-BooBit/7cada7b270145338c430edcf5a5da162b48a91c4/view/src/shared/images/BuBuWithLogo.png -------------------------------------------------------------------------------- /view/src/shared/images/down.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /view/src/shared/images/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | error 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /view/src/shared/images/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /view/src/shared/images/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | success 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /view/src/shared/images/up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /view/src/shared/model/calculateChangeRate.ts: -------------------------------------------------------------------------------- 1 | import formatPrice from './formatPrice'; 2 | 3 | const calculateChangeRate = (currentPrice: number, prevClosePrice: number) => { 4 | const adjustedPrevClosePrice = prevClosePrice === 0 ? 1 : prevClosePrice; 5 | const changeRate = ((currentPrice - prevClosePrice) / adjustedPrevClosePrice) * 100; 6 | return `${formatPrice(Number(changeRate.toFixed(2)))}%`; 7 | }; 8 | 9 | export default calculateChangeRate; 10 | -------------------------------------------------------------------------------- /view/src/shared/model/formatDate.ts: -------------------------------------------------------------------------------- 1 | const formatDate = (date: string) => { 2 | const d = new Date(date); 3 | 4 | const year = d.getFullYear(); 5 | const month = String(d.getMonth() + 1).padStart(2, '0'); 6 | const day = String(d.getDate()).padStart(2, '0'); 7 | const hours = String(d.getHours()).padStart(2, '0'); 8 | const minutes = String(d.getMinutes()).padStart(2, '0'); 9 | const seconds = String(d.getSeconds()).padStart(2, '0'); 10 | 11 | return `${year}.${month}.${day} ${hours}:${minutes}:${seconds}`; 12 | }; 13 | 14 | export default formatDate; 15 | -------------------------------------------------------------------------------- /view/src/shared/model/formatPrice.ts: -------------------------------------------------------------------------------- 1 | const formatPrice = (price: string | number) => { 2 | const value = typeof price === 'number' ? price.toString() : price; 3 | const [intPart, decimalPart] = value.split('.'); 4 | return Number(intPart).toLocaleString() + (decimalPart !== undefined ? `.${decimalPart}` : ''); 5 | }; 6 | 7 | export default formatPrice; 8 | -------------------------------------------------------------------------------- /view/src/shared/model/useGetAssets.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import getAssetsApi from '../api/getAssetsApi'; 3 | 4 | const useGetAssets = () => { 5 | return useQuery({ 6 | queryKey: ['assets'], 7 | queryFn: getAssetsApi, 8 | select: (data) => data.assets, 9 | }); 10 | }; 11 | 12 | export default useGetAssets; 13 | -------------------------------------------------------------------------------- /view/src/shared/model/useWebSocket.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { WebSocketMessage } from '../types/socket/WebSocketMessageType'; 3 | 4 | const useWebSocket = (url: string, handleMessage: (message: WebSocketMessage) => void) => { 5 | const [socket, setSocket] = useState(null); 6 | 7 | useEffect(() => { 8 | const ws = new WebSocket(url); 9 | setSocket(ws); 10 | 11 | ws.onopen = () => { 12 | console.log('웹소켓 연결 완료'); 13 | const initMessage = { 14 | event: 'CANDLE_CHART_INIT', 15 | timeScale: '1sec', // 원하는 시간 단위로 변경 가능 16 | }; 17 | ws.send(JSON.stringify(initMessage)); 18 | }; 19 | 20 | ws.onmessage = (event) => { 21 | const receivedData = JSON.parse(event.data); 22 | handleMessage(receivedData); // Pass the message to the parent component or handler directly 23 | }; 24 | 25 | return () => { 26 | ws.close(); 27 | }; 28 | }, [url, handleMessage]); 29 | 30 | // WebSocket에 메시지 보내기 31 | const sendMessage = (msg: string) => { 32 | if (socket) { 33 | socket.send(msg); 34 | } 35 | }; 36 | 37 | return { sendMessage }; 38 | }; 39 | 40 | export default useWebSocket; 41 | -------------------------------------------------------------------------------- /view/src/shared/store/ToastContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, ReactNode } from 'react'; 2 | 3 | type Toast = { 4 | id: string; 5 | message: string; 6 | type: 'success' | 'error' | 'info'; 7 | }; 8 | 9 | type ToastContextType = { 10 | toasts: Toast[]; 11 | addToast: (message: string, type?: Toast['type']) => void; 12 | removeToast: (id: string) => void; 13 | }; 14 | 15 | const ToastContext = createContext(undefined); 16 | 17 | export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { 18 | const [toasts, setToasts] = useState([]); 19 | 20 | const addToast = (message: string, type: Toast['type'] = 'info') => { 21 | const id = Math.random().toString(36).slice(2, 9); 22 | setToasts((prev) => [...prev, { id, message, type }]); 23 | setTimeout(() => removeToast(id), 3000); // 3초 후 자동 삭제 24 | }; 25 | 26 | const removeToast = (id: string) => { 27 | setToasts((prev) => prev.filter((toast) => toast.id !== id)); 28 | }; 29 | 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }; 36 | 37 | export const useToast = () => { 38 | const context = useContext(ToastContext); 39 | if (!context) { 40 | throw new Error('useToast must be used within a ToastProvider'); 41 | } 42 | return context; 43 | }; 44 | -------------------------------------------------------------------------------- /view/src/shared/store/auth/authActions.ts: -------------------------------------------------------------------------------- 1 | import { useAuth } from './authContext'; 2 | 3 | export const useAuthActions = () => { 4 | const { dispatch } = useAuth(); 5 | 6 | const login = () => { 7 | dispatch({ type: 'LOGIN' }); 8 | }; 9 | 10 | const logout = () => { 11 | dispatch({ type: 'LOGOUT' }); 12 | }; 13 | 14 | return { login, logout }; 15 | }; 16 | -------------------------------------------------------------------------------- /view/src/shared/store/auth/authContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useReducer } from 'react'; 2 | 3 | type AuthState = { 4 | isAuthenticated: boolean; 5 | }; 6 | 7 | type AuthAction = { type: 'LOGIN' } | { type: 'LOGOUT' }; 8 | 9 | const initialState: AuthState = { 10 | isAuthenticated: false, 11 | }; 12 | 13 | const authReducer = (state: AuthState, action: AuthAction): AuthState => { 14 | switch (action.type) { 15 | case 'LOGIN': 16 | return { isAuthenticated: true }; 17 | case 'LOGOUT': 18 | return { isAuthenticated: false }; 19 | default: 20 | return state; 21 | } 22 | }; 23 | 24 | const AuthContext = createContext<{ 25 | state: AuthState; 26 | dispatch: React.Dispatch; 27 | }>({ 28 | state: initialState, 29 | dispatch: () => undefined, 30 | }); 31 | 32 | export const AuthProvider = ({ children }: { children: ReactNode }) => { 33 | const [state, dispatch] = useReducer(authReducer, initialState); 34 | 35 | return {children}; 36 | }; 37 | 38 | export const useAuth = () => useContext(AuthContext); 39 | -------------------------------------------------------------------------------- /view/src/shared/types/ChartTimeScaleType.ts: -------------------------------------------------------------------------------- 1 | export type ChartTimeScaleType = 2 | | '1sec' 3 | | '1min' 4 | | '10min' 5 | | '30min' 6 | | '1hour' 7 | | '1day' 8 | | '1week' 9 | | '1month'; 10 | -------------------------------------------------------------------------------- /view/src/shared/types/LayoutProps.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export type LayoutProps = { 4 | children: ReactNode; 5 | paddingX?: string; 6 | flex?: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /view/src/shared/types/MyAsset.ts: -------------------------------------------------------------------------------- 1 | export interface MyAsset { 2 | currencyCode: string; 3 | name: string; 4 | amount: number; 5 | } 6 | -------------------------------------------------------------------------------- /view/src/shared/types/RecordType.ts: -------------------------------------------------------------------------------- 1 | export interface RecordType { 2 | tradeId: string; 3 | date: string; 4 | price: number; 5 | amount: number; 6 | tradePrice: number; 7 | gradient: 'POSITIVE' | 'NEGATIVE'; 8 | } 9 | -------------------------------------------------------------------------------- /view/src/shared/types/socket/BuyAndSellType.ts: -------------------------------------------------------------------------------- 1 | import { OrderType } from './OrderType'; 2 | 3 | export interface BuyAndSellType { 4 | event: 'BUY_AND_SELL'; 5 | data: { 6 | sell: OrderType[]; 7 | buy: OrderType[]; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /view/src/shared/types/socket/CandleChartType.ts: -------------------------------------------------------------------------------- 1 | import { CandleSocketType } from './CandleSocketType'; 2 | 3 | export interface CandleChartType { 4 | event: 'CANDLE_CHART' | 'CANDLE_CHART_INIT'; 5 | timeScale: '1sec' | '1min' | '15min' | '30min' | '1hour' | '1day' | '1year'; 6 | data: CandleSocketType[]; 7 | } 8 | -------------------------------------------------------------------------------- /view/src/shared/types/socket/CandleSocketType.ts: -------------------------------------------------------------------------------- 1 | export interface CandleSocketType { 2 | date: string; 3 | open: number; 4 | close: number; 5 | high: number; 6 | low: number; 7 | volume: number; 8 | } 9 | -------------------------------------------------------------------------------- /view/src/shared/types/socket/OrderType.ts: -------------------------------------------------------------------------------- 1 | export interface OrderType { 2 | price: number; 3 | priceChangeRate: number; 4 | amount: number; 5 | } 6 | -------------------------------------------------------------------------------- /view/src/shared/types/socket/SocketEventType.ts: -------------------------------------------------------------------------------- 1 | export type EventTypes = 'CANDLE_CHART' | 'CANDLE_CHART_INIT' | 'BUY_AND_SELL' | 'TRADE'; 2 | -------------------------------------------------------------------------------- /view/src/shared/types/socket/TradeType.ts: -------------------------------------------------------------------------------- 1 | import { RecordType } from '../RecordType'; 2 | 3 | export interface TradeType { 4 | event: 'TRADE'; 5 | data: RecordType[]; 6 | } 7 | -------------------------------------------------------------------------------- /view/src/shared/types/socket/WebSocketMessageType.ts: -------------------------------------------------------------------------------- 1 | import { BuyAndSellType } from './BuyAndSellType'; 2 | import { CandleChartType } from './CandleChartType'; 3 | import { TradeType } from './TradeType'; 4 | 5 | export type WebSocketMessage = CandleChartType | BuyAndSellType | TradeType; 6 | -------------------------------------------------------------------------------- /view/src/widgets/AuthLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutProps } from '../../shared/types/LayoutProps'; 3 | 4 | const AuthLayout: React.FC = ({ children }) => { 5 | return ( 6 |
    7 | {children} 8 |
    9 | ); 10 | }; 11 | 12 | export default AuthLayout; 13 | -------------------------------------------------------------------------------- /view/src/widgets/CashTransaction/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import MyAssetInfo from '../../entities/MyAssetInfo'; 3 | import MyAssetList from '../../entities/MyAssetList'; 4 | import useGetAssets from '../../shared/model/useGetAssets'; 5 | 6 | const CashTransaction = () => { 7 | const [selectedAssetIdx, setSelectedAssetIdx] = useState(1); 8 | 9 | const { data: assets } = useGetAssets(); 10 | 11 | return assets ? ( 12 |
    13 | 14 | 18 |
    19 | ) : ( 20 |
    21 | ); 22 | }; 23 | 24 | export default CashTransaction; 25 | -------------------------------------------------------------------------------- /view/src/widgets/Header/UI/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import logoImage from '../../../shared/images/BuBu.png'; 3 | 4 | const Logo = () => { 5 | const navigate = useNavigate(); 6 | 7 | const handleClick = () => { 8 | navigate('/'); 9 | }; 10 | 11 | return ( 12 | 16 | ); 17 | }; 18 | 19 | export default Logo; 20 | -------------------------------------------------------------------------------- /view/src/widgets/Header/api/signoutApi.ts: -------------------------------------------------------------------------------- 1 | const apiUrl = import.meta.env.VITE_AUTH_URL; 2 | const signoutApi = async () => { 3 | const response = await fetch(`${apiUrl}/api/auth/logout`, { 4 | method: 'POST', 5 | headers: { 6 | 'Content-Type': 'application/json', 7 | }, 8 | credentials: 'include', 9 | }); 10 | 11 | if (!response.ok) { 12 | throw new Error('로그아웃에 실패했습니다.'); 13 | } 14 | 15 | return response.json(); 16 | }; 17 | 18 | export default signoutApi; 19 | -------------------------------------------------------------------------------- /view/src/widgets/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useNavigate } from 'react-router-dom'; 2 | import {} from 'react-router-dom'; 3 | import Logo from './UI/Logo'; 4 | 5 | import { useAuth } from '../../shared/store/auth/authContext'; 6 | import useSignout from './model/useSignout'; 7 | 8 | const Header = () => { 9 | const navigate = useNavigate(); 10 | const { state } = useAuth(); 11 | const { mutate } = useSignout(); 12 | 13 | const handleLogout = (e: React.MouseEvent) => { 14 | e.preventDefault(); 15 | navigate('/'); 16 | mutate(); 17 | }; 18 | 19 | return ( 20 |
    21 | 22 | {state.isAuthenticated ? ( 23 |
    24 | 마이페이지 25 | 26 |
    27 | ) : ( 28 |
    29 | 로그인 30 | 회원가입 31 |
    32 | )} 33 |
    34 | ); 35 | }; 36 | 37 | export default Header; 38 | -------------------------------------------------------------------------------- /view/src/widgets/Header/model/useSignout.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | 3 | import signoutApi from '../api/signoutApi'; 4 | import { useAuthActions } from '../../../shared/store/auth/authActions'; 5 | import errorMessages from '../../../shared/consts/errorMessages'; 6 | import { useToast } from '../../../shared/store/ToastContext'; 7 | 8 | const useSignout = () => { 9 | const { addToast } = useToast(); 10 | const { logout } = useAuthActions(); 11 | const queryClient = useQueryClient(); 12 | return useMutation({ 13 | mutationFn: signoutApi, 14 | onSuccess: () => { 15 | logout(); 16 | queryClient.removeQueries(); 17 | }, 18 | onError: (error: unknown) => { 19 | if (error instanceof Error) { 20 | addToast(errorMessages.default.signout, 'error'); 21 | } else { 22 | addToast(errorMessages.default.general, 'error'); 23 | } 24 | }, 25 | }); 26 | }; 27 | 28 | export default useSignout; 29 | -------------------------------------------------------------------------------- /view/src/widgets/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LayoutProps } from '../../shared/types/LayoutProps'; 3 | 4 | const Layout: React.FC = ({ 5 | children, 6 | paddingX = 'px-[15rem]', 7 | flex = true, 8 | }) => { 9 | return ( 10 |
    13 | {children} 14 |
    15 | ); 16 | }; 17 | 18 | export default Layout; 19 | -------------------------------------------------------------------------------- /view/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "types": ["msw", "mime", "node", "qs", "validator", "yargs"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "Bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /view/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /view/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /view/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------