@@ -253,7 +295,7 @@ export default function TrendingKeywords() {
{/* TreeMap */}
- {loading ? (
+ {loading || filterLoading ? (
{/* 스켈레톤 헤더 */}
@@ -277,7 +319,7 @@ export default function TrendingKeywords() {
value="loc"
valueFormat=".0f"
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}
- labelSkipSize={8}
+ labelSkipSize={12}
labelTextColor={{ from: "color", modifiers: [["darker", 1.2]] }}
parentLabelPosition="left"
parentLabelTextColor={{
@@ -295,8 +337,8 @@ export default function TrendingKeywords() {
label={(node) => {
// 주식명이 너무 길면 줄여서 표시
const name = node.data.name;
- if (name.length > 8) {
- return name.substring(0, 7) + "...";
+ if (name.length > 6) {
+ return name.substring(0, 5) + "...";
}
return name;
}}
@@ -333,4 +375,6 @@ export default function TrendingKeywords() {
);
-}
+});
+
+export default TrendingKeywords;
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index 619f93f..ddbc433 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -184,6 +184,15 @@ const Header = () => {
// 현재 경로가 navItem의 경로와 일치하는지 확인하는 함수
const isActiveRoute = (path: string) => {
+ // 하위 경로까지 활성화 처리할 기본 섹션들
+ const sectionsWithSubroutes = ["/mypage", "/news", "/stock"];
+
+ if (sectionsWithSubroutes.includes(path)) {
+ return (
+ location.pathname === path || location.pathname.startsWith(path + "/")
+ );
+ }
+
return location.pathname === path;
};
diff --git a/src/components/layout/myPageLayout/index.tsx b/src/components/layout/myPageLayout/index.tsx
index 7bc8388..3b52e6d 100644
--- a/src/components/layout/myPageLayout/index.tsx
+++ b/src/components/layout/myPageLayout/index.tsx
@@ -1,11 +1,210 @@
-import { Outlet } from "react-router-dom";
+import { Outlet, useLocation, useNavigate } from "react-router-dom";
+import { useState } from "react";
+import { motion } from "framer-motion";
+import {
+ User,
+ CreditCard,
+ Heart,
+ Bell,
+ Shield,
+ HelpCircle,
+ LogOut,
+} from "lucide-react";
+import Toast from "@/components/common/Toast";
export default function MyPageLayout() {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const isSubPage = location.pathname !== "/mypage";
+
+ const [user] = useState({
+ name: "홍길동",
+ email: "hong@example.com",
+ avatar:
+ "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face",
+ membership: "프리미엄",
+ joinDate: "2024년 1월",
+ });
+ const [toast, setToast] = useState<{
+ message: string;
+ type: "success" | "error";
+ } | null>(null);
+
+ const menuItems = [
+ {
+ icon:
,
+ title: "개인정보 관리",
+ subtitle: "프로필 정보 수정 및 조회",
+ href: "/mypage/profile",
+ },
+ {
+ icon:
,
+ title: "결제 수단 관리",
+ subtitle: "카드 정보 변경 및 결제 내역",
+ href: "/mypage/payment",
+ },
+ {
+ icon:
,
+ title: "즐겨찾기 리스트",
+ subtitle: "관심 종목 관리",
+ href: "/mypage/favorites",
+ },
+ {
+ icon:
,
+ title: "알림 설정",
+ subtitle: "푸시 알림 및 이메일 설정",
+ href: "/mypage/notifications",
+ },
+ {
+ icon:
,
+ title: "보안 설정",
+ subtitle: "비밀번호 변경 및 2단계 인증",
+ href: "/mypage/security",
+ },
+ {
+ icon:
,
+ title: "고객 지원",
+ subtitle: "FAQ 및 문의하기",
+ href: "/mypage/support",
+ },
+ ];
+
+ const handleLogout = async () => {
+ const { logout } = await import("@/api/auth/logoutApi");
+ try {
+ await logout();
+ setToast({ message: "로그아웃 되었습니다.", type: "success" });
+ } catch (_) {
+ setToast({ message: "로그아웃 중 오류가 발생했습니다.", type: "error" });
+ } finally {
+ localStorage.removeItem("access_token");
+ setTimeout(() => {
+ window.location.href = "/";
+ }, 800);
+ }
+ };
+
return (
-
-
-
+ {toast && (
+
setToast(null)}
+ duration={1500}
+ />
+ )}
+
+ {isSubPage ? (
+ // 하위 페이지일 때 - 모바일에서는 뒤로가기만, 데스크톱에서는 사이드바
+
+ {/* 데스크톱 사이드바 - 모바일에서는 숨김 */}
+
+ {/* 프로필 섹션 */}
+
+
+
+

+
+
+
+ {user.name}
+
+
{user.email}
+
+ {user.membership}
+
+
+
+
+
+ {/* 메뉴 아이템들 */}
+
+ {menuItems.map((item, index) => (
+
+
+
+ ))}
+
+
+ {/* 로그아웃 버튼 */}
+
+
+
+
+
+ {/* 메인 콘텐츠 영역 */}
+
+
+
+
+ ) : (
+ // 기본 레이아웃 (메인 마이페이지일 때)
+
+
+
+ )}
);
}
diff --git a/src/components/news/Watchlist.tsx b/src/components/news/Watchlist.tsx
index 1d044ad..ce45a5b 100644
--- a/src/components/news/Watchlist.tsx
+++ b/src/components/news/Watchlist.tsx
@@ -1,6 +1,7 @@
import { Star, Search, ChevronLeft, ChevronRight } from "lucide-react";
import { useState, useEffect } from "react";
import { getMyFeed } from "../../api/news/feed";
+import { getStockList } from "../../api/stock/list";
interface Company {
name: string;
@@ -22,30 +23,76 @@ export default function Watchlist({
const itemsPerPage = 5;
useEffect(() => {
- // /news/my-feed에서 뉴스 받아와서 관련 기업 추출
- getMyFeed({ limit: 100 }).then((newsList) => {
- // 모든 뉴스의 related_companies를 flat하게 모으고 중복 제거
- const allCompanies = newsList
- .flatMap((news) => news.related_companies || [])
- .filter(Boolean);
- const uniqueCompanies = Array.from(new Set(allCompanies));
- // 이름/코드 분리 (예시: "삼성전자(005930)" 형식이면 분리, 아니면 이름만)
- const companyObjs: Company[] = uniqueCompanies.map((item) => {
- const match = item.match(/(.+)[(]([0-9A-Za-z]+)[)]/);
- if (match) {
- return { name: match[1], code: match[2] };
- } else {
- return { name: item, code: item };
+ const loadCompanies = async () => {
+ try {
+ // 먼저 my-feed에서 관련 기업 추출 시도
+ const newsList = await getMyFeed({ limit: 100 });
+ console.log("뉴스 데이터:", newsList);
+ console.log("첫 번째 뉴스 상세:", newsList[0]);
+
+ // 모든 뉴스의 related_companies를 flat하게 모으고 중복 제거
+ const allCompanies = newsList
+ .flatMap((news) => news.related_companies || [])
+ .filter(Boolean);
+
+ console.log("추출된 기업들:", allCompanies);
+
+ // related_companies가 비어있으면 대안으로 인기 종목 사용
+ if (allCompanies.length === 0) {
+ console.log("관련 기업이 없어서 인기 종목을 로딩합니다.");
+ const stockResponse = await getStockList("market_cap_desc");
+ const companyObjs: Company[] = stockResponse.data
+ .slice(0, 20)
+ .map((stock) => ({
+ name: stock.name,
+ code: stock.code,
+ }));
+ console.log("인기 종목으로 대체:", companyObjs);
+ setCompanies(companyObjs);
+ return;
+ }
+
+ const uniqueCompanies = Array.from(new Set(allCompanies));
+ console.log("중복 제거 후:", uniqueCompanies);
+
+ // 이름/코드 분리 (예시: "삼성전자(005930)" 형식이면 분리, 아니면 이름만)
+ const companyObjs: Company[] = uniqueCompanies.map((item) => {
+ const match = item.match(/(.+)[(]([0-9A-Za-z]+)[)]/);
+ if (match) {
+ return { name: match[1], code: match[2] };
+ } else {
+ return { name: item, code: item };
+ }
+ });
+
+ console.log("최종 기업 객체들:", companyObjs);
+ setCompanies(companyObjs);
+ } catch (error) {
+ console.error("관심기업 데이터 로딩 실패:", error);
+ // 에러 시에도 인기 종목으로 대체
+ try {
+ const stockResponse = await getStockList("market_cap_desc");
+ const companyObjs: Company[] = stockResponse.data
+ .slice(0, 20)
+ .map((stock) => ({
+ name: stock.name,
+ code: stock.code,
+ }));
+ setCompanies(companyObjs);
+ } catch (fallbackError) {
+ console.error("대체 데이터 로딩도 실패:", fallbackError);
+ setCompanies([]);
}
- });
- setCompanies(companyObjs);
- });
+ }
+ };
+
+ loadCompanies();
}, []);
const filteredCompanies = companies.filter(
(company) =>
company.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- company.code.includes(searchQuery),
+ company.code.includes(searchQuery)
);
const totalPages = Math.ceil(filteredCompanies.length / itemsPerPage);
@@ -85,7 +132,7 @@ export default function Watchlist({
key={index}
onClick={() =>
onCompanySelect(
- company === selectedCompany ? null : company,
+ company === selectedCompany ? null : company
)
}
className={`p-4 rounded-lg border transition-colors duration-200 cursor-pointer ${
diff --git a/src/pages/myPage/favorites.tsx b/src/pages/myPage/favorites.tsx
index 73dc768..290fbf4 100644
--- a/src/pages/myPage/favorites.tsx
+++ b/src/pages/myPage/favorites.tsx
@@ -1,228 +1,208 @@
import { useState } from "react";
import { motion } from "framer-motion";
-import { ArrowLeft, Heart, Trash2, Search } from "lucide-react";
+import {
+ ArrowLeft,
+ Heart,
+ TrendingUp,
+ TrendingDown,
+ Star,
+ Search,
+ MoreVertical,
+} from "lucide-react";
import { useNavigate } from "react-router-dom";
export default function FavoritesPage() {
const navigate = useNavigate();
- const [activeTab, setActiveTab] = useState<"stocks" | "news">("stocks");
const [searchTerm, setSearchTerm] = useState("");
- const favoriteStocks = [
+ const [favoriteStocks] = useState([
{
id: 1,
symbol: "005930",
name: "삼성전자",
- price: "75,800",
- change: "+2.3%",
- changeType: "up",
- sector: "전자",
+ price: 71500,
+ change: 1500,
+ changePercent: 2.14,
+ volume: "12.5M",
+ marketCap: "427조",
+ isFavorite: true,
},
{
id: 2,
symbol: "000660",
name: "SK하이닉스",
- price: "142,000",
- change: "-1.2%",
- changeType: "down",
- sector: "반도체",
+ price: 128000,
+ change: -2000,
+ changePercent: -1.54,
+ volume: "3.2M",
+ marketCap: "93조",
+ isFavorite: true,
},
{
id: 3,
symbol: "035420",
name: "NAVER",
- price: "198,500",
- change: "+0.8%",
- changeType: "up",
- sector: "IT",
- },
- ];
-
- const favoriteNews = [
- {
- id: 1,
- title: "삼성전자, 2분기 실적 예상치 상회",
- press: "한국경제",
- published_at: "2024-01-15",
- impact: "positive",
- },
- {
- id: 2,
- title: "SK하이닉스 메모리 가격 상승세",
- press: "매일경제",
- published_at: "2024-01-14",
- impact: "positive",
+ price: 198500,
+ change: 3500,
+ changePercent: 1.79,
+ volume: "1.8M",
+ marketCap: "32조",
+ isFavorite: true,
},
{
- id: 3,
- title: "NAVER AI 기술 개발 현황",
- press: "조선일보",
- published_at: "2024-01-13",
- impact: "neutral",
+ id: 4,
+ symbol: "051910",
+ name: "LG화학",
+ price: 425000,
+ change: -5000,
+ changePercent: -1.16,
+ volume: "0.8M",
+ marketCap: "30조",
+ isFavorite: true,
},
- ];
+ ]);
- const getImpactColor = (impact: string) => {
- switch (impact) {
- case "positive":
- return "text-green-600 bg-green-50 border-green-200";
- case "negative":
- return "text-red-600 bg-red-50 border-red-200";
- case "neutral":
- return "text-gray-600 bg-gray-50 border-gray-200";
- default:
- return "text-gray-600 bg-gray-50 border-gray-200";
- }
+ const handleRemoveFavorite = (id: number) => {
+ console.log("즐겨찾기 제거:", id);
};
- const getImpactText = (impact: string) => {
- switch (impact) {
- case "positive":
- return "긍정";
- case "negative":
- return "부정";
- case "neutral":
- return "중립";
- default:
- return "중립";
- }
- };
+ const filteredStocks = favoriteStocks.filter(
+ (stock) =>
+ stock.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ stock.symbol.includes(searchTerm)
+ );
return (
-
- {/* 헤더 */}
-
-
+
+ {/* 헤더 - 모바일에서만 표시 */}
+
+
즐겨찾기
- {/* 검색바 */}
-
-
-
- setSearchTerm(e.target.value)}
- className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#0A5C2B]/20 focus:border-[#0A5C2B]"
- />
-
-
+
+ {/* 검색 및 필터 */}
+
+
+
+ setSearchTerm(e.target.value)}
+ className="w-full pl-12 pr-4 py-3 bg-white/70 backdrop-blur-xl border border-white/20 rounded-2xl focus:outline-none focus:ring-2 focus:ring-[#0A5C2B]/20 focus:border-[#0A5C2B] transition-all"
+ />
+
+
- {/* 탭 */}
-
-
-
-
-
-
+ {/* 즐겨찾기 주식 */}
+
+
+
+ 즐겨찾기 주식
+
+
+ {filteredStocks.length}개
+
+
-
- {activeTab === "stocks" ? (
-
- {favoriteStocks.map((stock, index) => (
-
-
-
-
-
- {stock.name}
-
-
- {stock.symbol}
-
-
-
-
{stock.sector}
+ {filteredStocks.map((stock, index) => (
+
+
+
+
+
+ {stock.name}
+
+
+ {stock.symbol}
+
-
-
{stock.price}
-
+
+ ₩{stock.price.toLocaleString()}
+
+
= 0 ? "text-red-500" : "text-blue-500"
}`}
>
- {stock.change}
-
-
-
-
-
- ))}
-
- ) : (
-
- {favoriteNews.map((news, index) => (
-
-
-
-
-
- {getImpactText(news.impact)}
+ {stock.change >= 0 ? (
+
+ ) : (
+
+ )}
+
+ {stock.change >= 0 ? "+" : ""}
+ {stock.change.toLocaleString()}(
+ {stock.changePercent >= 0 ? "+" : ""}
+ {stock.changePercent}%)
-
-
-
- {news.title}
-
-
- {news.press}
- •
- {news.published_at}
-
+
+
+
-
- ))}
-
+
+
+ ))}
+
+
+ {/* 빈 상태 */}
+ {filteredStocks.length === 0 && (
+
+
+
+
+
+ 즐겨찾기 주식이 없습니다
+
+
+ 관심 있는 주식을 즐겨찾기에 추가해보세요
+
+
)}
diff --git a/src/pages/myPage/index.tsx b/src/pages/myPage/index.tsx
index 6f5bf18..79dbdd7 100644
--- a/src/pages/myPage/index.tsx
+++ b/src/pages/myPage/index.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
import { motion } from "framer-motion";
+import { useNavigate } from "react-router-dom";
import {
User,
CreditCard,
@@ -14,6 +15,7 @@ import {
import Toast from "@/components/common/Toast";
export default function MyPage() {
+ const navigate = useNavigate();
const [user] = useState({
name: "홍길동",
email: "hong@example.com",
@@ -32,37 +34,37 @@ export default function MyPage() {
icon: ,
title: "개인정보 관리",
subtitle: "프로필 정보 수정 및 조회",
- href: "/myPage/profile",
+ href: "/mypage/profile",
},
{
icon: ,
title: "결제 수단 관리",
subtitle: "카드 정보 변경 및 결제 내역",
- href: "/myPage/payment",
+ href: "/mypage/payment",
},
{
icon: ,
title: "즐겨찾기 리스트",
- subtitle: "관심 종목 및 뉴스 관리",
- href: "/myPage/favorites",
+ subtitle: "관심 종목 관리",
+ href: "/mypage/favorites",
},
{
icon: ,
title: "알림 설정",
subtitle: "푸시 알림 및 이메일 설정",
- href: "/myPage/notifications",
+ href: "/mypage/notifications",
},
{
icon: ,
title: "보안 설정",
subtitle: "비밀번호 변경 및 2단계 인증",
- href: "/myPage/security",
+ href: "/mypage/security",
},
{
icon: ,
title: "고객 지원",
subtitle: "FAQ 및 문의하기",
- href: "/myPage/support",
+ href: "/mypage/support",
},
];
@@ -91,6 +93,7 @@ export default function MyPage() {
duration={1500}
/>
)}
+
{/* 프로필 카드 */}