diff --git a/client/src/components/EntireList/EntireList.tsx b/client/src/components/EntireList/EntireList.tsx new file mode 100644 index 00000000..57f10af8 --- /dev/null +++ b/client/src/components/EntireList/EntireList.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import Header from './Header'; +import StockItem from './StockItem'; +import useCompanyData from '../../hooks/useCompanyData'; + +const EntireList: React.FC = ({ currentListType, onChangeListType }) => { + const [isMenuOpen, setMenuOpen] = useState(false); + const [showChangePrice, setShowChangePrice] = useState(false); + + // useCompanyData 훅 사용하여 데이터 가져오기 + const { data: companies, isLoading, isError } = useCompanyData(1, 14); + + // 'companies'가 'undefined'인 경우를 처리하기 위해 빈 배열로 초기화 + const companiesList = companies || []; + + return ( + +
+ + 평가 수익금: +5,000,000원 {/* 임의의 평가 수익금 */} + + + {isLoading ? ( +
Loading...
+ ) : isError ? ( +
Error fetching data
+ ) : ( + companiesList.map((company) => ( + + )) + )} +
+ + ); +}; + +// Props와 상태에 대한 타입 정의 +type EntireListProps = { + currentListType: '전체종목' | '관심종목' | '보유종목'; + onChangeListType: (type: '전체종목' | '관심종목' | '보유종목') => void; +}; + +// WatchList 컴포넌트에 대한 스타일드 컴포넌트 정의 +const WatchListContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; +`; + +const Divider1 = styled.div` +margin:0px; +padding:0px; +width: 100%; +height: 10px; +display: flex; +flex-direction: row; +border-bottom: 1px solid #2f4f4f; +`; + +const Divider2 = styled.div` +margin:0px; +padding:0px; +width: 100%; +height: 4.5px; +display: flex; +flex-direction: row; +border-bottom: 1px solid #2f4f4f; +`; + + + +const EvaluationProfit = styled.div` +font-size: 16px; +font-weight: bold; +margin: 8px 0; +text-align: center; +color: red; // 수익금이 플러스일 경우 초록색으로 표시 +`; +const StockList = styled.div` + width: 100%; + max-height: 740px; /* 스크롤이 발생할 최대 높이를 지정하세요 */ + overflow-y: auto; /* 세로 스크롤을 활성화합니다 */ +`; + +export default EntireList; diff --git a/client/src/components/EntireList/Header.tsx b/client/src/components/EntireList/Header.tsx new file mode 100644 index 00000000..50ea82a7 --- /dev/null +++ b/client/src/components/EntireList/Header.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import styled from 'styled-components'; +import Menu_icon from "../../asset/images/menu.png"; + +const ALL_LIST = "전체종목"; +const INTEREST_LIST = "관심종목"; +const HOLDING_LIST = "보유종목"; + +const Header: React.FC = ({ currentListType, onChangeListType, isMenuOpen, setMenuOpen }) => { + return ( + + setMenuOpen(!isMenuOpen)} + /> + {currentListType} + {isMenuOpen && ( + + { onChangeListType(ALL_LIST); setMenuOpen(false); }}>{ALL_LIST} + { onChangeListType(INTEREST_LIST); setMenuOpen(false); }}>{INTEREST_LIST} + { onChangeListType(HOLDING_LIST); setMenuOpen(false); }}>{HOLDING_LIST} + + )} + + ); +}; + +type HeaderProps = { + currentListType: string; + onChangeListType: (type: "전체종목" | "관심종목" | "보유종목") => void; + isMenuOpen: boolean; + setMenuOpen: React.Dispatch>; +}; + + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + position: relative; +`; + +const Icon = styled.img` + margin-top: 9.5px; + margin-left: 10px; + width: 24px; + height: 24px; + cursor: pointer; + margin-right: 10px; +`; + +const HeaderText = styled.span` + margin-top: 9.5px; + font-size: 18px; +`; + +const SlideMenu = styled.div` + position: absolute; + top: 100%; + left: 0; + width: 248px; + background-color: #f7f7f7; + border: 1px solid #e0e0e0; /* 밑에 가로줄 추가 */ + display: flex; + flex-direction: column; +`; + +const MenuItem = styled.button` + padding: 8px 16px; + border: none; + background-color: transparent; + cursor: pointer; + text-align: left; +`; + +export default Header; diff --git a/client/src/components/EntireList/StockItem.tsx b/client/src/components/EntireList/StockItem.tsx new file mode 100644 index 00000000..fade936a --- /dev/null +++ b/client/src/components/EntireList/StockItem.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import styled from 'styled-components'; +import logo from '../../asset/logos/SK_logo.png'; + +const StockItem: React.FC = ({ company, setShowChangePrice, showChangePrice }) => { + const isPositiveChange = parseFloat(company.stockChangeRate) > 0; + const priceColor = isPositiveChange ? 'red' : 'blue'; + + return ( + + + + {company.korName} + {company.code} + + + {company.stockPrice} + setShowChangePrice(true)} + onMouseLeave={() => setShowChangePrice(false)} + > + {showChangePrice ? `${company.stockChangeAmount}%` : `${company.stockChangeRate}%`} + + + + ); +}; + +type NewCompanyData = { + companyId: number; + code: string; + korName: string; + stockPrice: string; + stockChangeAmount: string; + stockChangeRate: string; +}; + +type StockItemProps = { + company: NewCompanyData; + setShowChangePrice: React.Dispatch>; + showChangePrice: boolean; +}; + +const StockItemWrapper = styled.div` + display: flex; + flex-direction: row; /* 수평으로 정렬 */ + justify-content: flex-start; /* 왼쪽 정렬 */ + align-items: flex-start; /* 위로 정렬 */ + padding: 8px 0; + border-bottom: 1px solid #e0e0e0; + width: 100%; + background-color: transparent; + cursor: pointer; +`; + +const Logo = styled.img` + border-radius: 50%; + width: 40px; + height: 40px; + margin-right: 12px; +`; + +const StockInfo = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-right: 16px; +`; + +const StockName = styled.span` + font-weight: bold; +`; + +const StockCode = styled.span` + color: gray; +`; + +const StockPriceSection = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-left: auto; /* 자동으로 왼쪽 여백 추가 */ +`; + +const StockPrice = styled.span<{ change: string }>` + color: ${(props) => props.change}; +`; + +const StockChange = styled.span<{ change: string }>` + color: ${(props) => props.change}; + cursor: pointer; +`; + +export default StockItem; diff --git a/client/src/components/HoldingList/Header.tsx b/client/src/components/HoldingList/Header.tsx new file mode 100644 index 00000000..2f08c4dd --- /dev/null +++ b/client/src/components/HoldingList/Header.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import styled from 'styled-components'; +import Menu_icon from "../../asset/images/menu.png"; + +const ALL_LIST = "전체종목"; +const INTEREST_LIST = "관심종목"; +const HOLDING_LIST = "보유종목"; + +const Header: React.FC = ({ currentListType, onChangeListType, isMenuOpen, setMenuOpen }) => { + return ( + + setMenuOpen(!isMenuOpen)} + /> + {currentListType} + {isMenuOpen && ( + + { onChangeListType(ALL_LIST); setMenuOpen(false); }}>{ALL_LIST} + { onChangeListType(INTEREST_LIST); setMenuOpen(false); }}>{INTEREST_LIST} + { onChangeListType(HOLDING_LIST); setMenuOpen(false); }}>{HOLDING_LIST} + + )} + + ); +}; + +type HeaderProps = { + currentListType: string; + onChangeListType: (type: "전체종목" | "관심종목" | "보유종목") => void; + isMenuOpen: boolean; + setMenuOpen: React.Dispatch>; +}; + + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + position: relative; +`; + +const Icon = styled.img` + margin-top: 9.5px; + width: 24px; + height: 24px; + cursor: pointer; + margin-right: 10px; +`; + +const HeaderText = styled.span` + margin-top: 9.5px; + font-size: 18px; +`; + +const SlideMenu = styled.div` + position: absolute; + top: 100%; + left: 0; + width: 248px; + background-color: #f7f7f7; + border: 1px solid #e0e0e0; /* 밑에 가로줄 추가 */ + display: flex; + flex-direction: column; +`; + +const MenuItem = styled.button` + padding: 8px 16px; + border: none; + background-color: transparent; + cursor: pointer; + text-align: left; +`; + +export default Header; diff --git a/client/src/components/HoldingList/HoldingList.tsx b/client/src/components/HoldingList/HoldingList.tsx new file mode 100644 index 00000000..c4bad0aa --- /dev/null +++ b/client/src/components/HoldingList/HoldingList.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; +import Header from './Header'; +import StockItem from './StockItem'; +import useCompanyData from '../../hooks/useCompanyData'; + +const WatchList: React.FC = ({ currentListType, onChangeListType }) => { + const [isMenuOpen, setMenuOpen] = useState(false); + const [showChangePrice, setShowChangePrice] = useState(false); + + // useCompanyData 훅 사용하여 데이터 가져오기 + const { data: companies, isLoading, isError } = useCompanyData(1, 14); + + // 'companies'가 'undefined'인 경우를 처리하기 위해 빈 배열로 초기화 + const companiesList = companies || []; + + return ( + +
+ + 평가 수익금: +5,000,000원 {/* 임의의 평가 수익금 */} + + + {isLoading ? ( +
Loading...
+ ) : isError ? ( +
Error fetching data
+ ) : ( + companiesList.map((company) => ( + + )) + )} +
+ + ); +}; + +// Props와 상태에 대한 타입 정의 +type WatchListProps = { + currentListType: '전체종목' | '관심종목' | '보유종목'; + onChangeListType: (type: '전체종목' | '관심종목' | '보유종목') => void; +}; + +// WatchList 컴포넌트에 대한 스타일드 컴포넌트 정의 +const WatchListContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; +`; + +const Divider1 = styled.div` +margin:0px; +padding:0px; +width: 100%; +height: 10px; +display: flex; +flex-direction: row; +border-bottom: 1px solid #2f4f4f; +`; + +const Divider2 = styled.div` +margin:0px; +padding:0px; +width: 100%; +height: 4.5px; +display: flex; +flex-direction: row; +border-bottom: 1px solid #2f4f4f; +`; + + + +const EvaluationProfit = styled.div` +font-size: 16px; +font-weight: bold; +margin: 8px 0; +text-align: center; +color: red; // 수익금이 플러스일 경우 초록색으로 표시 +`; +const StockList = styled.div` + width: 90%; + max-height: 800px; /* 스크롤이 발생할 최대 높이를 지정하세요 */ + overflow-y: auto; /* 세로 스크롤을 활성화합니다 */ +`; + +export default WatchList; diff --git a/client/src/components/watchlist/Holdings.tsx b/client/src/components/HoldingList/Holdings.tsx similarity index 91% rename from client/src/components/watchlist/Holdings.tsx rename to client/src/components/HoldingList/Holdings.tsx index 5f6f3857..e9c3ab78 100644 --- a/client/src/components/watchlist/Holdings.tsx +++ b/client/src/components/HoldingList/Holdings.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import Samsung_logo from "../../asset/logos/Samsung_logo.svg" import Menu_icon from "../../asset/images/menu.png" -const Holdings: React.FC = ({ currentListType, onChangeListType }) => { +const Holdings: React.FC = ({ currentListType, onChangeListType }) => { const [isMenuOpen, setMenuOpen] = useState(false); @@ -28,8 +28,9 @@ const Holdings: React.FC = ({ currentListType, onChangeListType } {currentListType} {isMenuOpen && ( - { onChangeListType('관심목록'); setMenuOpen(false); }}>관심목록 - { onChangeListType('투자목록'); setMenuOpen(false); }}>투자목록 + { onChangeListType('관심종목'); setMenuOpen(false); }}>관심종목 + { onChangeListType('보유종목'); setMenuOpen(false); }}>보유종목 + { onChangeListType('전체종목'); setMenuOpen(false); }}>전체종목 )}
@@ -76,9 +77,9 @@ const Holdings: React.FC = ({ currentListType, onChangeListType } ); }; -type HoldingsProps = { - currentListType: "관심목록" | "투자목록"; - onChangeListType: (type: "관심목록" | "투자목록") => void; +type holdingsProps = { + currentListType: '전체종목' | '관심종목' | '보유종목'; + onChangeListType: (type: '전체종목' | '관심종목' | '보유종목') => void; }; const getColorByChange = (change: string) => { diff --git a/client/src/components/HoldingList/StockItem.tsx b/client/src/components/HoldingList/StockItem.tsx new file mode 100644 index 00000000..fade936a --- /dev/null +++ b/client/src/components/HoldingList/StockItem.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import styled from 'styled-components'; +import logo from '../../asset/logos/SK_logo.png'; + +const StockItem: React.FC = ({ company, setShowChangePrice, showChangePrice }) => { + const isPositiveChange = parseFloat(company.stockChangeRate) > 0; + const priceColor = isPositiveChange ? 'red' : 'blue'; + + return ( + + + + {company.korName} + {company.code} + + + {company.stockPrice} + setShowChangePrice(true)} + onMouseLeave={() => setShowChangePrice(false)} + > + {showChangePrice ? `${company.stockChangeAmount}%` : `${company.stockChangeRate}%`} + + + + ); +}; + +type NewCompanyData = { + companyId: number; + code: string; + korName: string; + stockPrice: string; + stockChangeAmount: string; + stockChangeRate: string; +}; + +type StockItemProps = { + company: NewCompanyData; + setShowChangePrice: React.Dispatch>; + showChangePrice: boolean; +}; + +const StockItemWrapper = styled.div` + display: flex; + flex-direction: row; /* 수평으로 정렬 */ + justify-content: flex-start; /* 왼쪽 정렬 */ + align-items: flex-start; /* 위로 정렬 */ + padding: 8px 0; + border-bottom: 1px solid #e0e0e0; + width: 100%; + background-color: transparent; + cursor: pointer; +`; + +const Logo = styled.img` + border-radius: 50%; + width: 40px; + height: 40px; + margin-right: 12px; +`; + +const StockInfo = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-right: 16px; +`; + +const StockName = styled.span` + font-weight: bold; +`; + +const StockCode = styled.span` + color: gray; +`; + +const StockPriceSection = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-left: auto; /* 자동으로 왼쪽 여백 추가 */ +`; + +const StockPrice = styled.span<{ change: string }>` + color: ${(props) => props.change}; +`; + +const StockChange = styled.span<{ change: string }>` + color: ${(props) => props.change}; + cursor: pointer; +`; + +export default StockItem; diff --git a/client/src/components/Logins/EmailLogin.tsx b/client/src/components/Logins/EmailLogin.tsx index 630a7ea3..3703a616 100644 --- a/client/src/components/Logins/EmailLogin.tsx +++ b/client/src/components/Logins/EmailLogin.tsx @@ -49,8 +49,8 @@ const EmailLoginModal: React.FC = ({ onClose, onLogin }) = dispatch(setLoginState()); // 토큰들을 로컬 스토리지에 저장 - if (authToken) localStorage.setItem("authToken", authToken); - if (refreshToken) localStorage.setItem("refreshToken", refreshToken); + if (authToken) localStorage.setItem("Authorization", authToken); + if (refreshToken) localStorage.setItem("Refresh-token", refreshToken); onLogin(); onClose(); diff --git a/client/src/components/Logins/GoogleLoginButton.tsx b/client/src/components/Logins/GoogleLoginButton.tsx index 66afa158..5f3efc8a 100644 --- a/client/src/components/Logins/GoogleLoginButton.tsx +++ b/client/src/components/Logins/GoogleLoginButton.tsx @@ -19,17 +19,3 @@ const GoogleLoginButton: React.FC = ({ backendURL }) => { } export default GoogleLoginButton; - - - -// const authToken = response.headers["authorization"]; -// console.log(authToken); - -// const refreshToken = response.headers["refresh"]; - -// // 로그인 상태로 만들기 -// dispatch(setLoginState()); - -// // 토큰들을 로컬 스토리지에 저장 -// if (authToken) localStorage.setItem("authToken", authToken); -// if (refreshToken) localStorage.setItem("refreshToken", refreshToken); \ No newline at end of file diff --git a/client/src/components/Logins/GoogleSignin.tsx b/client/src/components/Logins/GoogleSignin.tsx index 8cce7821..ad268ad8 100644 --- a/client/src/components/Logins/GoogleSignin.tsx +++ b/client/src/components/Logins/GoogleSignin.tsx @@ -17,6 +17,9 @@ const GoogleSignIn: React.FC = () => { dispatch(setLoginState()); }; + + + const handleError = () => { console.log("Login Failed"); }; diff --git a/client/src/components/Logins/KakaoLoginButton.tsx b/client/src/components/Logins/KakaoLoginButton.tsx index 4b0c8faa..81ea6ea3 100644 --- a/client/src/components/Logins/KakaoLoginButton.tsx +++ b/client/src/components/Logins/KakaoLoginButton.tsx @@ -19,17 +19,3 @@ const KakaoLoginButton: React.FC = ({ backendURL }) => { } export default KakaoLoginButton; - -// 토큰 저장 로직 - -// const authToken = response.headers["authorization"]; -// console.log(authToken); - -// const refreshToken = response.headers["refresh"]; - -// // 로그인 상태로 만들기 -// dispatch(setLoginState()); - -// // 토큰들을 로컬 스토리지에 저장 -// if (authToken) localStorage.setItem("authToken", authToken); -// if (refreshToken) localStorage.setItem("refreshToken", refreshToken); \ No newline at end of file diff --git a/client/src/components/Logins/Login.html b/client/src/components/Logins/Login.html deleted file mode 100644 index afe16d2b..00000000 --- a/client/src/components/Logins/Login.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Google Login - - - - - \ No newline at end of file diff --git a/client/src/components/watchlist/Header.tsx b/client/src/components/watchlist/Header.tsx new file mode 100644 index 00000000..2f08c4dd --- /dev/null +++ b/client/src/components/watchlist/Header.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import styled from 'styled-components'; +import Menu_icon from "../../asset/images/menu.png"; + +const ALL_LIST = "전체종목"; +const INTEREST_LIST = "관심종목"; +const HOLDING_LIST = "보유종목"; + +const Header: React.FC = ({ currentListType, onChangeListType, isMenuOpen, setMenuOpen }) => { + return ( + + setMenuOpen(!isMenuOpen)} + /> + {currentListType} + {isMenuOpen && ( + + { onChangeListType(ALL_LIST); setMenuOpen(false); }}>{ALL_LIST} + { onChangeListType(INTEREST_LIST); setMenuOpen(false); }}>{INTEREST_LIST} + { onChangeListType(HOLDING_LIST); setMenuOpen(false); }}>{HOLDING_LIST} + + )} + + ); +}; + +type HeaderProps = { + currentListType: string; + onChangeListType: (type: "전체종목" | "관심종목" | "보유종목") => void; + isMenuOpen: boolean; + setMenuOpen: React.Dispatch>; +}; + + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + position: relative; +`; + +const Icon = styled.img` + margin-top: 9.5px; + width: 24px; + height: 24px; + cursor: pointer; + margin-right: 10px; +`; + +const HeaderText = styled.span` + margin-top: 9.5px; + font-size: 18px; +`; + +const SlideMenu = styled.div` + position: absolute; + top: 100%; + left: 0; + width: 248px; + background-color: #f7f7f7; + border: 1px solid #e0e0e0; /* 밑에 가로줄 추가 */ + display: flex; + flex-direction: column; +`; + +const MenuItem = styled.button` + padding: 8px 16px; + border: none; + background-color: transparent; + cursor: pointer; + text-align: left; +`; + +export default Header; diff --git a/client/src/components/watchlist/StockItem.tsx b/client/src/components/watchlist/StockItem.tsx new file mode 100644 index 00000000..fade936a --- /dev/null +++ b/client/src/components/watchlist/StockItem.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import styled from 'styled-components'; +import logo from '../../asset/logos/SK_logo.png'; + +const StockItem: React.FC = ({ company, setShowChangePrice, showChangePrice }) => { + const isPositiveChange = parseFloat(company.stockChangeRate) > 0; + const priceColor = isPositiveChange ? 'red' : 'blue'; + + return ( + + + + {company.korName} + {company.code} + + + {company.stockPrice} + setShowChangePrice(true)} + onMouseLeave={() => setShowChangePrice(false)} + > + {showChangePrice ? `${company.stockChangeAmount}%` : `${company.stockChangeRate}%`} + + + + ); +}; + +type NewCompanyData = { + companyId: number; + code: string; + korName: string; + stockPrice: string; + stockChangeAmount: string; + stockChangeRate: string; +}; + +type StockItemProps = { + company: NewCompanyData; + setShowChangePrice: React.Dispatch>; + showChangePrice: boolean; +}; + +const StockItemWrapper = styled.div` + display: flex; + flex-direction: row; /* 수평으로 정렬 */ + justify-content: flex-start; /* 왼쪽 정렬 */ + align-items: flex-start; /* 위로 정렬 */ + padding: 8px 0; + border-bottom: 1px solid #e0e0e0; + width: 100%; + background-color: transparent; + cursor: pointer; +`; + +const Logo = styled.img` + border-radius: 50%; + width: 40px; + height: 40px; + margin-right: 12px; +`; + +const StockInfo = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-right: 16px; +`; + +const StockName = styled.span` + font-weight: bold; +`; + +const StockCode = styled.span` + color: gray; +`; + +const StockPriceSection = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-left: auto; /* 자동으로 왼쪽 여백 추가 */ +`; + +const StockPrice = styled.span<{ change: string }>` + color: ${(props) => props.change}; +`; + +const StockChange = styled.span<{ change: string }>` + color: ${(props) => props.change}; + cursor: pointer; +`; + +export default StockItem; diff --git a/client/src/components/watchlist/StockSearchComponent.tsx b/client/src/components/watchlist/StockSearchComponent.tsx new file mode 100644 index 00000000..68339b9c --- /dev/null +++ b/client/src/components/watchlist/StockSearchComponent.tsx @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; +import { useDispatch } from "react-redux"; +import styled from "styled-components"; +import { changeCompanyId } from "../../reducer/CompanyId-Reducer"; +import useGetCompanyList from "../../hooks/useGetCompanyList"; + +const stockSearch = "종목 검색"; +const search = "검색"; +const noExistCompany = "noExistCompany"; +const existCompany = "existCompany"; + +const StockSearchComponent: React.FC = () => { + const dispatch = useDispatch(); + const { companyList } = useGetCompanyList(); + const [searchWord, setSearchWord] = useState(""); + + const handleChangeSearchWord = (e: React.ChangeEvent) => { + setSearchWord(e.target.value); + }; + + const handleSearchCompany = () => { + let searchResult: string = noExistCompany; + + companyList.forEach((company: CompanyProps) => { + if (company.korName === searchWord) { + searchResult = existCompany; + dispatch(changeCompanyId(company.companyId)); + } + }); + + if (searchResult === noExistCompany) { + dispatch(changeCompanyId(-1)); + } + }; + + const handlePressEnterToSearch = (e: React.KeyboardEvent) => { + if (e.code === "Enter" && e.nativeEvent.isComposing === false) { + handleSearchCompany(); + setSearchWord(""); + } + }; + + return ( + + + {search} + + ); +}; + +export default StockSearchComponent; + +interface CompanyProps { + companyId: number; + code: string; + korName: string; + stockAsBiResponseDto: null; + stockInfResponseDto: null; +} + +// 스타일 정의 + +const SearchContainer = styled.div` + display: flex; + align-items: center; + flex-grow: 0.7; +`; + +const StyledSearchInput = styled.input.attrs({ + type: "text", + placeholder: "검색...", +})` + width: 100%; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + flex: 1; +`; + +const StyledSearchButton = styled.button` + background-color: #fff; + color: #2f4f4f; + border: 1px solid #2f4f4f; + padding: 0.5rem 1rem; + border-radius: 5px; + cursor: pointer; + transition: background-color 0.3s; + &:hover { + background-color: #f2f2f2; + } + margin-left: 0.5rem; +`; diff --git a/client/src/components/watchlist/WatchList.tsx b/client/src/components/watchlist/WatchList.tsx index 18ccca84..4592a1af 100644 --- a/client/src/components/watchlist/WatchList.tsx +++ b/client/src/components/watchlist/WatchList.tsx @@ -1,210 +1,113 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; -import Samsung_logo from "../../asset/logos/Samsung_logo.svg" -import LG_logo from "../../asset/logos/LG_logo.svg" -import Sk_logo from "../../asset/logos/Sk_logo.png" -import POSCO_logo from "../../asset/logos/POSCO_logo.svg" -import Menu_icon from "../../asset/images/menu.png" +import StockSearchComponent from './StockSearchComponent'; +import Header from './Header'; +import StockItem from './StockItem'; +import useCompanyData from '../../hooks/useCompanyData'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../store/config.ts'; // Redux store의 RootState를 import해야 합니다. const WatchList: React.FC = ({ currentListType, onChangeListType }) => { - const [isMenuOpen, setMenuOpen] = useState(false); - - - const favoriteStocks = [ - { name: "삼성전자", code: "005930", price: "71,000원", change: "+6.13%", changePrice: "+4,100원", logo: Samsung_logo }, - { name: "LG에너지솔루션", code: "373220", price: "522,000원", change: "-4.04%", changePrice: "-22,000원", logo: LG_logo }, - { name: "SK하이닉스", code: "000660", price: "120,000원", change: "-1.48%", changePrice: "-1,800원", logo: Sk_logo }, - { name: "삼성바이오로직스", code: "207940", price: "733,000원", change: "-0.54%", changePrice: "-4,000원", logo: Samsung_logo }, - { name: "POSCO홀딩스", code: "005490", price: "560,000원", change: "-3.28%", changePrice: "-19,000원", logo: POSCO_logo }, - { name: "삼성전자우", code: "005935", price: "56,900원", change: "+5.37%", changePrice: "+2,900원", logo: Samsung_logo }, - { name: "삼성SDI", code: "006400", price: "596,000원", change: "-2.93%", changePrice: "-18,000원", logo: Samsung_logo } - ]; - const [showChangePrice, setShowChangePrice] = useState(false); - - return ( - -
- setMenuOpen(!isMenuOpen)} - /> - {currentListType} - {isMenuOpen && ( - - { onChangeListType('관심목록'); setMenuOpen(false); }}>관심목록 - { onChangeListType('투자목록'); setMenuOpen(false); }}>투자목록 - - )} -
- - { /* 종목 추가 로직 */ }}>종목 추가 - - {favoriteStocks.map(stock => ( - - - - {stock.name} - {stock.code} - - - {stock.price} - setShowChangePrice(true)} - onMouseLeave={() => setShowChangePrice(false)} - > - {showChangePrice ? stock.changePrice : stock.change} - - - - ))} -
- ); -}; -type WatchListProps = { - currentListType: "관심목록" | "투자목록"; - onChangeListType: (type: "관심목록" | "투자목록") => void; -}; + const loginStatus = useSelector((state: RootState) => state.login); -const getColorByChange = (change: string) => { - if (change.startsWith('+')) return 'red'; - if (change.startsWith('-')) return 'blue'; - return 'black'; -}; -const WatchListContainer = styled.div` - padding: 8px 0px; -`; + // useCompanyData 훅 사용하여 데이터 가져오기 + const { data: companies, isLoading, isError } = useCompanyData(1, 14); -const Header = styled.div` - display: flex; - align-items: center; - position: relative; -`; + // 'companies'가 'undefined'인 경우를 처리하기 위해 빈 배열로 초기화 + const companiesList = companies || []; -const Icon = styled.img` - width: 24px; // 너비를 설정합니다. 원하는 크기로 조절 가능합니다. - height: 24px; // 높이를 설정합니다. 원하는 크기로 조절 가능합니다. - cursor: pointer; - margin-right: 10px; -`; -const HeaderText = styled.span` - font-size: 18px; -`; + // 이미 검색된 회사 ID들을 저장하는 스택 형태의 상태 + const [searchedCompanyIds, setSearchedCompanyIds] = useState([]); -const SlideMenu = styled.div` - position: absolute; - top: 100%; - left: 0; - width: 248px; - background-color: #f7f7f7; - border: 1px solid #e0e0e0; - display: flex; - flex-direction: column; -`; + // Redux store에서 선택된 회사 ID 가져오기 + const selectedCompanyId = useSelector((state: RootState) => state.companyId); -const MenuItem = styled.button` - padding: 8px 16px; - border: none; - background-color: transparent; - cursor: pointer; - text-align: left; + // 새로운 회사 ID가 검색될 때마다 스택에 추가 + useEffect(() => { + if (selectedCompanyId !== -1 && !searchedCompanyIds.includes(selectedCompanyId)) { + setSearchedCompanyIds(prevIds => [...prevIds, selectedCompanyId]); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedCompanyId]); - &:hover { - background-color: #e0e0e0; - } -`; + return ( + +
+ + + + + {isLoading ? ( +
Loading...
+ ) : isError ? ( +
Error fetching data
+ ) : loginStatus === 1 ? ( + companiesList + .filter(company => searchedCompanyIds.includes(company.companyId)) + .map((company) => ( + + )) + ) : ( +
로그인이 필요합니다.
+ )} +
-const StockItem = styled.button` - display: flex; - justify-content: space-between; - align-items: flex-start; - padding: 8px 0; - border-bottom: 1px solid #e0e0e0; - width: 100%; - background-color: transparent; - cursor: pointer; - border: none; - text-align: left; -`; + + ); +}; -const Logo = styled.img` - border-radius: 50%; - width: 40px; - height: 40px; - margin-right: 12px; -`; +// Props와 상태에 대한 타입 정의 +type WatchListProps = { + currentListType: '전체종목' | '관심종목' | '보유종목'; + onChangeListType: (type: '전체종목' | '관심종목' | '보유종목') => void; +}; -const StockInfo = styled.div` +// WatchList 컴포넌트에 대한 스타일드 컴포넌트 정의 +const WatchListContainer = styled.div` display: flex; flex-direction: column; align-items: flex-start; - margin-right: 16px; -`; - -const StockName = styled.span` - font-weight: bold; -`; - -const StockCode = styled.span` - color: gray; `; -const StockPriceSection = styled.div` +const Divider1 = styled.div` + margin:0px; + padding:0px; + width: 100%; + height: 10px; display: flex; - flex-direction: column; - align-items: flex-start; + flex-direction: row; + border-bottom: 1px solid #2f4f4f; `; -const StockPrice = styled.span.attrs<{ change: string }>(({ change }) => ({ - style: { - color: getColorByChange(change), - }, -}))``; - -const StockChange = styled.span.attrs<{ change: string }>(({ change }) => ({ - style: { - color: getColorByChange(change), - }, - }))` - cursor: pointer; - `; - -const Divider1 = styled.div` - margin:0px; - padding:0px; - width: 100%; - height: 11px; - display: flex; - flex-direction: row; - border-bottom: 1px solid #2f4f4f; -`; const Divider2 = styled.div` - margin:0px; - padding:0px; - width: 100%; - height: 4px; - display: flex; - flex-direction: row; - border-bottom: 1px solid #2f4f4f; + margin:0px; + padding:0px; + width: 100%; + height: 4.5px; + display: flex; + flex-direction: row; + border-bottom: 1px solid #2f4f4f; `; -const AddStockButton = styled.button` - padding: 10px; - border: none; - background-color: transparent; - cursor: pointer; - text-align: left; - color: black; - - &:hover { - background-color: #e0e0e0; - } +const StockList = styled.div` + width: 90%; + max-height: 800px; /* 스크롤이 발생할 최대 높이를 지정하세요 */ + overflow-y: auto; /* 세로 스크롤을 활성화합니다 */ `; -export default WatchList; +export default WatchList; \ No newline at end of file diff --git a/client/src/hooks/useCompanyData.ts b/client/src/hooks/useCompanyData.ts new file mode 100644 index 00000000..5decdc9b --- /dev/null +++ b/client/src/hooks/useCompanyData.ts @@ -0,0 +1,57 @@ +import { useQuery } from 'react-query'; +import axios from 'axios'; + +const BASE_URL = 'http://ec2-13-125-246-160.ap-northeast-2.compute.amazonaws.com:8080'; + +// 데이터 타입 정의 +type CompanyData = { + companyId: number; + code: string; + korName: string; + stockInfResponseDto: { + stck_prpr: string; + prdy_vrss: string; + prdy_ctrt: string; + }; +}; + +// 커스텀 훅 정의 +function useCompanyData(startCompanyId: number, endCompanyId: number) { + const fetchData = async (companyId: number) => { + const url = `${BASE_URL}/companies/${companyId}`; + const response = await axios.get(url); + return response.data; + }; + + // companyId 범위에 대한 배열 생성 + const companyIds = Array.from({ length: endCompanyId - startCompanyId + 1 }, (_, index) => startCompanyId + index); + + // 리액트-쿼리의 useQuery 훅 사용 + const { data, isLoading, isError } = useQuery( + ['companyData', startCompanyId, endCompanyId], + async () => { + const promises = companyIds.map((companyId) => fetchData(companyId)); + return Promise.all(promises); + } + ); + + // 필요한 데이터 추출 및 저장 + const extractedData = data?.map((company) => { + return { + companyId: company.companyId, + code: company.code, + korName: company.korName, + stockPrice: company.stockInfResponseDto.stck_prpr, + stockChangeAmount: company.stockInfResponseDto.prdy_vrss, + stockChangeRate: company.stockInfResponseDto.prdy_ctrt, + }; + }); + + return { + data: extractedData, + isLoading, + isError, + }; +} + +export default useCompanyData; diff --git a/client/src/page/MainPage.tsx b/client/src/page/MainPage.tsx index fbb0dbfb..af3bf85b 100644 --- a/client/src/page/MainPage.tsx +++ b/client/src/page/MainPage.tsx @@ -10,8 +10,9 @@ import EmailSignupModal from "../components/Signups/EmailSignup"; import EmailVerificationModal from "../components/Signups/EmailCertify"; import PasswordSettingModal from "../components/Signups/Password"; import CentralChart from "../components/CentralChart/Index"; -import WatchList from "../components/WatchList/WatchList"; -import Holdings from "../components/WatchList/Holdings";// Assuming you have a Holdings component +import EntireList from "../components/EntireList/EntireList"; +import HoldingList from "../components/HoldingList/HoldingList"; +import WatchList from "../components/WatchList/WatchList"; // Assuming you have a Holdings component import CompareChartSection from "../components/CompareChartSection/Index"; import StockOrderSection from "../components/StockOrderSection/Index"; import Welcome from "../components/Signups/Welcome"; @@ -96,6 +97,7 @@ const MainPage = () => { const dispatch = useDispatch(); const isLogin = useSelector((state: RootState) => state.login); + console.log(isLogin); // 🔴 페이지 로드 시 로컬 스토리지의 토큰을 기반으로 로그인 상태를 확인합니다. useEffect(() => { @@ -118,19 +120,16 @@ const MainPage = () => { dispatch(setLoginState()); }; - // // 🔴 로그아웃 시 로컬스토리지에 있는 Auth 토큰 제거 - // const handleLogout = () => { - // dispatch(setLogoutState()); - // localStorage.removeItem("Authorization"); - // }; const handleLoginConfirmationClose = () => { setLoginConfirmationModalOpen(false); }; - const [selectedMenu, setSelectedMenu] = useState<"관심목록" | "투자목록">("투자목록"); // Default menu is 관심목록 + // 현재 선택된 메뉴 타입을 상태로 관리 + const [selectedMenu, setSelectedMenu] = useState<"전체종목" | "관심종목" | "보유종목">("전체종목"); - const handleMenuChange = (menu: "관심목록" | "투자목록") => { + // 메뉴 변경 핸들러 + const handleMenuChange = (menu: "전체종목" | "관심종목" | "보유종목") => { setSelectedMenu(menu); }; @@ -144,14 +143,22 @@ const MainPage = () => {
{!expandScreen.left && ( - {selectedMenu === "관심목록" ? : } + + {selectedMenu === "전체종목" ? ( + + ) : selectedMenu === "관심종목" ? ( + + ) : selectedMenu === "보유종목" ? ( + + ) : null} + )} {!expandScreen.right && }
{isOAuthModalOpen && ( - handleMenuChange("관심목록")} onHoldingsClick={() => handleMenuChange("투자목록")} /> + handleMenuChange("관심종목")} onHoldingsClick={() => handleMenuChange("보유종목")} /> )} {isEmailLoginModalOpen && }