[chore] solution/frontend: LoginPage 원복 — 로그인 화면은 건드릴 이유가 없었다
에디터 관문을 만들면서 로그인 화면까지 공용 폼으로 갈아엎었다. 요청 밖이라 되돌린다. 관문은 features/auth/SignInForm 을 그대로 쓴다.
This commit is contained in:
parent
969fb6773c
commit
94a083c895
@ -1,23 +1,65 @@
|
||||
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 다.
|
||||
const from = (location.state as {from?: string} | null)?.from ?? '/';
|
||||
// 각 라우터의 '/' 리다이렉트가 이미 그걸 안다. 여기 경로를 박으면 한쪽에서 404 다.
|
||||
const from = (location.state as {from?: string} | null)?.from ?? '/';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user