diff --git a/app/globals.css b/app/globals.css
index f3bcce2..b59ea82 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -38,16 +38,16 @@
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
- --color-brand-50: oklch(0.97 0.02 294);
- --color-brand-100: oklch(0.93 0.05 294);
- --color-brand-200: oklch(0.87 0.1 294);
- --color-brand-300: oklch(0.79 0.15 294);
- --color-brand-400: oklch(0.7 0.2 294);
- --color-brand-500: oklch(0.62 0.24 294);
- --color-brand-600: oklch(0.56 0.26 294);
- --color-brand-700: oklch(0.49 0.24 295);
- --color-brand-800: oklch(0.4 0.2 296);
- --color-brand-900: oklch(0.33 0.14 297);
+ --color-brand-50: var(--brand-50);
+ --color-brand-100: var(--brand-100);
+ --color-brand-200: var(--brand-200);
+ --color-brand-300: var(--brand-300);
+ --color-brand-400: var(--brand-400);
+ --color-brand-500: var(--brand-500);
+ --color-brand-600: var(--brand-600);
+ --color-brand-700: var(--brand-700);
+ --color-brand-800: var(--brand-800);
+ --color-brand-900: var(--brand-900);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
@@ -71,6 +71,41 @@
}
:root {
+ /* BRAND PALETTE CONTROL
+ * 이 블록만 수정하면 랜딩/대시보드의 보라 톤이 함께 바뀝니다.
+ */
+ /* 초기 브랜드 보라값(원본 기준) */
+ --brand-50: oklch(0.97 0.02 294);
+ --brand-100: oklch(0.93 0.05 294);
+ --brand-200: oklch(0.87 0.1 294);
+ --brand-300: oklch(0.79 0.15 294);
+ --brand-400: oklch(0.7 0.2 294);
+ --brand-500: oklch(0.62 0.24 294);
+ --brand-600: oklch(0.56 0.26 294);
+ --brand-700: oklch(0.49 0.24 295);
+ --brand-800: oklch(0.4 0.2 296);
+ --brand-900: oklch(0.33 0.14 297);
+
+ /* 차트(canvas): 봉 하락색은 요청대로 파란색 유지 */
+ --brand-chart-background-light: #ffffff;
+ --brand-chart-background-dark: #17131e;
+ --brand-chart-text-light: #6b21a8;
+ --brand-chart-text-dark: #e9d5ff;
+ --brand-chart-border-light: #e9d5ff;
+ --brand-chart-border-dark: rgba(216, 180, 254, 0.3);
+ --brand-chart-grid-light: #f3e8ff;
+ --brand-chart-grid-dark: rgba(216, 180, 254, 0.14);
+ --brand-chart-crosshair-light: #c084fc;
+ --brand-chart-crosshair-dark: rgba(233, 213, 255, 0.75);
+
+ --brand-chart-background: #ffffff;
+ --brand-chart-down: #2563eb;
+ --brand-chart-volume-down: rgba(37, 99, 235, 0.45);
+ --brand-chart-text: #6b21a8;
+ --brand-chart-border: var(--brand-chart-border-light);
+ --brand-chart-grid: var(--brand-chart-grid-light);
+ --brand-chart-crosshair: var(--brand-chart-crosshair-light);
+
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
@@ -78,7 +113,7 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.56 0.26 294);
+ --primary: var(--brand-600);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
@@ -89,7 +124,7 @@
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
- --ring: oklch(0.62 0.24 294);
+ --ring: var(--brand-500);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
@@ -97,7 +132,7 @@
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.56 0.26 294);
+ --sidebar-primary: var(--brand-600);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
@@ -106,37 +141,45 @@
}
.dark {
- --background: oklch(0.145 0 0);
+ /* 다크 모드 시인성 개선: 배경 대비는 유지하고, 카드/보더/보조 텍스트를 더 읽기 쉽게 조정 */
+ --background: oklch(0.17 0 0);
--foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
+ --card: oklch(0.235 0 0);
--card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
+ --popover: oklch(0.235 0 0);
--popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.56 0.26 294);
+ --primary: var(--brand-600);
--primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.269 0 0);
+ --secondary: oklch(0.285 0 0);
--secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
+ --muted: oklch(0.285 0 0);
+ --muted-foreground: oklch(0.83 0 0);
+ --accent: oklch(0.285 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.62 0.24 294);
+ --border: oklch(1 0 0 / 18%);
+ --input: oklch(1 0 0 / 22%);
+ --ring: var(--brand-500);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
+ --sidebar: oklch(0.235 0 0);
--sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.56 0.26 294);
+ --sidebar-primary: var(--brand-600);
--sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent: oklch(0.285 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
+ --sidebar-border: oklch(1 0 0 / 18%);
+ --sidebar-ring: oklch(0.78 0 0);
+
+ /* 다크 테마용 차트 배경/격자 대비 */
+ --brand-chart-background: var(--brand-chart-background-dark);
+ --brand-chart-text: var(--brand-chart-text-dark);
+ --brand-chart-border: var(--brand-chart-border-dark);
+ --brand-chart-grid: var(--brand-chart-grid-dark);
+ --brand-chart-crosshair: var(--brand-chart-crosshair-dark);
}
@layer base {
diff --git a/app/layout.tsx b/app/layout.tsx
index 40091a3..c6a25fd 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -33,8 +33,9 @@ const outfit = Outfit({
});
export const metadata: Metadata = {
- title: "AutoTrade",
- description: "Automated Crypto Trading Platform",
+ title: "Jurini - 감이 아닌 전략으로 시작하는 자동매매",
+ description:
+ "주린이를 위한 자동매매 파트너 Jurini. 손실 방어 규칙, 데이터 신호 분석, 실시간 자동 실행으로 초보의 첫 수익 루틴을 만듭니다.",
};
/**
diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx
index 9b6cd44..ed5f392 100644
--- a/components/theme-toggle.tsx
+++ b/components/theme-toggle.tsx
@@ -1,9 +1,9 @@
/**
* @file components/theme-toggle.tsx
- * @description 라이트/다크/시스템 테마 전환 토글 버튼
+ * @description 라이트/다크 테마 즉시 전환 토글 버튼
* @remarks
* - [레이어] Components/UI
- * - [사용자 행동] 버튼 클릭 -> 드롭다운 메뉴 -> 테마 선택 -> 즉시 반영
+ * - [사용자 행동] 버튼 클릭 -> 라이트/다크 즉시 전환
* - [연관 파일] theme-provider.tsx (next-themes)
*/
@@ -15,12 +15,6 @@ import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
interface ThemeToggleProps {
className?: string;
@@ -30,46 +24,41 @@ interface ThemeToggleProps {
/**
* 테마 토글 컴포넌트
* @remarks next-themes의 useTheme 훅 사용
- * @returns Dropdown 메뉴 형태의 테마 선택기
+ * @returns 단일 클릭으로 라이트/다크를 전환하는 버튼
+ * @see features/layout/components/header.tsx Header 액션 영역 - 사용자 테마 전환 버튼
*/
export function ThemeToggle({ className, iconClassName }: ThemeToggleProps) {
- const { setTheme } = useTheme();
+ const { resolvedTheme, setTheme } = useTheme();
+
+ const handleToggleTheme = React.useCallback(() => {
+ // 시스템 테마 사용 중에도 현재 화면 기준으로 명확히 라이트/다크를 토글합니다.
+ setTheme(resolvedTheme === "dark" ? "light" : "dark");
+ }, [resolvedTheme, setTheme]);
return (
-
- {/* ========== 트리거 버튼 ========== */}
-
-
- {/* 라이트 모드 아이콘 (회전 애니메이션) */}
-
- {/* 다크 모드 아이콘 (회전 애니메이션) */}
-
- Toggle theme
-
-
-
- {/* ========== 메뉴 컨텐츠 (우측 정렬) ========== */}
-
- setTheme("light")}>
- Light
-
- setTheme("dark")}>
- Dark
-
- setTheme("system")}>
- System
-
-
-
+
+ {/* ========== LIGHT ICON ========== */}
+
+ {/* ========== DARK ICON ========== */}
+
+ Toggle theme
+
);
}
diff --git a/features/auth/components/login-form.tsx b/features/auth/components/login-form.tsx
index 347a0cb..c76a187 100644
--- a/features/auth/components/login-form.tsx
+++ b/features/auth/components/login-form.tsx
@@ -26,7 +26,6 @@ import { InlineSpinner } from "@/components/ui/loading-spinner";
*/
export default function LoginForm() {
// ========== 상태 관리 ==========
- // 서버와 클라이언트 초기 렌더링을 일치시키기 위해 초기값은 고정
const [email, setEmail] = useState(() => {
if (typeof window === "undefined") return "";
return localStorage.getItem("auto-trade-saved-email") || "";
@@ -37,11 +36,6 @@ export default function LoginForm() {
});
const [isLoading, setIsLoading] = useState(false);
- // ========== 마운트 시 localStorage 데이터 복구 ==========
- // localStorage는 클라이언트 전용 외부 시스템이므로 useEffect에서 동기화하는 것이 올바른 패턴
- // React 공식 문서: "외부 시스템과 동기화"는 useEffect의 정확한 사용 사례
- // useState lazy initializer + window guard handles localStorage safely
-
// ========== 폼 제출 핸들러 ==========
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -83,7 +77,7 @@ export default function LoginForm() {
required
value={email}
onChange={(e) => setEmail(e.target.value)}
- className="h-11 transition-all duration-200"
+ className="h-11 transition-all duration-200 focus-visible:ring-brand-500"
/>
@@ -102,7 +96,7 @@ export default function LoginForm() {
minLength={8}
pattern="^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*]).{8,}$"
title="비밀번호는 최소 8자 이상, 대문자, 소문자, 숫자, 특수문자를 각각 1개 이상 포함해야 합니다."
- className="h-11 transition-all duration-200"
+ className="h-11 transition-all duration-200 focus-visible:ring-brand-500"
/>
@@ -121,10 +115,9 @@ export default function LoginForm() {
이메일 기억하기