kcg-monitoring/frontend/src/components/auth/LoginPage.tsx
htlee 2ca6371d87 feat: LoginPage DEV_LOGIN 환경변수 지원 추가
VITE_ENABLE_DEV_LOGIN=true로 프로덕션 빌드에서도 DEV LOGIN 활성화 가능.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:21:03 +09:00

162 lines
4.7 KiB
TypeScript

import { useEffect, useRef, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
interface LoginPageProps {
onGoogleLogin: (credential: string) => Promise<void>;
onDevLogin: () => void;
}
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const IS_DEV = import.meta.env.DEV;
const DEV_LOGIN_ENABLED = IS_DEV || import.meta.env.VITE_ENABLE_DEV_LOGIN === 'true';
function useGoogleIdentity(onCredential: (credential: string) => void) {
const btnRef = useRef<HTMLDivElement>(null);
const callbackRef = useRef(onCredential);
callbackRef.current = onCredential;
useEffect(() => {
if (!GOOGLE_CLIENT_ID) return;
const scriptId = 'google-gsi-script';
let script = document.getElementById(scriptId) as HTMLScriptElement | null;
const initGoogle = () => {
const google = (window as unknown as Record<string, unknown>).google as {
accounts: {
id: {
initialize: (config: {
client_id: string;
callback: (response: { credential: string }) => void;
}) => void;
renderButton: (
el: HTMLElement,
config: {
theme: string;
size: string;
width: number;
text: string;
},
) => void;
};
};
} | undefined;
if (!google?.accounts?.id || !btnRef.current) return;
google.accounts.id.initialize({
client_id: GOOGLE_CLIENT_ID,
callback: (response: { credential: string }) => {
callbackRef.current(response.credential);
},
});
google.accounts.id.renderButton(btnRef.current, {
theme: 'outline',
size: 'large',
width: 300,
text: 'signin_with',
});
};
if (!script) {
script = document.createElement('script');
script.id = scriptId;
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
script.defer = true;
script.onload = initGoogle;
document.head.appendChild(script);
} else {
initGoogle();
}
}, []);
return btnRef;
}
const LoginPage = ({ onGoogleLogin, onDevLogin }: LoginPageProps) => {
const { t } = useTranslation();
const [error, setError] = useState<string | null>(null);
const handleGoogleCredential = useCallback(
(credential: string) => {
setError(null);
onGoogleLogin(credential).catch(() => {
setError(t('auth.loginFailed'));
});
},
[onGoogleLogin, t],
);
const googleBtnRef = useGoogleIdentity(handleGoogleCredential);
return (
<div className="flex min-h-screen items-center justify-center bg-kcg-bg">
<div className="flex w-full max-w-sm flex-col items-center gap-6 rounded-xl border border-kcg-border bg-kcg-card p-8">
{/* Title */}
<div className="flex flex-col items-center gap-2">
<div className="relative inline-block">
<img src="/kcg.svg" alt="KCG" style={{ width: 120, height: 120 }} />
<span
className="absolute font-black tracking-widest text-kcg-danger"
style={{
bottom: 2,
right: -8,
fontSize: 14,
opacity: 0.85,
textShadow: '0 0 4px rgba(0,0,0,0.6)',
}}
>
DEMO
</span>
</div>
<h1 className="text-xl font-bold text-kcg-text">
{t('auth.title')}
</h1>
<p className="text-sm text-kcg-muted">
{t('auth.subtitle')}
</p>
</div>
{/* Error */}
{error && (
<div className="w-full rounded-lg bg-kcg-danger-bg px-4 py-2 text-center text-sm text-kcg-danger">
{error}
</div>
)}
{/* Google Login Button */}
{GOOGLE_CLIENT_ID && (
<>
<div ref={googleBtnRef} />
<p className="text-xs text-kcg-dim">
{t('auth.domainNotice')}
</p>
</>
)}
{/* Dev Login */}
{DEV_LOGIN_ENABLED && (
<>
<div className="w-full border-t border-kcg-border pt-4 text-center">
<span className="text-xs font-mono tracking-wider text-kcg-dim">
{t('auth.devNotice')}
</span>
</div>
<button
type="button"
onClick={onDevLogin}
className="w-full cursor-pointer rounded-lg border-2 border-kcg-danger bg-transparent px-4 py-3 text-sm font-bold text-kcg-danger transition-colors hover:bg-kcg-danger-bg"
>
{t('auth.devLogin')}
</button>
</>
)}
</div>
</div>
);
};
export default LoginPage;