-
);
}
diff --git a/packages/client/src/pages/trade/Trade.tsx b/packages/client/src/pages/trade/Trade.tsx
index 11601378..6e0126f1 100644
--- a/packages/client/src/pages/trade/Trade.tsx
+++ b/packages/client/src/pages/trade/Trade.tsx
@@ -2,42 +2,59 @@ import Chart from '@/pages/trade/components/chart/Chart';
import OrderBook from '@/pages/trade/components/order_book/OrderBook';
import OrderForm from '@/pages/trade/components/order_form/OrderForm';
import TradeHeader from '@/pages/trade/components/trade_header/TradeHeader';
-import { useParams } from 'react-router-dom';
-import { useSSETicker } from '@/hooks/SSE/useSSETicker';
-import { Suspense, useMemo, useState } from 'react';
import ChartSkeleton from '@/pages/trade/components/chart/ChartSkeleton';
import TradeFooter from '@/pages/trade/components/trade_footer/TradeFooter';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useSSETicker } from '@/hooks/SSE/useSSETicker';
+import { Suspense, useMemo, useState, useCallback } from 'react';
+import { useToast } from '@/hooks/ui/useToast';
+import { useEffect } from 'react';
+import { useValidCoin } from '@/hooks/market/useValidCoin';
function Trade() {
const { market } = useParams();
+ const toast = useToast();
+ const navigate = useNavigate();
+
const marketCode = useMemo(() => (market ? [{ market }] : []), [market]);
const { sseData: price } = useSSETicker(marketCode);
const [selectPrice, setSelectPrice] = useState
(null);
+ const { isValidCoin } = useValidCoin(market);
- if (!market || !price) return;
+ useEffect(() => {
+ if (!isValidCoin) {
+ toast.error('원화로 거래 불가능한 코인이에요');
+ navigate('/');
+ }
+ }, [isValidCoin]);
- const currentPrice = price[market]?.trade_price;
- const handleSelectPrice = (price: number) => {
+ const handleSelectPrice = useCallback((price: number) => {
setSelectPrice(price);
- };
+ }, []);
+
+ if (!market || !price) return null;
+ const currentPrice = price[market]?.trade_price;
+
return (
-
-
-
+ >
);
}
diff --git a/packages/client/src/pages/trade/components/chart/CandleChart.tsx b/packages/client/src/pages/trade/components/chart/CandleChart.tsx
index 485eff3a..3935ab57 100644
--- a/packages/client/src/pages/trade/components/chart/CandleChart.tsx
+++ b/packages/client/src/pages/trade/components/chart/CandleChart.tsx
@@ -1,62 +1,41 @@
-import { useRef, useEffect } from 'react';
+import { useEffect } from 'react';
import { Candle, CandlePeriod } from '@/types/chart';
-import { IChartApi, ISeriesApi } from 'lightweight-charts';
-import {
- initializeChart,
- setupCandlestickSeries,
-} from '@/pages/trade/components/chart/chartSetup';
-import { chartConfig } from '@/pages/trade/components/chart/config';
import { formatCandleData } from '@/utility/format/formatCandleData';
-import {
- handleResize,
- handleScroll,
-} from '@/pages/trade/components/chart/chartEvent';
+import { handleScroll } from '@/utility/chart/chartEvent';
+import { useRealTimeCandle } from '@/hooks/chart/useRealTimeCandle';
+import { useChartSetup } from '@/hooks/chart/useChartSetup';
type CandleChartProps = {
activePeriod: CandlePeriod;
minute: number | undefined;
data: Candle[];
+ refetch: () => Promise
;
fetchNextPage: () => Promise;
+ currentPrice: number;
};
function CandleChart({
activePeriod,
minute,
data,
+ refetch,
fetchNextPage,
+ currentPrice,
}: CandleChartProps) {
- const chartRef = useRef(null);
- const chartInstanceRef = useRef(null);
- const seriesRef = useRef | null>(null);
-
- useEffect(() => {
- if (!chartRef.current) return;
- chartInstanceRef.current = initializeChart(chartRef.current, chartConfig);
- seriesRef.current = setupCandlestickSeries(
- chartInstanceRef.current,
- [],
- chartConfig,
- );
- const resizeObserver = new ResizeObserver(() => {
- handleResize(chartRef, chartInstanceRef);
- });
-
- if (chartRef.current.parentElement) {
- resizeObserver.observe(chartRef.current.parentElement);
- }
-
- return () => {
- if (chartInstanceRef.current) {
- resizeObserver.disconnect();
- chartInstanceRef.current.remove();
- }
- };
- }, []);
+ const { chartRef, chartInstanceRef, seriesRef } = useChartSetup();
+ const { lastCandleRef } = useRealTimeCandle({
+ seriesRef,
+ currentPrice,
+ activePeriod,
+ refetch,
+ minute,
+ });
useEffect(() => {
if (!seriesRef.current || !chartInstanceRef.current) return;
const formattedData = formatCandleData(data);
seriesRef.current.setData(formattedData);
+ lastCandleRef.current = formattedData[formattedData.length - 1];
}, [data]);
useEffect(() => {
diff --git a/packages/client/src/pages/trade/components/chart/Chart.tsx b/packages/client/src/pages/trade/components/chart/Chart.tsx
index eba3bac2..d1120313 100644
--- a/packages/client/src/pages/trade/components/chart/Chart.tsx
+++ b/packages/client/src/pages/trade/components/chart/Chart.tsx
@@ -1,13 +1,22 @@
-import { usePeriodChart } from '@/hooks/market/usePeriodChart';
+import { usePeriodChart } from '@/hooks/chart/usePeriodChart';
import { useState } from 'react';
import ChartSelector from '@/pages/trade/components/chart/ChartSelector';
import { CandlePeriod } from '@/types/chart';
import CandleChart from '@/pages/trade/components/chart/CandleChart';
-function Chart({ market }: { market: string }) {
+type ChartProps = {
+ market: string;
+ currentPrice: number;
+};
+
+function Chart({ market, currentPrice }: ChartProps) {
const [activePeriod, setActivePeriod] = useState('days');
const [minute, setMinute] = useState();
- const { data, fetchNextPage } = usePeriodChart(market, activePeriod, minute);
+ const { data, refetch, fetchNextPage } = usePeriodChart(
+ market,
+ activePeriod,
+ minute,
+ );
const handleActivePeriod = (period: CandlePeriod, minute?: number) => {
setActivePeriod(period);
@@ -15,7 +24,7 @@ function Chart({ market }: { market: string }) {
};
return (
-
+
);
diff --git a/packages/client/src/pages/trade/components/order_book/OrderBook.tsx b/packages/client/src/pages/trade/components/order_book/OrderBook.tsx
index 8bc893f1..aea7f44b 100644
--- a/packages/client/src/pages/trade/components/order_book/OrderBook.tsx
+++ b/packages/client/src/pages/trade/components/order_book/OrderBook.tsx
@@ -29,7 +29,7 @@ function OrderBook({
const bids = formatBids(orderBook[market]);
return (
-
+
호가
diff --git a/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx
index 841f3487..01854de6 100644
--- a/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx
+++ b/packages/client/src/pages/trade/components/order_form/OrderBuyForm.tsx
@@ -36,7 +36,7 @@ function OrderBuyForm({ currentPrice, selectPrice }: OrderBuyFormProsp) {
+
주문하기
{TABS.map((tab) => (
diff --git a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx
index b1a857ae..724ee85b 100644
--- a/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx
+++ b/packages/client/src/pages/trade/components/order_form/OrderSellForm.tsx
@@ -7,7 +7,7 @@ import { useCheckCoin } from '@/hooks/trade/useCheckCoin';
import { useMarketParams } from '@/hooks/market/useMarketParams';
import { useOrderForm } from '@/hooks/trade/useOrderForm';
import { useMyAccount } from '@/hooks/auth/useMyAccount';
-import { calculateProfitInfo } from '@/utility/calculateProfit';
+import { calculateProfitInfo } from '@/utility/finance/calculateProfit';
type OrderSellFormProps = {
currentPrice: number;
@@ -56,14 +56,14 @@ function OrderSellForm({ currentPrice, selectPrice }: OrderSellFormProps) {
+
-
+
{duplicatedCoins?.map((coin, index) => (
+
{
if (chartRef.current && chartInstanceRef.current) {
const { width } =
- chartRef.current.parentElement?.getBoundingClientRect() || { width: 0 };
+ chartRef.current.parentElement?.getBoundingClientRect() || {
+ width: 0,
+ };
chartInstanceRef.current.applyOptions({
width: width,
});
diff --git a/packages/client/src/pages/trade/components/chart/chartSetup.ts b/packages/client/src/utility/chart/chartSetup.ts
similarity index 92%
rename from packages/client/src/pages/trade/components/chart/chartSetup.ts
rename to packages/client/src/utility/chart/chartSetup.ts
index 3b997be6..05f10784 100644
--- a/packages/client/src/pages/trade/components/chart/chartSetup.ts
+++ b/packages/client/src/utility/chart/chartSetup.ts
@@ -1,7 +1,7 @@
// src/components/Chart/utils/chartSetup.ts
import { IChartApi, createChart, ISeriesApi } from 'lightweight-charts';
import { CandleFormat } from '@/types/chart';
-import { ChartConfig } from '@/pages/trade/components/chart/config';
+import { ChartConfig } from '@/utility/chart/config';
export const initializeChart = (
container: HTMLElement,
diff --git a/packages/client/src/utility/chart/chartTimeUtils.ts b/packages/client/src/utility/chart/chartTimeUtils.ts
new file mode 100644
index 00000000..c070ddc3
--- /dev/null
+++ b/packages/client/src/utility/chart/chartTimeUtils.ts
@@ -0,0 +1,55 @@
+import { CandlePeriod } from '@/types/chart';
+import { Time } from 'lightweight-charts';
+
+export const getPeriodMs = (activePeriod: CandlePeriod, minute?: number) => {
+ switch (activePeriod) {
+ case 'minutes':
+ return (minute || 1) * 60 * 1000;
+ case 'days':
+ return 24 * 60 * 60 * 1000;
+ case 'weeks':
+ return 7 * 24 * 60 * 60 * 1000;
+ case 'months':
+ return 30 * 24 * 60 * 60 * 1000;
+ default:
+ return 60 * 1000;
+ }
+};
+
+export const getCurrentCandleStartTime = (
+ activePeriod: CandlePeriod,
+ minute?: number,
+) => {
+ const now = new Date();
+ const periodMs = getPeriodMs(activePeriod, minute);
+
+ switch (activePeriod) {
+ case 'minutes':
+ return ((Math.floor(now.getTime() / periodMs) * periodMs) / 1000) as Time;
+
+ case 'days': {
+ const startOfDay = new Date(now);
+ startOfDay.setUTCHours(0, 0, 0, 0);
+ return (startOfDay.getTime() / 1000) as Time;
+ }
+
+ case 'weeks': {
+ const startOfWeek = new Date(now);
+ startOfWeek.setUTCHours(0, 0, 0, 0);
+ const day = startOfWeek.getUTCDay();
+ const diff = startOfWeek.getUTCDate() - day + (day === 0 ? -6 : 1);
+ startOfWeek.setUTCDate(diff);
+ return (startOfWeek.getTime() / 1000) as Time;
+ }
+
+ case 'months': {
+ const startOfMonth = new Date(now);
+ startOfMonth.setUTCHours(0, 0, 0, 0);
+ startOfMonth.setUTCDate(1);
+ return (startOfMonth.getTime() / 1000) as Time;
+ }
+
+ default:
+ return ((Math.floor(now.getTime() / periodMs) * periodMs) / 1000) as Time;
+ }
+};
diff --git a/packages/client/src/pages/trade/components/chart/config.ts b/packages/client/src/utility/chart/config.ts
similarity index 92%
rename from packages/client/src/pages/trade/components/chart/config.ts
rename to packages/client/src/utility/chart/config.ts
index 68817a1c..754e3132 100644
--- a/packages/client/src/pages/trade/components/chart/config.ts
+++ b/packages/client/src/utility/chart/config.ts
@@ -20,6 +20,10 @@ export const chartConfig = {
vertLines: { color: '#1111' },
horzLines: { color: '#1111' },
},
+ timeScale: {
+ rightOffset: 5,
+ barSpacing: 10,
+ },
},
candleStickOptions: {
wickUpColor: 'rgb(225, 50, 85)',
diff --git a/packages/client/src/utility/calculateProfit.ts b/packages/client/src/utility/finance/calculateProfit.ts
similarity index 100%
rename from packages/client/src/utility/calculateProfit.ts
rename to packages/client/src/utility/finance/calculateProfit.ts
diff --git a/packages/client/src/utility/order.ts b/packages/client/src/utility/finance/calculateTotalPrice.ts
similarity index 100%
rename from packages/client/src/utility/order.ts
rename to packages/client/src/utility/finance/calculateTotalPrice.ts
diff --git a/packages/client/src/utility/portfolioEvaluator.ts b/packages/client/src/utility/finance/portfolioEvaluator.ts
similarity index 100%
rename from packages/client/src/utility/portfolioEvaluator.ts
rename to packages/client/src/utility/finance/portfolioEvaluator.ts
diff --git a/packages/client/src/utility/format/formatCandleData.ts b/packages/client/src/utility/format/formatCandleData.ts
index 403c9c1b..cda0aeb5 100644
--- a/packages/client/src/utility/format/formatCandleData.ts
+++ b/packages/client/src/utility/format/formatCandleData.ts
@@ -1,10 +1,10 @@
-import { Candle, CandleFormat } from '@/types/chart';
import { Time } from 'lightweight-charts';
-
+import { Candle, CandleFormat } from '@/types/chart';
export function formatCandleData(data: Candle[]): CandleFormat[] {
const uniqueData = data.reduce(
(acc, current) => {
- const timeKey = new Date(current.candle_date_time_kst).getTime();
+ const date = new Date(current.candle_date_time_kst);
+ const timeKey = date.getTime() + 9 * 60 * 60 * 1000;
acc[timeKey] = current;
return acc;
},
@@ -12,13 +12,17 @@ export function formatCandleData(data: Candle[]): CandleFormat[] {
);
const sortedData = Object.values(uniqueData).sort((a, b) => {
- const dateA = new Date(a.candle_date_time_kst).getTime();
- const dateB = new Date(b.candle_date_time_kst).getTime();
+ const dateA =
+ new Date(a.candle_date_time_kst).getTime() + 9 * 60 * 60 * 1000;
+ const dateB =
+ new Date(b.candle_date_time_kst).getTime() + 9 * 60 * 60 * 1000;
return dateA - dateB;
});
const formattedData = sortedData.map((candle) => ({
- time: (new Date(candle.candle_date_time_kst).getTime() / 1000) as Time,
+ time: ((new Date(candle.candle_date_time_kst).getTime() +
+ 9 * 60 * 60 * 1000) /
+ 1000) as Time,
open: candle.opening_price,
high: candle.high_price,
low: candle.low_price,
diff --git a/packages/client/src/utility/cookies.ts b/packages/client/src/utility/storage/cookies.ts
similarity index 100%
rename from packages/client/src/utility/cookies.ts
rename to packages/client/src/utility/storage/cookies.ts
diff --git a/packages/client/src/utility/recentlyMarket.ts b/packages/client/src/utility/storage/recentlyMarket.ts
similarity index 100%
rename from packages/client/src/utility/recentlyMarket.ts
rename to packages/client/src/utility/storage/recentlyMarket.ts
diff --git a/packages/client/src/utility/filter.ts b/packages/client/src/utility/validation/filter.ts
similarity index 100%
rename from packages/client/src/utility/filter.ts
rename to packages/client/src/utility/validation/filter.ts
diff --git a/packages/client/src/utility/typeGuard.ts b/packages/client/src/utility/validation/typeGuard.ts
similarity index 100%
rename from packages/client/src/utility/typeGuard.ts
rename to packages/client/src/utility/validation/typeGuard.ts
diff --git a/packages/server/package.json b/packages/server/package.json
index 6a9d36f6..cecfd906 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -25,6 +25,7 @@
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
+ "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-socket.io": "^10.4.7",
"@nestjs/schedule": "^4.1.1",
@@ -41,6 +42,9 @@
"ioredis": "^5.4.1",
"js-yaml": "^4.1.0",
"mysql2": "^3.11.3",
+ "passport": "^0.7.0",
+ "passport-google-oauth20": "^2.0.0",
+ "passport-kakao": "^1.0.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"tunnel-ssh": "^5.1.2",
@@ -58,6 +62,9 @@
"@types/jest": "^29.5.2",
"@types/js-yaml": "^4",
"@types/node": "^20.3.1",
+ "@types/passport": "^0",
+ "@types/passport-google-oauth20": "^2",
+ "@types/passport-kakao": "^1",
"@types/socket.io": "^3.0.2",
"@types/supertest": "^6.0.0",
"@types/ws": "^8.5.13",
diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts
index b2214dcd..f9a61ced 100644
--- a/packages/server/src/auth/auth.controller.ts
+++ b/packages/server/src/auth/auth.controller.ts
@@ -1,69 +1,151 @@
import {
- Body,
- Controller,
- Delete,
- Get,
- HttpCode,
- HttpStatus,
- Post,
- Request,
- UseGuards,
+ Body,
+ Controller,
+ Delete,
+ Get,
+ HttpCode,
+ HttpStatus,
+ Post,
+ Request,
+ Res,
+ UseGuards,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
+import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import {
- ApiBody,
- ApiBearerAuth,
- ApiSecurity,
- ApiResponse,
+ ApiBody,
+ ApiBearerAuth,
+ ApiSecurity,
+ ApiResponse,
} from '@nestjs/swagger';
import { SignInDto } from './dtos/sign-in.dto';
import { SignUpDto } from './dtos/sign-up.dto';
@Controller('auth')
export class AuthController {
- constructor(private authService: AuthService) {}
-
- @ApiBody({ type: SignInDto })
- @HttpCode(HttpStatus.OK)
- @Post('login')
- signIn(@Body() signInDto: Record) {
- return this.authService.signIn(signInDto.username);
- }
-
- @HttpCode(HttpStatus.OK)
- @Post('guest-login')
- guestSignIn() {
- return this.authService.guestSignIn();
- }
-
- @ApiResponse({
- status: HttpStatus.OK,
- description: 'New user successfully registered',
- })
- @ApiResponse({
- status: HttpStatus.BAD_REQUEST,
- description: 'Invalid input or user already exists',
- })
- @HttpCode(HttpStatus.CREATED)
- @Post('signup')
- async signUp(@Body() signUpDto: SignUpDto) {
- return this.authService.signUp(signUpDto.username);
- }
-
- @ApiBearerAuth('access-token')
- @ApiSecurity('access-token')
- @UseGuards(AuthGuard)
- @Delete('logout')
- logout(@Request() req) {
- return this.authService.logout(req.user.userId);
- }
-
- @UseGuards(AuthGuard)
- @ApiBearerAuth('access-token')
- @ApiSecurity('access-token')
- @Get('profile')
- getProfile(@Request() req) {
- return req.user;
- }
+ constructor(private authService: AuthService) {}
+
+ @ApiBody({ type: SignInDto })
+ @HttpCode(HttpStatus.OK)
+ @Post('login')
+ signIn(@Body() signInDto: Record) {
+ return this.authService.signIn(signInDto.username);
+ }
+
+ @HttpCode(HttpStatus.OK)
+ @Post('guest-login')
+ guestSignIn() {
+ return this.authService.guestSignIn();
+ }
+
+ @Get('google')
+ @UseGuards(PassportAuthGuard('google'))
+ async googleLogin() {}
+
+ @Get('google/callback')
+ @UseGuards(PassportAuthGuard('google'))
+ async googleLoginCallback(@Request() req, @Res() res): Promise {
+ const googleUser = req.user;
+
+ const signUpDto: SignUpDto = {
+ name: googleUser.name,
+ email: googleUser.email,
+ provider: googleUser.provider,
+ providerId: googleUser.id,
+ isGuest: false,
+ };
+
+ const tokens = await this.authService.validateOAuthLogin(signUpDto);
+ // 요청 Origin 기반으로 리다이렉트 URL 결정
+ const origin = req.headers['origin'];
+ const frontendURL =
+ origin && origin.includes('localhost')
+ ? 'http://localhost:5173'
+ : 'https://www.corinee.site';
+ const redirectURL = new URL('/auth/callback', frontendURL);
+
+ redirectURL.searchParams.append('access_token', tokens.access_token);
+ redirectURL.searchParams.append('refresh_token', tokens.refresh_token);
+ console.log(redirectURL);
+ return res.redirect(redirectURL.toString());
+ }
+
+ @Get('kakao')
+ @UseGuards(PassportAuthGuard('kakao'))
+ async kakaoLogin() {}
+
+ @Get('kakao/callback')
+ @UseGuards(PassportAuthGuard('kakao'))
+ async kakaoLoginCallback(@Request() req, @Res() res) {
+ const kakaoUser = req.user;
+
+ const signUpDto: SignUpDto = {
+ name: kakaoUser.name,
+ email: kakaoUser.email,
+ provider: kakaoUser.provider,
+ providerId: kakaoUser.id,
+ isGuest: false,
+ };
+
+ const tokens = await this.authService.validateOAuthLogin(signUpDto);
+
+ const origin = req.headers['origin'];
+ const frontendURL =
+ origin && origin.includes('localhost')
+ ? 'http://localhost:5173'
+ : 'https://www.corinee.site';
+ const redirectURL = new URL('/auth/callback', frontendURL);
+ redirectURL.searchParams.append('access_token', tokens.access_token);
+ redirectURL.searchParams.append('refresh_token', tokens.refresh_token);
+ return res.redirect(redirectURL.toString());
+ }
+
+ @ApiResponse({
+ status: HttpStatus.OK,
+ description: 'New user successfully registered',
+ })
+ @ApiResponse({
+ status: HttpStatus.BAD_REQUEST,
+ description: 'Invalid input or user already exists',
+ })
+ @HttpCode(HttpStatus.CREATED)
+ @Post('signup')
+ async signUp(@Body() signUpDto: SignUpDto) {
+ return this.authService.signUp(signUpDto);
+ }
+
+ @ApiBearerAuth('access-token')
+ @ApiSecurity('access-token')
+ @UseGuards(AuthGuard)
+ @Delete('logout')
+ logout(@Request() req) {
+ return this.authService.logout(req.user.userId);
+ }
+
+ @UseGuards(AuthGuard)
+ @ApiBearerAuth('access-token')
+ @ApiSecurity('access-token')
+ @Get('profile')
+ getProfile(@Request() req) {
+ return req.user;
+ }
+
+ @ApiBody({
+ schema: {
+ type: 'object',
+ properties: {
+ refreshToken: {
+ type: 'string',
+ description: 'Refresh token used for renewing access token',
+ example: 'your-refresh-token',
+ },
+ },
+ },
+ })
+ @HttpCode(HttpStatus.OK)
+ @Post('refresh')
+ refreshTokens(@Body() body: { refreshToken: string }) {
+ return this.authService.refreshTokens(body.refreshToken);
+ }
}
diff --git a/packages/server/src/auth/auth.guard.ts b/packages/server/src/auth/auth.guard.ts
index 025a47e0..705989d6 100644
--- a/packages/server/src/auth/auth.guard.ts
+++ b/packages/server/src/auth/auth.guard.ts
@@ -22,8 +22,6 @@ export class AuthGuard implements CanActivate {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwtConstants.secret,
});
- // 💡 We're assigning the payload to the request object here
- // so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
diff --git a/packages/server/src/auth/auth.module.ts b/packages/server/src/auth/auth.module.ts
index 3dafe996..dbce4048 100644
--- a/packages/server/src/auth/auth.module.ts
+++ b/packages/server/src/auth/auth.module.ts
@@ -8,18 +8,29 @@ import { AuthService } from './auth.service';
import { AccountRepository } from 'src/account/account.repository';
import { AuthController } from './auth.controller';
import { AccountModule } from 'src/account/account.module';
+import { KakaoStrategy } from './strategies/kakao.strategy';
+import { GoogleStrategy } from './strategies/google.strategy';
+import { PassportModule } from '@nestjs/passport';
@Module({
- imports: [
- TypeOrmModule.forFeature([User]),
- JwtModule.register({
- global: true,
- secret: jwtConstants.secret,
- signOptions: { expiresIn: '6000s' },
- }),
- AccountModule,
- ],
- providers: [UserRepository, AccountRepository, AuthService, JwtService],
- controllers: [AuthController],
- exports: [UserRepository],
+ imports: [
+ TypeOrmModule.forFeature([User]),
+ JwtModule.register({
+ global: true,
+ secret: jwtConstants.secret,
+ signOptions: { expiresIn: '6000s' },
+ }),
+ AccountModule,
+ PassportModule
+ ],
+ providers: [
+ UserRepository,
+ AccountRepository,
+ AuthService,
+ JwtService,
+ GoogleStrategy,
+ KakaoStrategy,
+ ],
+ controllers: [AuthController],
+ exports: [UserRepository],
})
export class AuthModule {}
diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts
index d3375bc9..11774387 100644
--- a/packages/server/src/auth/auth.service.ts
+++ b/packages/server/src/auth/auth.service.ts
@@ -1,16 +1,25 @@
-import { ConflictException, Injectable } from '@nestjs/common';
+import {
+ ConflictException,
+ ForbiddenException,
+ Injectable,
+ UnauthorizedException,
+} from '@nestjs/common';
import { UserRepository } from './user.repository';
import { JwtService } from '@nestjs/jwt';
import {
+ ACCESS_TOKEN_TTL,
DEFAULT_BTC,
DEFAULT_KRW,
DEFAULT_USDT,
+ GUEST_ID_TTL,
+ REFRESH_TOKEN_TTL,
jwtConstants,
} from './constants';
import { v4 as uuidv4 } from 'uuid';
import { AccountRepository } from 'src/account/account.repository';
import { RedisRepository } from 'src/redis/redis.repository';
import { User } from './user.entity';
+import { SignUpDto } from './dtos/sign-up.dto';
@Injectable()
export class AuthService {
constructor(
@@ -22,54 +31,62 @@ export class AuthService {
this.createAdminUser();
}
- async signIn(username: string): Promise<{ access_token: string }> {
+ async signIn(
+ username: string,
+ ): Promise<{ access_token: string; refresh_token: string }> {
const user = await this.userRepository.findOneBy({ username });
- const payload = { userId: user.id, userName: user.username };
- return {
- access_token: await this.jwtService.signAsync(payload, {
- secret: jwtConstants.secret,
- expiresIn: '1d',
- }),
- };
+ if (!user) {
+ throw new UnauthorizedException('Invalid credentials');
+ }
+ return this.generateTokens(user.id, user.username);
}
- async guestSignIn(): Promise<{ access_token: string }> {
- try{
- const username = `guest_${uuidv4()}`;
-
- await this.signUp(username, true);
+ async guestSignIn(): Promise<{
+ access_token: string;
+ refresh_token: string;
+ }> {
+ const guestName = `guest_${uuidv4()}`;
+ const user = { name: guestName, isGuest: true };
- const guestUser = await this.userRepository.findOneBy({ username });
+ await this.signUp(user);
- await this.redisRepository.setAuthData(
- `guest:${guestUser.id}`,
- JSON.stringify({ userId: guestUser.id }),
- 6000,
- );
+ const guestUser = await this.userRepository.findOneBy({
+ username: guestName,
+ });
- const payload = { userId: guestUser.id, userName: guestUser.username };
- return {
- access_token: await this.jwtService.signAsync(payload, {
- secret: jwtConstants.secret,
- expiresIn: '1d',
- }),
- };
- }catch(error){
- console.error(error)
- }
+ await this.redisRepository.setAuthData(
+ `guest:${guestUser.id}`,
+ JSON.stringify({ userId: guestUser.id }),
+ GUEST_ID_TTL,
+ );
+
+ return this.generateTokens(guestUser.id, guestUser.username);
}
- async signUp(
- username: string,
- isGuest = false,
- ): Promise<{ message: string }> {
- const existingUser = await this.userRepository.findOneBy({ username });
+ async signUp(user: {
+ name: string;
+ email?: string;
+ provider?: string;
+ providerId?: string;
+ isGuest?: boolean;
+ }): Promise<{ message: string }> {
+ const { name, email, provider, providerId, isGuest } = user;
+
+ const existingUser = isGuest
+ ? await this.userRepository.findOneBy({ username: name })
+ : await this.userRepository.findOne({
+ where: { provider, providerId },
+ });
+
if (existingUser) {
- throw new ConflictException('Username already exists');
+ throw new ConflictException('User already exists');
}
const newUser = await this.userRepository.save({
- username,
+ username: name,
+ email,
+ provider,
+ providerId,
isGuest,
});
@@ -87,20 +104,118 @@ export class AuthService {
};
}
+ async validateOAuthLogin(
+ signUpDto: SignUpDto,
+ ): Promise<{ access_token: string; refresh_token: string }> {
+ const { name, email, provider, providerId, isGuest } = signUpDto;
+
+ let user = await this.userRepository.findOne({
+ where: { provider, providerId },
+ });
+
+ if (!user) {
+ await this.signUp(
+ { name, email, provider, providerId, isGuest: false },
+ );
+ user = await this.userRepository.findOne({
+ where: { provider, providerId },
+ });
+ }
+
+ if (!user) {
+ throw new UnauthorizedException('OAuth user creation failed');
+ }
+
+ return this.generateTokens(user.id, user.username);
+ }
+
+ private async generateTokens(
+ userId: number,
+ username: string,
+ ): Promise<{ access_token: string; refresh_token: string }> {
+ const payload = { userId, userName: username };
+
+ const accessToken = await this.jwtService.signAsync(payload, {
+ secret: jwtConstants.secret,
+ expiresIn: ACCESS_TOKEN_TTL,
+ });
+
+ const refreshToken = await this.jwtService.signAsync(
+ { userId },
+ {
+ secret: jwtConstants.refreshSecret,
+ expiresIn: REFRESH_TOKEN_TTL,
+ },
+ );
+
+ await this.redisRepository.setAuthData(
+ `refresh:${userId}`,
+ refreshToken,
+ REFRESH_TOKEN_TTL,
+ );
+
+ return {
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ };
+ }
+
+ async refreshTokens(
+ refreshToken: string,
+ ): Promise<{ access_token: string; refresh_token: string }> {
+ try {
+ const payload = await this.jwtService.verifyAsync(refreshToken, {
+ secret: jwtConstants.refreshSecret,
+ });
+ const userId = payload.userId;
+
+ const storedToken = await this.redisRepository.getAuthData(
+ `refresh:${userId}`,
+ );
+
+ if (!storedToken) {
+ throw new ForbiddenException({
+ message: 'Refresh token has expired',
+ errorCode: 'REFRESH_TOKEN_EXPIRED',
+ });
+ }
+
+ if (storedToken !== refreshToken) {
+ throw new UnauthorizedException({
+ message: 'Invalid refresh token',
+ errorCode: 'INVALID_REFRESH_TOKEN',
+ });
+ }
+
+ const user = await this.userRepository.findOneBy({ id: userId });
+ if (!user) {
+ throw new UnauthorizedException('User not found');
+ }
+ return this.generateTokens(user.id, user.username);
+ } catch (error) {
+ throw new UnauthorizedException({
+ message: 'Failed to refresh tokens',
+ errorCode: 'TOKEN_REFRESH_FAILED',
+ });
+ }
+ }
+
async logout(userId: number): Promise<{ message: string }> {
- try{
+ try {
const user = await this.userRepository.findOneBy({ id: userId });
if (!user) {
throw new Error('User not found');
}
+ await this.redisRepository.deleteAuthData(`refresh:${userId}`);
+
if (user.isGuest) {
await this.userRepository.delete({ id: userId });
return { message: 'Guest user data successfully deleted' };
}
- }catch(error){
- console.error(error)
+ } catch (error) {
+ console.error(error);
}
}
diff --git a/packages/server/src/auth/constants.ts b/packages/server/src/auth/constants.ts
index 06bd4fce..4d8f03ae 100644
--- a/packages/server/src/auth/constants.ts
+++ b/packages/server/src/auth/constants.ts
@@ -1,8 +1,15 @@
export const jwtConstants = {
secret:
- 'DO NOT USE THIS VALUE. INSTEAD, CREATE A COMPLEX SECRET AND KEEP IT SAFE OUTSIDE OF THE SOURCE CODE.',
+ 'superSecureAccessTokenSecret',
+ refreshSecret:
+ 'superSecureAccessTokenSecret_superSecureAccessTokenSecret',
};
export const DEFAULT_KRW = 30000000;
export const DEFAULT_USDT = 300000;
export const DEFAULT_BTC = 0;
+
+export const GUEST_ID_TTL = 24 * 3600;
+
+export const REFRESH_TOKEN_TTL = 7 * 24 * 3600;
+export const ACCESS_TOKEN_TTL = '1d';
\ No newline at end of file
diff --git a/packages/server/src/auth/dtos/sign-up.dto.ts b/packages/server/src/auth/dtos/sign-up.dto.ts
index b3d9b70a..b64fd51b 100644
--- a/packages/server/src/auth/dtos/sign-up.dto.ts
+++ b/packages/server/src/auth/dtos/sign-up.dto.ts
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
-import { IsString } from 'class-validator';
+import { IsBoolean, IsString } from 'class-validator';
export class SignUpDto {
@ApiProperty({
@@ -8,5 +8,17 @@ export class SignUpDto {
required: true,
})
@IsString()
- username: string;
+ name: string;
+
+ @IsString()
+ email: string;
+
+ @IsBoolean()
+ isGuest: boolean;
+
+ @IsString()
+ provider: string;
+
+ @IsString()
+ providerId: string;
}
diff --git a/packages/server/src/auth/strategies/google.strategy.ts b/packages/server/src/auth/strategies/google.strategy.ts
new file mode 100644
index 00000000..e2035579
--- /dev/null
+++ b/packages/server/src/auth/strategies/google.strategy.ts
@@ -0,0 +1,33 @@
+import { PassportStrategy } from '@nestjs/passport';
+import { Injectable } from '@nestjs/common';
+import { Strategy, VerifyCallback } from 'passport-google-oauth20';
+
+@Injectable()
+export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
+ constructor() {
+ super({
+ clientID: process.env.GOOGLE_CLIENT_ID,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
+ callbackURL: `${process.env.CALLBACK_URL}/api/auth/google/callback`,
+ scope: ['email', 'profile'],
+ });
+ }
+
+ async validate(
+ accessToken: string,
+ refreshToken: string,
+ profile: any,
+ done: VerifyCallback,
+ ): Promise {
+ const { id, displayName, emails } = profile;
+ const user = {
+ provider: 'google',
+ id,
+ name: displayName,
+ email: emails?.[0]?.value,
+ accessToken,
+ refreshToken,
+ };
+ done(null, user);
+ }
+}
diff --git a/packages/server/src/auth/strategies/kakao.strategy.ts b/packages/server/src/auth/strategies/kakao.strategy.ts
new file mode 100644
index 00000000..c8010fae
--- /dev/null
+++ b/packages/server/src/auth/strategies/kakao.strategy.ts
@@ -0,0 +1,32 @@
+import { PassportStrategy } from '@nestjs/passport';
+import { Injectable } from '@nestjs/common';
+import { Strategy, Profile } from 'passport-kakao';
+
+@Injectable()
+export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
+ constructor() {
+ super({
+ clientID: process.env.KAKAO_CLIENT_ID,
+ clientSecret: '',
+ callbackURL: `${process.env.CALLBACK_URL}/api/auth/kakao/callback`
+ });
+ }
+
+ async validate(
+ accessToken: string,
+ refreshToken: string,
+ profile: Profile,
+ done: Function,
+ ): Promise {
+ const { id, username, _json } = profile;
+ const user = {
+ provider: 'kakao',
+ id,
+ name: username,
+ email: _json.kakao_account?.email,
+ accessToken,
+ refreshToken,
+ };
+ done(null, user);
+ }
+}
diff --git a/packages/server/src/auth/user.entity.ts b/packages/server/src/auth/user.entity.ts
index 4a8b6968..c9f56bad 100644
--- a/packages/server/src/auth/user.entity.ts
+++ b/packages/server/src/auth/user.entity.ts
@@ -12,7 +12,6 @@ import { Trade } from 'src/trade/trade.entity';
import { TradeHistory } from 'src/trade-history/trade-history.entity';
@Entity()
-@Unique(['username'])
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@@ -22,7 +21,16 @@ export class User extends BaseEntity {
@Column()
username: string;
+
+ @Column({ nullable: true })
+ email: string;
+
+ @Column({ nullable: true })
+ provider: string;
+ @Column({ nullable: true })
+ providerId: string;
+
@OneToOne(() => Account, (account) => account.user, {
cascade: true,
onDelete: 'CASCADE',
diff --git a/packages/server/src/main.ts b/packages/server/src/main.ts
index 2fb2fa8d..828a6213 100644
--- a/packages/server/src/main.ts
+++ b/packages/server/src/main.ts
@@ -1,9 +1,9 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {
- SwaggerModule,
- DocumentBuilder,
- SwaggerCustomOptions,
+ SwaggerModule,
+ DocumentBuilder,
+ SwaggerCustomOptions,
} from '@nestjs/swagger';
import { config } from 'dotenv';
import { setupSshTunnel } from './configs/ssh-tunnel';
@@ -12,39 +12,38 @@ import { AllExceptionsFilter } from 'common/all-exceptions.filter';
config();
async function bootstrap() {
- await setupSshTunnel();
- const app = await NestFactory.create(AppModule);
- app.enableCors({
- origin: true,
- methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
- credentials: true,
- });
+ await setupSshTunnel();
+ const app = await NestFactory.create(AppModule);
+ app.enableCors({
+ origin: true,
+ methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
+ credentials: true,
+ });
- const config = new DocumentBuilder()
- .setTitle('CorinEE API example')
- .setDescription('CorinEE API description')
- .setVersion('1.0')
- .addTag('corinee')
- .addBearerAuth(
- {
- type: 'http',
- scheme: 'bearer',
- name: 'Authorization',
- in: 'header',
- },
- 'access-token',
- )
- .build();
- const customOptions: SwaggerCustomOptions = {
- swaggerOptions: {
- persistAuthorization: true,
- },
- };
- const documentFactory = () => SwaggerModule.createDocument(app, config);
- SwaggerModule.setup('api', app, documentFactory);
+ const config = new DocumentBuilder()
+ .setTitle('CorinEE API example')
+ .setDescription('CorinEE API description')
+ .setVersion('1.0')
+ .addTag('corinee')
+ .addBearerAuth(
+ {
+ type: 'http',
+ scheme: 'bearer',
+ bearerFormat: 'JWT',
+ description: 'Access token used for authentication',
+ },
+ 'access-token',
+ ).build();
+ const customOptions: SwaggerCustomOptions = {
+ swaggerOptions: {
+ persistAuthorization: true,
+ },
+ };
+ const documentFactory = () => SwaggerModule.createDocument(app, config);
+ SwaggerModule.setup('api', app, documentFactory);
- app.setGlobalPrefix('api');
- app.useGlobalFilters(new AllExceptionsFilter());
+ app.setGlobalPrefix('api');
+ app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(process.env.PORT ?? 3000);
}
diff --git a/packages/server/src/trade-history/trade-history.entity.ts b/packages/server/src/trade-history/trade-history.entity.ts
index 2b1f0dbe..de11a547 100644
--- a/packages/server/src/trade-history/trade-history.entity.ts
+++ b/packages/server/src/trade-history/trade-history.entity.ts
@@ -1,39 +1,41 @@
import { User } from '@src/auth/user.entity';
import {
- Entity,
- PrimaryGeneratedColumn,
- Column,
- ManyToOne,
- CreateDateColumn,
- UpdateDateColumn,
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ManyToOne,
+ CreateDateColumn,
+ UpdateDateColumn,
} from 'typeorm';
@Entity()
export class TradeHistory {
- @PrimaryGeneratedColumn()
- tradeHistoryId: number;
+ @PrimaryGeneratedColumn()
+ tradeHistoryId: number;
- @Column()
- assetName: string;
+ @Column()
+ assetName: string;
- @Column()
- tradeType: string;
+ @Column()
+ tradeType: string;
- @Column()
- tradeCurrency: string;
+ @Column()
+ tradeCurrency: string;
- @Column('double')
- price: number;
+ @Column('double')
+ price: number;
- @Column('double')
- quantity: number;
+ @Column('double')
+ quantity: number;
- @Column({ type: 'timestamp' })
- createdAt: Date;
+ @Column({ type: 'timestamp' })
+ createdAt: Date;
- @CreateDateColumn({ type: 'timestamp' })
- tradeDate: Date;
+ @CreateDateColumn({ type: 'timestamp' })
+ tradeDate: Date;
- @ManyToOne(() => User, (user) => user.tradeHistories)
- user: User;
+ @ManyToOne(() => User, (user) => user.tradeHistories, {
+ onDelete: 'CASCADE',
+ })
+ user: User;
}
diff --git a/packages/server/src/trade/trade-ask.service.ts b/packages/server/src/trade/trade-ask.service.ts
index eaa420a2..a1513611 100644
--- a/packages/server/src/trade/trade-ask.service.ts
+++ b/packages/server/src/trade/trade-ask.service.ts
@@ -4,7 +4,7 @@ import {
OnModuleInit,
UnprocessableEntityException,
} from '@nestjs/common';
-import { DataSource, QueryRunner } from 'typeorm';
+import { DataSource } from 'typeorm';
import { AccountRepository } from 'src/account/account.repository';
import { AssetRepository } from 'src/asset/asset.repository';
import { TradeRepository } from './trade.repository';
@@ -44,7 +44,7 @@ export class AskService implements OnModuleInit {
}
})
if(!asset) return 0;
- return asset.quantity * (percent / 100);
+ return parseFloat((asset.quantity * (percent / 100)).toFixed(8));
}
async createAskTrade(user, askDto) {
if(askDto.receivedAmount * askDto.receivedPrice < 5000) throw new BadRequestException();
@@ -67,14 +67,14 @@ export class AskService implements OnModuleInit {
});
}
const userAsset = await this.checkCurrency(askDto, userAccount, queryRunner)
- const assetBalance = userAsset.quantity - askDto.receivedAmount;
+ const assetBalance = parseFloat((userAsset.quantity - askDto.receivedAmount).toFixed(8));
if(assetBalance <= 0){
await this.assetRepository.delete({
assetId: userAsset.assetId
})
}else{
userAsset.quantity = assetBalance
- userAsset.price -= Math.floor(askDto.receivedPrice + askDto.receivedAmount)
+ userAsset.price -= parseFloat(askDto.receivedPrice.toFixed(8)) * parseFloat(askDto.receivedAmount.toFixed(8))
this.assetRepository.updateAssetPrice(userAsset, queryRunner);
}
await this.tradeRepository.createTrade(askDto, user.userId,'sell', queryRunner);
@@ -177,8 +177,8 @@ export class AskService implements OnModuleInit {
try {
const buyData = { ...tradeData };
buyData.quantity =
- tradeData.quantity >= bid_size ? bid_size.toFixed(8) : tradeData.quantity.toFixed(8)
- buyData.price = (bid_price * krw).toFixed(8);
+ tradeData.quantity >= bid_size ? parseFloat(bid_size.toFixed(8)) : parseFloat(tradeData.quantity.toFixed(8))
+ buyData.price = parseFloat((bid_price * krw).toFixed(8));
if(buyData.quantity<0.00000001){
await queryRunner.commitTransaction();
return true;
@@ -192,7 +192,7 @@ export class AskService implements OnModuleInit {
);
if (!asset && tradeData.price > buyData.price) {
- asset.price = Math.floor(asset.price + (tradeData.price - buyData.price) * buyData.quantity);
+ asset.price = parseFloat((asset.price + (tradeData.price - buyData.price) * buyData.quantity).toFixed(8));
await this.assetRepository.updateAssetPrice(asset, queryRunner);
}
@@ -205,13 +205,13 @@ export class AskService implements OnModuleInit {
const BTC_QUANTITY = account.BTC - buyData.quantity
await this.accountRepository.updateAccountBTC(account.id, BTC_QUANTITY, queryRunner)
}
- const change = Math.floor(account[typeReceived] + buyData.price * buyData.quantity)
+ const change = parseFloat((account[typeReceived] + buyData.price * buyData.quantity).toFixed(8))
await this.accountRepository.updateAccountCurrency(typeReceived, change, account.id, queryRunner)
tradeData.quantity -= buyData.quantity;
- if (tradeData.quantity === 0) {
+ if (tradeData.quantity <= 0.00000001) {
await this.tradeRepository.deleteTrade(tradeId, queryRunner);
} else{
await this.tradeRepository.updateTradeTransaction(
diff --git a/packages/server/src/trade/trade-bid.service.ts b/packages/server/src/trade/trade-bid.service.ts
index f1708968..e4024385 100644
--- a/packages/server/src/trade/trade-bid.service.ts
+++ b/packages/server/src/trade/trade-bid.service.ts
@@ -37,7 +37,7 @@ export class BidService implements OnModuleInit {
async calculatePercentBuy(user, moneyType: string, percent: number) {
const money = await this.accountRepository.getMyMoney(user, moneyType);
- return Number(money) * (percent / 100);
+ return parseFloat((money * (percent / 100)).toFixed(8));
}
async createBidTrade(user, bidDto) {
if(bidDto.receivedAmount * bidDto.receivedPrice < 5000) throw new BadRequestException();
@@ -62,7 +62,7 @@ export class BidService implements OnModuleInit {
const accountBalance = await this.checkCurrency(user, bidDto);
await this.accountRepository.updateAccountCurrency(
bidDto.typeGiven,
- Math.floor(accountBalance),
+ parseFloat(accountBalance.toFixed(8)),
userAccount.id,
queryRunner,
);
@@ -87,7 +87,7 @@ export class BidService implements OnModuleInit {
}
async checkCurrency(user, bidDto) {
const { typeGiven, receivedPrice, receivedAmount } = bidDto;
- const givenAmount = Math.floor(receivedPrice * receivedAmount);
+ const givenAmount = parseFloat((receivedPrice * receivedAmount).toFixed(8));
const userAccount = await this.accountRepository.findOne({
where: {
user: { id: user.userId },
@@ -159,13 +159,12 @@ export class BidService implements OnModuleInit {
let result = false;
try {
const buyData = {...tradeData};
- buyData.quantity = buyData.quantity >= ask_size ? ask_size.toFixed(8) : buyData.quantity.toFixed(8)
+ buyData.quantity = buyData.quantity >= ask_size ? parseFloat(ask_size.toFixed(8)) : parseFloat(buyData.quantity.toFixed(8))
if(buyData.quantity<0.00000001){
await queryRunner.commitTransaction();
return true;
}
- buyData.price = (ask_price * krw).toFixed(8);
-
+ buyData.price = parseFloat((ask_price * krw).toFixed(8));
const user = await this.userRepository.getUser(userId);
await this.tradeHistoryRepository.createTradeHistory(
@@ -179,27 +178,26 @@ export class BidService implements OnModuleInit {
});
if (asset) {
- asset.price = Math.floor(asset.price + buyData.price * buyData.quantity);
- asset.quantity += buyData.quantity;
-
+ asset.price = parseFloat((asset.price + buyData.price * buyData.quantity).toFixed(8));
+ asset.quantity += parseFloat(buyData.quantity.toFixed(8));
await this.assetRepository.updateAssetQuantityPrice(asset, queryRunner);
} else {
await this.assetRepository.createAsset(
bidDto,
- Math.floor(buyData.price * buyData.quantity),
+ parseFloat((buyData.price * buyData.quantity).toFixed(8)),
buyData.quantity,
queryRunner,
);
}
- tradeData.quantity -= buyData.quantity;
+ tradeData.quantity -= parseFloat(buyData.quantity.toFixed(8));
- if (tradeData.quantity === 0) {
+ if (tradeData.quantity <= 0.00000001) {
await this.tradeRepository.deleteTrade(tradeId, queryRunner);
} else await this.tradeRepository.updateTradeTransaction(tradeData, queryRunner);
const change = (tradeData.price - buyData.price) * buyData.quantity;
- const returnChange = Math.floor(change + account[typeGiven])
+ const returnChange = parseFloat((change + account[typeGiven]).toFixed(8))
const new_asset = await this.assetRepository.findOne({
where: {account:{id:account.id}, assetName: "BTC"}
})
diff --git a/packages/server/src/trade/trade.entity.ts b/packages/server/src/trade/trade.entity.ts
index 8d0fb4aa..f238d427 100644
--- a/packages/server/src/trade/trade.entity.ts
+++ b/packages/server/src/trade/trade.entity.ts
@@ -30,6 +30,8 @@ export class Trade {
@CreateDateColumn({ type: 'timestamp' })
createdAt: Date;
- @ManyToOne(() => User, (user) => user.trades)
+ @ManyToOne(() => User, (user) => user.trades, {
+ onDelete: 'CASCADE',
+ })
user: User;
}
diff --git a/packages/server/src/upbit/chart.repository.ts b/packages/server/src/upbit/chart.repository.ts
index 8f2bee54..f9ef2720 100644
--- a/packages/server/src/upbit/chart.repository.ts
+++ b/packages/server/src/upbit/chart.repository.ts
@@ -30,4 +30,10 @@ export class ChartRepository {
console.error("DB Searching Error : "+error)
}
}
+ async getSimpleChartData(key){
+ const data = await this.chartRedis.get(key);
+ if(!data){
+ return false;
+ }else return JSON.parse(data)
+ }
}
diff --git a/packages/server/src/upbit/chart.service.ts b/packages/server/src/upbit/chart.service.ts
index ce27db6c..0944cedf 100644
--- a/packages/server/src/upbit/chart.service.ts
+++ b/packages/server/src/upbit/chart.service.ts
@@ -18,10 +18,6 @@ export class ChartService implements OnModuleInit{
this.cleanQueue()
}
async upbitApiDoor(type,coin,to, minute){
- console.log("type : "+type)
- console.log("market : "+coin)
- console.log("minute : "+minute)
- console.log("to : "+to)
const validMinutes = ["1", "3", "5", "10", "15", "30", "60", "240"];
if (type === 'minutes') {
if (!minute || !validMinutes.includes(minute)) {
@@ -201,4 +197,79 @@ export class ChartService implements OnModuleInit{
}
setTimeout(()=>this.cleanQueue(),100)
}
+ makeCandle(coinData){
+ const name = coinData.code;
+ // date와 time을 각각 파싱
+ const year = coinData.trade_date.slice(0, 4);
+ const month = coinData.trade_date.slice(4, 6);
+ const day = coinData.trade_date.slice(6, 8);
+
+ const hour = coinData.trade_time.slice(0, 2);
+ const minute = coinData.trade_time.slice(2, 4);
+ const second = coinData.trade_time.slice(4, 6);
+
+ const tradeDate = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}`);
+ const kstDate = new Date(tradeDate.getTime() + 9 * 60 * 60 * 1000 * 2);
+
+ const price = coinData.trade_price;
+ const timestamp = coinData.trade_timestamp;
+ const candle_acc_trade_volume = coinData.trade_volume
+ const candle_acc_trade_price = price * candle_acc_trade_volume;
+ const candle = {
+ market : name,
+ candle_date_time_utc : kstDate.toISOString().slice(0,19),
+ candle_date_time_kst : kstDate.toISOString().slice(0,19),
+ opening_price : price,
+ high_price : price,
+ low_price : price,
+ trade_price : price,
+ timestamp : timestamp,
+ candle_acc_trade_price : candle_acc_trade_price,
+ candle_acc_trade_volume : candle_acc_trade_volume,
+ prev_closing_price : 0,
+ change_price : 0,
+ change_rate : 0,
+ }
+
+ const type = ['years','months','weeks','days','minutes','seconds'];
+ const minute_type = ["1", "3", "5", "10", "15", "30", "60", "240"];
+ type.forEach(async (key)=>{
+ if(key === 'minutes'){
+ const keys = [];
+ minute_type.forEach((min)=>{
+ keys.push(this.formatDate(kstDate, key, name, min));
+ })
+ keys.forEach(async (min)=>{
+ const candleData = await this.chartRepository.getSimpleChartData(min);
+ if(!candleData){
+ this.chartRepository.setChartData(min,JSON.stringify(candle))
+ }else{
+ candleData.trade_price = price;
+ candleData.high_price = candleData.high_price < price ? price : candleData.high_price;
+ candleData.low_price = candleData.low_price > price ? price : candleData.low_price;
+ candleData.timestamp = timestamp;
+ candleData.candle_acc_trade_price = candle_acc_trade_price;
+ candleData.candle_acc_trade_volume += candle_acc_trade_volume;
+
+ this.chartRepository.setChartData(min,JSON.stringify(candleData))
+ }
+ })
+ }else{
+ const redisKey = this.formatDate(kstDate, key, name, null);
+ const candleData = await this.chartRepository.getSimpleChartData(redisKey);
+ if(!candleData){
+ this.chartRepository.setChartData(redisKey,JSON.stringify(candle))
+ }else{
+ candleData.trade_price = price;
+ candleData.high_price = candleData.high_price < price ? price : candleData.high_price;
+ candleData.low_price = candleData.low_price > price ? price : candleData.low_price;
+ candleData.timestamp = timestamp;
+ candleData.candle_acc_trade_price = candle_acc_trade_price;
+ candleData.candle_acc_trade_volume += candle_acc_trade_volume;
+
+ this.chartRepository.setChartData(redisKey,JSON.stringify(candleData))
+ }
+ }
+ })
+ }
}
\ No newline at end of file
diff --git a/packages/server/src/upbit/coin-list.service.ts b/packages/server/src/upbit/coin-list.service.ts
index 2553edea..37c4c3e5 100644
--- a/packages/server/src/upbit/coin-list.service.ts
+++ b/packages/server/src/upbit/coin-list.service.ts
@@ -34,15 +34,15 @@ export class CoinListService implements OnModuleInit {
});
}
async getSimpleCoin(coins) {
- console.log(coins);
+ console.log(coins);
let krwCoinInfo = this.coinDataUpdaterService.getKrwCoinInfo();
while (!krwCoinInfo) {
await new Promise((resolve) => setTimeout(resolve, 100));
krwCoinInfo = this.coinDataUpdaterService.getKrwCoinInfo();
}
- if (!coins.length) return [];
-
+ if (!coins.length) return [];
+
return krwCoinInfo
.filter((coin) => coins.includes(coin.market))
.map((coin) => {
@@ -82,6 +82,19 @@ export class CoinListService implements OnModuleInit {
.filter((coin) => coin.market.startsWith('USDT'));
}
+ getCoinTickers(coins) {
+ const coinData = this.coinDataUpdaterService.getCoinLatestInfo();
+
+ const filteredData = Array.from(coinData.entries())
+ .filter(([symbol]) => !coins || coins.includes(symbol))
+ .map(([symbol, details]) => ({
+ code: symbol,
+ ...details,
+ }));
+
+ return filteredData;
+ }
+
convertToCodeCoinDto = (coin) => {
coin.korean_name = this.coinDataUpdaterService
.getCoinNameList()
diff --git a/packages/server/src/upbit/coin-ticker-websocket.service.ts b/packages/server/src/upbit/coin-ticker-websocket.service.ts
index 8af7110a..de8582cd 100644
--- a/packages/server/src/upbit/coin-ticker-websocket.service.ts
+++ b/packages/server/src/upbit/coin-ticker-websocket.service.ts
@@ -7,6 +7,7 @@ import {
UPBIT_WEBSOCKET_CONNECTION_TIME,
UPBIT_WEBSOCKET_URL,
} from 'common/upbit';
+import { ChartService } from './chart.service';
@Injectable()
export class CoinTickerService implements OnModuleInit {
@@ -16,6 +17,7 @@ export class CoinTickerService implements OnModuleInit {
constructor(
private readonly coinListService: CoinListService,
private readonly sseService: SseService,
+ private readonly chartService: ChartService
) {}
onModuleInit() {
@@ -38,6 +40,7 @@ export class CoinTickerService implements OnModuleInit {
const message = JSON.parse(data.toString());
if (message.error) throw new Error(JSON.stringify(message));
this.sseService.coinTickerSendEvent(message);
+ //this.chartService.makeCandle(message);
} catch (error) {
console.error('CoinTickerWebSocket 오류:', error);
}
diff --git a/packages/server/src/upbit/upbit.controller.ts b/packages/server/src/upbit/upbit.controller.ts
index cca4c634..422ae9e5 100644
--- a/packages/server/src/upbit/upbit.controller.ts
+++ b/packages/server/src/upbit/upbit.controller.ts
@@ -8,87 +8,96 @@ import { ApiQuery } from '@nestjs/swagger';
@Controller('upbit')
export class UpbitController {
- constructor(
- private readonly sseService: SseService,
- private readonly coinListService: CoinListService,
- private readonly chartService : ChartService
- ) {}
+ constructor(
+ private readonly sseService: SseService,
+ private readonly coinListService: CoinListService,
+ private readonly chartService: ChartService,
+ ) {}
- @Sse('price-updates')
- priceUpdates(@Query('coins') coins: string[]): Observable {
- coins = coins || [];
- const initData = this.sseService.initPriceStream(
- coins,
- this.coinListService.convertToMarketCoinDto,
- );
- return concat(
- initData,
- this.sseService.getPriceUpdatesStream(
- coins,
- this.coinListService.convertToCodeCoinDto,
- ),
- );
- }
- @Sse('orderbook')
- orderbookUpdates(@Query('coins') coins: string[]): Observable {
- coins = coins || [];
- return this.sseService.getOrderbookUpdatesStream(
- coins,
- this.coinListService.convertToOrderbookDto,
- );
- }
+ @Sse('price-updates')
+ priceUpdates(@Query('coins') coins: string[]): Observable {
+ coins = coins || [];
+ const initData = this.sseService.initPriceStream(
+ coins,
+ this.coinListService.convertToMarketCoinDto,
+ );
+ return concat(
+ initData,
+ this.sseService.getPriceUpdatesStream(
+ coins,
+ this.coinListService.convertToCodeCoinDto,
+ ),
+ );
+ }
+ @Sse('orderbook')
+ orderbookUpdates(@Query('coins') coins: string[]): Observable {
+ coins = coins || [];
+ return this.sseService.getOrderbookUpdatesStream(
+ coins,
+ this.coinListService.convertToOrderbookDto,
+ );
+ }
- @Get('market/all')
- getAllMarkets() {
- return this.coinListService.getAllCoinList();
- }
- @Get('market/krw')
- getKRWMarkets() {
- return this.coinListService.getKRWCoinList();
- }
- @Get('market/btc')
- getBTCMarkets() {
- return this.coinListService.getBTCCoinList();
- }
- @Get('market/usdt')
- getUSDTMarkets() {
- return this.coinListService.getUSDTCoinList();
- }
+ @Get('market/all')
+ getAllMarkets() {
+ return this.coinListService.getAllCoinList();
+ }
+ @Get('market/krw')
+ getKRWMarkets() {
+ return this.coinListService.getKRWCoinList();
+ }
+ @Get('market/btc')
+ getBTCMarkets() {
+ return this.coinListService.getBTCCoinList();
+ }
+ @Get('market/usdt')
+ getUSDTMarkets() {
+ return this.coinListService.getUSDTCoinList();
+ }
- @Get('market/top20-trade/krw')
- getTop20TradeKRW() {
- return this.coinListService.getMostTradeCoin();
- }
- @Get('market/simplelist/krw')
- getSomeKRW(@Query('market') market: string[]) {
- const marketList = market || [];
- return this.coinListService.getSimpleCoin(marketList);
- }
+ @Get('market/top20-trade/krw')
+ getTop20TradeKRW() {
+ return this.coinListService.getMostTradeCoin();
+ }
+ @Get('market/simplelist/krw')
+ getSomeKRW(@Query('market') market: string[]) {
+ const marketList = market || [];
+ return this.coinListService.getSimpleCoin(marketList);
+ }
+ @Get('market/tickers')
+ @ApiQuery({ name: 'coins', required: false, type: String })
+ getCoinTickers(@Query('coins') coins?: string) {
+ return this.coinListService.getCoinTickers(coins);
+ }
- @Get('candle/:type/:minute?')
- @ApiQuery({ name: 'minute', required: false, type: String })
- async getCandle(
- @Res() res: Response,
- @Param('type') type : string,
- @Query('market') market: string,
- @Query('to') to:string,
- @Param('minute') minute? :string
- ){
- try{
- console.log("type : "+type)
- console.log("market : "+market)
- console.log("minute : "+minute)
- console.log("to : "+to)
- const response = await this.chartService.upbitApiDoor(type, market, to, minute)
+ @Get('candle/:type/:minute?')
+ @ApiQuery({ name: 'minute', required: false, type: String })
+ async getCandle(
+ @Res() res: Response,
+ @Param('type') type: string,
+ @Query('market') market: string,
+ @Query('to') to: string,
+ @Param('minute') minute?: string,
+ ) {
+ try {
+ console.log('type : ' + type);
+ console.log('market : ' + market);
+ console.log('minute : ' + minute);
+ console.log('to : ' + to);
+ const response = await this.chartService.upbitApiDoor(
+ type,
+ market,
+ to,
+ minute,
+ );
- return res.status(response.statusCode).json(response)
- }catch(error){
- console.error("error"+error)
- return res.status(error.status)
- .json({
- message: error.message || '서버오류입니다.',
- error: error?.response || null,
- });;
- }
- }
+ return res.status(response.statusCode).json(response);
+ } catch (error) {
+ console.error('error' + error);
+ return res.status(error.status).json({
+ message: error.message || '서버오류입니다.',
+ error: error?.response || null,
+ });
+ }
+ }
}