diff --git a/solution/frontend/src/pages/LoginPage.tsx b/solution/frontend/src/pages/LoginPage.tsx index 4ff1a0f..88a4183 100644 --- a/solution/frontend/src/pages/LoginPage.tsx +++ b/solution/frontend/src/pages/LoginPage.tsx @@ -1,42 +1,117 @@ +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 ; + 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 (
- navigate(from, {replace: true})} - header={ -
- Web4Ai -

관리자

-

사업장·정보 확인·발행을 관리합니다.

-
- } - footer={ -

- 사이트는 로그인 없이 만들어 볼 수 있습니다. 로그인은 편집 화면에서 한 번만 필요합니다.{' '} - - 빌더로 이동 - +

+
+ Web4Ai +

관리자

+

+ 사업장·정보 확인·발행을 관리합니다.

- } - /> +
+ +
+
+ + setId(e.target.value)} + autoComplete="username" + required + /> +
+
+ + setPassword(e.target.value)} + autoComplete="current-password" + required + /> +
+
+ + + +

+ 빌더는 로그인 없이도 사용할 수 있습니다.{' '} + + 빌더로 이동 + +

+
); }