kcg-monitoring/frontend/src/components/auth/LoginPage.tsx
htlee 1e8c0659e5 fix: S&P Global 사진 URL 목록 API 연동 + 로그인 DEMO 표기
- ShipLayer: IMO 기반 /signal-batch/api/v1/shipimg/{imo} API로 실제 이미지 목록 조회
- 각 이미지 path + _2.jpg(원본) 사용 (기존 잘못된 _1→_2→_3 번호 패턴 제거)
- IMO별 이미지 목록 캐시(spgImageCache) 적용
- LoginPage: KCG 로고 우측 하단에 DEMO 문구 오버레이

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

203 lines
5.6 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">
<div className="relative inline-block">
<img src="/kcg.svg" alt="KCG" style={{ width: 120, height: 120 }} />
<span
className="absolute font-black tracking-widest"
style={{
bottom: 2,
right: -8,
fontSize: 14,
color: 'var(--kcg-danger)',
opacity: 0.85,
textShadow: '0 0 4px rgba(0,0,0,0.6)',
}}
>
DEMO
</span>
</div>
<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;