"구글 로그인 버튼이 없다" 는 지적이 맞았다. 버튼을 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 통과.
132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
/**
|
|
* 로그인 폼 한 벌.
|
|
*
|
|
* ★ 로그인 화면과 에디터 진입 관문이 같은 폼을 쓴다. 두 벌로 두면 토큰을 심는 순서
|
|
* (signIn → me)가 한쪽에서만 지켜지고, 그 실수는 "로그인은 됐는데 계속 401" 로 나타난다.
|
|
*/
|
|
import {useState, type FormEvent, type ReactNode} from 'react';
|
|
import {LogIn} from 'lucide-react';
|
|
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 {
|
|
/** 폼 위에 붙는 제목·설명. 화면마다 하는 말이 다르다. */
|
|
header: ReactNode;
|
|
/** 폼 아래 각주(빌더로 돌아가기 등). */
|
|
footer?: ReactNode;
|
|
submitLabel?: string;
|
|
onSignedIn?: () => void;
|
|
}
|
|
|
|
export function SignInForm({header, footer, submitLabel = '로그인', onSignedIn}: SignInFormProps) {
|
|
const [id, setId] = useState('');
|
|
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);
|
|
try {
|
|
const res = await login({id, password});
|
|
if (res.result?.success === false) {
|
|
notifyApiError({data: res}, '아이디 또는 비밀번호를 확인해 주세요.');
|
|
return;
|
|
}
|
|
// 토큰 심는 순서(signIn → me)는 lib/session 한 곳에만 둔다 — 이 파일 맨 위 주석이
|
|
// 경고하던 그 중복이다. 로그인 화면·가입 화면·자동 로그인이 전부 같은 함수를 쓴다.
|
|
if (!(await establishSession(res, id))) {
|
|
notifyApiError({data: res}, '로그인 응답에 토큰이 없습니다.');
|
|
return;
|
|
}
|
|
onSignedIn?.();
|
|
} catch (error) {
|
|
notifyApiError(error, '로그인에 실패했습니다.');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="w-full max-w-sm space-y-4 rounded-2xl border border-border bg-card p-7"
|
|
>
|
|
{header}
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label htmlFor="login-id" className="mb-1.5 block text-xs font-semibold">
|
|
아이디
|
|
</label>
|
|
<Input
|
|
id="login-id"
|
|
value={id}
|
|
onChange={(e) => setId(e.target.value)}
|
|
autoComplete="username"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="login-pw" className="mb-1.5 block text-xs font-semibold">
|
|
비밀번호
|
|
</label>
|
|
<Input
|
|
id="login-pw"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
autoComplete="current-password"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button type="submit" variant="primary" className="w-full" isLoading={isSubmitting}>
|
|
<LogIn />
|
|
<span>{submitLabel}</span>
|
|
</Button>
|
|
|
|
{/* ★ 로그인 화면이 여기 하나만 있는 게 아니다 — 에디터 관문·2단계도 이 폼을 쓴다.
|
|
구글 버튼을 LoginPage 에만 붙여 두면 정작 사장님이 만나는 자리엔 없다. */}
|
|
{isGoogleLoginEnabled() && (
|
|
<>
|
|
<div className="flex items-center gap-2">
|
|
<span className="h-px flex-1 bg-border" />
|
|
<span className="text-[11px] text-muted-foreground">또는</span>
|
|
<span className="h-px flex-1 bg-border" />
|
|
</div>
|
|
<GoogleSignInButton onCredential={handleGoogle} text="signin_with" />
|
|
</>
|
|
)}
|
|
|
|
{footer}
|
|
</form>
|
|
);
|
|
}
|