Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion frontend/src/components/Sidebar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
position: fixed;
top: 90px;
left: 0;
width: 200px;
width: 264px;
height: calc(100vh - 90px);
padding: 0 32px;
background-color: #02040d;
Expand Down
48 changes: 29 additions & 19 deletions frontend/src/components/stockgame/Betting.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import styles from './Betting.module.css';
import icon1 from '../../assets/at_icon_1.png';
// import icon1 from '../../assets/at_icon_1.png';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

주석 처리된 코드 제거 필요

주석 처리된 코드는 버전 관리 시스템에서 이력을 확인할 수 있으므로, 코드베이스를 깔끔하게 유지하기 위해 제거하는 것이 좋습니다.

다음 주석 처리된 코드를 제거하세요:

  • Line 2: // import icon1 from '../../assets/at_icon_1.png';
  • Lines 9-10: 주석 처리된 data 할당
  • Lines 61-66: 상승 베팅 정보 표시 부분
  • Lines 77-82: 하락 베팅 정보 표시 부분

Also applies to: 9-10, 61-66, 77-82

🤖 Prompt for AI Agents
frontend/src/components/stockgame/Betting.jsx around lines 2, 9-10, 61-66, and
77-82: remove the leftover commented-out code to keep the codebase clean —
specifically delete the commented import on line 2, the commented data
assignment on lines 9-10, the commented "up bet" display block on lines 61-66,
and the commented "down bet" display block on lines 77-82; ensure no functional
code is altered and run the component build/test to verify no references to
those comments remain.

import StockInfoItem from './StockInfoItem';
import { mockDailyBet, mockWeeklyBet } from '../../utils/bettingInfo';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { dailyBet, weeklyBet } from '../../utils/bettingInfo';

const DailyBetting = ({ period }) => {
const [isBetting, setIsBetting] = useState('none');
const mockBettingInfo = period === 'daily' ? mockDailyBet : mockWeeklyBet;
// const data = period === 'daily' ? mockDailyBet : mockWeeklyBet;
// const data = period === 'daily' ? dailyBet() : weeklyBet();
const [data, setData] = useState(null);

useEffect(() => {
async function fetchData() {
if (period === 'daily') {
const res = await dailyBet();
setData(res);
} else {
const res = await weeklyBet();
setData(res);
}
}
fetchData();
}, [period]);

const onClickUpBet = () => {
if (isBetting !== 'none') {
Expand All @@ -30,30 +45,25 @@ const DailyBetting = ({ period }) => {
}
};

const formatDate = (dateStr) => {
const [year, month, day] = dateStr.split('-');
return `${year}년 ${month}월 ${day}일`;
};
if (!data) return <div>Loading...</div>;

return (
<div className={styles['daily-betting-card']}>
<span className={styles['date']}>
{formatDate(mockBettingInfo.date)} 베팅
</span>
<span className={styles['date']}>{data.title}</span>
{/* 베팅종목 정보 */}
<div className={styles['stock-info']}>
<StockInfoItem label="종목" value={mockBettingInfo.symbol} />
<StockInfoItem label="종가" value={mockBettingInfo.closePrice} />
<StockInfoItem label="종목" value={data.symbol} />
<StockInfoItem label="종가" value={data.previousClosePrice} />
{/* 베팅 정보 */}
<div className={styles['bet-info']}>
{/* 상승 베팅 */}
<div className={styles['upper-bet']}>
<div className={styles['bet-point']}>
{/* <div className={styles['bet-point']}>
<img src={icon1} className={styles['icon']} />
<span>+{mockBettingInfo.upperBet}P</span>
<span>+{data.upperBet}P</span>
</div>
<div className={styles['divider']} />
<span>{mockBettingInfo.upBetCount}명</span>
<span>{data.upBetCount}명</span> */}
<button
className={`${styles['up-button']} ${isBetting === 'up' && styles.upActive}`}
onClick={onClickUpBet}
Expand All @@ -64,12 +74,12 @@ const DailyBetting = ({ period }) => {
</div>
{/* 하락 베팅 */}
<div className={styles['lower-bet']}>
<div className={styles['bet-point']}>
{/* <div className={styles['bet-point']}>
<img src={icon1} className={styles['icon']} />
<span>-{mockBettingInfo.lowerBet}P</span>
<span>-{data.lowerBet}P</span>
</div>
<div className={styles['divider']} />
<span>{mockBettingInfo.downBetCount}명</span>
<span>{data.downBetCount}명</span> */}
<button
className={`${styles['down-button']} ${isBetting === 'down' && styles.downActive}`}
onClick={onClickDownBet}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/stockgame/Betting.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
background-color: #f9f9f9;
border-radius: 12px;
width: 880px;
height: 170px;
height: 200px;
/* max-width: 880px;
min-width: 440px; */
padding: 30px;
position: relative;
gap: 25px;
}

.date {
Expand Down
87 changes: 59 additions & 28 deletions frontend/src/components/stockgame/BettingHistory.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,51 @@
import styles from './BettingHistory.module.css';
import icon1 from '../../assets/at_icon_1.png';
// import icon1 from '../../assets/at_icon_1.png';
import StockInfoItem from './StockInfoItem';
import { dailyBettingHistory } from '../../utils/dailyBettingHistory';
import { weeklyBettingHistory } from '../../utils/weeklyBettingHistory';
import Pagination from './Pagination';
import { useState, useEffect } from 'react';
import {
getDailyBetHistory,
getWeeklyBetHistory,
} from '../../utils/bettingHistory';

const BettingHistory = ({ type }) => {
const formatDate = (dateStr) => {
const [year, month, day] = dateStr.split('-');
return `${year}년 ${month}월 ${day}일`;
};

const [data, setData] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const itemsPerPage = 5;

useEffect(() => {
async function fetchData() {
setLoading(true);
setError(null);
try {
if (type === 'daily') {
const res = await getDailyBetHistory();
setData(res);
} else {
const res = await getWeeklyBetHistory();
setData(res);
}
} catch (err) {
setError(err.message || '데이터를 불러오는데 실패했습니다.');
setData([]);
} finally {
setLoading(false);
}
}
fetchData();
}, [type]);
Comment on lines +18 to +38
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

비동기 작업 정리 메커니즘이 누락되었습니다.

컴포넌트가 언마운트되는 동안 비동기 작업이 진행 중인 경우, 언마운트된 컴포넌트에서 상태 업데이트가 발생하여 메모리 누수 및 React 경고가 발생할 수 있습니다.

다음과 같이 cleanup 함수를 추가하세요:

  useEffect(() => {
+   let isMounted = true;
    async function fetchData() {
      setLoading(true);
      setError(null);
      try {
        if (type === 'daily') {
          const res = await getDailyBetHistory();
-         setData(res);
+         if (isMounted) setData(res);
        } else {
          const res = await getWeeklyBetHistory();
-         setData(res);
+         if (isMounted) setData(res);
        }
      } catch (err) {
-       setError(err.message || '데이터를 불러오는데 실패했습니다.');
-       setData([]);
+       if (isMounted) {
+         setError(err.message || '데이터를 불러오는데 실패했습니다.');
+         setData([]);
+       }
      } finally {
-       setLoading(false);
+       if (isMounted) setLoading(false);
      }
    }
    fetchData();
+   return () => {
+     isMounted = false;
+   };
  }, [type]);
🤖 Prompt for AI Agents
In frontend/src/components/stockgame/BettingHistory.jsx around lines 18 to 38,
the useEffect fetchData flow lacks a cleanup to prevent state updates after
unmount — add a cleanup that cancels or ignores the async work: create an
AbortController or an isMounted flag before calling fetchData, pass the signal
if using fetch-based helpers or check the flag after awaited calls, and in the
effect return a cleanup that either aborts the request or sets the flag to
false; before every setState (setLoading, setError, setData) verify the
controller is not aborted or isMounted is true so updates won’t run on an
unmounted component.


// 주간/일간 바뀔 때 페이지 초기화
useEffect(() => {
setCurrentPage(1);
}, [type]);

const mockBetHistory =
type === 'weekly' ? weeklyBettingHistory : dailyBettingHistory;

const totalPages = Math.ceil(mockBetHistory.length / itemsPerPage);
const totalPages = Math.ceil(data.length / itemsPerPage);

// 현재 페이지에 해당하는 데이터만 slice
const currentData = mockBetHistory.slice(
const currentData = data.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
Expand All @@ -35,43 +54,55 @@ const BettingHistory = ({ type }) => {
setCurrentPage(page);
};

const getPercentChange = (prev, next) => {
return (((next - prev) / prev) * 100).toFixed(2);
};
Comment on lines +57 to +59
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

0으로 나누기 오류 가능성이 있습니다.

prev가 0인 경우 Infinity 또는 NaN이 반환되어 UI가 깨질 수 있습니다.

다음과 같이 가드를 추가하세요:

 const getPercentChange = (prev, next) => {
+  if (!prev || prev === 0) return '0.00';
   return (((next - prev) / prev) * 100).toFixed(2);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getPercentChange = (prev, next) => {
return (((next - prev) / prev) * 100).toFixed(2);
};
const getPercentChange = (prev, next) => {
if (!prev || prev === 0) return '0.00';
return (((next - prev) / prev) * 100).toFixed(2);
};
🤖 Prompt for AI Agents
In frontend/src/components/stockgame/BettingHistory.jsx around lines 46 to 48,
the getPercentChange function can divide by zero when prev is 0; add a guard at
the start that validates prev is a finite non-zero number and handle
zero/invalid cases by returning a safe value (e.g. a placeholder string like "—"
or "0.00" depending on UI expectations) instead of performing the division,
while keeping the existing toFixed(2) formatting for normal cases.


if (loading) return <div>Loading...</div>;
else if (error) return <div>에러: {error}</div>;
else if (data.length === 0) {
return <div>베팅 내역이 없습니다.</div>;
}

return (
<>
{currentData.map((history, index) => (
{currentData.map((item, index) => (
<div
key={index}
className={`${styles['daily-betting-card']} ${history.isCorrect ? styles['correct-card'] : styles['incorrect-card']}`}
className={`${styles['daily-betting-card']} ${item.collect ? styles['correct-card'] : styles['incorrect-card']}`}
>
<button
className={`${styles['result-icon']} ${history.isCorrect ? styles.correct : styles.incorrect}`}
className={`${styles['result-icon']} ${item.collect ? styles.correct : styles.incorrect}`}
>
{history.isCorrect ? 'v' : 'x'}
{item.collect ? 'v' : 'x'}
</button>
<span className={styles['date']}>
{formatDate(history.date)} 베팅
</span>
<span className={styles['date']}>{item.round.title}</span>
<div className={styles['stock-info']}>
<StockInfoItem label="종목" value={history.symbol} />
<StockInfoItem label="종목" value={item.round.symbol} />
<StockInfoItem
label="다음 날 종가"
value={history.nextClosePrice}
value={item.round.settleClosePrice}
/>
<StockInfoItem label="종가" value={history.closePrice} />
<StockInfoItem label="종가" value={item.round.previousClosePrice} />
<div className={styles['stock-change']}>
<span className={styles['change-value']}>{'->'}</span>
<span className={styles['change-value']}>
{history.changePercent}%
{getPercentChange(
item.round.previousClosePrice,
item.round.settleClosePrice
)}
%
</span>
</div>
{/* 베팅 결과 */}
<div className={styles['bet-result']}>
<div className={styles['bet-point']}>
{/* <div className={styles['bet-point']}>
<img src={icon1} className={styles['icon']} />+
<span>{history.points}P</span>
<span>{item.points}P</span>
</div>
<div className={styles['divider']} />
<span>{history.participants}명</span>
{history.result === 'UP' ? (
<span>{item.participants}명</span> */}
{item.round.resultOption === 'RISE' ? (
<button className={styles['up-button']}>상승 ↑</button>
) : (
<button className={styles['down-button']}>하락 ↓</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
background-color: #f9f9f9;
border-radius: 12px;
width: 880px;
height: 130px;
height: 200px;
/* max-width: 880px;
min-width: 440px; */
padding: 30px;
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/pages/StockGame.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
justify-content: center;
padding: 45px;
gap: 25px;
margin-left: 50px;
}

.title {
Expand All @@ -26,7 +27,6 @@
cursor: pointer;
color: #9c9c9c;
font-weight: 700;
font-style: bold;
font-size: 26px;
border: none;
background: none;
Expand All @@ -51,7 +51,7 @@
gap: 30px;
}

.daily-betting span {
.daily-betting > span {
font-weight: 500;
font-style: medium;
font-size: 20px;
Expand All @@ -68,7 +68,6 @@
background: none;

font-weight: 600;
font-style: semi-bold;
font-size: 16px;
color: #171717;
}
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/utils/bettingHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { api } from '../utils/axios.js';

const betHistory = async () => {
try {
const res = await api.get('/api/user-bets/history');
return res.data;
} catch (error) {
console.log(error.message);
return null;
}
};

export const getDailyBetHistory = async () => {
const data = await betHistory();
if (!data) return [];
return data.filter((item) => item.round.scope === 'DAILY');
};

export const getWeeklyBetHistory = async () => {
const data = await betHistory();
if (!data) return [];
return data.filter((item) => item.round.scope === 'WEEKLY');
};
34 changes: 18 additions & 16 deletions frontend/src/utils/bettingInfo.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
export const mockDailyBet = {
date: '2025-08-25',
symbol: 'AAPL',
closePrice: 96.3,
upperBet: 300,
lowerBet: 300,
upBetCount: 200,
downBetCount: 200,
import { api } from '../utils/axios.js';

export const dailyBet = async () => {
try {
const res = await api.get('/api/bet-rounds/DAILY');
return res.data;
} catch (error) {
console.log(error.message);
return null;
}
};

export const mockWeeklyBet = {
date: '2025-08-18',
symbol: 'AAPL',
closePrice: 98.5,
upperBet: 350,
lowerBet: 300,
upBetCount: 150,
downBetCount: 230,
export const weeklyBet = async () => {
try {
const res = await api.get('/api/bet-rounds/WEEKLY');
return res.data;
} catch (error) {
console.log(error.message);
return null;
}
};
Loading