[chore] solution/frontend: LoginPage 원복 — 로그인 화면은 건드릴 이유가 없었다

에디터 관문을 만들면서 로그인 화면까지 공용 폼으로 갈아엎었다. 요청 밖이라 되돌린다.
관문은 features/auth/SignInForm 을 그대로 쓴다.
This commit is contained in:
Mina Choi 2026-09-02 09:05:39 +09:00
parent 969fb6773c
commit 94a083c895

View File

@ -1,11 +1,21 @@
import {useState, type FormEvent} from 'react';
import {Navigate, useLocation, useNavigate} from 'react-router';
import {SignInForm} from '@/features/auth/SignInForm';
import {useAuthStore} from '@/stores/auth';
import {LogIn} from 'lucide-react';
import {login, me, UserRole} from '@/api';
import {Button} from '@/components/ui/button';
import {Input} from '@/components/ui/input';
import {notifyApiError} from '@/lib/notify';
import {toAuthUser, useAuthStore} from '@/stores/auth';
export function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
const user = useAuthStore((s) => s.user);
const signIn = useAuthStore((s) => s.signIn);
const [id, setId] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// ★ 기본 도착지를 '/' 로 둔다. 앱마다 홈이 다르고(사장님 → 빌더, 내부 → 사업장 목록)
// 각 라우터의 '/' 리다이렉트가 이미 그걸 안다. 여기 경로를 박으면 한쪽에서 404 다.
@ -13,11 +23,43 @@ export function LoginPage() {
if (user) return <Navigate to={from} replace />;
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;
}
// ★ RemoveNoneResponse 라 성공 응답에서도 토큰 필드가 빠져 올 수 있다.
// 빈 토큰으로 로그인 상태를 만들면 이후 모든 요청이 401 로 흐른다 — 여기서 끊는다.
if (!res.access_token || !res.refresh_token) {
notifyApiError({data: res}, '로그인 응답에 토큰이 없습니다.');
return;
}
const tokens = {accessToken: res.access_token, refreshToken: res.refresh_token};
// 토큰을 먼저 심어야 뒤이은 me() 가 Authorization 을 달고 나간다.
signIn(tokens, {userId: '', id, role: UserRole.USER});
const meRes = await me();
// 신원이 안 왔으면 방금 심은 임시 사용자를 그대로 둔다 — 토큰은 유효하므로 화면은 진행시킨다.
if (meRes.user_id && meRes.id) signIn(tokens, toAuthUser(meRes));
navigate(from, {replace: true});
} catch (error) {
notifyApiError(error, '로그인에 실패했습니다.');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="flex min-h-screen items-center justify-center bg-muted/30 px-4">
<SignInForm
onSignedIn={() => navigate(from, {replace: true})}
header={
<form
onSubmit={handleSubmit}
className="w-full max-w-sm space-y-4 rounded-2xl border border-border bg-card p-7"
>
<div className="space-y-1 text-center">
<img
src="/brand/web4ai-wordmark.svg"
@ -25,18 +67,51 @@ export function LoginPage() {
className="mx-auto mb-3 h-9 w-auto"
/>
<h1 className="text-sm font-medium text-muted-foreground"></h1>
<p className="text-xs text-muted-foreground">· · .</p>
<p className="text-xs text-muted-foreground">
· · .
</p>
</div>
}
footer={
<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></span>
</Button>
<p className="text-center text-[11px] leading-relaxed text-muted-foreground">
. .{' '}
.{' '}
<a href="/builder" className="font-medium text-primary underline-offset-2 hover:underline">
</a>
</p>
}
/>
</form>
</div>
);
}