Skip to content

[FE] 브랜치 병합 #221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Nov 27, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion FE/src/components/Login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import useLoginModalStore from 'store/useLoginModalStore';
import Input from './Input';
import { ChatBubbleOvalLeftIcon } from '@heroicons/react/16/solid';
import { ChatBubbleOvalLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
import { FormEvent, useEffect, useState } from 'react';
import { login } from 'service/auth';
import useAuthStore from 'store/authStore';
@@ -95,6 +95,9 @@ export default function Login() {
<ChatBubbleOvalLeftIcon className='size-5' />
카카오 계정으로 로그인
</button>
<button className='absolute right-8 top-8' onClick={toggleModal}>
<XMarkIcon className='h-7 w-7 text-juga-grayscale-500' />
</button>
</section>
</>
);
8 changes: 7 additions & 1 deletion FE/src/components/Mypage/Account.tsx
Original file line number Diff line number Diff line change
@@ -2,12 +2,16 @@ import { useQuery } from '@tanstack/react-query';
import AccountCondition from './AccountCondition';
import MyStocksList from './MyStocksList';
import { getAssets } from 'service/assets';
import { isWithinTimeRange } from 'utils/common';

export default function Account() {
const { data, isLoading, isError } = useQuery(
['account', 'assets'],
() => getAssets(),
{ staleTime: 1000 },
{
staleTime: 1000,
refetchInterval: isWithinTimeRange('09:00', '15:30') ? 5000 : false,
},
);

if (isLoading) return <div>loading</div>;
@@ -16,6 +20,8 @@ export default function Account() {

const { asset, stocks } = data;

console.log(asset, stocks);

return (
<div className='flex min-h-[500px] flex-col gap-3'>
<AccountCondition asset={asset} />
26 changes: 14 additions & 12 deletions FE/src/components/Mypage/MyInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PencilSquareIcon } from '@heroicons/react/16/solid';
import { useQuery } from '@tanstack/react-query';
import { getMyProfile } from 'service/user';

@@ -12,22 +13,23 @@ export default function MyInfo() {
if (!data) return <div>No data</div>;
if (isError) return <div>error</div>;

const { name, email } = data;
const { name } = data;

return (
<div className='flex flex-col items-center p-6 text-lg'>
<div className='w-full px-40'>
<div className='flex items-center justify-between py-2 border-b'>
<p className='font-medium text-left text-jugagrayscale-400 w-28'>
Username
<div className='flex w-[50%] items-center gap-2 py-2'>
<div className='flex items-center justify-between w-full'>
<p className='w-28 min-w-[100px] truncate font-medium text-juga-grayscale-black'>
username
</p>
<p className='font-semibold text-jugagrayscale-500'>{name}</p>
</div>
<div className='flex items-center justify-between py-2'>
<p className='font-medium text-left text-jugagrayscale-400 w-28'>
Email
</p>
<p className='font-semibold text-jugagrayscale-500'>{email}</p>
<div className='flex items-center gap-2'>
<p className='min-w-[50px] truncate font-semibold text-juga-grayscale-500'>
{name}
</p>
<button>
<PencilSquareIcon className='w-5 h-5' />
</button>
</div>
</div>
</div>
</div>
26 changes: 20 additions & 6 deletions FE/src/components/Mypage/MyStocksList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MyStockListUnit } from 'types';
import { calcYield } from 'utils/common';

type MyStocksListProps = {
stocks: MyStockListUnit[];
@@ -9,22 +10,35 @@ export default function MyStocksList({ stocks }: MyStocksListProps) {
<div className='flex flex-col flex-1 w-full p-4 mx-auto bg-white rounded-md shadow-md'>
<div className='flex pb-2 text-sm font-bold border-b'>
<p className='w-1/2 text-left truncate'>종목</p>
<p className='w-1/4 text-center'>보유 수량</p>
<p className='w-1/4 text-right'>평균 가격</p>
<p className='w-1/6 text-center'>보유 수량</p>
<p className='w-1/6 text-center'>현재가</p>
<p className='w-1/6 text-center'>평균 매수가</p>
<p className='w-1/6 text-right'>수익률</p>
</div>

<ul className='flex flex-col text-sm divide-y'>
{stocks.map((stock) => {
const { code, name, avg_price, quantity } = stock;
const { code, name, avg_price, quantity, stck_prpr } = stock;

const stockYield = calcYield(avg_price, +stck_prpr);

return (
<li className='flex py-2' key={code}>
<div className='flex w-1/2 gap-2 text-left truncate'>
<p className='font-semibold'>{name}</p>
<p className='text-gray-500'>{code}</p>
</div>
<p className='w-1/4 text-center'>{quantity}</p>
<p className='w-1/4 text-right truncate'>
{Math.floor(avg_price).toLocaleString()}원
<p className='w-1/6 text-center'>{quantity}</p>
<p className='w-1/6 text-center truncate'>
{(+stck_prpr).toLocaleString()}원
</p>
<p className='w-1/6 text-center'>
{avg_price.toLocaleString()}원
</p>
<p
className={`w-1/6 truncate text-right ${stockYield < 0 ? 'text-juga-blue-50' : 'text-juga-red-60'}`}
>
{stockYield.toFixed(2)}%
</p>
</li>
);
20 changes: 10 additions & 10 deletions FE/src/components/News/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
import { NewsMockDataType } from './newsMockData.ts';
import { NewsDataType } from './NewsDataType.ts';
import { formatDate } from '../../utils/formatTime.ts';

type CardWithImageProps = {
data: NewsMockDataType;
data: NewsDataType;
};
export default function Card({ data }: CardWithImageProps) {
return (
<a
className='flex cursor-pointer flex-col rounded-lg border p-4 transition-all hover:bg-juga-grayscale-50'
href={data.link}
href={data.originallink}
target='_blank'
rel='noopener noreferrer'
>
<div className={'mb-2 flex w-full flex-row items-center justify-between'}>
<div className={'flex flex-row items-center gap-3'}>
<span className='rounded-full bg-juga-blue-10 px-2 py-0.5 text-xs text-juga-blue-50'>
증권
</span>
<h3 className='w-[320px] truncate text-left text-base font-medium'>
{data.title}
</h3>
</div>
<span className={'w-fit text-sm text-gray-500'}>{data.date}</span>
<span className={'w-fit text-sm text-gray-500'}>
{formatDate(data.pubDate)}
</span>
</div>
<div className='flex w-full items-center justify-between gap-4'>
<p className='w-96 truncate text-left text-sm text-juga-grayscale-500'>
{data.img}
{data.description}
</p>
<span className='whitespace-nowrap text-sm text-juga-grayscale-500'>
{data.publisher}
<span className='rounded-full bg-juga-blue-10 px-2 py-0.5 text-xs text-juga-blue-50'>
{data.query}
</span>
</div>
</a>
23 changes: 19 additions & 4 deletions FE/src/components/News/News.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import Card from './Card.tsx';
import { newsMockData } from './newsMockData.ts';
import { useQuery } from '@tanstack/react-query';
import { getNewsData } from '../../service/getNewsData.ts';
import { NewsDataType } from './NewsDataType.ts';

export default function News() {
const { data, isLoading, isError } = useQuery({
queryKey: ['News'],
queryFn: () => getNewsData(),
staleTime: 1000 * 60,
});

if (isError) return <div>Error!!</div>;
if (isLoading) return <div>Loading...</div>;

const randomNewsIndex = Math.floor(Math.random() * 16);

return (
<div className='w-full'>
<div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold'>주요 뉴스</h2>
</div>

<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2'>
{newsMockData.slice(0, 4).map((news, index) => (
<Card key={index} data={news} />
))}
{data.news
.slice(randomNewsIndex, randomNewsIndex + 4)
.map((news: NewsDataType, index: number) => (
<Card key={index} data={news} />
))}
</div>
</div>
);
7 changes: 7 additions & 0 deletions FE/src/components/News/NewsDataType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type NewsDataType = {
title: string;
description: string;
pubDate: string;
originallink: string;
query: string;
};
4 changes: 3 additions & 1 deletion FE/src/components/Search/SearchCardHighlight.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { formatNoSpecialChar } from '../../utils/formatNoSpecialChar.ts';

type SearchCardHighLightProps = {
text: string;
highlight: string;
@@ -11,7 +13,7 @@ export const SearchCardHighLight = ({
return <div>{text}</div>;
}

const targetWord = highlight.trim();
const targetWord = formatNoSpecialChar(highlight.trim());

const parts = text.trim().split(new RegExp(`(${targetWord})`, 'gi'));
return (
6 changes: 3 additions & 3 deletions FE/src/components/Search/index.tsx
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
import Lottie from 'lottie-react';
import searchAnimation from 'assets/searchAnimation.json';
import { useSearchHistory } from './searchHistoryHook.ts';
import { formatNoSpecialChar } from '../../utils/formatNoSpecialChar.ts';

export default function SearchModal() {
const { isOpen, toggleSearchModal } = useSearchModalStore();
@@ -23,20 +24,19 @@
shouldSearch ? searchInput : '',
500,
);

const { data, isLoading, isFetching } = useQuery({
queryKey: ['search', debounceValue],
queryFn: () => getSearchResults(debounceValue),
queryFn: () => getSearchResults(formatNoSpecialChar(debounceValue)),
enabled: !!debounceValue && !isDebouncing,
staleTime: 1000,
cacheTime: 1000 * 60,
});

useEffect(() => {
if (data && data.length > 0 && debounceValue && !isLoading) {
addSearchHistory(debounceValue);
addSearchHistory(formatNoSpecialChar(debounceValue));
}
}, [data, debounceValue]);

Check warning on line 39 in FE/src/components/Search/index.tsx

GitHub Actions / FE-test-and-build

React Hook useEffect has missing dependencies: 'addSearchHistory' and 'isLoading'. Either include them or remove the dependency array

Check warning on line 39 in FE/src/components/Search/index.tsx

GitHub Actions / build-and-deploy (fe, FE, 5173, juga-docker-fe, ENV_FE)

React Hook useEffect has missing dependencies: 'addSearchHistory' and 'isLoading'. Either include them or remove the dependency array

if (!isOpen) return null;

7 changes: 2 additions & 5 deletions FE/src/components/StocksDetail/Chart.tsx
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@
import { drawUpperYAxis } from 'utils/chart/drawUpperYAxis.ts';
import { drawLowerYAxis } from 'utils/chart/drawLowerYAxis.ts';
import { drawChartGrid } from 'utils/chart/drawChartGrid.ts';
import { drawMouseGrid } from '../../utils/chart/drawMouseGrid.ts';
import { drawMouseGrid } from 'utils/chart/drawMouseGrid.ts';

const categories: { label: string; value: TiemCategory }[] = [
{ label: '일', value: 'D' },
@@ -46,7 +46,6 @@
const upperChartY = useRef<HTMLCanvasElement>(null);
const lowerChartY = useRef<HTMLCanvasElement>(null);
const chartX = useRef<HTMLCanvasElement>(null);
// RAF 관리를 위한 ref
const rafRef = useRef<number>();
const [timeCategory, setTimeCategory] = useState<TiemCategory>('D');
const [charSizeConfig, setChartSizeConfig] = useState<ChartSizeConfigType>({
@@ -64,14 +63,12 @@
y: 0,
});


const { data, isLoading } = useQuery(
['stocksChartData', code, timeCategory],
() => getStocksChartDataByCode(code, timeCategory),
{ staleTime: 1000 },
);


const handleMouseDown = useCallback((e: MouseEvent) => {
e.preventDefault();
setIsDragging(true);
@@ -285,7 +282,7 @@
);
}
},
[

Check warning on line 285 in FE/src/components/StocksDetail/Chart.tsx

GitHub Actions / FE-test-and-build

React Hook useCallback has unnecessary dependencies: 'drawBarChart', 'drawCandleChart', 'drawChartGrid', 'drawLineChart', 'drawLowerYAxis', 'drawUpperYAxis', 'drawXAxis', and 'padding'. Either exclude them or remove the dependency array. Outer scope values like 'padding' aren't valid dependencies because mutating them doesn't re-render the component

Check warning on line 285 in FE/src/components/StocksDetail/Chart.tsx

GitHub Actions / build-and-deploy (fe, FE, 5173, juga-docker-fe, ENV_FE)

React Hook useCallback has unnecessary dependencies: 'drawBarChart', 'drawCandleChart', 'drawChartGrid', 'drawLineChart', 'drawLowerYAxis', 'drawUpperYAxis', 'drawXAxis', and 'padding'. Either exclude them or remove the dependency array. Outer scope values like 'padding' aren't valid dependencies because mutating them doesn't re-render the component
padding,
upperLabelNum,
lowerLabelNum,
@@ -361,7 +358,7 @@

return (
<div className='box-border flex h-[260px] flex-col items-center rounded-lg bg-white p-3'>
<div className='flex items-center justify-between w-full h-fit'>
<div className='flex h-fit w-full items-center justify-between'>
<p className='font-semibold'>차트</p>
<nav className='flex gap-4 text-sm'>
{categories.map(({ label, value }) => (
5 changes: 2 additions & 3 deletions FE/src/components/StocksDetail/PriceSection.tsx
Original file line number Diff line number Diff line change
@@ -31,21 +31,20 @@
},
);
},
[id, buttonFlag],

Check warning on line 34 in FE/src/components/StocksDetail/PriceSection.tsx

GitHub Actions / FE-test-and-build

React Hook useCallback has a missing dependency: 'queryClient'. Either include it or remove the dependency array

Check warning on line 34 in FE/src/components/StocksDetail/PriceSection.tsx

GitHub Actions / build-and-deploy (fe, FE, 5173, juga-docker-fe, ENV_FE)

React Hook useCallback has a missing dependency: 'queryClient'. Either include it or remove the dependency array
);

useEffect(() => {
// 이벤트 리스너 등록
if (!buttonFlag) return;
const handleTradeHistory = (chartData: PriceDataType) => {
addData(chartData);
};

socket.on(`trade-history/${id}`, handleTradeHistory);

return () => {
socket.off(`trade-history/${id}`, handleTradeHistory);
};
}, [id, addData]);
}, [id, addData, buttonFlag]);

useEffect(() => {
const tmpIndex = buttonFlag ? 0 : 1;
4 changes: 2 additions & 2 deletions FE/src/components/StocksDetail/PriceTableColumn.tsx
Original file line number Diff line number Diff line change
@@ -21,8 +21,8 @@ export default function PriceTableColumn({ viewMode }: Props) {
return (
<thead className={'z-1 sticky top-0 bg-white'}>
<tr className={'h-10 border-b text-gray-500'}>
<th className={'px-4 py-1 text-left font-medium'}>채결가</th>
<th className={'px-4 py-1 text-right font-medium'}>채결량(주)</th>
<th className={'px-4 py-1 text-left font-medium'}>체결가</th>
<th className={'px-4 py-1 text-right font-medium'}>체결량(주)</th>
<th className={'px-4 py-1 text-right font-medium'}>등락률</th>
{/*<th className={'px-4 py-1 text-right font-medium'}>거래량(주)</th>*/}
<th className={'px-4 py-1 text-right font-medium'}>시간</th>
8 changes: 4 additions & 4 deletions FE/src/components/StocksDetail/TradeSection/SellSection.tsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import { ChangeEvent, FocusEvent, FormEvent, useRef, useState } from 'react';
import { StockDetailType } from 'types';
import useAuthStore from 'store/authStore';
import useTradeAlertModalStore from 'store/tradeAlertModalStore';
import { isNumericString } from 'utils/common';
import { calcYield, isNumericString } from 'utils/common';
import TradeAlertModal from './TradeAlertModal';

type SellSectionProps = {
@@ -43,7 +43,7 @@ export default function SellSection({ code, detailInfo }: SellSectionProps) {

const pl = (+currPrice - avg_price) * count;
const totalPrice = +currPrice * count;
const plRate = ((pl / totalPrice) * 100).toFixed(2);
const plRate = calcYield(avg_price, +currPrice);

const handlePriceChange = (e: ChangeEvent<HTMLInputElement>) => {
const s = e.target.value.replace(/,/g, '');
@@ -170,9 +170,9 @@ export default function SellSection({ code, detailInfo }: SellSectionProps) {
<div className='flex justify-between'>
<p>예상 수익률</p>
<p
className={`${+plRate < 0 ? 'text-juga-blue-50' : 'text-juga-red-60'}`}
className={`${plRate < 0 ? 'text-juga-blue-50' : 'text-juga-red-60'}`}
>
{plRate}%
{plRate.toFixed(2)}%
</p>
</div>
</div>
7 changes: 7 additions & 0 deletions FE/src/service/getNewsData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const getNewsData = async () => {
const response = await fetch(`${import.meta.env.VITE_API_URL}/news`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};
6 changes: 6 additions & 0 deletions FE/src/types.ts
Original file line number Diff line number Diff line change
@@ -38,6 +38,8 @@ export type StockChartUnit = {
stck_lwpr: string;
acml_vol: string;
prdy_vrss_sign: string;
mov_avg_5: string;
mov_avg_20: string;
};

export type MypageSectionType = 'account' | 'order' | 'info';
@@ -55,6 +57,10 @@ export type MyStockListUnit = {
avg_price: number;
code: string;
name: string;
stck_prpr: string;
prdy_vrss: string;
prdy_vrss_sign: string;
prdy_ctrt: string;
quantity: number;
};

9 changes: 8 additions & 1 deletion FE/src/utils/chart/drawCandleChart.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,14 @@ export function drawCandleChart(
const n = data.length;

const values = data
.map((d) => [+d.stck_hgpr, +d.stck_lwpr, +d.stck_clpr, +d.stck_oprc])
.map((d) => [
+d.stck_hgpr,
+d.stck_lwpr,
+d.stck_clpr,
+d.stck_oprc,
Math.floor(+d.mov_avg_5),
Math.floor(+d.mov_avg_20),
])
.flat();
const yMax = Math.round(Math.max(...values) * (1 + weight));
const yMin = Math.round(Math.min(...values) * (1 - weight));
28 changes: 18 additions & 10 deletions FE/src/utils/chart/drawLineChart.ts
Original file line number Diff line number Diff line change
@@ -9,35 +9,43 @@ export function drawLineChart(
height: number,
padding: Padding,
weight: number = 0,
lineWidth: number = 1,
lineWidth: number = 4,
) {
if (data.length === 0) return;

ctx.beginPath();

const n = data.length;
const yMax = Math.round(
Math.max(...data.map((d: StockChartUnit) => +d.stck_oprc)) * (1 + weight),
);
const yMin = Math.round(
Math.min(...data.map((d: StockChartUnit) => +d.stck_oprc)) * (1 - weight),
);
const gap = Math.floor(width / n);

const values = data
.map((d) => [
+d.stck_hgpr,
+d.stck_lwpr,
+d.stck_clpr,
+d.stck_oprc,
Math.floor(+d.mov_avg_5),
Math.floor(+d.mov_avg_20),
])
.flat();
const yMax = Math.round(Math.max(...values) * (1 + weight));
const yMin = Math.round(Math.min(...values) * (1 - weight));

data.forEach((e, i) => {
const cx = x + padding.left + (width * i) / (n - 1);
const cx = x + padding.left + (width * i) / (n - 1) + gap / 2;
const cy =
y +
padding.top +
height -
(height * (+e.stck_oprc - yMin)) / (yMax - yMin);
(height * (+e.mov_avg_5 - yMin)) / (yMax - yMin);

if (i === 0) {
ctx.moveTo(cx, cy);
} else {
ctx.lineTo(cx, cy);
}
});

ctx.strokeStyle = '#000';
ctx.lineWidth = lineWidth;
ctx.stroke();
}
9 changes: 8 additions & 1 deletion FE/src/utils/chart/drawUpperYAxis.ts
Original file line number Diff line number Diff line change
@@ -15,7 +15,14 @@ export const drawUpperYAxis = (
upperChartHeight: number,
) => {
const values = data
.map((d) => [+d.stck_hgpr, +d.stck_lwpr, +d.stck_clpr, +d.stck_oprc])
.map((d) => [
+d.stck_hgpr,
+d.stck_lwpr,
+d.stck_clpr,
+d.stck_oprc,
Math.floor(+d.mov_avg_5),
Math.floor(+d.mov_avg_20),
])
.flat();
const yMax = Math.round(Math.max(...values) * (1 + weight));
const yMin = Math.round(Math.min(...values) * (1 - weight));
29 changes: 29 additions & 0 deletions FE/src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -37,3 +37,32 @@ export function getTradeCommision(price: number) {

return Math.floor(price * rate);
}

export function calcYield(a: number, b: number) {
if (a === 0) return 0;

const result = ((b - a) / a) * 100;
return result;
}

export function isWithinTimeRange(startTime: string, endTime: string): boolean {
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes(); // 현재 시간을 분 단위로 계산

const [startHour, startMinute] = startTime.split(':').map(Number);
const [endHour, endMinute] = endTime.split(':').map(Number);

if (
isNaN(startHour) ||
isNaN(startMinute) ||
isNaN(endHour) ||
isNaN(endMinute)
) {
throw new Error('Invalid time format. Use "HH:MM" format.');
}

const startMinutes = startHour * 60 + startMinute; // 시작 시간(분 단위)
const endMinutes = endHour * 60 + endMinute; // 종료 시간(분 단위)

return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
}
3 changes: 3 additions & 0 deletions FE/src/utils/formatNoSpecialChar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const formatNoSpecialChar = (query: string) => {
return query.replace(/[^a-zA-Z0-9- ]|\\/g, '');
};
5 changes: 5 additions & 0 deletions FE/src/utils/formatTime.ts
Original file line number Diff line number Diff line change
@@ -5,3 +5,8 @@ export function formatTime(time: string) {
const day = time.slice(6, 8);
return `${year}.${mon}.${day}`;
}

export function formatDate(dateString: string) {
const date = new Date(dateString);
return `${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
3 changes: 2 additions & 1 deletion FE/src/utils/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import { formatNoSpecialChar } from './formatNoSpecialChar.ts';

export const useDebounce = (value: string, delay: number) => {
const [debounceValue, setDebounceValue] = useState(value);
@@ -8,7 +9,7 @@ export const useDebounce = (value: string, delay: number) => {
setIsDebouncing(true);

const handler = setTimeout(() => {
setDebounceValue(value);
setDebounceValue(formatNoSpecialChar(value));
setIsDebouncing(false);
}, delay);