- 로그인 화면: kcg.svg 로고 적용 (이모지 교체) - 헤더 우측: 사용자 프로필/이름 + 로그아웃 버튼 추가 - 브라우저 탭: favicon → kcg.svg, 제목 → kcg-dashboard-demo - 프로덕션 빌드: console/debugger 자동 제거 - CORS: CorsFilter 최우선 순위 등록 (AuthFilter 이전) - deploy.yml: secrets → .env 파일로 배포 - systemd/nginx: 경로 /devdata/services/kcg/ 반영
188 lines
5.2 KiB
TypeScript
188 lines
5.2 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;
|
|
|
|
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"
|
|
style={{ backgroundColor: 'var(--kcg-bg)' }}
|
|
>
|
|
<div
|
|
className="flex w-full max-w-sm flex-col items-center gap-6 rounded-xl border p-8"
|
|
style={{
|
|
backgroundColor: 'var(--kcg-card)',
|
|
borderColor: 'var(--kcg-border)',
|
|
}}
|
|
>
|
|
{/* Title */}
|
|
<div className="flex flex-col items-center gap-2">
|
|
<img src="/kcg.svg" alt="KCG" style={{ width: 120, height: 120 }} />
|
|
<h1
|
|
className="text-xl font-bold"
|
|
style={{ color: 'var(--kcg-text)' }}
|
|
>
|
|
{t('auth.title')}
|
|
</h1>
|
|
<p
|
|
className="text-sm"
|
|
style={{ color: 'var(--kcg-muted)' }}
|
|
>
|
|
{t('auth.subtitle')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div
|
|
className="w-full rounded-lg px-4 py-2 text-center text-sm"
|
|
style={{
|
|
backgroundColor: 'var(--kcg-danger-bg)',
|
|
color: 'var(--kcg-danger)',
|
|
}}
|
|
>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* Google Login Button */}
|
|
{GOOGLE_CLIENT_ID && (
|
|
<>
|
|
<div ref={googleBtnRef} />
|
|
<p
|
|
className="text-xs"
|
|
style={{ color: 'var(--kcg-dim)' }}
|
|
>
|
|
{t('auth.domainNotice')}
|
|
</p>
|
|
</>
|
|
)}
|
|
|
|
{/* Dev Login */}
|
|
{IS_DEV && (
|
|
<>
|
|
<div
|
|
className="w-full border-t pt-4 text-center"
|
|
style={{ borderColor: 'var(--kcg-border)' }}
|
|
>
|
|
<span
|
|
className="text-xs font-mono tracking-wider"
|
|
style={{ color: 'var(--kcg-dim)' }}
|
|
>
|
|
{t('auth.devNotice')}
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onDevLogin}
|
|
className="w-full cursor-pointer rounded-lg border-2 px-4 py-3 text-sm font-bold transition-colors"
|
|
style={{
|
|
borderColor: 'var(--kcg-danger)',
|
|
color: 'var(--kcg-danger)',
|
|
backgroundColor: 'transparent',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.backgroundColor = 'var(--kcg-danger-bg)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.backgroundColor = 'transparent';
|
|
}}
|
|
>
|
|
{t('auth.devLogin')}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LoginPage;
|