VITE_ENABLE_DEV_LOGIN=true로 프로덕션 빌드에서도 DEV LOGIN 활성화 가능. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
162 lines
4.7 KiB
TypeScript
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;
|