feat: 인증 페이지 API 연동 (회원가입/로그인/이메일 인증)
All checks were successful
SPMS_BO/pipeline/head This commit looks good
All checks were successful
SPMS_BO/pipeline/head This commit looks good
Reviewed-on: https://git.ipstein.myds.me/SPMS/SPMS_WEB/pulls/8
This commit is contained in:
commit
1e8d33102e
|
|
@ -1,2 +1,2 @@
|
|||
VITE_API_BASE_URL=http://localhost:8080/api
|
||||
VITE_API_BASE_URL=
|
||||
VITE_APP_TITLE=SPMS
|
||||
|
|
|
|||
|
|
@ -1,39 +1,58 @@
|
|||
import { apiClient } from "./client";
|
||||
import type { ApiResponse } from "@/types/api";
|
||||
import type { User } from "@/types/user";
|
||||
import type {
|
||||
SignupRequest,
|
||||
SignupResponse,
|
||||
EmailCheckRequest,
|
||||
EmailCheckResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
EmailVerifyRequest,
|
||||
EmailVerifyResponse,
|
||||
EmailResendRequest,
|
||||
EmailResendResponse,
|
||||
TokenRefreshRequest,
|
||||
TokenRefreshResponse,
|
||||
LogoutResponse,
|
||||
TempPasswordRequest,
|
||||
} from "@/features/auth/types";
|
||||
|
||||
interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
/** 회원가입 */
|
||||
export function signup(data: SignupRequest) {
|
||||
return apiClient.post<ApiResponse<SignupResponse>>("/v1/in/auth/signup", data);
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
accessToken: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface SignupRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
/** 이메일 중복 체크 */
|
||||
export function checkEmail(data: EmailCheckRequest) {
|
||||
return apiClient.post<ApiResponse<EmailCheckResponse>>("/v1/in/auth/email/check", data);
|
||||
}
|
||||
|
||||
/** 로그인 */
|
||||
export function login(data: LoginRequest) {
|
||||
return apiClient.post<ApiResponse<LoginResponse>>("/auth/login", data);
|
||||
}
|
||||
|
||||
/** 회원가입 */
|
||||
export function signup(data: SignupRequest) {
|
||||
return apiClient.post<ApiResponse<{ message: string }>>("/auth/signup", data);
|
||||
return apiClient.post<ApiResponse<LoginResponse>>("/v1/in/auth/login", data);
|
||||
}
|
||||
|
||||
/** 이메일 인증 */
|
||||
export function verifyEmail(token: string) {
|
||||
return apiClient.post<ApiResponse<{ message: string }>>("/auth/verify-email", { token });
|
||||
export function verifyEmail(data: EmailVerifyRequest) {
|
||||
return apiClient.post<ApiResponse<EmailVerifyResponse>>("/v1/in/auth/email/verify", data);
|
||||
}
|
||||
|
||||
/** 인증코드 재전송 */
|
||||
export function resendVerifyEmail(data: EmailResendRequest) {
|
||||
return apiClient.post<ApiResponse<EmailResendResponse>>("/v1/in/auth/email/verify/resend", data);
|
||||
}
|
||||
|
||||
/** 토큰 갱신 */
|
||||
export function refreshToken(data: TokenRefreshRequest) {
|
||||
return apiClient.post<ApiResponse<TokenRefreshResponse>>("/v1/in/auth/token/refresh", data);
|
||||
}
|
||||
|
||||
/** 로그아웃 */
|
||||
export function logout() {
|
||||
return apiClient.post<ApiResponse<null>>("/auth/logout");
|
||||
return apiClient.post<ApiResponse<LogoutResponse>>("/v1/in/auth/logout");
|
||||
}
|
||||
|
||||
/** 임시 비밀번호 발급 */
|
||||
export function requestTempPassword(data: TempPasswordRequest) {
|
||||
return apiClient.post<ApiResponse<null>>("/v1/in/account/password/temp", data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import axios from "axios";
|
|||
|
||||
/** Axios 인스턴스 */
|
||||
export const apiClient = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL ?? "/api",
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL ?? "",
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { useForm } from "react-hook-form";
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { AxiosError } from "axios";
|
||||
import { login } from "@/api/auth.api";
|
||||
import type { ApiError } from "@/types/api";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import ResetPasswordModal from "../components/ResetPasswordModal";
|
||||
|
||||
|
|
@ -19,7 +22,7 @@ type LoginForm = z.infer<typeof loginSchema>;
|
|||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const setUser = useAuthStore((s) => s.setUser);
|
||||
const setAuth = useAuthStore((s) => s.setAuth);
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [resetOpen, setResetOpen] = useState(false);
|
||||
|
|
@ -47,44 +50,72 @@ export default function LoginPage() {
|
|||
setLoginError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
// TODO: 실제 API 연동 시 교체
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
|
||||
/*
|
||||
* 테스트 계정
|
||||
* - admin@spms.com / 1234 → 이메일 인증 완료 → 홈 이동
|
||||
* - test@spms.com / 1234 → 이메일 미인증 → 인증 페이지 이동
|
||||
*/
|
||||
if (
|
||||
(data.email === "admin@spms.com" || data.email === "test@spms.com") &&
|
||||
data.password === "1234"
|
||||
) {
|
||||
const emailVerified = data.email === "admin@spms.com";
|
||||
try {
|
||||
const res = await login({ email: data.email, password: data.password });
|
||||
const d = res.data.data;
|
||||
|
||||
setIsLoading(false);
|
||||
setLoginSuccess(true);
|
||||
|
||||
if (emailVerified) {
|
||||
/* 이메일 인증 완료 → 인증 상태 저장 후 홈 이동 */
|
||||
setUser({
|
||||
id: 1,
|
||||
email: data.email,
|
||||
name: "관리자",
|
||||
role: "ADMIN",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
setTimeout(() => navigate("/", { replace: true }), 1000);
|
||||
} else {
|
||||
/* 이메일 미인증 → 인증 페이지로 바로 이동 (인증 상태 저장 안 함) */
|
||||
setTimeout(() => navigate("/verify-email", { replace: true }), 1000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (d.next_action) {
|
||||
case "GO_DASHBOARD":
|
||||
/* 토큰 & 유저 정보 저장 → 홈 이동 */
|
||||
setAuth(
|
||||
{
|
||||
adminCode: d.admin.admin_code ?? "",
|
||||
email: d.admin.email ?? "",
|
||||
name: d.admin.name ?? "",
|
||||
role: d.admin.role ?? "",
|
||||
},
|
||||
d.access_token ?? "",
|
||||
d.refresh_token ?? "",
|
||||
);
|
||||
setTimeout(() => navigate("/", { replace: true }), 1000);
|
||||
break;
|
||||
|
||||
setIsLoading(false);
|
||||
setLoginError("이메일 또는 비밀번호가 올바르지 않습니다.");
|
||||
triggerShake(["email", "password"]);
|
||||
case "VERIFY_EMAIL":
|
||||
/* 이메일 미인증 → 인증 페이지 이동 */
|
||||
setTimeout(
|
||||
() =>
|
||||
navigate("/verify-email", {
|
||||
replace: true,
|
||||
state: {
|
||||
verifySessionId: d.verify_session_id,
|
||||
accessToken: d.access_token,
|
||||
refreshToken: d.refresh_token,
|
||||
admin: d.admin,
|
||||
},
|
||||
}),
|
||||
1000,
|
||||
);
|
||||
break;
|
||||
|
||||
case "CHANGE_PASSWORD":
|
||||
/* 비밀번호 변경 필요 → 토큰 저장 후 비밀번호 변경으로 이동 */
|
||||
setAuth(
|
||||
{
|
||||
adminCode: d.admin.admin_code ?? "",
|
||||
email: d.admin.email ?? "",
|
||||
name: d.admin.name ?? "",
|
||||
role: d.admin.role ?? "",
|
||||
},
|
||||
d.access_token ?? "",
|
||||
d.refresh_token ?? "",
|
||||
);
|
||||
setTimeout(() => navigate("/profile/edit", { replace: true }), 1000);
|
||||
break;
|
||||
|
||||
default:
|
||||
setTimeout(() => navigate("/", { replace: true }), 1000);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
const axiosErr = err as AxiosError<ApiError>;
|
||||
const msg = axiosErr.response?.data?.msg;
|
||||
setLoginError(msg ?? "이메일 또는 비밀번호가 올바르지 않습니다.");
|
||||
triggerShake(["email", "password"]);
|
||||
}
|
||||
};
|
||||
|
||||
/* 유효성 실패 → shake */
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { useForm } from "react-hook-form";
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { AxiosError } from "axios";
|
||||
import { signup } from "@/api/auth.api";
|
||||
import type { ApiError } from "@/types/api";
|
||||
|
||||
/* ───── 정규식 ───── */
|
||||
const pwRegex =
|
||||
|
|
@ -45,12 +48,15 @@ export default function SignupPage() {
|
|||
const [emailSentModal, setEmailSentModal] = useState(false);
|
||||
const [termsModal, setTermsModal] = useState(false);
|
||||
const [privacyModal, setPrivacyModal] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
setValue,
|
||||
setError,
|
||||
formState: { errors },
|
||||
} = useForm<SignupForm>({
|
||||
resolver: zodResolver(signupSchema),
|
||||
|
|
@ -86,10 +92,34 @@ export default function SignupPage() {
|
|||
setValue("phone", v, { shouldValidate: true });
|
||||
};
|
||||
|
||||
/* 가입 성공 */
|
||||
const onSubmit = (_data: SignupForm) => {
|
||||
// TODO: API 연동
|
||||
setEmailSentModal(true);
|
||||
/* 가입 요청 */
|
||||
const onSubmit = async (data: SignupForm) => {
|
||||
setIsLoading(true);
|
||||
setServerError(null);
|
||||
try {
|
||||
await signup({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
agreeTerms: data.agree,
|
||||
agreePrivacy: data.agree,
|
||||
});
|
||||
setEmailSentModal(true);
|
||||
} catch (err) {
|
||||
const axiosErr = err as AxiosError<ApiError>;
|
||||
const code = axiosErr.response?.data?.code;
|
||||
const msg = axiosErr.response?.data?.msg;
|
||||
|
||||
if (axiosErr.response?.status === 409 || code === "EMAIL_DUPLICATE") {
|
||||
setError("email", { message: msg ?? "이미 사용 중인 이메일입니다." });
|
||||
triggerShake(["email"]);
|
||||
} else {
|
||||
setServerError(msg ?? "회원가입에 실패했습니다. 다시 시도해주세요.");
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* 유효성 실패 → shake */
|
||||
|
|
@ -153,6 +183,19 @@ export default function SignupPage() {
|
|||
onSubmit={handleSubmit(onSubmit, onError)}
|
||||
noValidate
|
||||
>
|
||||
{/* 서버 에러 */}
|
||||
{serverError && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
<span
|
||||
className="material-symbols-outlined flex-shrink-0"
|
||||
style={{ fontSize: "14px" }}
|
||||
>
|
||||
error
|
||||
</span>
|
||||
<span>{serverError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 이메일 */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-sm font-bold text-gray-700">
|
||||
|
|
@ -335,9 +378,19 @@ export default function SignupPage() {
|
|||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary py-3.5 px-4 font-bold text-white shadow-lg shadow-blue-500/20 transition-all hover:bg-[#1d4ed8] active:scale-[0.98]"
|
||||
disabled={isLoading}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary py-3.5 px-4 font-bold text-white shadow-lg shadow-blue-500/20 transition-all hover:bg-[#1d4ed8] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
가입하기
|
||||
{isLoading ? (
|
||||
<>
|
||||
<span className="material-symbols-outlined animate-spin text-lg">
|
||||
progress_activity
|
||||
</span>
|
||||
가입 처리 중…
|
||||
</>
|
||||
) : (
|
||||
"가입하기"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,23 @@
|
|||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { AxiosError } from "axios";
|
||||
import { verifyEmail, resendVerifyEmail } from "@/api/auth.api";
|
||||
import type { ApiError } from "@/types/api";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
interface LocationState {
|
||||
verifySessionId?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
admin?: { admin_code: string | null; email: string | null; name: string | null; role: string | null };
|
||||
}
|
||||
|
||||
export default function VerifyEmailPage() {
|
||||
const navigate = useNavigate();
|
||||
const setUser = useAuthStore((s) => s.setUser);
|
||||
const location = useLocation();
|
||||
const setAuth = useAuthStore((s) => s.setAuth);
|
||||
const state = (location.state as LocationState) ?? {};
|
||||
const verifySessionId = state.verifySessionId ?? null;
|
||||
|
||||
const [code, setCode] = useState<string[]>(Array(6).fill(""));
|
||||
const [codeError, setCodeError] = useState<string | null>(null);
|
||||
|
|
@ -13,6 +26,7 @@ export default function VerifyEmailPage() {
|
|||
const [resendModal, setResendModal] = useState(false);
|
||||
const [homeOverlay, setHomeOverlay] = useState(false);
|
||||
const [resendCooldown, setResendCooldown] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
|
|
@ -84,32 +98,47 @@ export default function VerifyEmailPage() {
|
|||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* 인증하기
|
||||
* 테스트: "123456" → 성공, 그 외 → 실패
|
||||
*/
|
||||
const handleVerify = () => {
|
||||
/* 인증하기 */
|
||||
const handleVerify = async () => {
|
||||
const fullCode = code.join("");
|
||||
if (fullCode.length !== 6) return;
|
||||
|
||||
if (fullCode === "123456") {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await verifyEmail({
|
||||
code: fullCode,
|
||||
verify_session_id: verifySessionId,
|
||||
});
|
||||
setSuccessModal(true);
|
||||
} else {
|
||||
setCodeError("인증 코드가 올바르지 않습니다.");
|
||||
} catch (err) {
|
||||
const axiosErr = err as AxiosError<ApiError>;
|
||||
const msg = axiosErr.response?.data?.msg;
|
||||
setCodeError(msg ?? "인증 코드가 올바르지 않습니다.");
|
||||
triggerShake();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/* 재전송 */
|
||||
const handleResend = () => {
|
||||
if (resendCooldown > 0) return;
|
||||
const handleResend = async () => {
|
||||
if (resendCooldown > 0 || !verifySessionId) return;
|
||||
|
||||
// TODO: API 연동
|
||||
setResendModal(true);
|
||||
setResendCooldown(60);
|
||||
setCode(Array(6).fill(""));
|
||||
setCodeError(null);
|
||||
inputRefs.current[0]?.focus();
|
||||
try {
|
||||
const res = await resendVerifyEmail({
|
||||
verify_session_id: verifySessionId,
|
||||
});
|
||||
const d = res.data.data;
|
||||
setResendModal(true);
|
||||
setResendCooldown(d.cooldown_seconds || 60);
|
||||
setCode(Array(6).fill(""));
|
||||
setCodeError(null);
|
||||
inputRefs.current[0]?.focus();
|
||||
} catch (err) {
|
||||
const axiosErr = err as AxiosError<ApiError>;
|
||||
const msg = axiosErr.response?.data?.msg;
|
||||
setCodeError(msg ?? "재전송에 실패했습니다. 잠시 후 다시 시도해주세요.");
|
||||
}
|
||||
};
|
||||
|
||||
/* 재전송 모달 닫기 */
|
||||
|
|
@ -123,15 +152,19 @@ export default function VerifyEmailPage() {
|
|||
setSuccessModal(false);
|
||||
setHomeOverlay(true);
|
||||
|
||||
// 인증 완료 → 인증 상태 저장
|
||||
setUser({
|
||||
id: 1,
|
||||
email: "test@spms.com",
|
||||
name: "테스트 사용자",
|
||||
role: "ADMIN",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
/* 인증 완료 → 인증 상태 저장 */
|
||||
if (state.admin) {
|
||||
setAuth(
|
||||
{
|
||||
adminCode: state.admin.admin_code ?? "",
|
||||
email: state.admin.email ?? "",
|
||||
name: state.admin.name ?? "",
|
||||
role: state.admin.role ?? "",
|
||||
},
|
||||
state.accessToken ?? "",
|
||||
state.refreshToken ?? "",
|
||||
);
|
||||
}
|
||||
|
||||
setTimeout(() => navigate("/", { replace: true }), 1000);
|
||||
};
|
||||
|
|
@ -217,11 +250,11 @@ export default function VerifyEmailPage() {
|
|||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!isFilled}
|
||||
disabled={!isFilled || isLoading}
|
||||
onClick={handleVerify}
|
||||
className="rounded bg-primary px-5 py-2.5 text-sm font-medium text-white shadow-sm shadow-primary/30 transition hover:bg-[#1d4ed8] disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
인증하기
|
||||
{isLoading ? "인증 중…" : "인증하기"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,102 @@
|
|||
/** 로그인 요청 */
|
||||
/* ───── 회원가입 ───── */
|
||||
|
||||
export interface SignupRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
agreeTerms: boolean;
|
||||
agreePrivacy: boolean;
|
||||
}
|
||||
|
||||
export interface SignupResponse {
|
||||
admin_code: string | null;
|
||||
email: string | null;
|
||||
verify_session_id: string | null;
|
||||
email_sent: boolean;
|
||||
}
|
||||
|
||||
/* ───── 이메일 중복 체크 ───── */
|
||||
|
||||
export interface EmailCheckRequest {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface EmailCheckResponse {
|
||||
email: string | null;
|
||||
is_available: boolean;
|
||||
}
|
||||
|
||||
/* ───── 로그인 ───── */
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
/** 로그인 응답 */
|
||||
export interface LoginResponse {
|
||||
accessToken: string;
|
||||
user: {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
role: string;
|
||||
};
|
||||
access_token: string | null;
|
||||
refresh_token: string | null;
|
||||
expires_in: number;
|
||||
next_action: "GO_DASHBOARD" | "VERIFY_EMAIL" | "CHANGE_PASSWORD" | null;
|
||||
email_verified: boolean;
|
||||
verify_session_id: string | null;
|
||||
email_sent: boolean | null;
|
||||
must_change_password: boolean | null;
|
||||
admin: AdminInfo;
|
||||
}
|
||||
|
||||
/** 회원가입 요청 */
|
||||
export interface SignupRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
export interface AdminInfo {
|
||||
admin_code: string | null;
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
role: string | null;
|
||||
}
|
||||
|
||||
/* ───── 이메일 인증 ───── */
|
||||
|
||||
export interface EmailVerifyRequest {
|
||||
code: string;
|
||||
verify_session_id?: string | null;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export interface EmailVerifyResponse {
|
||||
verified: boolean;
|
||||
next_action: string | null;
|
||||
}
|
||||
|
||||
export interface EmailResendRequest {
|
||||
verify_session_id: string;
|
||||
}
|
||||
|
||||
export interface EmailResendResponse {
|
||||
resent: boolean;
|
||||
cooldown_seconds: number;
|
||||
expires_in_seconds: number;
|
||||
}
|
||||
|
||||
/* ───── 토큰 ───── */
|
||||
|
||||
export interface TokenRefreshRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface TokenRefreshResponse {
|
||||
access_token: string | null;
|
||||
refresh_token: string | null;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
/* ───── 로그아웃 ───── */
|
||||
|
||||
export interface LogoutResponse {
|
||||
logged_out: boolean;
|
||||
redirect_to: string | null;
|
||||
}
|
||||
|
||||
/* ───── 비밀번호 ───── */
|
||||
|
||||
export interface TempPasswordRequest {
|
||||
email: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,39 @@
|
|||
import { create } from "zustand";
|
||||
import type { User } from "@/types/user";
|
||||
|
||||
/* localStorage에서 유저 정보 복원 */
|
||||
function loadUser(): User | null {
|
||||
try {
|
||||
const raw = localStorage.getItem("user");
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const savedUser = loadUser();
|
||||
const savedToken = localStorage.getItem("accessToken");
|
||||
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
setUser: (user: User) => void;
|
||||
setAuth: (user: User, accessToken: string, refreshToken: string) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
setUser: (user) => set({ user, isAuthenticated: true }),
|
||||
logout: () => set({ user: null, isAuthenticated: false }),
|
||||
user: savedUser,
|
||||
isAuthenticated: !!(savedUser && savedToken),
|
||||
setAuth: (user, accessToken, refreshToken) => {
|
||||
localStorage.setItem("accessToken", accessToken);
|
||||
localStorage.setItem("refreshToken", refreshToken);
|
||||
localStorage.setItem("user", JSON.stringify(user));
|
||||
set({ user, isAuthenticated: true });
|
||||
},
|
||||
logout: () => {
|
||||
localStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("refreshToken");
|
||||
localStorage.removeItem("user");
|
||||
set({ user: null, isAuthenticated: false });
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
/** API 응답 공통 래퍼 */
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
result: boolean;
|
||||
code: string | null;
|
||||
msg: string | null;
|
||||
data: T;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** 페이지네이션 응답 */
|
||||
export interface PaginatedResponse<T> {
|
||||
success: boolean;
|
||||
data: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
result: boolean;
|
||||
code: string | null;
|
||||
msg: string | null;
|
||||
data: {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** API 에러 */
|
||||
/** API 에러 (axios error.response.data) */
|
||||
export interface ApiError {
|
||||
success: false;
|
||||
message: string;
|
||||
code?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
result: false;
|
||||
code: string | null;
|
||||
msg: string | null;
|
||||
data: null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,7 @@
|
|||
/** 사용자 역할 */
|
||||
export const UserRole = {
|
||||
ADMIN: "ADMIN",
|
||||
MANAGER: "MANAGER",
|
||||
USER: "USER",
|
||||
} as const;
|
||||
|
||||
export type UserRole = (typeof UserRole)[keyof typeof UserRole];
|
||||
|
||||
/** 사용자 */
|
||||
/** 사용자 (백엔드 AdminInfo 기반) */
|
||||
export interface User {
|
||||
id: number;
|
||||
adminCode: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
profileImage?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
role: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,13 @@ export default defineConfig({
|
|||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/v1': {
|
||||
target: 'https://devspms.ipstein.myds.me',
|
||||
changeOrigin: true,
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user