From 9d70d431b2350e2a3ae292848476ea51db421251 Mon Sep 17 00:00:00 2001 From: jjunier Date: Tue, 12 May 2026 15:34:38 +0900 Subject: [PATCH] feat(frontend): implement login user state flow [D2C-36] - add login request type and login API client - implement login form with validation and submission feedback - persist logged-in user in localStorage after successful login - redirect authenticated users away from the login page - reuse auth layout and header state synchronization from signup flow --- apps/web/src/features/auth/LoginPage.tsx | 140 ++++++++++++++++++++++- apps/web/src/services/authApi.ts | 9 +- apps/web/src/styles/global.css | 5 + apps/web/src/types/auth.ts | 5 + 4 files changed, 155 insertions(+), 4 deletions(-) diff --git a/apps/web/src/features/auth/LoginPage.tsx b/apps/web/src/features/auth/LoginPage.tsx index 8d3a8f7..c9beffc 100644 --- a/apps/web/src/features/auth/LoginPage.tsx +++ b/apps/web/src/features/auth/LoginPage.tsx @@ -1,8 +1,142 @@ +import { type FormEvent, useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { login } from "../../services/authApi"; +import { getStoredUser, setStoredUser } from "../../stores/userStore"; + +type LoginFormState = { + email: string; + password: string; +}; + +const initialFormState: LoginFormState = { + email: "", + password: "", +}; + +function validateLoginForm(form: LoginFormState) { + if (!form.email.trim()) { + return "이메일을 입력해주세요."; + } + + if (!form.email.includes("@")) { + return "올바른 이메일 형식으로 입력해주세요."; + } + + if (!form.password.trim()) { + return "비밀번호를 입력해주세요."; + } + + return null; +} + export function LoginPage() { + const navigate = useNavigate(); + + const [form, setForm] = useState(initialFormState); + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + const storedUser = getStoredUser(); + + if (storedUser) { + navigate("/products", { replace: true }); + } + }, [navigate]); + + const handleInputChange = (field: keyof LoginFormState, value: string) => { + setForm((currentForm) => ({ + ...currentForm, + [field]: value, + })); + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + + const validationMessage = validateLoginForm(form); + + if (validationMessage) { + setErrorMessage(validationMessage); + setSuccessMessage(null); + return; + } + + try { + setIsSubmitting(true); + setErrorMessage(null); + setSuccessMessage(null); + + const loggedInUser = await login({ + email: form.email.trim(), + password: form.password, + }); + + setStoredUser(loggedInUser); + setSuccessMessage("로그인이 완료되었습니다. 상품 목록으로 이동합니다."); + + setTimeout(() => { + navigate("/products"); + }, 600); + } catch { + setErrorMessage("로그인에 실패했습니다. 이메일 또는 비밀번호를 확인해주세요."); + } finally { + setIsSubmitting(false); + } + }; + return ( -
-

로그인

-

D2C-36에서 로그인 폼과 사용자 상태 저장 흐름을 구현합니다.

+
+
+
+

Sign In

+

로그인

+

+ 기존 계정으로 로그인하여 장바구니, 주문, 리뷰 흐름을 이어서 검증합니다. +

+
+ +
+ + + + + {errorMessage &&
{errorMessage}
} + {successMessage &&
{successMessage}
} + + +
+ +
+ 아직 계정이 없으신가요? + 회원가입 +
+
); } \ No newline at end of file diff --git a/apps/web/src/services/authApi.ts b/apps/web/src/services/authApi.ts index 0f66a2b..3022f27 100644 --- a/apps/web/src/services/authApi.ts +++ b/apps/web/src/services/authApi.ts @@ -1,9 +1,16 @@ import { apiClient } from "./apiClient"; -import type { AuthUser, SignupRequest } from "../types/auth"; +import type { AuthUser, LoginRequest, SignupRequest } from "../types/auth"; export function signup(payload: SignupRequest) { return apiClient("/auth/signup", { method: "POST", body: payload, }); +} + +export function login(payload: LoginRequest) { + return apiClient("/auth/login", { + method: "POST", + body: payload, + }); } \ No newline at end of file diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index 327f9c9..075a1cd 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -812,6 +812,11 @@ textarea { outline-offset: 2px; } +.form-field input:-webkit-autofill { + box-shadow: 0 0 0 1000px #ffffff inset; + -webkit-text-fill-color: #111827; +} + .checkbox-field { display: flex; align-items: center; diff --git a/apps/web/src/types/auth.ts b/apps/web/src/types/auth.ts index a009ed4..78bd2a4 100644 --- a/apps/web/src/types/auth.ts +++ b/apps/web/src/types/auth.ts @@ -5,6 +5,11 @@ export type SignupRequest = { marketing_opt_in_yn: boolean; }; +export type LoginRequest = { + email: string; + password: string; +} + export type AuthUser = { user_id: string; email: string;