KIM-JINO5 преди 2 месеца
родител
ревизия
cfe2771092
променени са 60 файла, в които са добавени 1133 реда и са изтрити 839 реда
  1. 3 1
      app/(auth)/approval/page.tsx
  2. 4 4
      app/(auth)/approval/style.scss
  3. 3 2
      app/(auth)/forgot-password/page.tsx
  4. 4 4
      app/(auth)/forgot-password/style.scss
  5. 6 1
      app/(auth)/layout.tsx
  6. 60 5
      app/(auth)/login/page.tsx
  7. 9 5
      app/(auth)/login/style.scss
  8. 4 4
      app/(auth)/register/style.scss
  9. 4 4
      app/(auth)/reset-password/style.scss
  10. 2 2
      app/(auth)/welcome/style.scss
  11. 7 7
      app/(main)/(account)/change-approve/style.scss
  12. 7 7
      app/(main)/(account)/change-email/style.scss
  13. 5 5
      app/(main)/(account)/change-intro/style.scss
  14. 8 8
      app/(main)/(account)/change-name/style.scss
  15. 7 7
      app/(main)/(account)/change-password/style.scss
  16. 8 8
      app/(main)/(account)/change-summary/style.scss
  17. 5 5
      app/(main)/(account)/change-thumb/style.scss
  18. 13 13
      app/(main)/(account)/exp-logs/style.scss
  19. 13 13
      app/(main)/(account)/login-log/style.scss
  20. 8 8
      app/(main)/(account)/my-comments/style.scss
  21. 9 9
      app/(main)/(account)/my-posts/style.scss
  22. 4 4
      app/(main)/(account)/profile/style.scss
  23. 5 5
      app/(main)/(account)/style.scss
  24. 5 5
      app/(main)/(account)/withdraw/style.scss
  25. 5 5
      app/(main)/(forum)/board/[code]/style.scss
  26. 37 37
      app/(main)/(forum)/board/_component/style.scss
  27. 23 23
      app/(main)/(forum)/comment/style.scss
  28. 10 10
      app/(main)/(forum)/latest/_component/style.scss
  29. 11 11
      app/(main)/(forum)/post/[id]/style.scss
  30. 6 6
      app/(main)/(forum)/post/edit/[id]/style.scss
  31. 6 6
      app/(main)/(forum)/post/write/style.scss
  32. 1 1
      app/(main)/news/view.tsx
  33. 10 10
      app/(main)/support/faq/style.scss
  34. 2 2
      app/(main)/support/style.scss
  35. 1 1
      app/api/auth/[...path]/route.ts
  36. 37 0
      app/auth/login/google/callback/route.ts
  37. 19 0
      app/auth/login/google/complete/page.tsx
  38. 17 11
      app/component/Layout.tsx
  39. 13 13
      app/component/PopupModal.scss
  40. 47 47
      app/component/chat/chat-sidebar.scss
  41. 31 25
      app/component/crypto/TradingChart.tsx
  42. 7 7
      app/component/crypto/dashboard.scss
  43. 13 13
      app/component/crypto/market-header.scss
  44. 25 25
      app/component/crypto/mobile-coin-list.scss
  45. 10 10
      app/component/crypto/orderbook.scss
  46. 21 21
      app/component/crypto/sidebar.scss
  47. 10 10
      app/component/crypto/trade-history.scss
  48. 15 15
      app/component/crypto/trading-chart.scss
  49. 70 15
      app/globals.scss
  50. 56 10
      app/layout.tsx
  51. 21 21
      app/styles/common.module.scss
  52. 2 2
      app/styles/editor.scss
  53. 2 2
      app/styles/emoji-picker.scss
  54. 7 7
      app/styles/quill.scss
  55. 49 0
      contexts/themeProvider.tsx
  56. 8 0
      hooks/useTheme.ts
  57. 1 0
      lib/api/forum/board.ts
  58. 336 336
      package-lock.json
  59. 1 1
      package.json
  60. 0 0
      public/resources/no-image-2.png

+ 3 - 1
app/(auth)/approval/page.tsx

@@ -181,7 +181,9 @@ export default function Approval()
 
 					<form id="fApproval" method="post" acceptCharset="utf-8" autoComplete="off" className="grid p-4" onSubmit={handleSubmit}>
 						<p>보안을 위해 인증번호를 발송했습니다. <br />수신된 인증번호를 입력해주세요.</p>
-						<br />
+						<div>
+                            <br />
+                        </div>
 
 						<label htmlFor="email">수신 이메일</label>
 						<input type="email" name="email" id="email" value={email} disabled />

+ 4 - 4
app/(auth)/approval/style.scss

@@ -3,7 +3,7 @@
 
     fieldset {
         position: relative;
-        border: 1px solid #cecece;
+        border: 1px solid var(--border-strong);
         border-radius: 3px;
         padding: 20px 26px;
         align-content: center;
@@ -15,7 +15,7 @@
             top: -0.625rem;
             left: 1.25rem;
             padding: 0 5px;
-            background: white;
+            background: var(--bg-page);
             font-size: 1rem;
             font-weight: bold;
         }
@@ -67,12 +67,12 @@
                     a {
                         display: inline-block;
                         padding: 0.438rem 0;
-                        color: #0060a9;
+                        color: var(--text-link);
                         text-decoration: none;
 
                         &:hover {
                             text-decoration: underline;
-                            color: #c7511f;
+                            color: var(--text-link-hover);
                         }
                     }
                 }

+ 3 - 2
app/(auth)/forgot-password/page.tsx

@@ -75,8 +75,9 @@ export default function ForgotPassword()
 					<form method="post" acceptCharset="utf-8" autoComplete="off" className="grid pt-4 pl-4 pr-4 pb-1" onSubmit={handleSubmit}>
 						<p>{process.env.SITE_NAME} 계정과 연결된 이메일 주소를 입력해주세요.</p>
 						<p>해당 이메일로 인증번호가 발송되며 아래 입력란에 인증번호를 확인하면 비밀번호 재설정이 가능합니다.</p>
-
-						<br />
+						<div>
+							<br />
+						</div>
 						<label htmlFor="email">이메일</label>
 						<input type="email" name="email" id="email" ref={emailRef} maxLength={30} onChange={e => setEmail(e.target.value)} autoComplete="off" />
 						<button type="submit" className="btn btn-submit" disabled={loading}>

+ 4 - 4
app/(auth)/forgot-password/style.scss

@@ -3,7 +3,7 @@
 
     fieldset {
         position: relative;
-        border: 1px solid #cecece;
+        border: 1px solid var(--border-strong);
         border-radius: 3px;
         padding: 20px 26px;
         align-content: center;
@@ -15,7 +15,7 @@
             top: -0.625rem;
             left: 1.25rem;
             padding: 0 5px;
-            background: white;
+            background: var(--bg-page);
             font-size: 1rem;
             font-weight: bold;
         }
@@ -43,13 +43,13 @@
 
             a {
                 display: block;
-                color: #0060a9;
+                color: var(--text-link);
                 text-decoration: none;
                 text-align: center;
 
                 &:hover {
                     text-decoration: underline;
-                    color: #c7511f;
+                    color: var(--text-link-hover);
                 }
             }
         }

+ 6 - 1
app/(auth)/layout.tsx

@@ -5,11 +5,14 @@ import Image from 'next/image';
 import Link from 'next/link';
 import { useRouter } from 'next/navigation';
 import { useEffect } from 'react';
+import { GoogleOAuthProvider } from '@react-oauth/google';
 import useAuth from '@/hooks/useAuth';
+import { useConfigContext } from '@/contexts/configProvider';
 
 export default function Layout({ children }: { children: React.ReactNode }) {
     const router = useRouter();
     const { isAuthenticated, isLoading } = useAuth();
+    const config = useConfigContext();
 
     // 이미 로그인된 상태면 홈으로 리다이렉트
     useEffect(() => {
@@ -24,7 +27,9 @@ export default function Layout({ children }: { children: React.ReactNode }) {
                 <Link href="/" className="inline-block w-28 sm:w-30 md:w-36 lg:w-44">
                     <Image src="/resources/m-logo.png" alt="bitforum" width={256} height={64} className="w-full h-auto" priority />
                 </Link>
-                {children}
+                <GoogleOAuthProvider clientId={config?.external?.googleClientId ?? ''} nonce="" locale="ko">
+                    {children}
+                </GoogleOAuthProvider>
             </div>
             <address>
                 <hr />

+ 60 - 5
app/(auth)/login/page.tsx

@@ -4,6 +4,7 @@ import './style.scss';
 import Link from 'next/link';
 import { Checkbox } from '@/components/ui/checkbox';
 import { useState, useEffect, useRef } from 'react';
+import { GoogleLogin } from '@react-oauth/google';
 import { fetchApi, throwError } from '@/lib/utils/client';
 import { LoginRequest } from '@/types/request/auth';
 import { LoginResponse } from '@/types/response/auth';
@@ -19,6 +20,8 @@ export default function Page()
     const [rememberMe, setRememberMe] = useState<boolean>(false);
     const emailRef = useRef<HTMLInputElement>(null);
     const passwordRef = useRef<HTMLInputElement>(null);
+    const googleBtnRef = useRef<HTMLDivElement>(null);
+    const [googleBtnWidth, setGoogleBtnWidth] = useState<number>(0);
 
     useEffect(() => {
         if (error) {
@@ -27,6 +30,18 @@ export default function Page()
         }
     }, [error]);
 
+    useEffect(() => {
+        if (googleBtnRef.current) {
+            const observer = new ResizeObserver(entries => {
+                for (const entry of entries) {
+                    setGoogleBtnWidth(Math.floor(entry.contentRect.width));
+                }
+            });
+            observer.observe(googleBtnRef.current);
+            return () => observer.disconnect();
+        }
+    }, []);
+
     const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
         e.preventDefault();
 
@@ -59,6 +74,28 @@ export default function Page()
         }
     }
 
+    // 구글 로그인
+    const handleGoogleLogin = async (credentialResponse: { credential?: string }) => {
+        try {
+            const res = await fetchApi<LoginResponse>('/api/auth/google-login', {
+                method: 'POST',
+                body: { credential: credentialResponse.credential }
+            });
+
+            throwError(res);
+            login(rememberMe);
+
+        } catch (err) {
+            if (err instanceof Error) {
+                setError(err.message);
+            }
+        }
+    };
+
+    const handleGoogleLoginFailed = () => {
+        setError('Google 로그인에 실패했습니다.');
+    };
+
     return (
 		<>
 			<div id="loginForm" className="row-start-2 flex flex-row flex-wrap gap-2">
@@ -66,7 +103,7 @@ export default function Page()
 					<legend>로그인</legend>
 					<form method="post" acceptCharset="utf-8" autoComplete="off" className="grid p-4" onSubmit={handleSubmit}>
 						<label htmlFor="email">이메일</label>
-						<input type="email" name="email" id="email" ref={emailRef} maxLength={30} onChange={e => setEmail(e.target.value)} autoComplete="off" />
+						<input type="email" name="email" id="email" ref={emailRef} maxLength={30} onChange={e => setEmail(e.target.value)} autoComplete="off" autoFocus />
 
 						<label htmlFor="password">비밀번호</label>
 						<input type="password" name="password" id="password" ref={passwordRef} maxLength={20} onChange={e => setPassword(e.target.value)} />
@@ -75,10 +112,21 @@ export default function Page()
 							{loading ? "로그인 중..." : "로그인"}
 						</button>
 
-						<section className="mt-3 text-center">
-							<Checkbox name="remember_me" id="rememberMe" checked={rememberMe} onCheckedChange={(checked) => setRememberMe(checked === true)} />
-							<label htmlFor="rememberMe">로그인 상태 유지</label>
-						</section>
+                        <div ref={googleBtnRef} className='w-full mt-2'>
+                            {googleBtnWidth > 0 && (
+                                <GoogleLogin
+                                    onSuccess={handleGoogleLogin}
+                                    onError={handleGoogleLoginFailed}
+                                    width={googleBtnWidth}
+                                    size="large"
+                                    shape="rectangular"
+                                    context="signin"
+                                    ux_mode="redirect"
+                                    login_uri={`${window.location.origin}/auth/login/google/callback`}
+                                    logo_alignment="center"
+                                />
+                            )}
+                        </div>
 					</form>
                     <hr hidden/>
 				</fieldset>
@@ -92,7 +140,9 @@ export default function Page()
 							</Link>
 						</dd>
 					</dl>
+
 					<hr />
+
 					<dl>
 						<dt>비밀번호를 잊으셨나요?</dt>
 						<dd>비밀번호를 깜박했다면 다시 설정할 수 있어요!</dd>
@@ -102,6 +152,11 @@ export default function Page()
 							</Link>
 						</dd>
 					</dl>
+
+                    <section className="mt-3">
+                        <Checkbox name="remember_me" id="rememberMe" checked={rememberMe} onCheckedChange={(checked) => setRememberMe(checked === true)} />
+                        <label htmlFor="rememberMe">로그인 상태 유지</label>
+                    </section>
 				</fieldset>
 			</div>
 		</>

+ 9 - 5
app/(auth)/login/style.scss

@@ -1,7 +1,7 @@
 #loginForm {
     fieldset {
         position: relative;
-        border: 1px solid #cecece;
+        border: 1px solid var(--border-strong);
         border-radius: 3px;
         padding: 20px 26px;
         align-content: center;
@@ -11,7 +11,7 @@
             top: -0.625rem;
             left: 1.25rem;
             padding: 0 5px;
-            background: white;
+            background: var(--bg-page);
             font-size: 1rem;
             font-weight: bold;
         }
@@ -44,7 +44,7 @@
         dl {
             dt {
                 font-weight: bold;
-                padding-bottom: 4px;
+                padding-bottom: 8px;
             }
 
             dd {
@@ -53,12 +53,16 @@
                 a {
                     display: block;
                     padding: 0.438rem 0;
-                    color: #0060a9;
+                    color: var(--text-link);
                     text-decoration: none;
 
                     &:hover {
                         text-decoration: underline;
-                        color: #c7511f;
+                        color: var(--text-link-hover);
+                    }
+
+                    small {
+                        vertical-align: text-top;
                     }
                 }
             }

+ 4 - 4
app/(auth)/register/style.scss

@@ -3,7 +3,7 @@
 
     fieldset {
         position: relative;
-        border: 1px solid #cecece;
+        border: 1px solid var(--border-strong);
         border-radius: 3px;
         padding: 20px 26px;
         align-content: center;
@@ -15,7 +15,7 @@
             top: -0.625rem;
             left: 1.25rem;
             padding: 0 5px;
-            background: white;
+            background: var(--bg-page);
             font-size: 1rem;
             font-weight: bold;
         }
@@ -72,11 +72,11 @@
                         }
 
                         button {
-                            color: #0060a9;
+                            color: var(--text-link);
 
                             &:hover {
                                 text-decoration: underline;
-                                color: #c7511f;
+                                color: var(--text-link-hover);
                             }
                         }
                     }

+ 4 - 4
app/(auth)/reset-password/style.scss

@@ -3,7 +3,7 @@
 
     fieldset {
         position: relative;
-        border: 1px solid #cecece;
+        border: 1px solid var(--border-strong);
         border-radius: 3px;
         padding: 20px 26px;
         align-content: center;
@@ -15,7 +15,7 @@
             top: -0.625rem;
             left: 1.25rem;
             padding: 0 5px;
-            background: white;
+            background: var(--bg-page);
             font-size: 1rem;
             font-weight: bold;
         }
@@ -46,11 +46,11 @@
                 }
 
                 button {
-                    color: #0060a9;
+                    color: var(--text-link);
 
                     &:hover {
                         text-decoration: underline;
-                        color: #c7511f;
+                        color: var(--text-link-hover);
                     }
                 }
             }

+ 2 - 2
app/(auth)/welcome/style.scss

@@ -3,7 +3,7 @@
 
     fieldset {
         position: relative;
-        border: 1px solid #cecece;
+        border: 1px solid var(--border-strong);
         border-radius: 3px;
         padding: 20px 26px;
         align-content: center;
@@ -15,7 +15,7 @@
             top: -0.625rem;
             left: 1.25rem;
             padding: 0 5px;
-            background: white;
+            background: var(--bg-page);
             font-size: 1rem;
             font-weight: bold;
         }

+ 7 - 7
app/(main)/(account)/change-approve/style.scss

@@ -7,7 +7,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -25,12 +25,12 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 				}
 
 				td {
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					input[type="checkbox"] {
 						transform: scale(1.3);
@@ -45,7 +45,7 @@
 		}
 
 		tfoot {
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {
@@ -61,8 +61,8 @@
 	}
 
 	dl {
-		background-color: #f1f1f1;
-		border: 1px solid #ccc;
+		background-color: var(--bg-subtle);
+		border: 1px solid var(--border-strong);
 		line-height: 1.5;
 		padding: 15px 16px 12px 16px;
 

+ 7 - 7
app/(main)/(account)/change-email/style.scss

@@ -7,7 +7,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -25,12 +25,12 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 				}
 
 				td {
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					a {
 						padding: 3px 16px;
@@ -48,7 +48,7 @@
 		}
 
 		tfoot {
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {
@@ -64,8 +64,8 @@
 	}
 
 	dl {
-		background-color: #f1f1f1;
-		border: 1px solid #ccc;
+		background-color: var(--bg-subtle);
+		border: 1px solid var(--border-strong);
 		line-height: 1.5;
 		padding: 15px 16px 12px 16px;
 

+ 5 - 5
app/(main)/(account)/change-intro/style.scss

@@ -28,9 +28,9 @@
 		}
 
 		tfoot {
-			border: 1px solid #ccc;
+			border: 1px solid var(--border-strong);
 			border-width: 0 1px 1px 1px;
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {
@@ -43,7 +43,7 @@
 
 					.btn-delete {
 						background-color: #e6f4fa;
-						color: #333;
+						color: var(--text-primary);
 						border: 1px solid #b3b3b3;
 						-webkit-box-shadow: inset 0 -1px 0 0 #b3b3b3;
 						box-shadow: inset 0 -1px 0 0 #b3b3b3;
@@ -58,8 +58,8 @@
 	}
 
 	dl {
-		background-color: #f1f1f1;
-		border: 1px solid #ccc;
+		background-color: var(--bg-subtle);
+		border: 1px solid var(--border-strong);
 		line-height: 1.5;
 		padding: 15px 16px 12px 16px;
 

+ 8 - 8
app/(main)/(account)/change-name/style.scss

@@ -7,7 +7,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -25,12 +25,12 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 				}
 
 				td {
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					a {
 						padding: 3px 16px;
@@ -48,7 +48,7 @@
 		}
 
 		tfoot {
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {
@@ -61,7 +61,7 @@
 
 					.btn-delete {
 						background-color: #e6f4fa;
-						color: #333;
+						color: var(--text-primary);
 						border: 1px solid #b3b3b3;
 						-webkit-box-shadow: inset 0 -1px 0 0 #b3b3b3;
 						box-shadow: inset 0 -1px 0 0 #b3b3b3;
@@ -76,8 +76,8 @@
 	}
 
 	dl {
-		background-color: #f1f1f1;
-		border: 1px solid #ccc;
+		background-color: var(--bg-subtle);
+		border: 1px solid var(--border-strong);
 		line-height: 1.5;
 		padding: 15px 16px 12px 16px;
 

+ 7 - 7
app/(main)/(account)/change-password/style.scss

@@ -7,7 +7,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -25,12 +25,12 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 				}
 
 				td {
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					a {
 						padding: 3px 16px;
@@ -48,7 +48,7 @@
 		}
 
 		tfoot {
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {
@@ -64,8 +64,8 @@
 	}
 
 	dl {
-		background-color: #f1f1f1;
-		border: 1px solid #ccc;
+		background-color: var(--bg-subtle);
+		border: 1px solid var(--border-strong);
 		line-height: 1.5;
 		padding: 15px 16px 12px 16px;
 

+ 8 - 8
app/(main)/(account)/change-summary/style.scss

@@ -7,7 +7,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -25,12 +25,12 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 				}
 
 				td {
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					a {
 						padding: 3px 16px;
@@ -48,7 +48,7 @@
 		}
 
 		tfoot {
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {
@@ -61,7 +61,7 @@
 
 					.btn-delete {
 						background-color: #e6f4fa;
-						color: #333;
+						color: var(--text-primary);
 						border: 1px solid #b3b3b3;
 						-webkit-box-shadow: inset 0 -1px 0 0 #b3b3b3;
 						box-shadow: inset 0 -1px 0 0 #b3b3b3;
@@ -76,8 +76,8 @@
 	}
 
 	dl {
-		background-color: #f1f1f1;
-		border: 1px solid #ccc;
+		background-color: var(--bg-subtle);
+		border: 1px solid var(--border-strong);
 		line-height: 1.5;
 		padding: 15px 16px 12px 16px;
 

+ 5 - 5
app/(main)/(account)/change-thumb/style.scss

@@ -7,7 +7,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -25,8 +25,8 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 					padding: 2px;
 
 					figure {
@@ -46,7 +46,7 @@
 
 				td {
 					padding: 12px 13px;
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					label, button {
 						padding: 5px 15px;
@@ -56,7 +56,7 @@
 		}
 
 		tfoot {
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {

+ 13 - 13
app/(main)/(account)/exp-logs/style.scss

@@ -25,30 +25,30 @@
 		button {
 			padding: 0.5rem 1rem;
 			font-size: 0.875rem;
-			color: #666;
+			color: var(--text-secondary);
 			border-bottom: 2px solid transparent;
 			transition: color 0.2s, border-color 0.2s;
 
 			&:hover {
-				color: #e47911;
-				border-bottom-color: #e47911;
+				color: var(--brand-orange);
+				border-bottom-color: var(--brand-orange);
 			}
 
 			&.active {
-				color: #0060a9;
+				color: var(--text-link);
 				font-weight: 600;
-				border-bottom-color: #0060a9;
+				border-bottom-color: var(--text-link);
 			}
 		}
 	}
 
 	.mypage-list {
-		border-top: 1px solid #eaeaea;
+		border-top: 1px solid var(--border-default);
 		margin: 0.75rem 0;
 
 		article:nth-of-type(1) {
 			background-color: #f9fafb;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 			padding: 0.5rem 0;
 
 			ul {
@@ -63,7 +63,7 @@
 				li {
 					text-align: center;
 					font-size: 0.875rem;
-					color: #666;
+					color: var(--text-secondary);
 				}
 			}
 
@@ -78,7 +78,7 @@
 			section {
 				padding: 0.5rem 0;
 				box-sizing: inherit;
-				border-bottom: 1px solid #eaeaea;
+				border-bottom: 1px solid var(--border-default);
 
 				&:hover {
 					background-color: #faffd1;
@@ -128,7 +128,7 @@
 
 							li {
 								font-size: 0.813rem;
-								color: #888;
+								color: var(--text-muted);
 
 								&:last-child {
 									flex-grow: 1;
@@ -152,18 +152,18 @@
 			> .empty {
 				text-align: center;
 				padding: 2.5rem;
-				color: #999;
+				color: var(--text-muted);
 			}
 		}
 
 		.amount-plus {
 			font-weight: 600;
-			color: #15803d;
+			color: var(--color-success);
 		}
 
 		.amount-minus {
 			font-weight: 600;
-			color: #dc2626;
+			color: var(--color-danger);
 		}
 	}
 }

+ 13 - 13
app/(main)/(account)/login-log/style.scss

@@ -25,30 +25,30 @@
 		button {
 			padding: 0.5rem 1rem;
 			font-size: 0.875rem;
-			color: #666;
+			color: var(--text-secondary);
 			border-bottom: 2px solid transparent;
 			transition: color 0.2s, border-color 0.2s;
 
 			&:hover {
-				color: #e47911;
-				border-bottom-color: #e47911;
+				color: var(--brand-orange);
+				border-bottom-color: var(--brand-orange);
 			}
 
 			&.active {
-				color: #0060a9;
+				color: var(--text-link);
 				font-weight: 600;
-				border-bottom-color: #0060a9;
+				border-bottom-color: var(--text-link);
 			}
 		}
 	}
 
 	.mypage-list {
-		border-top: 1px solid #eaeaea;
+		border-top: 1px solid var(--border-default);
 		margin: 0.75rem 0;
 
 		article:nth-of-type(1) {
 			background-color: #f9fafb;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 			padding: 0.5rem 0;
 
 			ul {
@@ -64,7 +64,7 @@
 				li {
 					text-align: center;
 					font-size: 0.875rem;
-					color: #666;
+					color: var(--text-secondary);
 				}
 			}
 
@@ -79,7 +79,7 @@
 			section {
 				padding: 0.5rem 0;
 				box-sizing: inherit;
-				border-bottom: 1px solid #eaeaea;
+				border-bottom: 1px solid var(--border-default);
 
 				&:hover {
 					background-color: #faffd1;
@@ -129,7 +129,7 @@
 
 							li {
 								font-size: 0.813rem;
-								color: #888;
+								color: var(--text-muted);
 
 								&.ua {
 									overflow: hidden;
@@ -156,7 +156,7 @@
 			> .empty {
 				text-align: center;
 				padding: 2.5rem;
-				color: #999;
+				color: var(--text-muted);
 			}
 		}
 
@@ -166,7 +166,7 @@
 			border-radius: 0.25rem;
 			font-size: 0.75rem;
 			font-weight: 600;
-			color: #15803d;
+			color: var(--color-success);
 			background-color: #dcfce7;
 		}
 
@@ -176,7 +176,7 @@
 			border-radius: 0.25rem;
 			font-size: 0.75rem;
 			font-weight: 600;
-			color: #dc2626;
+			color: var(--color-danger);
 			background-color: #fee2e2;
 		}
 	}

+ 8 - 8
app/(main)/(account)/my-comments/style.scss

@@ -12,12 +12,12 @@
 	}
 
 	.mypage-list {
-		border-top: 1px solid #eaeaea;
+		border-top: 1px solid var(--border-default);
 		margin: 0.75rem 0;
 
 		article:nth-of-type(1) {
 			background-color: #f9fafb;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 			padding: 0.5rem 0;
 
 			ul {
@@ -34,7 +34,7 @@
 				li {
 					text-align: center;
 					font-size: 0.875rem;
-					color: #666;
+					color: var(--text-secondary);
 				}
 			}
 
@@ -49,7 +49,7 @@
 			section {
 				padding: 0.5rem 0;
 				box-sizing: inherit;
-				border-bottom: 1px solid #eaeaea;
+				border-bottom: 1px solid var(--border-default);
 
 				&:hover {
 					background-color: #faffd1;
@@ -112,7 +112,7 @@
 
 							li {
 								font-size: 0.813rem;
-								color: #888;
+								color: var(--text-muted);
 
 								&:first-child {
 									min-width: 0;
@@ -131,12 +131,12 @@
 				}
 
 				a {
-					color: #0060a9;
+					color: var(--text-link);
 					text-decoration: none;
 
 					&:hover {
 						text-decoration: underline;
-						color: #c7511f;
+						color: var(--text-link-hover);
 					}
 
 					> em {
@@ -158,7 +158,7 @@
 			> .empty {
 				text-align: center;
 				padding: 2.5rem;
-				color: #999;
+				color: var(--text-muted);
 			}
 		}
 	}

+ 9 - 9
app/(main)/(account)/my-posts/style.scss

@@ -12,12 +12,12 @@
 	}
 
 	.mypage-list {
-		border-top: 1px solid #eaeaea;
+		border-top: 1px solid var(--border-default);
 		margin: 0.75rem 0;
 
 		article:nth-of-type(1) {
 			background-color: #f9fafb;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 			padding: 0.5rem 0;
 
 			ul {
@@ -35,7 +35,7 @@
 				li {
 					text-align: center;
 					font-size: 0.875rem;
-					color: #666;
+					color: var(--text-secondary);
 				}
 			}
 
@@ -50,7 +50,7 @@
 			section {
 				padding: 0.5rem 0;
 				box-sizing: inherit;
-				border-bottom: 1px solid #eaeaea;
+				border-bottom: 1px solid var(--border-default);
 
 				&:hover {
 					background-color: #faffd1;
@@ -112,7 +112,7 @@
 
 							li {
 								font-size: 0.813rem;
-								color: #888;
+								color: var(--text-muted);
 
 								&:last-child {
 									flex-grow: 1;
@@ -124,12 +124,12 @@
 				}
 
 				a {
-					color: #0060a9;
+					color: var(--text-link);
 					text-decoration: none;
 
 					&:hover {
 						text-decoration: underline;
-						color: #c7511f;
+						color: var(--text-link-hover);
 					}
 
 					> em {
@@ -137,7 +137,7 @@
 						font-style: normal;
 
 						> span {
-							color: #d13232;
+							color: var(--color-danger);
 							font-size: 0.813rem;
 							vertical-align: text-top;
 							padding-right: 4px;
@@ -163,7 +163,7 @@
 			> .empty {
 				text-align: center;
 				padding: 2.5rem;
-				color: #999;
+				color: var(--text-muted);
 			}
 		}
 	}

+ 4 - 4
app/(main)/(account)/profile/style.scss

@@ -8,7 +8,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -26,12 +26,12 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 				}
 
 				td {
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					a {
 						height: min-content;

+ 5 - 5
app/(main)/(account)/style.scss

@@ -27,7 +27,7 @@
 	}
 
 	a {
-		color: #0D6295;
+		color: var(--text-link);
 		white-space: nowrap;
 		border-radius: 4px;
 		flex-shrink: 0;
@@ -39,12 +39,12 @@
 		outline: none;
 
 		&:hover {
-			color: #e47911;
+			color: var(--brand-orange);
 			text-decoration: underline;
 		}
 
 		&.active {
-			color: #e47911;
+			color: var(--brand-orange);
 			font-weight: 600;
 		}
 	}
@@ -65,12 +65,12 @@
 			height: 28px;
 			border: none;
 			background: transparent;
-			color: #666;
+			color: var(--text-secondary);
 			font-size: 0.75rem;
 			cursor: pointer;
 
 			&:active {
-				color: #e47911;
+				color: var(--brand-orange);
 			}
 		}
 	}

+ 5 - 5
app/(main)/(account)/withdraw/style.scss

@@ -7,7 +7,7 @@
 	}
 
 	table {
-		border: 1px solid #ccc;
+		border: 1px solid var(--border-strong);
 
 		caption {
 			text-align: left;
@@ -25,8 +25,8 @@
 				}
 
 				th {
-					background: #f9f9f9;
-					border: 1px solid #ccc;
+					background: var(--bg-elevated);
+					border: 1px solid var(--border-strong);
 
 					hr {
 						margin: 10px 0;
@@ -35,7 +35,7 @@
 
 				td {
 					text-align: center;
-					border-bottom: 1px solid #ccc;
+					border-bottom: 1px solid var(--border-strong);
 
 					input[type="checkbox"] {
 						transform: scale(1.3);
@@ -50,7 +50,7 @@
 		}
 
 		tfoot {
-			background: #fbfbfb;
+			background: var(--bg-elevated);
 
 			tr {
 				td {

+ 5 - 5
app/(main)/(forum)/board/[code]/style.scss

@@ -63,20 +63,20 @@
 						display: block;
 						line-height: inherit;
 						padding: 0.25rem 0.75rem;
-						background: #f1f1f1;
+						background: var(--bg-subtle);
 						border-radius: 6px;
 						white-space: nowrap;
 						cursor: pointer;
 
 						&:hover, &:focus {
-							color: #c7511f;
-							background: #e1e1e1;
+							color: var(--text-link-hover);
+							background: var(--border-default);
 							text-decoration: underline;
 						}
 
 						&.active {
-							background: #000;
-							color: #f1f1f1;
+							background: var(--text-primary);
+							color: var(--bg-page);
 						}
 					}
 			  	}

+ 37 - 37
app/(main)/(forum)/board/_component/style.scss

@@ -8,12 +8,12 @@
 
 // 일반 게시판
 section.default-list-layout {
-	border-top: 1px solid #eaeaea;
+	border-top: 1px solid var(--border-default);
 	margin: 0.75rem 0;
 
 	article:nth-of-type(1) {
-		background-color: #f9fafb;
-		border-bottom: 1px solid #eaeaea;
+		background-color: var(--bg-elevated);
+		border-bottom: 1px solid var(--border-default);
 		padding: 0.5rem 0;
 
 		ul {
@@ -48,30 +48,30 @@ section.default-list-layout {
 		section {
 			padding: 0.5rem 0;
 			box-sizing: inherit;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 
 			&.active,
 			&:hover {
-				background-color: #faffd1;
+				background-color: var(--list-row-active);
 			}
 
 			button {
-				color: #333;
+				color: var(--text-primary);
 				padding-right: 0.5rem;
 
 				&:hover {
 					text-decoration: underline;
-					color: #0060a9;
+					color: var(--text-link);
 				}
 			}
 
 			a {
-				color: #0060a9;
+				color: var(--text-link);
 				text-decoration: none;
 
 				&:hover {
 					text-decoration: underline;
-					color: #c7511f;
+					color: var(--text-link-hover);
 				}
 
 				> em {
@@ -79,7 +79,7 @@ section.default-list-layout {
 					font-style: normal;
 
 					> span {
-						color: #d13232;
+						color: var(--color-danger);
 						font-size: 0.813rem;
 						vertical-align: text-top;
 						padding-right: 4px;
@@ -171,12 +171,12 @@ section.default-list-layout {
 
 // 1:1 문의 게시판
 section.qna-list-layout {
-	border-top: 1px solid #eaeaea;
+	border-top: 1px solid var(--border-default);
 	margin: 0.75rem 0;
 
 	article:nth-of-type(1) {
-		background-color: #f9fafb;
-		border-bottom: 1px solid #eaeaea;
+		background-color: var(--bg-elevated);
+		border-bottom: 1px solid var(--border-default);
 		padding: 0.5rem 0;
 
 		ul {
@@ -210,30 +210,30 @@ section.qna-list-layout {
 		section {
 			padding: 0.5rem 0;
 			box-sizing: inherit;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 
 			&.active,
 			&:hover {
-				background-color: #faffd1;
+				background-color: var(--list-row-active);
 			}
 
 			button {
-				color: #333;
+				color: var(--text-primary);
 				padding-right: 0.5rem;
 
 				&:hover {
 					text-decoration: underline;
-					color: #0060a9;
+					color: var(--text-link);
 				}
 			}
 
 			a {
-				color: #0060a9;
+				color: var(--text-link);
 				text-decoration: none;
 
 				&:hover {
 					text-decoration: underline;
-					color: #c7511f;
+					color: var(--text-link-hover);
 				}
 
 				> em {
@@ -241,7 +241,7 @@ section.qna-list-layout {
 					font-style: normal;
 
 					> span {
-						color: #d13232;
+						color: var(--color-danger);
 						font-size: 0.813rem;
 						vertical-align: text-top;
 						padding-right: 4px;
@@ -331,12 +331,12 @@ section.qna-list-layout {
 
 // 공지사항
 section.notice-list-layout {
-	border-top: 1px solid #eaeaea;
+	border-top: 1px solid var(--border-default);
 	margin: 0.75rem 0;
 
 	article:nth-of-type(1) {
-		background-color: #f9fafb;
-		border-bottom: 1px solid #eaeaea;
+		background-color: var(--bg-elevated);
+		border-bottom: 1px solid var(--border-default);
 		padding: 0.5rem 0;
 
 		ul {
@@ -370,30 +370,30 @@ section.notice-list-layout {
 		section {
 			padding: 0.5rem 0;
 			box-sizing: inherit;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 
 			&.active,
 			&:hover {
-				background-color: #faffd1;
+				background-color: var(--list-row-active);
 			}
 
 			button {
-				color: #333;
+				color: var(--text-primary);
 				padding-right: 0.5rem;
 
 				&:hover {
 					text-decoration: underline;
-					color: #0060a9;
+					color: var(--text-link);
 				}
 			}
 
 			a {
-				color: #0060a9;
+				color: var(--text-link);
 				text-decoration: none;
 
 				&:hover {
 					text-decoration: underline;
-					color: #c7511f;
+					color: var(--text-link-hover);
 				}
 
 				> em {
@@ -401,7 +401,7 @@ section.notice-list-layout {
 					font-style: normal;
 
 					> span {
-						color: #d13232;
+						color: var(--color-danger);
 						font-size: 0.813rem;
 						vertical-align: text-top;
 						padding-right: 4px;
@@ -494,7 +494,7 @@ section.notice-list-layout {
 	display: grid;
 	grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
 	gap: 12px;
-	border-bottom: 1px solid #eaeaea;
+	border-bottom: 1px solid var(--border-default);
 	padding-bottom: 0.75rem;
 	justify-items: center;
 	align-items: start;
@@ -531,24 +531,24 @@ section.notice-list-layout {
 
 					// 말머리
 					button {
-						color: #333;
+						color: var(--text-primary);
 						padding-right: 0.5rem;
 
 						&:hover {
 							text-decoration: underline;
-							color: #0060a9;
+							color: var(--text-link);
 						}
 					}
 
 					a {
 						display: inline-block;
-						color: #0060a9;
+						color: var(--text-link);
 						text-decoration: none;
 						padding-top: 7px;
 
 						&:hover {
 							text-decoration: underline;
-							color: #c7511f;
+							color: var(--text-link-hover);
 						}
 
 						> em {
@@ -556,7 +556,7 @@ section.notice-list-layout {
 							overflow-wrap: anywhere;
 
 							> span {
-								color: #d13232;
+								color: var(--color-danger);
 								font-size: 0.813rem;
 								vertical-align: text-bottom;
 								padding-right: 4px;
@@ -601,6 +601,6 @@ section.notice-list-layout {
 	> p {
 		padding: 6.25rem 0;
 		text-align: center;
-		border-bottom: 1px solid #eaeaea;
+		border-bottom: 1px solid var(--border-default);
 	}
 }

+ 23 - 23
app/(main)/(forum)/comment/style.scss

@@ -21,7 +21,7 @@
 
 				> em {
 					font-style: normal;
-					color: #d51b28;
+					color: var(--color-danger);
 				}
 			}
 
@@ -147,10 +147,10 @@
 		left: 0;
 		right: 0;
 		z-index: 10;
-		background: #fff;
-		border: 1px solid #ddd;
+		background: var(--bg-page);
+		border: 1px solid var(--border-default);
 		border-radius: 6px;
-		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
+		box-shadow: 0 2px 8px var(--shadow-color);
 		max-height: 200px;
 		overflow-y: auto;
 
@@ -180,12 +180,12 @@
 
 				small {
 					font-size: 0.75rem;
-					color: #999;
+					color: var(--text-muted);
 				}
 
 				&.selected,
 				&:hover {
-					background: #f5f5f5;
+					background: var(--bg-subtle);
 				}
 			}
 		}
@@ -195,8 +195,8 @@
 	.qna-pending-notice {
 		text-align: center;
 		padding: 2rem;
-		color: #92400e;
-		background: #fefce8;
+		color: var(--color-warning-text);
+		background: var(--color-warning-bg);
 		border: 1px solid rgba(202, 138, 4, 0.2);
 		border-radius: 0.5rem;
 		margin: 0 0 1rem 0;
@@ -220,8 +220,8 @@
 	.comment-list {
 		position: relative;
 		margin-top: 10px;
-		border-top: 1px solid #eee;
-		border-bottom: 1px solid #eee;
+		border-top: 1px solid var(--border-default);
+		border-bottom: 1px solid var(--border-default);
 
 		ol {
 			list-style: none;
@@ -236,7 +236,7 @@
     			flex-direction: row;
 				column-gap: 20px;
 				padding: 14px 0 10px 0;
-				border-bottom: 1px solid #eee;
+				border-bottom: 1px solid var(--border-default);
 
 				// 대댓글 래퍼는 그리드 해제
 				&:has(> ol) {
@@ -295,7 +295,7 @@
 									&::after {
 										content: '·';
 										margin-left: 7px;
-										color: #999;
+										color: var(--text-muted);
 									}
 
 									// 마지막 li는 점 숨김
@@ -324,8 +324,8 @@
 								padding: 0 4px;
 
 								&:hover, &:focus, &:active {
-									background-color: #f0f0f0;
-									outline: 1px solid #dedede;
+									background-color: var(--bg-subtle);
+									outline: 1px solid var(--border-default);
 									border-radius: 3px;
 								}
 							}
@@ -346,9 +346,9 @@
 								width: max-content;
 								max-width: 100%;
 								padding: 5px 40px;
-								border: 1px solid #ccc;
+								border: 1px solid var(--border-strong);
 								border-radius: 5px;
-								background: #f9f9f9;
+								background: var(--bg-elevated);
 								margin: 5px 0;
 								box-sizing: border-box;
 
@@ -377,8 +377,8 @@
 								}
 
 								&:hover {
-									background-color: #f0f0f0;
-									border-color: #c7511f;
+									background-color: var(--bg-subtle);
+									border-color: var(--text-link-hover);
 									cursor: pointer;
 								}
 							}
@@ -394,7 +394,7 @@
 							gap: 0;
 
 							> div {
-								color: #666;
+								color: var(--text-secondary);
 
 								// 답글
 								&:nth-of-type(1) {
@@ -402,10 +402,10 @@
 									text-align: left;
 
 									> button {
-										color: #0D6295;
+										color: var(--text-link);
 
 										&:hover {
-											color: #e47911;
+											color: var(--brand-orange);
 											text-decoration: underline;
 										}
 									}
@@ -419,8 +419,8 @@
 										border-radius: 3px;
 
 										&:hover, &:focus, &:active {
-											outline: 1px solid #dedede;
-											background-color: #f0f0f0;
+											outline: 1px solid var(--border-default);
+											background-color: var(--bg-subtle);
 										}
 
 										> svg {

+ 10 - 10
app/(main)/(forum)/latest/_component/style.scss

@@ -1,10 +1,10 @@
 section.latest-list-layout {
-	border-top: 1px solid #eaeaea;
+	border-top: 1px solid var(--border-default);
 	margin: 0.75rem 0;
 
 	article:nth-of-type(1) {
-		background-color: #f9fafb;
-		border-bottom: 1px solid #eaeaea;
+		background-color: var(--bg-elevated);
+		border-bottom: 1px solid var(--border-default);
 		padding: 0.5rem 0;
 
 		ul {
@@ -41,7 +41,7 @@ section.latest-list-layout {
 		section {
 			padding: 0.5rem 0;
 			box-sizing: inherit;
-			border-bottom: 1px solid #eaeaea;
+			border-bottom: 1px solid var(--border-default);
 
 			&.active,
 			&:hover {
@@ -49,23 +49,23 @@ section.latest-list-layout {
 			}
 
 			.prefix {
-				color: #333;
+				color: var(--text-primary);
 				padding-right: 0.5rem;
 			}
 
 			.board-name {
-				color: #666;
+				color: var(--text-secondary);
 				font-size: 0.813rem;
 				padding-right: 0.5rem;
 			}
 
 			a {
-				color: #0060a9;
+				color: var(--text-link);
 				text-decoration: none;
 
 				&:hover {
 					text-decoration: underline;
-					color: #c7511f;
+					color: var(--text-link-hover);
 				}
 
 				> em {
@@ -73,7 +73,7 @@ section.latest-list-layout {
 					font-style: normal;
 
 					> span {
-						color: #d13232;
+						color: var(--color-danger);
 						font-size: 0.813rem;
 						vertical-align: text-top;
 						padding-right: 4px;
@@ -110,7 +110,7 @@ section.latest-list-layout {
 
 					&:nth-child(2) {
 						text-align: left;
-						color: #666;
+						color: var(--text-secondary);
 						font-size: 0.875rem;
 					}
 

+ 11 - 11
app/(main)/(forum)/post/[id]/style.scss

@@ -88,7 +88,7 @@
 							&::after {
 								content: '·';
 								margin-left: 10px;
-								color: #999;
+								color: var(--text-muted);
 							}
 
 							// 마지막 li는 점 숨김
@@ -98,12 +98,12 @@
 							}
 
 							a, button {
-								color: #0D6295;
+								color: var(--text-link);
 								text-decoration: none;
 
 								&:hover {
 									text-decoration: underline;
-									color: #c7511f;
+									color: var(--text-link-hover);
 								}
 							}
 						}
@@ -157,9 +157,9 @@
 						width: max-content;
 						max-width: 100%;
 						padding: 5px 40px;
-						border: 1px solid #ccc;
+						border: 1px solid var(--border-strong);
 						border-radius: 5px;
-						background: #f9f9f9;
+						background: var(--bg-elevated);
 						margin: 5px 0;
 						box-sizing: border-box;
 
@@ -188,20 +188,20 @@
 						}
 
 						&:hover {
-							background-color: #f0f0f0;
-							border-color: #c7511f;
+							background-color: var(--bg-subtle);
+							border-color: var(--text-link-hover);
 							cursor: pointer;
 						}
 					}
 
 					p a {
-						color: #0D6295;
+						color: var(--text-link);
 						text-decoration: none;
 
 						&:hover,
 						&:focus {
 							text-decoration: underline;
-							color: #c7511f;
+							color: var(--text-link-hover);
 						}
 					}
 				}
@@ -212,12 +212,12 @@
 						padding-right: 6px;
 
 						a {
-							color: #0D6295;
+							color: var(--text-link);
 							text-decoration: none;
 
 							&:hover {
 								text-decoration: underline;
-								color: #c7511f;
+								color: var(--text-link-hover);
 							}
 						}
 					}

+ 6 - 6
app/(main)/(forum)/post/edit/[id]/style.scss

@@ -21,12 +21,12 @@
 
 			select, input, textarea {
 				padding: 0.625rem;
-				border-color: #ccced1;
+				border-color: var(--border-strong);
 				border-width: 0.0625rem;
 
 				&:focus {
 					border: 0.0625rem solid hsl(218, 81.8%, 56.9%);
-					box-shadow: 0.125rem 0.125rem 0.1875rem rgba(0, 0, 0, .1) inset, 0 0;
+					box-shadow: 0.125rem 0.125rem 0.1875rem var(--shadow-color) inset, 0 0;
 					outline: none;
 				}
 			}
@@ -78,7 +78,7 @@
 			&#postTag {
 				margin-bottom: 0;
 				padding: 0.5rem 0.625rem 0.4375rem 0.625rem;
-				border: 0.0625rem solid #ccced1;
+				border: 0.0625rem solid var(--border-strong);
 				border-width: 0 0.0625rem 0.0625rem 0.0625rem;
 				position: relative;
 				bottom: 0.0625rem;
@@ -92,14 +92,14 @@
 					gap: 0.625rem;
 
 					> .tag {
-						border: 0.0625rem solid #ccced1;
+						border: 0.0625rem solid var(--border-strong);
 						border-radius: 0.125rem;
 						padding: 0.0625rem 0.625rem;
-						background-color: #f9fafb;
+						background-color: var(--bg-elevated);
 
 						> small {
 							font-weight: normal;
-							color: #333;
+							color: var(--text-primary);
 
 							&:hover {
 								text-decoration: underline;

+ 6 - 6
app/(main)/(forum)/post/write/style.scss

@@ -21,12 +21,12 @@
 
 			select, input, textarea {
 				padding: 0.625rem;
-				border-color: #ccced1;
+				border-color: var(--border-strong);
 				border-width: 0.0625rem;
 
 				&:focus {
 					border: 0.0625rem solid hsl(218, 81.8%, 56.9%);
-					box-shadow: 0.125rem 0.125rem 0.1875rem rgba(0, 0, 0, .1) inset, 0 0;
+					box-shadow: 0.125rem 0.125rem 0.1875rem var(--shadow-color) inset, 0 0;
 					outline: none;
 				}
 			}
@@ -78,7 +78,7 @@
 			&#postTag {
 				margin-bottom: 0;
 				padding: 0.5rem 0.625rem 0.4375rem 0.625rem;
-				border: 0.0625rem solid #ccced1;
+				border: 0.0625rem solid var(--border-strong);
 				border-width: 0 0.0625rem 0.0625rem 0.0625rem;
 				position: relative;
 				bottom: 0.0625rem;
@@ -92,14 +92,14 @@
 					gap: 0.625rem;
 
 					> .tag {
-						border: 0.0625rem solid #ccced1;
+						border: 0.0625rem solid var(--border-strong);
 						border-radius: 0.125rem;
 						padding: 0.0625rem 0.625rem;
-						background-color: #f9fafb;
+						background-color: var(--bg-elevated);
 
 						> small {
 							font-weight: normal;
-							color: #333;
+							color: var(--text-primary);
 
 							&:hover {
 								text-decoration: underline;

+ 1 - 1
app/(main)/news/view.tsx

@@ -58,7 +58,7 @@ export default function View() {
 						<article>
 							<div>
 								<img
-									src={row.imageUrl || '/resources/no-image.png'}
+									src={row.imageUrl || '/resources/no-image-2.png'}
 									alt={row.title}
 									onError={(e) => { (e.target as HTMLImageElement).src = '/resources/no-image.png'; }}
 								/>

+ 10 - 10
app/(main)/support/faq/style.scss

@@ -46,7 +46,7 @@
 						font-size: 16px;
 
 						&.active > button {
-							color: #e47911;
+							color: var(--brand-orange);
 							font-weight: 600;
 						}
 
@@ -54,7 +54,7 @@
 							cursor: pointer;
 
 							&:hover {
-								color: #e47911;
+								color: var(--brand-orange);
 								background-color: var(--color-primary-100);
 								text-decoration: underline;
 							}
@@ -112,7 +112,7 @@
 				&::before {
 					content: 'Q.';
 					font-weight: 700;
-					color: #e47911;
+					color: var(--brand-orange);
 					flex-shrink: 0;
 				}
 
@@ -121,8 +121,8 @@
 					display: inline-block;
 					width: 8px;
 					height: 8px;
-					border-right: 2px solid #999;
-					border-bottom: 2px solid #999;
+					border-right: 2px solid var(--text-muted);
+					border-bottom: 2px solid var(--text-muted);
 					transform: rotate(45deg);
 					transition: transform 0.2s ease;
 					flex-shrink: 0;
@@ -130,13 +130,13 @@
 				}
 
 				&:hover {
-					background-color: var(--color-primary-50, #f9f9f9);
+					background-color: var(--bg-elevated);
 				}
 
 				> .faq-category {
 					font-size: 13px;
-					color: #666;
-					background-color: var(--color-primary-100, #f0f0f0);
+					color: var(--text-secondary);
+					background-color: var(--bg-subtle);
 					padding: 2px 8px;
 					border-radius: 4px;
 					flex-shrink: 0;
@@ -170,7 +170,7 @@
 				}
 
 				> .no-answer {
-					color: #999;
+					color: var(--text-muted);
 					font-style: italic;
 				}
 			}
@@ -180,7 +180,7 @@
 		> .no-results {
 			padding: 48px 0;
 			text-align: center;
-			color: #999;
+			color: var(--text-muted);
 			font-size: 15px;
 		}
 	}

+ 2 - 2
app/(main)/support/style.scss

@@ -13,11 +13,11 @@
 
 	> article {
 		> a {
-			color: #0D6295;
+			color: var(--text-link);
 			transition: color 0.3s;
 
 			&:hover, &.active {
-				color: #e47911;
+				color: var(--brand-orange);
 				text-decoration: underline;
 			}
 

+ 1 - 1
app/api/auth/[...path]/route.ts

@@ -30,7 +30,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
 	const response = NextResponse.json(res);
 
 	// 로그인 또는 토큰 갱신 성공 시 쿠키 설정
-	if ((path[0] === 'login' || path[0] === 'refresh-token') && res.success && res.data) {
+	if ((path[0] === 'login' || path[0] === 'google-login' || path[0] === 'refresh-token') && res.success && res.data) {
 		const data = res.data as LoginResponse;
 		const cookieOptions = { httpOnly: true, path: '/' };
 		response.cookies.set('accessToken', data.accessToken, cookieOptions);

+ 37 - 0
app/auth/login/google/callback/route.ts

@@ -0,0 +1,37 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { ResultDto } from '@/types/response/common';
+import { LoginResponse } from '@/types/response/auth';
+import { fetchJson } from '@/lib/utils/server';
+
+export async function POST(request: NextRequest)
+{
+	// Google redirect 모드는 form data로 credential을 전송
+	const formData = await request.formData();
+	const credential = formData.get('credential') as string;
+
+	if (!credential) {
+		const url = new URL('/login', request.url);
+		url.searchParams.set('error', 'Google 인증 정보가 없습니다.');
+		return NextResponse.redirect(url);
+	}
+
+	// 백엔드 google-login API 호출
+	const res: ResultDto = await fetchJson('/api/auth/google-login', {
+		method: 'POST',
+		body: JSON.stringify({ credential })
+	});
+
+	if (res.success && res.data) {
+		const data = res.data as LoginResponse;
+		const response = NextResponse.redirect(new URL('/auth/login/google/complete', request.url));
+		const cookieOptions = { httpOnly: true, path: '/' };
+		response.cookies.set('accessToken', data.accessToken, cookieOptions);
+		response.cookies.set('refreshToken', data.refreshToken, cookieOptions);
+		return response;
+	}
+
+	// 실패 시 로그인 페이지로 리다이렉트
+	const url = new URL('/login', request.url);
+	url.searchParams.set('error', res.message || 'Google 로그인에 실패했습니다.');
+	return NextResponse.redirect(url);
+}

+ 19 - 0
app/auth/login/google/complete/page.tsx

@@ -0,0 +1,19 @@
+'use client';
+
+import { useEffect } from 'react';
+import useAuth from '@/hooks/useAuth';
+
+export default function Page()
+{
+	const { login } = useAuth();
+
+	useEffect(() => {
+		login(true);
+	}, []);
+
+	return (
+		<div className="flex items-center justify-center min-h-screen">
+			<p>Google 로그인 처리 중...</p>
+		</div>
+	);
+}

+ 17 - 11
app/component/Layout.tsx

@@ -6,8 +6,9 @@ import { usePathname, useRouter } from 'next/navigation';
 import Styles from '../styles/common.module.scss';
 import useAuth from '@/hooks/useAuth';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faBars, faXmark, faCoins, faComments, faNewspaper, faHeadset, faUser, faRightToBracket } from '@fortawesome/free-solid-svg-icons';
+import { faBars, faXmark, faCoins, faComments, faNewspaper, faHeadset, faUser, faRightToBracket, faSun, faMoon } from '@fortawesome/free-solid-svg-icons';
 import { faCommentDots, faClock } from '@fortawesome/free-regular-svg-icons';
+import useTheme from '@/hooks/useTheme';
 import ChatSidebar from '@/app/component/chat/ChatSidebar';
 
 type Props = {
@@ -16,6 +17,7 @@ type Props = {
 
 export default function Layout({ children }: Props) {
 	const { isAuthenticated, isLoading, logout } = useAuth();
+	const { toggleTheme, isDark } = useTheme();
 	const [sidebarOpen, setSidebarOpen] = useState(false);
 	const [chatOpen, setChatOpen] = useState(false);
 	const pathname = usePathname();
@@ -89,11 +91,17 @@ export default function Layout({ children }: Props) {
                                 </Link>
                             </li>
                         </ul>
+						<ul className='flex gap-4 items-center'>
+							<li>
+								<button type='button' onClick={toggleTheme} aria-label='다크모드 전환' title={isDark ? '라이트 모드' : '다크 모드'}>
+									<FontAwesomeIcon icon={isDark ? faSun : faMoon} />
+								</button>
+							</li>
+							<li className={`${Styles.clock} text-sm opacity-70`} style={{ fontVariantNumeric: 'tabular-nums' }}>
+								<FontAwesomeIcon icon={faClock} className='mr-1' />{currentTime}
+							</li>
 						{!isAuthenticated ? (
-							<ul className='flex gap-4 items-center'>
-								<li className={`${Styles.clock} text-sm opacity-70`} style={{ fontVariantNumeric: 'tabular-nums' }}>
-									<FontAwesomeIcon icon={faClock} className='mr-1' />{currentTime}
-								</li>
+							<>
 								<li>
 									<a href='/login'>
 										로그인
@@ -104,12 +112,9 @@ export default function Layout({ children }: Props) {
 										회원가입
 									</a>
 								</li>
-							</ul>
+							</>
 						) : (
-							<ul className='flex gap-4 items-center'>
-								<li className={`${Styles.clock} text-sm opacity-70`} style={{ fontVariantNumeric: 'tabular-nums' }}>
-									<FontAwesomeIcon icon={faClock} className='mr-1' />{currentTime}
-								</li>
+							<>
 								<li>
 									<Link href='/profile'>
 										내 정보
@@ -120,8 +125,9 @@ export default function Layout({ children }: Props) {
 										로그아웃
 									</button>
 								</li>
-							</ul>
+							</>
 						)}
+						</ul>
                     </nav>
 					<button type='button' className={Styles.chatToggle} onClick={toggleChat} aria-label='채팅'>
 						<FontAwesomeIcon icon={faCommentDots} />

+ 13 - 13
app/component/PopupModal.scss

@@ -6,7 +6,7 @@
 	display: flex;
 	align-items: center;
 	justify-content: center;
-	background: rgba(0, 0, 0, 0.5);
+	background: var(--overlay-color);
 	backdrop-filter: blur(4px);
 	-webkit-backdrop-filter: blur(4px);
 	animation: popupFadeIn 0.2s ease-out;
@@ -18,10 +18,10 @@
 	width: 90%;
 	max-width: 500px;
 	max-height: 80vh;
-	background: #fff;
+	background: var(--bg-page);
 	border-radius: 12px;
 	overflow: hidden;
-	box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+	box-shadow: 0 20px 60px var(--overlay-color);
 	animation: popupSlideUp 0.25s ease-out;
 }
 
@@ -38,14 +38,14 @@
 	.swiper-button-prev,
 	.swiper-button-next {
 		color: #fff;
-		background: rgba(0, 0, 0, 0.3);
+		background: var(--overlay-color);
 		width: 36px;
 		height: 36px;
 		border-radius: 50%;
 		transition: background 0.2s;
 
 		&:hover {
-			background: rgba(0, 0, 0, 0.5);
+			background: var(--overlay-color);
 		}
 
 		&::after {
@@ -74,14 +74,14 @@
 	font-size: 1.15rem;
 	font-weight: 700;
 	margin-bottom: 12px;
-	color: #111;
+	color: var(--text-primary);
 	line-height: 1.4;
 }
 
 .popup-slide-content {
 	font-size: 0.95rem;
 	line-height: 1.7;
-	color: #333;
+	color: var(--text-primary);
 	word-break: keep-all;
 
 	img {
@@ -113,8 +113,8 @@
 	align-items: center;
 	justify-content: space-between;
 	padding: 10px 16px;
-	border-top: 1px solid #eee;
-	background: #f9f9f9;
+	border-top: 1px solid var(--border-default);
+	background: var(--bg-elevated);
 }
 
 .popup-modal-dismiss {
@@ -133,7 +133,7 @@
 
 	span {
 		font-size: 0.85rem;
-		color: #666;
+		color: var(--text-secondary);
 	}
 }
 
@@ -141,15 +141,15 @@
 	padding: 6px 16px;
 	font-size: 0.85rem;
 	font-weight: 600;
-	color: #555;
-	background: #e5e5e5;
+	color: var(--text-secondary);
+	background: var(--border-default);
 	border: none;
 	border-radius: 6px;
 	cursor: pointer;
 	transition: background 0.15s;
 
 	&:hover {
-		background: #d4d4d4;
+		background: var(--border-strong);
 	}
 }
 

+ 47 - 47
app/component/chat/chat-sidebar.scss

@@ -2,7 +2,7 @@
 	display: flex;
 	flex-direction: column;
 	height: 100%;
-	background: #fff;
+	background: var(--bg-page);
 	position: relative;
 
 	.chat-header {
@@ -10,14 +10,14 @@
 		align-items: center;
 		justify-content: space-between;
 		padding: 2px 12px;
-		background: #f4f4f4;
-		border-bottom: 1px solid #e0e0e0;
+		background: var(--bg-subtle);
+		border-bottom: 1px solid var(--border-default);
 		flex-shrink: 0;
 
 		.chat-header-title {
 			font-size: 0.875rem;
 			font-weight: 600;
-			color: #333;
+			color: var(--text-primary);
 		}
 
 		.chat-header-actions {
@@ -33,7 +33,7 @@
 				border: none;
 				background: transparent;
 				font-size: 0.75rem;
-				color: #666;
+				color: var(--text-secondary);
 				cursor: pointer;
 				border-radius: 4px;
 				transition: background 0.15s, color 0.15s;
@@ -47,8 +47,8 @@
 				}
 
 				&:hover {
-					background: #e0e0e0;
-					color: #333;
+					background: var(--border-default);
+					color: var(--text-primary);
 				}
 			}
 
@@ -62,13 +62,13 @@
 				border: none;
 				background: transparent;
 				cursor: pointer;
-				color: #666;
+				color: var(--text-secondary);
 				border-radius: 4px;
 				transition: background 0.15s, color 0.15s;
 
 				&:hover {
-					background: #e0e0e0;
-					color: #333;
+					background: var(--border-default);
+					color: var(--text-primary);
 				}
 			}
 		}
@@ -82,8 +82,8 @@
 			top: 100%;
 			right: 0;
 			min-width: 150px;
-			background: #fff;
-			border: 1px solid #e0e0e0;
+			background: var(--bg-page);
+			border: 1px solid var(--border-default);
 			border-radius: 3px;
 			z-index: 10;
 			overflow: hidden;
@@ -98,25 +98,25 @@
 				background: transparent;
 				cursor: pointer;
 				font-size: 0.8125rem;
-				color: #333;
+				color: var(--text-primary);
 				text-align: left;
 				transition: background 0.15s;
 
 				svg {
 					width: 14px;
-					color: #888;
+					color: var(--text-muted);
 				}
 
 				&:hover {
-					background: #f5f5f5;
+					background: var(--bg-subtle);
 				}
 
 				&:disabled {
-					color: #ccc;
+					color: var(--border-strong);
 					cursor: not-allowed;
 
 					svg {
-						color: #ccc;
+						color: var(--border-strong);
 					}
 
 					&:hover {
@@ -140,14 +140,14 @@
 		}
 
 		&::-webkit-scrollbar-thumb {
-			background: #ccc;
+			background: var(--border-strong);
 			border-radius: 2px;
 		}
 
 		.chat-system {
 			text-align: center;
 			font-size: 0.75rem;
-			color: #999;
+			color: var(--text-muted);
 			padding: 4px 0;
 		}
 
@@ -161,14 +161,14 @@
 
 			.chat-message-time {
 				font-size: 0.625rem;
-				color: #bbb;
+				color: var(--text-muted);
 				flex-shrink: 0;
 			}
 
 			.chat-message-name {
 				font-size: 0.8125em;
 				font-weight: 600;
-				color: #555;
+				color: var(--text-secondary);
 				flex-shrink: 0;
 				max-width: 80px;
 				overflow: hidden;
@@ -178,16 +178,16 @@
 
 			.chat-message-content {
 				font-size: 1em;
-				color: #222;
+				color: var(--text-primary);
 			}
 		}
 	}
 
 	.chat-input-area {
 		flex-shrink: 0;
-		border-top: 1px solid #e0e0e0;
+		border-top: 1px solid var(--border-default);
 		padding: 8px 10px;
-		background: #fafafa;
+		background: var(--bg-elevated);
 
 		.chat-input-row {
 			display: flex;
@@ -196,7 +196,7 @@
 			input {
 				flex: 1;
 				padding: 8px 10px;
-				border: 1px solid #ddd;
+				border: 1px solid var(--border-default);
 				border-radius: 4px;
 				font-size: 0.8125rem;
 				outline: none;
@@ -207,8 +207,8 @@
 				}
 
 				&:disabled {
-					background: #f0f0f0;
-					color: #999;
+					background: var(--bg-subtle);
+					color: var(--text-muted);
 				}
 			}
 
@@ -219,7 +219,7 @@
 				width: 36px;
 				height: 36px;
 				border: none;
-				background: #F7931A;
+				background: var(--brand-orange);
 				color: #fff;
 				border-radius: 4px;
 				cursor: pointer;
@@ -227,11 +227,11 @@
 				transition: background 0.15s;
 
 				&:hover {
-					background: #e5851a;
+					background: var(--brand-orange-hover);
 				}
 
 				&:disabled {
-					background: #ccc;
+					background: var(--border-strong);
 					cursor: not-allowed;
 				}
 			}
@@ -240,16 +240,16 @@
 		.chat-login-notice {
 			text-align: center;
 			font-size: 0.8125rem;
-			color: #888;
+			color: var(--text-muted);
 			padding: 2px 0;
 
 			a {
-				color: #F7931A;
+				color: var(--brand-orange);
 				font-weight: 600;
 				text-decoration: underline;
 
 				&:hover {
-					color: #e5851a;
+					color: var(--brand-orange-hover);
 				}
 			}
 		}
@@ -262,7 +262,7 @@
 		left: 0;
 		right: 0;
 		bottom: 0;
-		background: rgba(0, 0, 0, 0.3);
+		background: var(--overlay-color);
 		z-index: 11;
 	}
 
@@ -274,9 +274,9 @@
 		transform: translate(-50%, -50%);
 		width: calc(100% - 24px);
 		max-height: 60%;
-		background: #fff;
+		background: var(--bg-page);
 		border-radius: 8px;
-		box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
+		box-shadow: 0 8px 24px var(--shadow-color);
 		overflow: hidden;
 		z-index: 12;
 
@@ -285,13 +285,13 @@
 			align-items: center;
 			justify-content: space-between;
 			padding: 12px 16px;
-			border-bottom: 1px solid #e0e0e0;
-			background: #f4f4f4;
+			border-bottom: 1px solid var(--border-default);
+			background: var(--bg-subtle);
 
 			h3 {
 				font-size: 0.875rem;
 				font-weight: 600;
-				color: #333;
+				color: var(--text-primary);
 				margin: 0;
 			}
 
@@ -305,12 +305,12 @@
 				background: transparent;
 				cursor: pointer;
 				font-size: 1.25rem;
-				color: #666;
+				color: var(--text-secondary);
 				border-radius: 4px;
 
 				&:hover {
-					background: #e0e0e0;
-					color: #333;
+					background: var(--border-default);
+					color: var(--text-primary);
 				}
 			}
 		}
@@ -328,27 +328,27 @@
 				gap: 8px;
 				padding: 10px 16px;
 				font-size: 0.8125rem;
-				color: #333;
-				border-bottom: 1px solid #f0f0f0;
+				color: var(--text-primary);
+				border-bottom: 1px solid var(--bg-subtle);
 
 				&:last-child {
 					border-bottom: none;
 				}
 
 				.chat-participant-icon {
-					color: #999;
+					color: var(--text-muted);
 					font-size: 0.75rem;
 				}
 			}
 
 			.chat-participants-empty {
 				justify-content: center;
-				color: #999;
+				color: var(--text-muted);
 				padding: 24px 16px;
 			}
 
 			.chat-participants-guest {
-				color: #999;
+				color: var(--text-muted);
 				font-style: italic;
 			}
 		}

+ 31 - 25
app/component/crypto/TradingChart.tsx

@@ -4,6 +4,7 @@ import { useEffect, useRef, useState, useCallback } from 'react';
 import { useCryptoContext } from '@/contexts/cryptoProvider';
 import useCandles from '@/hooks/useCandles';
 import useDragScroll from '@/hooks/useDragScroll';
+import useTheme from '@/hooks/useTheme';
 import './trading-chart.scss';
 
 type IntervalType = 'sec' | '1' | '3' | '5' | '10' | '15' | '30' | '60' | '240' | 'day' | 'week' | 'month';
@@ -30,6 +31,7 @@ const MA_COLORS = {
 
 export default function TradingChart() {
 	const { selectedMarket } = useCryptoContext();
+	const { isDark } = useTheme();
 	const [interval, setInterval] = useState<IntervalType>('15');
 	const [showMA, setShowMA] = useState({ ma5: true, ma10: true, ma20: true });
 	const [isFullscreen, setIsFullscreen] = useState(false);
@@ -54,6 +56,33 @@ export default function TradingChart() {
 	const prevDataKeyRef = useRef('');
 	const prevCandleLenRef = useRef(0);
 
+	const getChartColors = useCallback((dark: boolean) => ({
+		layout: {
+			background: { color: dark ? '#171717' : '#ffffff' },
+			textColor: dark ? '#e5e5e5' : '#333333',
+			fontSize: 12,
+		},
+		grid: {
+			vertLines: { color: dark ? '#2a2a2a' : '#f0f0f0' },
+			horzLines: { color: dark ? '#2a2a2a' : '#f0f0f0' },
+		},
+		crosshair: {
+			mode: 0 as const,
+			vertLine: { color: '#9B9B9B', width: 1 as const, style: 3 as const, labelBackgroundColor: '#505050' },
+			horzLine: { color: '#9B9B9B', width: 1 as const, style: 3 as const, labelBackgroundColor: '#505050' },
+		},
+		rightPriceScale: {
+			borderColor: dark ? '#333' : '#e0e0e0',
+			scaleMargins: { top: 0.1, bottom: 0.25 },
+		},
+		timeScale: {
+			borderColor: dark ? '#333' : '#e0e0e0',
+			timeVisible: true,
+			secondsVisible: false,
+			rightOffset: 5,
+		},
+	}), []);
+
 	// 차트 초기화
 	useEffect(() => {
 		if (!chartContainerRef.current) return;
@@ -72,30 +101,7 @@ export default function TradingChart() {
 			const chart = createChart(container, {
 				width: container.clientWidth,
 				height: container.clientHeight || 500,
-				layout: {
-					background: { color: '#ffffff' },
-					textColor: '#333333',
-					fontSize: 12,
-				},
-				grid: {
-					vertLines: { color: '#f0f0f0' },
-					horzLines: { color: '#f0f0f0' },
-				},
-				crosshair: {
-					mode: 0,
-					vertLine: { color: '#9B9B9B', width: 1, style: 3, labelBackgroundColor: '#505050' },
-					horzLine: { color: '#9B9B9B', width: 1, style: 3, labelBackgroundColor: '#505050' },
-				},
-				rightPriceScale: {
-					borderColor: '#e0e0e0',
-					scaleMargins: { top: 0.1, bottom: 0.25 },
-				},
-				timeScale: {
-					borderColor: '#e0e0e0',
-					timeVisible: true,
-					secondsVisible: false,
-					rightOffset: 5,
-				},
+				...getChartColors(isDark),
 			});
 
 			const candleSeries = chart.addSeries(CandlestickSeries, {
@@ -169,7 +175,7 @@ export default function TradingChart() {
 				chartRef.current = null;
 			}
 		};
-	}, []);
+	}, [isDark, getChartColors]);
 
 	// secondsVisible 동적 변경
 	useEffect(() => {

+ 7 - 7
app/component/crypto/dashboard.scss

@@ -6,7 +6,7 @@
 
 	.resize-handle-h {
 		height: 5px;
-		background: #eee;
+		background: var(--border-default);
 		cursor: row-resize;
 		flex-shrink: 0;
 		transition: background 0.15s;
@@ -20,8 +20,8 @@
 			transform: translate(-50%, -50%);
 			width: 30px;
 			height: 3px;
-			border-top: 1px solid #ccc;
-			border-bottom: 1px solid #ccc;
+			border-top: 1px solid var(--border-strong);
+			border-bottom: 1px solid var(--border-strong);
 		}
 
 		&:hover {
@@ -33,7 +33,7 @@
 		display: flex;
 		flex: 1;
 		min-height: 0;
-		border-top: 1px solid #eee;
+		border-top: 1px solid var(--border-default);
 
 		.dashboard-orderbook {
 			overflow: hidden;
@@ -47,7 +47,7 @@
 
 		.resize-handle-v {
 			width: 5px;
-			background: #eee;
+			background: var(--border-default);
 			cursor: col-resize;
 			flex-shrink: 0;
 			transition: background 0.15s;
@@ -61,8 +61,8 @@
 				transform: translate(-50%, -50%);
 				width: 3px;
 				height: 30px;
-				border-left: 1px solid #ccc;
-				border-right: 1px solid #ccc;
+				border-left: 1px solid var(--border-strong);
+				border-right: 1px solid var(--border-strong);
 			}
 
 			&:hover {

+ 13 - 13
app/component/crypto/market-header.scss

@@ -3,13 +3,13 @@
 	align-items: center;
 	gap: 20px;
 	padding: 10px 16px;
-	background: #fff;
-	border-bottom: 1px solid #eee;
+	background: var(--bg-page);
+	border-bottom: 1px solid var(--border-default);
 	flex-wrap: wrap;
 	min-height: 48px;
 
 	.market-header-loading {
-		color: #999;
+		color: var(--text-muted);
 		font-size: 0.875rem;
 	}
 
@@ -17,12 +17,12 @@
 		.symbol {
 			font-size: 1.25rem;
 			font-weight: 700;
-			color: #222;
+			color: var(--text-primary);
 		}
 
 		.pair {
 			font-size: 0.875rem;
-			color: #999;
+			color: var(--text-muted);
 			margin-left: 2px;
 		}
 	}
@@ -56,14 +56,14 @@
 
 			.label {
 				font-size: 0.688rem;
-				color: #999;
+				color: var(--text-muted);
 			}
 
 			.value {
 				font-size: 0.813rem;
 				font-weight: 500;
 				font-variant-numeric: tabular-nums;
-				color: #333;
+				color: var(--text-primary);
 			}
 		}
 
@@ -78,7 +78,7 @@
 					transition: background 0.15s;
 
 					&:hover {
-						background: #f5f5f5;
+						background: var(--bg-subtle);
 					}
 				}
 
@@ -86,10 +86,10 @@
 					position: absolute;
 					top: calc(100% + 6px);
 					right: 0;
-					background: #fff;
-					border: 1px solid #e0e0e0;
+					background: var(--bg-page);
+					border: 1px solid var(--border-default);
 					border-radius: 6px;
-					box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+					box-shadow: 0 4px 12px var(--shadow-color);
 					padding: 8px 12px;
 					white-space: nowrap;
 					z-index: 100;
@@ -99,14 +99,14 @@
 
 					.volume-popover-label {
 						font-size: 0.625rem;
-						color: #999;
+						color: var(--text-muted);
 					}
 
 					.volume-popover-value {
 						font-size: 0.813rem;
 						font-weight: 600;
 						font-variant-numeric: tabular-nums;
-						color: #333;
+						color: var(--text-primary);
 					}
 				}
 			}

+ 25 - 25
app/component/crypto/mobile-coin-list.scss

@@ -2,18 +2,18 @@
 	display: flex;
 	flex-direction: column;
 	height: 100%;
-	background: #fafafa;
+	background: var(--bg-elevated);
 
 	.mcl-tabs {
 		display: flex;
-		border-bottom: 1px solid #eee;
+		border-bottom: 1px solid var(--border-default);
 
 		.tab {
 			flex: 1;
 			padding: 10px 0;
 			font-size: 0.813rem;
 			font-weight: 600;
-			color: #999;
+			color: var(--text-muted);
 			background: transparent;
 			border: none;
 			border-bottom: 2px solid transparent;
@@ -21,45 +21,45 @@
 			transition: color 0.15s, border-color 0.15s;
 
 			&:hover {
-				color: #232323;
+				color: var(--text-primary);
 			}
 
 			&.active {
-				color: #F7931A;
-				border-bottom-color: #F7931A;
+				color: var(--brand-orange);
+				border-bottom-color: var(--brand-orange);
 			}
 		}
 	}
 
 	.mcl-search {
 		padding: 8px 12px;
-		border-bottom: 1px solid #eee;
+		border-bottom: 1px solid var(--border-default);
 
 		input {
 			width: 100%;
 			padding: 8px 12px;
 			font-size: 0.875rem;
-			border: 1px solid #ddd;
+			border: 1px solid var(--border-default);
 			border-radius: 4px;
 			outline: none;
 
 			&:focus {
-				border-color: #F7931A;
+				border-color: var(--brand-orange);
 			}
 		}
 	}
 
 	.mcl-sort {
 		display: flex;
-		border-bottom: 1px solid #eee;
-		background: #f5f5f5;
+		border-bottom: 1px solid var(--border-default);
+		background: var(--bg-subtle);
 		padding: 0 12px;
 
 		button {
 			padding: 6px 0;
 			font-size: 0.688rem;
 			font-weight: 500;
-			color: #888;
+			color: var(--text-muted);
 			background: transparent;
 			border: none;
 			cursor: pointer;
@@ -70,11 +70,11 @@
 			transition: color 0.15s;
 
 			&:hover {
-				color: #333;
+				color: var(--text-primary);
 			}
 
 			&.active {
-				color: #F7931A;
+				color: var(--brand-orange);
 				font-weight: 700;
 			}
 
@@ -115,14 +115,14 @@
 		width: 100%;
 		padding: 10px 12px;
 		border: 1px solid transparent;
-		border-bottom: 1px solid #f0f0f0;
+		border-bottom: 1px solid var(--bg-subtle);
 		background: transparent;
 		cursor: pointer;
 		text-align: left;
 		transition: background 0.15s;
 
 		&:hover {
-			background: #f0f0f0;
+			background: var(--bg-subtle);
 		}
 
 		&:active {
@@ -131,7 +131,7 @@
 
 		&.active {
 			background: #fff3e0;
-			border-left: 3px solid #F7931A;
+			border-left: 3px solid var(--brand-orange);
 		}
 
 		.col-name {
@@ -144,7 +144,7 @@
 			.name {
 				font-weight: 600;
 				font-size: 0.813rem;
-				color: #222;
+				color: var(--text-primary);
 				white-space: nowrap;
 				overflow: hidden;
 				text-overflow: ellipsis;
@@ -152,7 +152,7 @@
 
 			.symbol {
 				font-size: 0.688rem;
-				color: #999;
+				color: var(--text-muted);
 			}
 		}
 
@@ -176,7 +176,7 @@
 			flex: 1.5;
 			text-align: right;
 			font-size: 0.688rem;
-			color: #666;
+			color: var(--text-secondary);
 			font-variant-numeric: tabular-nums;
 		}
 
@@ -196,7 +196,7 @@
 	.mcl-empty {
 		padding: 24px;
 		text-align: center;
-		color: #999;
+		color: var(--text-muted);
 		font-size: 0.875rem;
 	}
 }
@@ -238,8 +238,8 @@
 	align-items: center;
 	gap: 8px;
 	padding: 10px 12px;
-	background: #fff;
-	border-bottom: 1px solid #eee;
+	background: var(--bg-page);
+	border-bottom: 1px solid var(--border-default);
 
 	button {
 		display: flex;
@@ -248,13 +248,13 @@
 		padding: 4px 8px;
 		font-size: 0.875rem;
 		font-weight: 600;
-		color: #333;
+		color: var(--text-primary);
 		background: transparent;
 		border: none;
 		cursor: pointer;
 
 		&:hover {
-			color: #F7931A;
+			color: var(--brand-orange);
 		}
 
 		svg {

+ 10 - 10
app/component/crypto/orderbook.scss

@@ -1,7 +1,7 @@
 .orderbook {
 	display: flex;
 	flex-direction: column;
-	background: #fff;
+	background: var(--bg-page);
 	height: 100%;
 
 	.orderbook-title {
@@ -11,8 +11,8 @@
 		font-size: 0.813rem;
 		font-weight: 600;
 		padding: 8px 12px;
-		border-bottom: 1px solid #eee;
-		color: #333;
+		border-bottom: 1px solid var(--border-default);
+		color: var(--text-primary);
 
 		.orderbook-total {
 			display: flex;
@@ -33,7 +33,7 @@
 	.orderbook-loading {
 		padding: 24px;
 		text-align: center;
-		color: #999;
+		color: var(--text-muted);
 		font-size: 0.813rem;
 	}
 
@@ -115,7 +115,7 @@
 		.size {
 			position: relative;
 			z-index: 1;
-			color: #555;
+			color: var(--text-secondary);
 		}
 
 		// 체결 플래시 효과
@@ -159,18 +159,18 @@
 		align-items: center;
 		padding: 5px 12px;
 		font-size: 0.75rem;
-		background: #f9f9f9;
-		border-top: 1px solid #eee;
-		border-bottom: 1px solid #eee;
+		background: var(--bg-elevated);
+		border-top: 1px solid var(--border-default);
+		border-bottom: 1px solid var(--border-default);
 		flex-shrink: 0;
 
 		.spread-price {
 			font-weight: 600;
-			color: #333;
+			color: var(--text-primary);
 		}
 
 		.spread-info {
-			color: #999;
+			color: var(--text-muted);
 			font-size: 0.688rem;
 		}
 	}

+ 21 - 21
app/component/crypto/sidebar.scss

@@ -2,18 +2,18 @@
 	display: flex;
 	flex-direction: column;
 	height: 100%;
-	background: #fafafa;
+	background: var(--bg-elevated);
 
 	.sidebar-tabs {
 		display: flex;
-		border-bottom: 1px solid #eee;
+		border-bottom: 1px solid var(--border-default);
 
 		.tab {
 			flex: 1;
 			padding: 8px 0;
 			font-size: 0.75rem;
 			font-weight: 600;
-			color: #999;
+			color: var(--text-muted);
 			background: transparent;
 			border: none;
 			border-bottom: 2px solid transparent;
@@ -21,45 +21,45 @@
 			transition: color 0.15s, border-color 0.15s;
 
 			&:hover {
-				color: #232323;
+				color: var(--text-primary);
 			}
 
 			&.active {
-				color: #F7931A;
-				border-bottom-color: #F7931A;
+				color: var(--brand-orange);
+				border-bottom-color: var(--brand-orange);
 			}
 		}
 	}
 
 	.sidebar-search {
 		padding: 8px;
-		border-bottom: 1px solid #eee;
+		border-bottom: 1px solid var(--border-default);
 
 		input {
 			width: 100%;
 			padding: 6px 10px;
 			font-size: 0.813rem;
-			border: 1px solid #ddd;
+			border: 1px solid var(--border-default);
 			border-radius: 4px;
 			outline: none;
 
 			&:focus {
-				border-color: #F7931A;
+				border-color: var(--brand-orange);
 			}
 		}
 	}
 
 	.sidebar-sort {
 		display: flex;
-		border-bottom: 1px solid #eee;
-		background: #f5f5f5;
+		border-bottom: 1px solid var(--border-default);
+		background: var(--bg-subtle);
 
 		button {
 			flex: 1;
 			padding: 5px 0;
 			font-size: 0.688rem;
 			font-weight: 500;
-			color: #888;
+			color: var(--text-muted);
 			background: transparent;
 			border: none;
 			cursor: pointer;
@@ -70,11 +70,11 @@
 			transition: color 0.15s;
 
 			&:hover {
-				color: #333;
+				color: var(--text-primary);
 			}
 
 			&.active {
-				color: #F7931A;
+				color: var(--brand-orange);
 				font-weight: 700;
 			}
 
@@ -97,19 +97,19 @@
 		width: 100%;
 		padding: 8px 12px;
 		border: 1px solid transparent;
-		border-bottom: 1px solid #f0f0f0;
+		border-bottom: 1px solid var(--bg-subtle);
 		background: transparent;
 		cursor: pointer;
 		text-align: left;
 		transition: background 0.15s;
 
 		&:hover {
-			background: #f0f0f0;
+			background: var(--bg-subtle);
 		}
 
 		&.active {
-			background: #fff3e0;
-			border-left: 3px solid #F7931A;
+			background: var(--sidebar-active-bg, #fff3e0);
+			border-left: 3px solid var(--brand-orange);
 		}
 
 		.item-info {
@@ -120,12 +120,12 @@
 			.item-name {
 				font-weight: 600;
 				font-size: 0.813rem;
-				color: #222;
+				color: var(--text-primary);
 			}
 
 			.item-symbol {
 				font-size: 0.688rem;
-				color: #999;
+				color: var(--text-muted);
 			}
 		}
 
@@ -163,7 +163,7 @@
 	.sidebar-empty {
 		padding: 24px;
 		text-align: center;
-		color: #999;
+		color: var(--text-muted);
 		font-size: 0.813rem;
 	}
 }

+ 10 - 10
app/component/crypto/trade-history.scss

@@ -1,7 +1,7 @@
 .trade-history {
 	display: flex;
 	flex-direction: column;
-	background: #fff;
+	background: var(--bg-page);
 	height: 100%;
 
 	.trade-title {
@@ -11,8 +11,8 @@
 		font-size: 0.813rem;
 		font-weight: 600;
 		padding: 8px 12px;
-		border-bottom: 1px solid #eee;
-		color: #333;
+		border-bottom: 1px solid var(--border-default);
+		color: var(--text-primary);
 
 		.trade-title-left {
 			display: flex;
@@ -35,11 +35,11 @@
 			transition: background 0.15s;
 
 			&:hover {
-				background: #f0f0f0;
+				background: var(--bg-subtle);
 			}
 
 			&.active {
-				color: #F7931A;
+				color: var(--brand-orange);
 			}
 
 			&.muted {
@@ -65,7 +65,7 @@
 	.trade-loading {
 		padding: 24px;
 		text-align: center;
-		color: #999;
+		color: var(--text-muted);
 		font-size: 0.813rem;
 	}
 
@@ -74,8 +74,8 @@
 		justify-content: space-between;
 		padding: 4px 12px;
 		font-size: 0.688rem;
-		color: #999;
-		border-bottom: 1px solid #f5f5f5;
+		color: var(--text-muted);
+		border-bottom: 1px solid var(--bg-subtle);
 
 		span:last-child {
 			text-align: right;
@@ -110,13 +110,13 @@
 		}
 
 		.size {
-			color: #555;
+			color: var(--text-secondary);
 			flex: 1;
 			text-align: center;
 		}
 
 		.time {
-			color: #999;
+			color: var(--text-muted);
 			font-size: 0.688rem;
 			flex-shrink: 0;
 			text-align: right;

+ 15 - 15
app/component/crypto/trading-chart.scss

@@ -1,7 +1,7 @@
 .trading-chart {
 	display: flex;
 	flex-direction: column;
-	background: #fff;
+	background: var(--bg-page);
 	height: 100%;
 
 	&.fullscreen {
@@ -17,7 +17,7 @@
 		display: flex;
 		align-items: center;
 		padding: 6px 12px;
-		border-bottom: 1px solid #f0f0f0;
+		border-bottom: 1px solid var(--bg-subtle);
 		overflow-x: auto;
 		flex-wrap: nowrap;
 		gap: 8px;
@@ -43,7 +43,7 @@
 			padding: 4px 10px;
 			font-size: 0.75rem;
 			font-weight: 500;
-			color: #666;
+			color: var(--text-secondary);
 			background: transparent;
 			border: 1px solid transparent;
 			border-radius: 3px;
@@ -52,14 +52,14 @@
 			transition: all 0.15s;
 
 			&:hover {
-				background: #f5f5f5;
-				color: #333;
+				background: var(--bg-subtle);
+				color: var(--text-primary);
 			}
 
 			&.active {
-				background: #F7931A;
+				background: var(--brand-orange);
 				color: #fff;
-				border-color: #F7931A;
+				border-color: var(--brand-orange);
 			}
 		}
 
@@ -81,9 +81,9 @@
 			padding: 3px 8px;
 			font-size: 0.7rem;
 			font-weight: 600;
-			color: #999;
+			color: var(--text-muted);
 			background: transparent;
-			border: 1px solid #ddd;
+			border: 1px solid var(--border-default);
 			border-radius: 3px;
 			cursor: pointer;
 			white-space: nowrap;
@@ -91,7 +91,7 @@
 
 			&:hover {
 				border-color: #bbb;
-				color: #666;
+				color: var(--text-secondary);
 			}
 
 			&.active {
@@ -107,18 +107,18 @@
 			justify-content: center;
 			width: 28px;
 			height: 28px;
-			border: 1px solid #ddd;
+			border: 1px solid var(--border-default);
 			border-radius: 3px;
 			background: transparent;
 			cursor: pointer;
 			font-size: 1rem;
-			color: #666;
+			color: var(--text-secondary);
 			transition: all 0.15s;
 
 			&:hover {
 				border-color: #bbb;
-				background: #f5f5f5;
-				color: #333;
+				background: var(--bg-subtle);
+				color: var(--text-primary);
 			}
 		}
 	}
@@ -134,7 +134,7 @@
 			top: 50%;
 			left: 50%;
 			transform: translate(-50%, -50%);
-			color: #999;
+			color: var(--text-muted);
 			font-size: 0.875rem;
 			z-index: 1;
 		}

+ 70 - 15
app/globals.scss

@@ -48,6 +48,33 @@ body {
 		--crypto-up: 0 70% 55%;
 		--crypto-down: 220 70% 55%;
 		--crypto-neutral: 0 0% 50%;
+
+		/* 시맨틱 색상 변수 */
+		--text-primary: #333;
+		--text-secondary: #666;
+		--text-muted: #999;
+		--text-link: #0060a9;
+		--text-link-hover: #c7511f;
+		--bg-page: #fff;
+		--bg-elevated: #f9f9f9;
+		--bg-subtle: #f4f4f4;
+		--bg-input: #fff;
+		--border-default: #eaeaea;
+		--border-light: #eee;
+		--border-strong: #ccc;
+		--shadow-color: rgba(0,0,0,0.12);
+		--brand-orange: #F7931A;
+		--brand-orange-hover: #e5851a;
+		--color-danger: #d51b28;
+		--color-success: #4caf50;
+		--color-warning-bg: #fefce8;
+		--color-warning-text: #92400e;
+		--overlay-color: rgba(0,0,0,0.5);
+		--sidebar-active-bg: #fff3e0;
+		--list-row-active: #faffd1;
+		--btn-default-border: #b3b3b3;
+		--btn-default-hover: #e3e3e3;
+		--btn-submit-shadow: #d38817;
 	}
   	.dark {
         --background: 0 0% 3.9%;
@@ -77,6 +104,33 @@ body {
 		--crypto-up: 0 70% 60%;
 		--crypto-down: 220 70% 60%;
 		--crypto-neutral: 0 0% 63.9%;
+
+		/* 시맨틱 색상 변수 */
+		--text-primary: #e5e5e5;
+		--text-secondary: #a3a3a3;
+		--text-muted: #737373;
+		--text-link: #60a5fa;
+		--text-link-hover: #f59e0b;
+		--bg-page: #171717;
+		--bg-elevated: #1e1e1e;
+		--bg-subtle: #262626;
+		--bg-input: #262626;
+		--border-default: #333;
+		--border-light: #2a2a2a;
+		--border-strong: #444;
+		--shadow-color: rgba(0,0,0,0.4);
+		--brand-orange: #F7931A;
+		--brand-orange-hover: #f59e0b;
+		--color-danger: #ef4444;
+		--color-success: #22c55e;
+		--color-warning-bg: #422006;
+		--color-warning-text: #fef08a;
+		--overlay-color: rgba(0,0,0,0.7);
+		--sidebar-active-bg: #3d2800;
+		--list-row-active: #2a2a00;
+		--btn-default-border: #555;
+		--btn-default-hover: #333;
+		--btn-submit-shadow: #b8700f;
     }
 }
 
@@ -135,15 +189,16 @@ body {
 select, input, textarea {
     font-size: 1rem;
     padding: 5px;
-    color: #333333;
-    border: 1px solid #9c9c9c;
+    color: var(--text-primary);
+    background: var(--bg-input);
+    border: 1px solid var(--border-strong);
     border-radius: 3px;
     transition: border-color 0.3s ease;
 	line-height: inherit;
 
     &:focus {
         outline: none;
-        border-color: #9c9c9c;
+        border-color: var(--text-link);
         -webkit-box-shadow: inset 1px 2px 2px rgba(0, 0, 0, 0.05), 0 0 4px rgba(44, 112, 170, 0.8);
         box-shadow: inset 1px 2px 2px rgba(0, 0, 0, 0.05), 0 0 4px rgba(44, 112, 170, 0.8);
     }
@@ -161,27 +216,27 @@ select, input, textarea {
 
 // 기본 버튼
 .btn-default {
-    color: #333;
-    background: #f1f1f1;
-    border: 1px solid #b3b3b3;
-    -webkit-box-shadow: inset 0 -1px 0 0 #b3b3b3;
-    box-shadow: inset 0 -1px 0 0 #b3b3b3;
+    color: var(--text-primary);
+    background: var(--bg-subtle);
+    border: 1px solid var(--btn-default-border);
+    -webkit-box-shadow: inset 0 -1px 0 0 var(--btn-default-border);
+    box-shadow: inset 0 -1px 0 0 var(--btn-default-border);
 
     &:hover {
-        background: #e3e3e3;
+        background: var(--btn-default-hover);
     }
 }
 
 // 제출 버튼
 .btn-submit {
     color: #fff;
-    background: #F7931A;
-	border: 1px solid #f1880f;
-    -webkit-box-shadow: inset 0 -2px 0 0 #d38817;
-    box-shadow: inset 0 -2px 0 0 #d38817;
+    background: var(--brand-orange);
+	border: 1px solid var(--brand-orange);
+    -webkit-box-shadow: inset 0 -2px 0 0 var(--btn-submit-shadow);
+    box-shadow: inset 0 -2px 0 0 var(--btn-submit-shadow);
 
     &:hover {
-        background: #E07D0A;
-        border-color: #E07D0A;
+        background: var(--brand-orange-hover);
+        border-color: var(--brand-orange-hover);
     }
 }

+ 56 - 10
app/layout.tsx

@@ -7,6 +7,7 @@ import { SignalRProvider } from '@/contexts/signalrProvider';
 import { AuthProvider } from "@/contexts/authProvider";
 import { MemberProvider } from "@/contexts/memberProvider";
 import { ConfigProvider } from "@/contexts/configProvider";
+import { ThemeProvider } from "@/contexts/themeProvider";
 import { getAccessToken, getSignalRCryptoUrl, getSignalRChatUrl } from "@/lib/utils/server";
 import { fetchConfig } from "@/lib/api/system";
 
@@ -20,8 +21,48 @@ const geistMono = Geist_Mono({
     subsets: ["latin"],
 });
 
+type ColorSchemeEnum = 'normal' | 'light' | 'dark' | 'light dark' | 'dark light' | 'only light';
+
+function parseMetaAdds(html: string | null | undefined) {
+	if (!html) return { other: {} as Record<string, string>, themeColor: [] as { media?: string; color: string }[], colorScheme: undefined as ColorSchemeEnum | undefined };
+
+	const other: Record<string, string> = {};
+	const themeColor: { media?: string; color: string }[] = [];
+	const validColorSchemes: ColorSchemeEnum[] = ['normal', 'light', 'dark', 'light dark', 'dark light', 'only light'];
+	let colorScheme: ColorSchemeEnum | undefined;
+
+	const tagRegex = /<meta\s+([^>]+)\/?>/gi;
+	const attrRegex = /(\w[\w-]*)=["']([^"']*?)["']/gi;
+
+	let tagMatch;
+	while ((tagMatch = tagRegex.exec(html)) !== null) {
+		const attrs: Record<string, string> = {};
+		let attrMatch;
+		while ((attrMatch = attrRegex.exec(tagMatch[1])) !== null) {
+			attrs[attrMatch[1].toLowerCase()] = attrMatch[2];
+		}
+
+		const name = attrs['name'];
+		const content = attrs['content'];
+		if (!name || !content) continue;
+
+		if (name === 'theme-color') {
+			themeColor.push(attrs['media'] ? { media: attrs['media'], color: content } : { color: content });
+		} else if (name === 'color-scheme') {
+			if (validColorSchemes.includes(content as ColorSchemeEnum)) {
+				colorScheme = content as ColorSchemeEnum;
+			}
+		} else {
+			other[name] = content;
+		}
+	}
+
+	return { other, themeColor, colorScheme };
+}
+
 export async function generateMetadata(): Promise<Metadata> {
     const config = (await fetchConfig())?.data;
+    const metaAdds = parseMetaAdds(config?.meta?.adds);
 
     return {
         title: config?.basic?.siteName ?? 'bitforum',
@@ -31,8 +72,11 @@ export async function generateMetadata(): Promise<Metadata> {
         applicationName: config?.meta.applicationName,
         generator: config?.meta.generator,
         robots: config?.meta.robots,
+        ...(metaAdds.themeColor.length > 0 && { themeColor: metaAdds.themeColor }),
+        ...(metaAdds.colorScheme && { colorScheme: metaAdds.colorScheme }),
         other: {
             'naver-site-verification': '18dc0d1c5cb466a765e5f5093f94638ed6537d1c',
+            ...metaAdds.other,
         },
     };
 }
@@ -49,7 +93,7 @@ export default async function RootLayout({
     const config = (await fetchConfig())?.data;
 
     return (
-        <html lang="ko">
+        <html lang="ko" suppressHydrationWarning>
             <head>
                 <link rel="manifest" href="/manifest.json" />
                 <Script src="https://www.googletagmanager.com/gtag/js?id=G-DY6YFW4CTM" strategy="afterInteractive" />
@@ -63,15 +107,17 @@ export default async function RootLayout({
                 </Script>
             </head>
             <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
-                <SignalRProvider accessToken={accessToken} signalRCryptoUrl={signalRCryptoUrl} signalRChatUrl={signalRChatUrl}>
-                    <AuthProvider>
-                        <MemberProvider>
-                            <ConfigProvider initialConfig={config}>
-                                {children}
-                            </ConfigProvider>
-                        </MemberProvider>
-                    </AuthProvider>
-                </SignalRProvider>
+                <ThemeProvider>
+                    <SignalRProvider accessToken={accessToken} signalRCryptoUrl={signalRCryptoUrl} signalRChatUrl={signalRChatUrl}>
+                        <AuthProvider>
+                            <MemberProvider>
+                                <ConfigProvider initialConfig={config}>
+                                    {children}
+                                </ConfigProvider>
+                            </MemberProvider>
+                        </AuthProvider>
+                    </SignalRProvider>
+                </ThemeProvider>
             </body>
         </html>
     );

+ 21 - 21
app/styles/common.module.scss

@@ -35,8 +35,8 @@
     > .header {
         grid-area: header;
         height: 56px;
-        background: #f4f4f4;
-        border-bottom: 1px solid #dedede;
+        background: var(--bg-subtle);
+        border-bottom: 1px solid var(--border-default);
 
         // 햄버거 메뉴 (모바일 전용)
         .hamburger {
@@ -49,7 +49,7 @@
             background: transparent;
             cursor: pointer;
             font-size: 1.25rem;
-            color: #333;
+            color: var(--text-primary);
             flex-shrink: 0;
 
             &:hover {
@@ -109,7 +109,7 @@
             background: transparent;
             cursor: pointer;
             font-size: 1.25rem;
-            color: #333;
+            color: var(--text-primary);
             flex-shrink: 0;
 
             &:hover {
@@ -141,12 +141,12 @@
         grid-area: aside;
         overflow-y: auto;
         transition: transform 0.3s ease;
-        background: #fff;
+        background: var(--bg-page);
 
         @media (min-width: 1124px) {
             position: relative;
             transform: translateX(0);
-            border-right: 1px solid #dedede;
+            border-right: 1px solid var(--border-default);
         }
 
         // 모바일: 왼쪽에서 슬라이드
@@ -158,7 +158,7 @@
             height: calc(100vh - 56px);
             z-index: 1000;
             transform: translateX(-100%);
-            border-right: 1px solid #dedede;
+            border-right: 1px solid var(--border-default);
             box-shadow: none;
         }
     }
@@ -168,7 +168,7 @@
         > .aside {
             @media (max-width: 1125px) {
                 transform: translateX(0);
-                box-shadow: 4px 0 12px rgba(0, 0, 0, 0.15);
+                box-shadow: 4px 0 12px var(--shadow-color);
             }
         }
 
@@ -184,19 +184,19 @@
 		position: relative;
         grid-area: main;
         overflow-y: auto;
-        border-bottom: 1px solid #dedede;
+        border-bottom: 1px solid var(--border-default);
     }
 
     // 우측 사이드바 (채팅)
     > .chatAside {
         grid-area: chatAside;
         transition: transform 0.3s ease;
-        background: #fff;
+        background: var(--bg-page);
 
         @media (min-width: 1124px) {
             position: relative;
             transform: translateX(0);
-            border-left: 1px solid #dedede;
+            border-left: 1px solid var(--border-default);
         }
 
         // 모바일: 우측에서 슬라이드
@@ -216,7 +216,7 @@
         > .chatAside {
             @media (max-width: 1125px) {
                 transform: translateX(0);
-                box-shadow: -4px 0 12px rgba(0, 0, 0, 0.15);
+                box-shadow: -4px 0 12px var(--shadow-color);
             }
         }
 
@@ -237,7 +237,7 @@
             left: 0;
             right: 0;
             bottom: 0;
-            background: rgba(0, 0, 0, 0.4);
+            background: var(--overlay-color);
             z-index: 999;
         }
     }
@@ -295,8 +295,8 @@
             left: 0;
             right: 0;
             height: 56px;
-            background: #fff;
-            border-top: 1px solid #dedede;
+            background: var(--bg-page);
+            border-top: 1px solid var(--border-default);
             z-index: 1001;
             justify-content: space-around;
             align-items: center;
@@ -308,7 +308,7 @@
                 justify-content: center;
                 gap: 2px;
                 font-size: 0.625rem;
-                color: #888;
+                color: var(--text-muted);
                 text-decoration: none;
                 flex: 1;
                 height: 100%;
@@ -321,12 +321,12 @@
                 }
 
                 &:hover {
-                    color: #555;
+                    color: var(--text-secondary);
                 }
             }
 
             button.active, a.active {
-                color: #333;
+                color: var(--text-primary);
                 font-weight: 600;
             }
         }
@@ -347,12 +347,12 @@
 			border-radius: 3px;
 
 			&:hover {
-				background: #f0f0f0;
-				outline: 1px solid #dedede;
+				background: var(--bg-subtle);
+				outline: 1px solid var(--border-default);
 			}
 
 			&.active {
-				background: #f0f0f0;
+				background: var(--bg-subtle);
 				outline: 1px solid #b3b3b3;
 				pointer-events: none;
 			}

+ 2 - 2
app/styles/editor.scss

@@ -53,9 +53,9 @@
 	width: max-content;
 	max-width: 100%;
 	padding: 5px 40px;
-	border: 1px solid #ccc;
+	border: 1px solid var(--border-strong);
 	border-radius: 5px;
-	background: #f9f9f9;
+	background: var(--bg-elevated);
 	margin: 5px 0;
 	box-sizing: border-box;
 

+ 2 - 2
app/styles/emoji-picker.scss

@@ -2,11 +2,11 @@
 	position: relative;
 
 	> button {
-		color: #666;
+		color: var(--text-secondary);
 
 		&:hover, &:focus, &:active {
 			background-color: transparent;
-			color: #e47911;
+			color: var(--brand-orange);
 		}
 	}
 

+ 7 - 7
app/styles/quill.scss

@@ -3,10 +3,10 @@
         position: relative;
         display: block;
         margin: 8px 0;
-		border: 1px solid #eee;
+		border: 1px solid var(--border-default);
         border-radius: 8px;
         padding: 8px;
-        background: #fafafa;
+        background: var(--bg-elevated);
 		white-space: initial;
 		justify-self: start;
 		user-select: auto;
@@ -32,13 +32,13 @@
 			top: 100%;
 			left: 50%;
 			transform: translate(-50%, -50%);
-			background: #fff;
-			border: 1px solid #ccc;
+			background: var(--bg-page);
+			border: 1px solid var(--border-strong);
 			padding: 4px;
 			border-radius: 4px;
 
             button {
-                background: #eee;
+                background: var(--border-default);
                 border: none;
                 border-radius: 4px;
                 padding: 4px 8px;
@@ -52,11 +52,11 @@
 
 		&:hover {
 			cursor: grab;
-			outline: 1px solid #999;
+			outline: 1px solid var(--text-muted);
 		}
 
 		&.active {
-			border: 1px solid #4caf50;
+			border: 1px solid var(--color-success);
 
 			.file-card-actions {
 				display: flex;

+ 49 - 0
contexts/themeProvider.tsx

@@ -0,0 +1,49 @@
+'use client';
+
+import { createContext, useContext, useEffect, useState, useCallback } from 'react';
+
+type Theme = 'light' | 'dark';
+
+const ThemeContext = createContext<{
+	theme: Theme;
+	toggleTheme: () => void;
+}>({
+	theme: 'light',
+	toggleTheme: () => {},
+});
+
+export function useThemeContext() {
+	return useContext(ThemeContext);
+}
+
+export function ThemeProvider({ children }: { children: React.ReactNode }) {
+	const [theme, setTheme] = useState<Theme>('light');
+	const [mounted, setMounted] = useState(false);
+
+	useEffect(() => {
+		const stored = localStorage.getItem('bitforum-theme') as Theme | null;
+		const initial = stored ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
+		setTheme(initial);
+		document.documentElement.classList.toggle('dark', initial === 'dark');
+		setMounted(true);
+	}, []);
+
+	const toggleTheme = useCallback(() => {
+		setTheme((prev) => {
+			const next = prev === 'light' ? 'dark' : 'light';
+			localStorage.setItem('bitforum-theme', next);
+			document.documentElement.classList.toggle('dark', next === 'dark');
+			return next;
+		});
+	}, []);
+
+	if (!mounted) {
+		return <>{children}</>;
+	}
+
+	return (
+		<ThemeContext.Provider value={{ theme, toggleTheme }}>
+			{children}
+		</ThemeContext.Provider>
+	);
+}

+ 8 - 0
hooks/useTheme.ts

@@ -0,0 +1,8 @@
+'use client';
+
+import { useThemeContext } from '@/contexts/themeProvider';
+
+export default function useTheme() {
+	const { theme, toggleTheme } = useThemeContext();
+	return { theme, toggleTheme, isDark: theme === 'dark' };
+}

+ 1 - 0
lib/api/forum/board.ts

@@ -66,6 +66,7 @@ export async function fetchBoardPosts(params: BoardPostsRequest): Promise<Result
     queryParams.set('boardID', String(params.boardID));
     queryParams.set('page', String(params.page));
     queryParams.set('perPage', String(params.perPage));
+    queryParams.set('IsDeleted', "true");
 
     if (params.boardPrefixID) queryParams.set('boardPrefixID', String(params.boardPrefixID));
     if (params.sort !== undefined && params.sort !== null) queryParams.set('sort', String(params.sort));

Файловите разлики са ограничени, защото са твърде много
+ 336 - 336
package-lock.json


+ 1 - 1
package.json

@@ -23,6 +23,7 @@
         "@radix-ui/react-label": "^2.1.5",
         "@radix-ui/react-select": "^2.2.4",
         "@radix-ui/react-slot": "^1.1.1",
+        "@react-oauth/google": "^0.13.4",
         "axios": "^1.7.9",
         "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
@@ -33,7 +34,6 @@
         "next": "^15.3.0",
         "postcss-loader": "^4.3.0",
         "qrcode.react": "^4.2.0",
-        "quill": "^2.0.3",
         "raw-loader": "^4.0.2",
         "react": "^19.1.0",
         "react-dom": "^19.1.0",

+ 0 - 0
public/resources/b-logo.png → public/resources/no-image-2.png


Някои файлове не бяха показани, защото твърде много файлове са промени