From 128596fc74604356ea165b00e8fafc98e8a594e5 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Wed, 2 Sep 2026 22:34:28 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20solution/frontend:=20=EA=B5=AC?= =?UTF-8?q?=EA=B8=80=20=EB=B2=84=ED=8A=BC=EC=9D=84=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B4=80=EB=AC=B8=EC=97=90=EB=8F=84=20=C2=B7=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=ED=95=9C=EA=B8=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "구글 로그인 버튼이 없다" 는 지적이 맞았다. 버튼을 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 통과. --- .../components/auth/GoogleSignInButton.tsx | 6 ++- .../frontend/src/features/auth/SignInForm.tsx | 38 ++++++++++++++++++- solution/frontend/src/lib/googleIdentity.ts | 4 +- solution/frontend/src/pages/LoginPage.tsx | 2 +- 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/solution/frontend/src/components/auth/GoogleSignInButton.tsx b/solution/frontend/src/components/auth/GoogleSignInButton.tsx index fd3d3aa..75b47d0 100644 --- a/solution/frontend/src/components/auth/GoogleSignInButton.tsx +++ b/solution/frontend/src/components/auth/GoogleSignInButton.tsx @@ -11,7 +11,7 @@ import {GOOGLE_CLIENT_ID, isGoogleLoginEnabled, loadGoogleIdentity} from '@/lib/ */ export function GoogleSignInButton({ onCredential, - text = 'continue_with', + text = 'signin_with', }: { onCredential: (credential: string) => void; text?: 'continue_with' | 'signin_with' | 'signup_with'; @@ -46,7 +46,9 @@ export function GoogleSignInButton({ size: 'large', shape: 'rectangular', text, - locale: 'ko', + // ★ 'ko' 로는 영문('Continue with Google')이 그대로 나왔다. 지역까지 줘야 한국어다. + // 문구는 우리가 못 정한다 — 구글 브랜드 가이드라 GIS 가 주는 번역을 그대로 쓴다. + locale: 'ko_KR', logo_alignment: 'center', // GIS 는 숫자 px 만 받는다(최대 400). 컨테이너 폭이 잡히기 전이면 최소값으로 그린다. width: holder.current.offsetWidth || 320, diff --git a/solution/frontend/src/features/auth/SignInForm.tsx b/solution/frontend/src/features/auth/SignInForm.tsx index ba5e622..0979015 100644 --- a/solution/frontend/src/features/auth/SignInForm.tsx +++ b/solution/frontend/src/features/auth/SignInForm.tsx @@ -6,10 +6,12 @@ */ import {useState, type FormEvent, type ReactNode} from 'react'; import {LogIn} from 'lucide-react'; -import {login} from '@/api'; +import {googleLogin, login} from '@/api'; +import {GoogleSignInButton} from '@/components/auth/GoogleSignInButton'; import {Button} from '@/components/ui/button'; import {Input} from '@/components/ui/input'; import {notifyApiError} from '@/lib/notify'; +import {isGoogleLoginEnabled} from '@/lib/googleIdentity'; import {establishSession} from '@/lib/session'; interface SignInFormProps { @@ -26,6 +28,27 @@ export function SignInForm({header, footer, submitLabel = '로그인', onSignedI const [password, setPassword] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + // 구글은 가입과 로그인이 같은 동작이다 — 처음 온 계정이면 백엔드가 그 자리에서 만든다. + const handleGoogle = async (credential: string) => { + setIsSubmitting(true); + try { + const res = await googleLogin({credential}); + if (res.result?.success === false) { + notifyApiError({data: res}, '구글 로그인에 실패했습니다.'); + return; + } + if (!(await establishSession(res, ''))) { + notifyApiError({data: res}, '로그인 응답에 토큰이 없습니다.'); + return; + } + onSignedIn?.(); + } catch (error) { + notifyApiError(error, '구글 로그인에 실패했습니다.'); + } finally { + setIsSubmitting(false); + } + }; + const handleSubmit = async (event: FormEvent) => { event.preventDefault(); setIsSubmitting(true); @@ -89,6 +112,19 @@ export function SignInForm({header, footer, submitLabel = '로그인', onSignedI {submitLabel} + {/* ★ 로그인 화면이 여기 하나만 있는 게 아니다 — 에디터 관문·2단계도 이 폼을 쓴다. + 구글 버튼을 LoginPage 에만 붙여 두면 정작 사장님이 만나는 자리엔 없다. */} + {isGoogleLoginEnabled() && ( + <> +
+ + 또는 + +
+ + + )} + {footer} ); diff --git a/solution/frontend/src/lib/googleIdentity.ts b/solution/frontend/src/lib/googleIdentity.ts index 4199b42..4660b90 100644 --- a/solution/frontend/src/lib/googleIdentity.ts +++ b/solution/frontend/src/lib/googleIdentity.ts @@ -8,7 +8,9 @@ * ★ client_id 는 비밀이 아니다(번들에 그대로 들어간다). 이 값으로 할 수 있는 건 "우리 앱 앞으로" * 토큰을 받는 것뿐이고, 그 토큰이 우리 계정이 되려면 백엔드의 aud 대조를 통과해야 한다. */ -const SCRIPT_URL = 'https://accounts.google.com/gsi/client'; +// ★ 언어는 **스크립트 URL 의 hl** 로 잡는다. renderButton 의 locale 옵션은 안 먹었다 +// (locale:'ko'·'ko_KR' 둘 다 'Continue with Google' 이 그대로 나왔다 — 실측). +const SCRIPT_URL = 'https://accounts.google.com/gsi/client?hl=ko'; const SCRIPT_ID = 'google-identity-services'; export const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID ?? ''; diff --git a/solution/frontend/src/pages/LoginPage.tsx b/solution/frontend/src/pages/LoginPage.tsx index 1ecc207..ed04e24 100644 --- a/solution/frontend/src/pages/LoginPage.tsx +++ b/solution/frontend/src/pages/LoginPage.tsx @@ -135,7 +135,7 @@ export function LoginPage({selfServe = true}: {selfServe?: boolean}) { 또는 - + )}