Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
aced76d
[feat] 실전투자 -> 실전예측
kyun9-cloud Jul 28, 2025
5ffd800
[feat] 실전투자 -> 실전예측
kyun9-cloud Jul 28, 2025
8611819
Merge pull request #138 from InserToken/feat/1-design-yeakyung
kyun9-cloud Jul 28, 2025
e7d2b06
[refactor] 툴팁색변경
jiminseon Jul 28, 2025
7ea4ed9
Merge pull request #140 from InserToken/feat/35-pracMain/minseon
jiminseon Jul 28, 2025
d4af097
적중률
takeitEasyhwan Jul 28, 2025
9eba5b7
Merge branch 'develop' of https://github.com/InserToken/UpAndDown-Cli…
takeitEasyhwan Jul 28, 2025
78701e1
Merge pull request #141 from InserToken/feat/82-financialChart
takeitEasyhwan Jul 28, 2025
6da60f6
[feat] chart error
kyun9-cloud Jul 28, 2025
04b1963
Merge branch 'develop' into feat/1-design-yeakyung
kyun9-cloud Jul 28, 2025
d43ae94
Merge pull request #142 from InserToken/feat/1-design-yeakyung
kyun9-cloud Jul 28, 2025
f24c6a8
마이페이지 버튼
takeitEasyhwan Jul 28, 2025
9d39fe6
Merge pull request #143 from InserToken/feat/82-financialChart
takeitEasyhwan Jul 28, 2025
4eb4b0a
Update README.md
takeitEasyhwan Jul 28, 2025
ffd6398
[modify] 답변 disabled 추가
jiminseon Jul 28, 2025
5db90d6
Merge pull request #144 from InserToken/feat/35-pracMain/minseon
jiminseon Jul 28, 2025
2237655
[Fix]덧셈 수정
EunseoJun Jul 28, 2025
9dcec70
[Fix]덧셈 수정
EunseoJun Jul 28, 2025
0d5ebd7
Merge branch 'develop' of https://github.com/InserToken/UpAndDown-Cli…
EunseoJun Jul 28, 2025
1d894d8
Merge pull request #145 from InserToken/feat/37-indicator/Eunseo3
EunseoJun Jul 28, 2025
00969ce
avg 수정
takeitEasyhwan Jul 28, 2025
5140746
[modify] 툴팁 다시 수정
jiminseon Jul 28, 2025
ebdf0e3
Merge pull request #146 from InserToken/feat/35-pracMain/minseon
jiminseon Jul 28, 2025
908761d
Merge pull request #147 from InserToken/feat/37-indicator/Eunseo3
EunseoJun Jul 28, 2025
a77b46a
[feat] minor css #1
kyun9-cloud Jul 28, 2025
1ea9678
Merge pull request #148 from InserToken/feat/1-design-yeakyung
kyun9-cloud Jul 28, 2025
148accc
[feat] minor css #1
kyun9-cloud Jul 28, 2025
730b6a4
Merge pull request #149 from InserToken/feat/1-design-yeakyung
kyun9-cloud Jul 28, 2025
13c5b87
[modify] navbar 햄버거
jiminseon Jul 29, 2025
86a6c73
Update PracticePage.client.tsx
EunseoJun Jul 29, 2025
f4326da
[feat] tutorial
kyun9-cloud Jul 29, 2025
440a55a
Merge pull request #151 from InserToken/feat/1-design-yeakyung
kyun9-cloud Jul 29, 2025
1224032
[feat] navbar 충돌수정
kyun9-cloud Jul 29, 2025
a03686e
Merge pull request #152 from InserToken/feat/1-design-yeakyung
kyun9-cloud Jul 29, 2025
f6d74b9
Merge branch 'develop' into feat/35-pracMain/minseon
kyun9-cloud Jul 29, 2025
f75c55f
Merge pull request #150 from InserToken/feat/35-pracMain/minseon
kyun9-cloud Jul 29, 2025
0168a82
[modify] 수정
jiminseon Jul 29, 2025
3b044e4
Merge branch 'develop' of https://github.com/InserToken/UpAndDown-Cli…
jiminseon Jul 29, 2025
88cc5b7
Merge pull request #153 from InserToken/feat/35-pracMain/minseon
jiminseon Jul 29, 2025
37c5872
Update PracticePage.client.tsx
jiminseon Jul 29, 2025
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# UpAndDown-Client
About [오르락내리락] 과거의 차트로 공부하고! 나만의 종목으로 예측까지 해보는 주식 차트 학습 서비스
# Candly-Client
About [Candly] 과거의 차트로 공부하고! 나만의 종목으로 예측까지 해보는 주식 차트 학습 서비스
219 changes: 118 additions & 101 deletions src/components/Navbar/navbar.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import Link from "next/link";
import { checkUserStatus, getStock } from "@/services/userStock-service";
import { useAuthStore } from "@/stores/authStore";
import Image from "next/image";
import { TutorialOverlay } from "../blocks/Tutorial/TutorialPopup.client";

const menuItems = [
{ label: "홈", href: "/" },
{ label: "연습문제", href: "/practice" },
{ label: "실전투자", href: "/investment", dynamic: true },
{ label: "실전예측", href: "/investment", dynamic: true },
{ label: "랭킹", href: "/ranking" },
{ label: "마이페이지", href: "/mypage" },
];
Expand All @@ -21,48 +22,37 @@ export default function Navbar() {
const auth = useAuthStore((s) => s.auth);
const clearAuth = useAuthStore((s) => s.clearAuth);
const loginRequiredPaths = ["/", "/practice", "/ranking", "/mypage"];
const [showTutorial, setShowTutorial] = useState(false);
const handleCloseTutorial = () => {
localStorage.setItem("hideTutorial", "true");
setShowTutorial(false);
};

// hydration mismatch 방지용
const [mounted, setMounted] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);

useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;

const handleInvestClick = async () => {
try {
if (!auth?.token) {
console.warn("로그인 필요");
return router.push("/auth/login");
}
if (!auth?.token) return router.push("/auth/login");

const status = await checkUserStatus(auth.token);
//("이미 연동 완료된 user: ", status.hasHoldings);

if (status.hasHoldings) {
const stockData = await getStock(auth.token);
const firstCode = stockData.stocks[0]?.stock_code._id;
//console.log("주식 조회", firstCode);
if (firstCode) {
router.push(`/investment/${firstCode}`);
} else {
router.push("/investment");
}
} else {
router.push("/investment");
}
const stockData = await getStock(auth.token);
const firstCode = stockData.stocks[0]?.stock_code._id;
router.push(firstCode ? `/investment/${firstCode}` : "/investment");
} catch (err) {
console.error("실전투자 이동 중 오류:", err);
router.push("/investment");
}
};

const handleLogout = async () => {
try {
await fetch("/api/auth/logout", { method: "POST" });
} catch (e) {
console.warn("서버 로그아웃 실패");
}
} catch {}
sessionStorage.removeItem("token");
clearAuth();
router.replace("/auth/login");
Expand All @@ -72,16 +62,18 @@ export default function Navbar() {
router.push("/auth/login");
};

const navButtonClass = (isActive: boolean) =>
"text-base transition-colors cursor-pointer " +
(isActive
? "text-[#396FFB] hover:text-blue-500 font-semibold"
: "text-[#E2E2E2] hover:text-white");

return (
<nav className="h-[98px] flex items-center px-8 pl-10 whitespace-nowrap fixed bg-inherit w-screen z-20">

<nav className="h-[98px] flex items-center px-8 pl-10 fixed bg-inherit w-full z-20 text-nowrap">
{/* 로고 & 홈 이동 */}
<button
onClick={() => {
if (!auth?.token) {
router.push("/auth/login");
} else {
router.push("/");
}
}}
onClick={() => router.push(auth?.token ? "/" : "/auth/login")}
className="flex items-center pr-2.5 bg-transparent border-none cursor-pointer"
>
<Image
Expand All @@ -93,98 +85,123 @@ export default function Navbar() {
/>
<div className="text-2xl pr-20">Candly</div>
</button>
<ul className="flex gap-15">

{/* 데스크탑 메뉴 */}
<ul className="hidden md:flex gap-15">
{menuItems.map((item) => {
const isActive =
pathname === item.href || pathname.startsWith(`${item.href}/`);
const isLoginRequired = loginRequiredPaths.includes(item.href);

if (item.dynamic) {
return (
<li key={item.href}>
<button
onClick={handleInvestClick}
className={
"text-base transition-colors cursor-pointer " +
(isActive
? "text-[#396FFB] hover:text-blue-500 font-semibold"
: "text-[#E2E2E2] hover:text-white")
}
>
{item.label}
</button>
</li>
);
}
// 로그인 필요한 메뉴 (홈, 연습문제, 랭킹, 마이페이지)
if (isLoginRequired) {
return (
<li key={item.href}>
<button
onClick={() => {
if (!auth?.token) {
router.push("/auth/login");
} else {
router.push(item.href);
}
}}
className={
"text-base transition-colors cursor-pointer " +
(isActive
? "text-[#396FFB] hover:text-blue-500 font-semibold"
: "text-[#E2E2E2] hover:text-white")
}
type="button"
>
{item.label}
</button>
</li>
);
}
const onClick = item.dynamic
? handleInvestClick
: () => {
if (!auth?.token && isLoginRequired) {
router.push("/auth/login");
} else {
router.push(item.href);
}
};

// 나머지(로그인 필요없는 메뉴) (실제로 위에서 다 처리됨)
return (
<li key={item.href}>
<Link href={item.href}>
<span
className={
"text-base cursor-pointer transition-colors " +
(isActive
? "text-[#396FFB] font-semibold"
: "text-[#E2E2E2] hover:text-white")
}
>
{item.label}
</span>
</Link>
<button onClick={onClick} className={navButtonClass(isActive)}>
{item.label}
</button>
</li>
);
})}
</ul>

{/* 로그인/로그아웃 버튼 */}
<div className="ml-auto pr-5">
{mounted ? (
auth?.token ? (
{/* 오른쪽 버튼 (데스크탑 전용) */}
<div className="ml-auto hidden md:flex gap-4 items-center pr-5">
<button
onClick={() => {
setShowTutorial(true);
}}
className="text-sm px-4 text-[#E2E2E2] hover:text-white"
>
튜토리얼
</button>
{auth?.token ? (
<button
onClick={handleLogout}
className="text-sm text-[#E2E2E2] hover:text-white transition"
>
로그아웃
</button>
) : (
<button
onClick={handleLoginClick}
className="text-sm text-[#E2E2E2] hover:text-white transition"
>
로그인
</button>
)}
</div>

{/* 모바일 햄버거 버튼 */}
<button
onClick={() => setMenuOpen(!menuOpen)}
className="ml-auto md:hidden text-white text-xl"
>
</button>

{/* 모바일 메뉴 */}
{menuOpen && (
<div className="absolute top-[98px] left-0 w-full bg-black border-t border-neutral-700 px-5 py-4 flex flex-col gap-4 md:hidden z-40">
{menuItems.map((item) => {
const isLoginRequired = loginRequiredPaths.includes(item.href);
const onClick = item.dynamic
? handleInvestClick
: () => {
if (!auth?.token && isLoginRequired) {
router.push("/auth/login");
} else {
router.push(item.href);
}
setMenuOpen(false);
};
return (
<button
key={item.href}
onClick={onClick}
className="text-left text-base text-[#E2E2E2] hover:text-white"
>
{item.label}
</button>
);
})}
{/* <button
onClick={() => {
setShowTutorial(true);
}}
className="text-left text-base text-[#E2E2E2] hover:text-white"
>
튜토리얼
</button> */}
{auth?.token ? (
<button
onClick={handleLogout}
className="text-sm text-[#E2E2E2] hover:text-white transition"
className="text-left text-base text-[#E2E2E2] hover:text-white"
>
로그아웃
</button>
) : (
<button
onClick={handleLoginClick}
className="text-sm text-[#E2E2E2] hover:text-white transition"
onClick={() => {
router.push("/auth/login");
setMenuOpen(false);
}}
className="text-left text-base text-[#E2E2E2] hover:text-white"
>
로그인
</button>
)
) : (
// 서버에서 렌더링되는 placeholder (같은 구조 유지)
<div className="w-[64px] h-[20px]" />
)}
</div>
)}
</div>
)}
{showTutorial && <TutorialOverlay onClose={handleCloseTutorial} />}
</nav>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export default function InvestmentStockClient() {
</>
)}
<span
className="px-1 cursor-pointer text-gray-400 hover:bg-gray-800 rounded-sm"
className="cursor-pointer text-gray-400 hover:bg-gray-800 rounded-sm"
onClick={() => setShowIndicators((prev) => !prev)}
>
{showIndicators ? "– 보조지표 접기" : "+ 보조지표 설정"}
Expand Down
14 changes: 4 additions & 10 deletions src/components/blocks/Loading/Loading.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,25 @@ export default function LoadingPage() {
const wait = elapsed < minDelay ? minDelay - elapsed : 0;

setTimeout(() => {
//("✅ getStock result:", result);

if ("message" in result) {
setStatus("error");
setErrorMessage(result.message);
return;
}

const rawStocks = Array.isArray(result)
? result
: Array.isArray(result.output1)
? result.output1
: null;

if (!rawStocks) {
if (!result) {
setStatus("error");
setErrorMessage("응답 형식 오류");
return;
}

const mapped = rawStocks.map(
const mapped = result.map(
(item: { pdno: string; prdt_name: string }) => ({
stock_code: item.pdno,
company: item.prdt_name,
})
);
// console.log("map", mapped);

setStocks(mapped);
setStatus("done");
Expand All @@ -66,6 +59,7 @@ export default function LoadingPage() {
const handleChart = () => {
if (stocks.length > 0) {
const firstCode = stocks[0].stock_code;
// console.log("stocks", stocks[0].stock_code);
router.replace(`/investment/${firstCode}`);
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/blocks/MainHome/MainHome.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function MainHomeClient() {
router.push("/investment");
}
} catch (err) {
console.error("실전투자 이동 중 오류:", err);
console.error("실전예측 이동 중 오류:", err);
router.push("/investment");
}
};
Expand Down
18 changes: 15 additions & 3 deletions src/components/blocks/MyPageInvestment/MyPageInvestment.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,20 @@ export default function MyPageInvestmentClient() {
});

const validScores = stockResult.stocks
.map((item: any) => item.cumulative_score)
.filter((score: any) => typeof score === "number");
.map((item: any) => {
const predictCount =
investResult.find(
(s: any) =>
s.stock_code === item.stock_code?._id ||
s.stock_id === item._id
)?.scores?.length ?? 0;

return predictCount > 0 && typeof item.cumulative_score === "number"
? item.cumulative_score
: null;
})
.filter((score: any) => score !== null);

const avg =
validScores.length > 0
? Math.round(
Expand All @@ -124,7 +136,7 @@ export default function MyPageInvestmentClient() {

return (
<div>
<p className="text-2xl font-semibold mb-6">실전투자 히스토리</p>
<p className="text-2xl font-semibold mb-6">실전예측 히스토리</p>

<div className="flex items-center gap-4 mb-8">
<div className="h-20 w-55 bg-[#16161A] rounded-lg text-center flex items-baseline gap-1.5 justify-center pt-4">
Expand Down
Loading