Skip to content
Open
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
45 changes: 34 additions & 11 deletions src/components/common/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { Button } from "@/components/ui/button";
import LogoIcon from "@/assets/icons/Logo.svg?react";
import { useNavigate } from "react-router-dom";
import { useAuthStatus } from "@/hooks/login/query/useAuthStatus";

const Header = () => {
const navigate = useNavigate();
const { data: auth } = useAuthStatus();

const handleLogin = () => {
navigate("/login");
}
};

const handleSignup = () => {
navigate("/signup");
}
};

const goMyPage = () => {
navigate("/portfolio");
};

return (
<header className="w-full min-w-[768px] border-b border-gray-200 py-3">
<div className="mx-auto flex max-w-screen-xl items-center justify-between px-12">
Expand All @@ -28,7 +35,7 @@ const Header = () => {
지표
</a>
<a href="/quant" className="transition hover:text-gold-300">
퀀트
퀀트
</a>
<a href="/" className="transition hover:text-gold-300">
서비스
Expand All @@ -37,14 +44,30 @@ const Header = () => {
</div>

<div className="flex items-center gap-4">
<Button
variant="outline"
className="h-9 border-gray-300 px-6 text-gray-800 hover:bg-gray-100"
onClick={handleSignup}
>
회원가입
</Button>
<Button className="h-9 bg-[#F4B224] px-6 text-white hover:bg-[#d9991b]" onClick={handleLogin}>로그인</Button>
{auth?.isAuthenticated ? (
<button
onClick={goMyPage}
className="text-sm text-gold-300 hover:underline"
>
{(auth.user?.name || auth.user?.email || "사용자") + "님"} 마이페이지
</button>
) : (
<>
<Button
variant="outline"
className="h-9 border-gray-300 px-6 text-gray-800 hover:bg-gray-100"
onClick={handleSignup}
>
회원가입
</Button>
<Button
className="h-9 bg-[#F4B224] px-6 text-white hover:bg-[#d9991b]"
onClick={handleLogin}
>
로그인
</Button>
</>
)}
</div>
</div>
</header>
Expand Down
14 changes: 13 additions & 1 deletion src/hooks/login/query/useAuthStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

const checkAuthStatus = async (): Promise<AuthStatusResponse> => {
const isLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost';
const enableLocalAuto = (import.meta as any)?.env?.VITE_ENABLE_LOCAL_AUTOLOGIN !== 'false';

if (isLocalhost && enableLocalAuto) {
const mockUser = {
id: 'local-dev-user',
email: '[email protected]',
name: 'Local Developer',
};
return { isAuthenticated: true, user: mockUser };
}

try {
const response = await axios.get('YOUR_BACKEND_API_URL/auth/status', {
withCredentials: true
Expand All @@ -26,6 +38,6 @@ export const useAuthStatus = () => {
queryKey: ['auth'],
queryFn: checkAuthStatus,
retry: false,
staleTime: 5 * 60 * 1000,
staleTime: 5 * 60 * 1000,
});
};
90 changes: 81 additions & 9 deletions src/pages/account/AccountPage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,87 @@
import React from "react";
import React, { useState } from "react";
import { useAuthStatus } from "@/hooks/login/query/useAuthStatus";
import PortfolioHeader from "@/pages/portfoliio/components/PortfolioHeader";

const PortfolioPage = () => {
return (
<div className="min-h-screen">
<div className="max-w-screen-xl mx-auto px-4 sm:px-16 lg:px-24 py-12">
<PortfolioHeader />
const AccountPage = () => {
const { data: auth } = useAuthStatus();
const userName = auth?.user?.name || "크레임";
const userEmail = auth?.user?.email || "[email protected]";
const [notificationsEnabled, setNotificationsEnabled] = useState(true);

return (
<div className="min-h-screen bg-[#FBF8F2]">
<div className="max-w-screen-xl mx-auto px-6 lg:px-12 py-10">
<PortfolioHeader />

{/* Profile info card */}
<div className="mt-8 rounded-xl border border-[#E9E2D3] bg-white p-8 shadow-sm">
<div className="mb-6 flex items-center justify-between">
<h2 className="text-lg font-semibold text-[#4A3F2F]">내 정보</h2>
<button className="text-sm text-[#C9A24E] hover:underline">수정하기</button>
</div>
<div className="rounded-lg border border-[#3BA0FF] p-6">
<dl className="grid grid-cols-2 gap-y-4 gap-x-8 text-sm text-[#4A3F2F]">
<div>
<dt className="text-[#8D7C64] mb-1">이름</dt>
<dd>{userName}</dd>
</div>
<div>
<dt className="text-[#8D7C64] mb-1">생년월일</dt>
<dd>0000.00.00</dd>
</div>
<div>
<dt className="text-[#8D7C64] mb-1">아이디</dt>
<dd>crame25</dd>
</div>
<div>
<dt className="text-[#8D7C64] mb-1">이메일</dt>
<dd>{userEmail}</dd>
</div>
<div>
<dt className="text-[#8D7C64] mb-1">전화번호</dt>
<dd>000-0000-0000</dd>
</div>
</dl>
</div>
</div>

{/* Account settings */}
<div className="mt-8 rounded-xl border border-[#E9E2D3] bg-white">
{/* 알림 설정 */}
<div className="flex items-center justify-between p-6 border-b border-[#EFE8DA]">
<div>
<div className="text-base font-semibold text-[#4A3F2F]">알림 설정</div>
<div className="text-sm text-[#8D7C64]">거래 및 시장 알림을 받습니다</div>
</div>
<label className="inline-flex items-center">
<input
type="checkbox"
className="h-4 w-4 accent-[#C9A24E]"
checked={notificationsEnabled}
onChange={() => setNotificationsEnabled(v => !v)}
/>
</label>
</div>

{/* 소셜 연동 */}
<div className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<img src="/google.svg" alt="Google" className="h-6 w-6" />
<div>
<div className="text-sm text-[#8D7C64]">ID : {userEmail}</div>
<div className="text-sm text-[#8D7C64]">연결일자 : 2000.00.00</div>
</div>
</div>
<button className="rounded-md border border-[#E0D7C8] px-4 py-2 text-sm text-[#4A3F2F] hover:bg-[#FAF6F0]">
연결끊기
</button>
</div>
</div>
</div>
);
</div>
</div>
);
};


export default PortfolioPage;
export default AccountPage;
19 changes: 15 additions & 4 deletions src/pages/login/GoogleLoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const GoogleLoginPage = () => {
const location = useLocation();
const state = location.state as LocationState;
const errorMessage = state?.error;

const CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const REDIRECT_URI = "http://localhost:5173/login/auth/google";
const SCOPE = "email profile openid";
Expand All @@ -24,9 +24,12 @@ const GoogleLoginPage = () => {
}

useEffect(() => {
// 이미 로그인된 사용자는 홈 페이지로 리디렉션
if (authData?.isAuthenticated) {
navigate('/home');
const isLocalhost = typeof window !== 'undefined' && window.location.hostname === 'localhost';
const enableLocalAuto = (import.meta as any)?.env?.VITE_ENABLE_LOCAL_AUTOLOGIN !== 'false';

// 로컬 자동로그인 상태에서는 로그인 페이지에서 자동 리다이렉트하지 않음
if (authData?.isAuthenticated && !(isLocalhost && enableLocalAuto)) {
navigate('/');
}
}, [authData, navigate]);

Expand All @@ -50,6 +53,14 @@ const GoogleLoginPage = () => {
<div onClick={handleLogin}>
<GoogleButton />
</div>
{window.location.hostname === 'localhost' && (
<button
onClick={() => navigate('/')}
className="mt-4 text-sm text-blue-600 underline"
>
로컬 강제 로그인(바이패스)
</button>
)}
</div>
)
}
Expand Down
149 changes: 149 additions & 0 deletions src/pages/portfoliio/ApiKeyPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useMemo, useState } from "react";
import PortfolioHeader from "@/pages/portfoliio/components/PortfolioHeader";

type ApiKeyItem = {
id: string;
name: string;
publicKey: string;
secretKey: string;
createdAt: string;
manager: string;
};

const initialRows: ApiKeyItem[] = [
{ id: "1", name: "Key1", publicKey: "Public Key1", secretKey: "Secret Key1", createdAt: "생성일1", manager: "관리1" },
{ id: "2", name: "Key2", publicKey: "Public Key2", secretKey: "Secret Key2", createdAt: "생성일2", manager: "관리2" },
{ id: "3", name: "Key3", publicKey: "Public Key3", secretKey: "Secret Key3", createdAt: "생성일3", manager: "관리3" },
{ id: "4", name: "Key4", publicKey: "Public Key4", secretKey: "Secret Key4", createdAt: "생성일4", manager: "관리4" },
];

const ApiKeyPage = () => {
const [rows, setRows] = useState<ApiKeyItem[]>(initialRows);
const [open, setOpen] = useState(false);
const [form, setForm] = useState({ name: "", publicKey: "", secretKey: "" });

const isValid = useMemo(() => form.name && form.publicKey && form.secretKey, [form]);

const onConfirm = () => {
if (!isValid) return;
const now = new Date();
const createdAt = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, "0")}.${String(now.getDate()).padStart(2, "0")}`;
setRows(prev => [
...prev,
{
id: String(prev.length + 1),
name: form.name,
publicKey: form.publicKey,
secretKey: form.secretKey,
createdAt,
manager: `관리${prev.length + 1}`,
},
]);
setForm({ name: "", publicKey: "", secretKey: "" });
setOpen(false);
};

const onDelete = (id: string) => {
const ok = window.confirm("해당 API Key를 삭제하시겠습니까?");
if (!ok) return;
setRows(prev => prev.filter(r => r.id !== id));
};

return (
<div className="min-h-screen bg-[#FBF8F2]">
<div className="max-w-screen-xl mx-auto px-6 lg:px-12 py-10">
<PortfolioHeader />

{/* 안내 */}
<div className="mt-8">
<h2 className="text-lg font-semibold text-[#4A3F2F]">API Key 설정 안내</h2>
<p className="mt-2 text-sm text-[#8D7C64]">거래소 API를 등록하시면 자동매매 기능을 이용하실 수 있습니다. API Key는 안전하게 암호화되어 저장됩니다.</p>
</div>

{/* 표 */}
<div className="mt-6 rounded-xl border border-[#E9E2D3] bg-white p-0 overflow-hidden">
<div className="flex items-center justify-between px-6 py-4">
<div className="text-base font-semibold text-[#4A3F2F]">API Key 관리</div>
<button onClick={() => setOpen(true)} className="text-sm text-[#C9A24E] hover:underline">+ 새로운 API Key 추가</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full text-left text-sm">
<thead className="bg-[#FAF6F0] text-[#8D7C64]">
<tr>
<th className="px-6 py-3">Public Key</th>
<th className="px-6 py-3">Secret Key</th>
<th className="px-6 py-3">생성일</th>
<th className="px-6 py-3">관리</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id} className="border-t border-[#EFE8DA] text-[#4A3F2F]">
<td className="px-6 py-3">{r.publicKey}</td>
<td className="px-6 py-3">{r.secretKey}</td>
<td className="px-6 py-3">{r.createdAt}</td>
<td className="px-6 py-3">
<div className="flex items-center gap-3">
<span>{r.manager}</span>
<button
onClick={() => onDelete(r.id)}
className="rounded-md border border-[#E0D7C8] px-3 py-1 text-xs text-[#A35C5C] hover:bg-[#FFF4F4]"
>
삭제
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>

{/* 모달 */}
{open && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
<div className="w-[720px] rounded-2xl bg-white p-8 shadow-xl">
<h3 className="text-xl font-bold text-[#4A3F2F] mb-6">API key 입력</h3>
<div className="space-y-4">
<div>
<label className="mb-1 block text-sm text-[#8D7C64]">Key 이름</label>
<input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="Key 이름을 입력해주세요"
className="w-full rounded-md border border-[#E0D7C8] px-3 py-3 focus:outline-none focus:ring-2 focus:ring-[#F4B224]"
/>
</div>
<div>
<label className="mb-1 block text-sm text-[#8D7C64]">Public Key</label>
<input
value={form.publicKey}
onChange={(e) => setForm({ ...form, publicKey: e.target.value })}
placeholder="Public Key를 입력해주세요"
className="w-full rounded-md border border-[#E0D7C8] px-3 py-3 focus:outline-none focus:ring-2 focus:ring-[#F4B224]"
/>
</div>
<div>
<label className="mb-1 block text-sm text-[#8D7C64]">Secret Key</label>
<input
value={form.secretKey}
onChange={(e) => setForm({ ...form, secretKey: e.target.value })}
placeholder="Secret Key를 입력해주세요"
className="w-full rounded-md border border-[#E0D7C8] px-3 py-3 focus:outline-none focus:ring-2 focus:ring-[#F4B224]"
/>
</div>
</div>
<div className="mt-8 flex justify-end gap-3">
<button onClick={() => setOpen(false)} className="rounded-md border border-[#E0D7C8] px-5 py-2 text-sm text-[#4A3F2F] hover:bg-[#FAF6F0]">취소</button>
<button disabled={!isValid} onClick={onConfirm} className={`rounded-md px-5 py-2 text-sm text-white ${isValid ? "bg-[#F4B224] hover:bg-[#d9991b]" : "bg-[#E0D7C8]"}`}>확인</button>
</div>
</div>
</div>
)}
</div>
);
};

export default ApiKeyPage;
Loading