o2o-site-AEO/solution/frontend/src/components/auth/GoogleSignInButton.tsx
Mina Choi 128596fc74 [feat] solution/frontend: 구글 버튼을 로그인 관문에도 · 문구 한글화
"구글 로그인 버튼이 없다" 는 지적이 맞았다. 버튼을 LoginPage·SignupPage 에만 붙여 뒀는데,
이 앱에서 사장님이 실제로 로그인 화면을 만나는 자리는 **에디터 진입 관문**(EditorSignInGate →
SignInForm)이다. 정작 거기엔 없었다.

- features/auth/SignInForm: 구글 버튼 추가. 관문·로그인 화면이 같은 폼을 쓰므로 한 곳만 고치면 된다
- lib/googleIdentity: 스크립트를 ?hl=ko 로 받는다. renderButton 의 locale 옵션은 안 먹었다 —
  'ko'·'ko_KR' 둘 다 'Continue with Google' 이 그대로 나왔다(실측)
- 버튼 문구는 signin_with('Google 계정으로 로그인'). 가입 화면만 signup_with 로 둔다.
  문구 자체는 고를 수 없다 — 구글 브랜드 가이드라 GIS 가 주는 번역을 그대로 쓴다

브라우저 확인(localhost:80): 로그인 화면에 'Google 계정으로 로그인' 한글 노출.
tsc·eslint·vite build 통과.
2026-09-02 22:34:28 +09:00

79 lines
3.0 KiB
TypeScript

import {useEffect, useRef, useState} from 'react';
import {GOOGLE_CLIENT_ID, isGoogleLoginEnabled, loadGoogleIdentity} from '@/lib/googleIdentity';
/**
* 구글이 그려 주는 버튼을 그대로 쓴다.
*
* ★ 우리가 직접 만든 버튼을 쓰지 않는 이유: 구글 브랜드 가이드가 로고·문구·비율을 정해 두고,
* 그걸 어긋나게 그리면 심사에서 걸린다. renderButton 이 그 규격을 지켜 준다.
* ★ 스크립트가 안 오면(광고 차단기·사내망) 빈 칸이 남는다 — 안내 문구로 바꿔 준다.
* "눌러도 아무 일이 없는 자리"가 화면에 남는 게 제일 나쁘다.
*/
export function GoogleSignInButton({
onCredential,
text = 'signin_with',
}: {
onCredential: (credential: string) => void;
text?: 'continue_with' | 'signin_with' | 'signup_with';
}) {
const holder = useRef<HTMLDivElement>(null);
const [failed, setFailed] = useState(false);
// 콜백은 ref 로 들고 간다 — GIS 는 initialize 시점의 함수를 붙잡고 있어서,
// 의존성에 넣으면 렌더마다 버튼을 다시 그리게 된다.
const handler = useRef(onCredential);
handler.current = onCredential;
useEffect(() => {
if (!isGoogleLoginEnabled()) return;
let alive = true;
loadGoogleIdentity()
.then((api) => {
if (!alive || !holder.current) return;
api.initialize({
client_id: GOOGLE_CLIENT_ID,
callback: (res) => {
if (res.credential) handler.current(res.credential);
},
// 화면을 열자마자 마지막 계정으로 자동 로그인되는 동작은 끈다 —
// 계정이 여러 개인 사람이 원하지 않은 계정으로 들어간다.
auto_select: false,
});
api.renderButton(holder.current, {
type: 'standard',
theme: 'outline',
size: 'large',
shape: 'rectangular',
text,
// ★ 'ko' 로는 영문('Continue with Google')이 그대로 나왔다. 지역까지 줘야 한국어다.
// 문구는 우리가 못 정한다 — 구글 브랜드 가이드라 GIS 가 주는 번역을 그대로 쓴다.
locale: 'ko_KR',
logo_alignment: 'center',
// GIS 는 숫자 px 만 받는다(최대 400). 컨테이너 폭이 잡히기 전이면 최소값으로 그린다.
width: holder.current.offsetWidth || 320,
});
})
.catch(() => {
if (alive) setFailed(true);
});
return () => {
alive = false;
};
}, [text]);
if (!isGoogleLoginEnabled()) return null;
if (failed) {
return (
<p className="text-center text-[11px] leading-relaxed text-muted-foreground">
구글 로그인을 불러오지 못했습니다. 아이디/비밀번호로 로그인해 주세요.
</p>
);
}
// 높이를 미리 잡아 둔다 — 버튼이 뒤늦게 그려지면서 아래 내용이 밀리지 않게.
return <div ref={holder} className="flex h-10 w-full items-center justify-center" />;
}