최신 트렌드 적용한
React Router v6.4 , react-hook-form, axiosInstance, typescript, tailwind를 활용한 회원가입 폼 전송 처리 개발방법
소스 :
https://github.com/braverokmc79/springboot-restful-web
설치
1) vite 로 리액트 + typescript, 프로젝트를 생성한다.
2) tailwind 설정
3) react-router-dom,axio, react-hook-form 라이브러리 설치
npm i react-router-dom axio react-hook-form
라우트 설정
React Router와의 통합
React Router와 React Hook Form을 통합하여 폼 제출 시 라우트의 액션 함수를 호출하는 방법을 알아보겠습니다.
예제: React Router v6.4+에서 React Hook Form 사용
- 라우트 설정
// src/App.jsx import React from 'react'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import SignupForm, { signupAction } from './components/SignupForm'; import LoginForm from './components/LoginForm'; const router = createBrowserRouter([ { path: '/signup', element: <SignupForm />, action: signupAction, }, { path: '/login', element: <LoginForm />, }, ]); const App = () => { return <RouterProvider router={router} />; }; export default App;
1. 공통 Axios 설정:
axiosInstance.ts 파일에서 API 요청을 위한 Axios 인스턴스를 설정합니다.
Axios 인스턴스 생성:
baseURL은 환경 변수에서 가져오고, 기본적으로 요청 헤더에 Content-Type을 application/json으로 설정합니다. 또한, localStorage에 저장된 토큰이 있으면 이를 자동으로 요청 헤더에 추가해줍니다.인터셉터:
- 요청 인터셉터: API 요청 전에 localStorage에서 토큰을 가져와 헤더에 추가하여 인증 처리를 자동화합니다.
- 응답 인터셉터: 응답 데이터를 처리하며, 만약 응답 코드가 401(Unauthorized)일 경우, 로그인 페이지로 리다이렉트하는 등의 처리를 할 수 있습니다.
axiosInstance.ts
// src/utils/axiosInstance.ts import axios from 'axios'; // axios 인스턴스 생성 const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, // 요청 타임아웃 설정 (ms) headers: { 'Content-Type': 'application/json', // 필요 시 인증 토큰이나 다른 헤더를 추가할 수 있습니다. // 'Authorization': `Bearer ${token}` }, }); // 요청 인터셉터 추가 axiosInstance.interceptors.request.use( (config) => { // 요청 전 실행할 코드 (ex: 토큰 자동으로 추가) const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { // 요청 에러 처리 return Promise.reject(error); } ); // 응답 인터셉터 추가 axiosInstance.interceptors.response.use( (response) => { // 응답 데이터 처리 return response; }, (error) => { // 응답 에러 처리 (ex: 401 에러 처리) if (error.response.status === 401) { console.error('Unauthorized, redirect to login'); // 로그아웃 처리 또는 로그인 페이지로 리다이렉트 등 } return Promise.reject(error); } ); export default axiosInstance;
설명:
- Axios 인스턴스 생성: baseURL과 Content-Type을 설정하고, 기본적으로 JSON 형태로 데이터를 전송합니다. 요청 및 응답 시 인터셉터를 사용해 토큰을 자동으로 추가하고 에러를 처리합니다.
- 토큰 자동 추가: 요청 전 localStorage에서 토큰을 확인해 자동으로 Authorization 헤더에 추가합니다.
- 401 에러 처리: 서버로부터 인증 실패(401)를 받을 경우, 로그인 리다이렉트 같은 처리를 할 수 있습니다.
2. 공통 API 요청 함수들:
다양한 API 요청(GET, POST, PUT, DELETE)을 처리하는 함수들이 있습니다. 각 함수는 Axios 인스턴스를 사용하여 서버와 데이터를 주고받으며, 응답 데이터를 apiData에 저장하고, 오류 발생 시 error 객체에 저장합니다.
- postApiData: 회원가입과 같은 POST 요청을 처리합니다.
- getApiData, putApiData, deleteApiData도 각각 GET, PUT, DELETE 요청을 처리하는 함수들입니다.
axiosActions.ts
import axiosInstance from "@/utils/axiosInstance"; export interface ApiResponse<T> { apiData: T | null; error: Error | null; } // GET 요청 함수 export async function getApiData<T>(url: string): Promise<ApiResponse<T>> { let apiData: T | null = null; let error: Error | null = null; try { //테스트를 5초 지연을 추가 //await new Promise(resolve => setTimeout(resolve, 5000)); const response = await axiosInstance.get<T>(url); apiData = response.data; } catch (err) { if (err instanceof Error) { error = err; } } return { apiData, error }; } // POST 요청 함수 export async function postApiData<T, U>(url: string, payload: T): Promise<ApiResponse<U>> { let apiData: U | null = null; let error: Error | null = null; try { //테스트를 위한 5초 지연을 추가 await new Promise(resolve => setTimeout(resolve, 5000)); const response = await axiosInstance.post<U>(url, payload); apiData = response.data; } catch (err) { if (err instanceof Error) { error = err; } } return { apiData, error }; } // PUT 요청 함수 export async function putApiData<T, U>(url: string, payload: T): Promise<ApiResponse<U>> { let apiData: U | null = null; let error: Error | null = null; try { const response = await axiosInstance.put<U>(url, payload); apiData = response.data; } catch (err) { if (err instanceof Error) { error = err; } } return { apiData, error }; } // DELETE 요청 함수 export async function deleteApiData<T>(url: string): Promise<ApiResponse<T>> { let apiData: T | null = null; let error: Error | null = null; try { const response = await axiosInstance.delete<T>(url); apiData = response.data; } catch (err) { if (err instanceof Error) { error = err; } } return { apiData, error }; }
설명:
- API 요청 처리: axiosInstance를 통해 GET, POST 등의 요청을 보냅니다. 성공하면 데이터를 반환하고, 실패하면 에러를 반환합니다.
- 타입 안정성: TypeScript의 제네릭을 활용해 응답 데이터 타입을 명확하게 지정할 수 있습니다.
3. 공통 Axios 오류 처리:
handleAxiosError 함수는 서버로부터 받은 오류 응답을 처리합니다. 만약 서버에서 특정 필드에 대한 에러 메시지가 반환되면, 이를 처리해 클라이언트 측에서 에러 메시지를 표시할 수 있게 해줍니다.
handleAxiosError.ts
여기서 errorField 는 백엔드에서 설정한 변수인 errorField 를 받아 옵니다.
import { json } from "react-router-dom"; import { AxiosError } from "axios"; interface ErrorResponseData { message: string; errorField: string; } // AxiosError를 처리하는 유틸리티 함수 export function handleAxiosError(error: AxiosError) { const fieldErrors: Record<string, string> = {}; if (error?.response?.data as ErrorResponseData) { const errorData =error?.response?.data as ErrorResponseData; // 백엔드에서 전달된 에러 메시지 처리 if (errorData.message) { fieldErrors[errorData.errorField] = errorData.message; // 에러 응답을 json 형태로 반환 return json( { alertMessage: errorData.message, fieldErrors, errorField: errorData.errorField, isLoading: false, }, { status: 400 } ); } } // 기본 에러 메시지 처리 return json( { alertMessage: error.message, isLoading: false }, { status: 400 } ); }
설명:
- 에러 메시지 처리: 서버로부터 받은 에러 응답을 처리하며, 발생한 에러를 특정 필드와 연결해 사용자에게 알릴 수 있습니다.
- 예를 들어, 잘못된 입력값이 있는 경우 해당 필드에 에러 메시지를 표시합니다
4. 회원가입 처리 로직 (action 함수):
회원가입 폼의 데이터를 서버에 전송하고, 그에 따른 응답을 처리하는 부분입니다.
- action 함수는 request.formData()를 통해 폼 데이터를 수집하고, 이를 postApiData 함수를 통해 /api/users/signup 엔드포인트로 전송합니다.
- 만약 에러가 발생하면, handleAxiosError 함수로 에러를 처리합니다.
- 성공 시, 메시지를 반환하거나 로그인 페이지로 리다이렉트할 수 있습니다.
sinupAction.ts
import { json } from "react-router-dom"; import { postApiData } from "./axiosActions"; import { AxiosError } from "axios"; import { ApiResponse } from "@/dto/TodoDTO"; import { handleAxiosError } from "@/utils/handleAxiosError"; // 회원 가입 action export async function action({ request }: { request: Request }) { const formData = await request.formData(); const signupData = Object.fromEntries(formData); // 여기에 실제 회원가입 처리 로직 추가 (API 호출 등) const {apiData, error}= await postApiData(`/api/users/signup`, signupData); // 에러 발생 시 handleAxiosError로 처리 if (error && error instanceof AxiosError) { return handleAxiosError(error); // 분리된 함수 호출 } // 회원가입 성공 시 처리 const apiResponseData =apiData as ApiResponse if(apiResponseData &&apiResponseData.code===1){ return json({ alertMessage: "회원가입을 축하합니다", isLoading:false, signUpResult:true }, { status: 200 }); } //또는 // 회원가입 성공 시 로그인 페이지로 리다이렉트 //return redirect("/login"); }
회원가입 처리: postApiData를 통해 서버에 회원가입 데이터를 전송하고, 성공 시 성공 메시지를 반환하거나 에러가 발생하면 handleAxiosError로 처리합니다.
5. 회원가입 컴포넌트 (SignupComponent):
react-hook-form 설정 및 사용방법 참조
1) 공식문서
https://www.react-hook-form.com/
2)
https://macaronics.net/m04/react/view/2301
SignupComponent는 실제로 사용자 입력을 받는 회원가입 폼을 구현한 부분입니다. 이 폼은 react-hook-form 라이브러리를 사용해 폼 상태를 관리하고 유효성 검사를 수행합니다.
폼 상태 관리 및 제출:
useForm 훅을 사용해 각 필드의 값과 에러를 관리합니다. handleSubmit을 통해 폼 데이터를 제출하며, 서버 응답을 받아 에러 메시지나 성공 메시지를 표시합니다.폼 유효성 검사:
각 입력 필드에 대해 필수 입력 여부와 같은 유효성 검사를 설정하고, 에러가 발생할 경우 Tailwind CSS로 스타일링된 에러 메시지를 보여줍니다.서버 응답 처리:
useActionData를 사용해 서버에서 반환된 데이터를 받아, 회원가입 성공 여부나 에러 메시지를 처리합니다. 예를 들어, 회원가입 성공 시에는 로그인 페이지로 리다이렉트되고, 에러가 있을 경우 해당 필드에 에러 메시지를 표시합니다.다이얼로그 컴포넌트:
서버에서 받은 메시지를 다이얼로그로 표시해 사용자에게 알려줍니다.
SignupComponent.tsx
import React, { useEffect, useState } from "react"; import { Form, useActionData, Link,useSubmit } from "react-router-dom"; import { FieldValues, useForm } from "react-hook-form"; import DialogConfirmComponent from "../common/dialog/DialogConfirmComponent"; export interface actionDataType { alertMessage?: string; isLoading?: boolean; signUpResult?: boolean; fieldErrors?: Record<string, string>; errorField?: string; } const SignupComponent: React.FC = () => { const [redirectURL, setRedirectURL] = useState(''); const actionData = useActionData() as actionDataType; const [isDialogOpen, setIsDialogOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const { register, handleSubmit, formState: { errors }, watch, setError } = useForm(); const submit = useSubmit(); useEffect(() => { if (actionData?.alertMessage) { setIsDialogOpen(true); } if (!actionData?.isLoading) { setIsLoading(false); } if (actionData?.signUpResult) { setRedirectURL("/login"); } // 서버에서 넘어온 에러 메시지를 해당 필드에 설정 if (actionData?.fieldErrors && actionData?.errorField) { setError(actionData.errorField, { type: "server", message: actionData.fieldErrors[actionData.errorField] }); console.log("fieldErrors ============", actionData.fieldErrors); } }, [actionData, setError]); const onSubmit = (data: FieldValues) => { //진행중 표시 setIsLoading(true); submit(data, { method: "post" }); }; return ( <> <Form method="post" onSubmit={handleSubmit(onSubmit)}> <div className="flex items-center justify-center bg-gray-100 py-20"> <div className="w-full max-w-2xl p-8 bg-white rounded-lg shadow-lg"> <h2 className="text-2xl font-bold text-center text-gray-800 mb-6"> 회원 가입 </h2> {isLoading && ( <div className="w-full text-blue-600 font-bold my-5 items-center text-center"> 회원 가입 중... </div> )} <div className="mb-4 flex flex-col"> <div className="flex flex-row"> <label className="w-3/12 block text-sm font-medium text-gray-700 mb-2"> 아이디 </label> <input type="text" {...register("username", { required: "아이디를 입력하세요." })} className={`w-9/12 px-4 py-2 border ${errors.username ? "border-red-500" : "border-gray-300"} rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500`} placeholder="아이디" /> </div> {errors.username && ( <div className="flex flex-row"> <span className="w-3/12"></span> <p className="text-red-500 text-sm">{String(errors.username.message)}</p> </div> )} </div> <div className="mb-4 flex flex-col"> <div className="flex flex-row"> <label className="w-3/12 block text-sm font-medium text-gray-700 mb-2"> 이름 </label> <input type="text" {...register("name", { required: "이름을 입력하세요." })} className={`w-9/12 px-4 py-2 border ${errors.name ? "border-red-500" : "border-gray-300"} rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500`} placeholder="이름" /> </div> {errors.name && ( <div className="flex flex-row"> <span className="w-3/12"></span> <p className="text-red-500 text-sm">{String(errors.name.message)}</p> </div> )} </div> <div className="mb-6 flex flex-col"> <div className="flex flex-row"> <label className="w-3/12 block text-sm font-medium text-gray-700 mb-2"> 비번 </label> <input type="password" {...register("password", { required: "비밀번호를 입력하세요." })} className={`w-9/12 px-4 py-2 border ${errors.password ? "border-red-500" : "border-gray-300"} rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500`} placeholder="비밀번호" /> </div> {errors.password && ( <div className="flex flex-row"> <span className="w-3/12"></span> <p className="text-red-500 text-sm">{String(errors.password.message)}</p> </div> )} </div> <div className="mb-6 flex flex-col"> <div className="flex flex-row"> <label className="w-3/12 block text-sm font-medium text-gray-700 mb-2"> 비밀번호 확인 </label> <input type="password" {...register("confirmPassword", { required: "비밀번호 확인을 입력하세요.", validate: (value) => value === watch('password') || "비밀번호가 일치하지 않습니다." })} className={`w-9/12 px-4 py-2 border ${errors.confirmPassword ? "border-red-500" : "border-gray-300"} rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500`} placeholder="비밀번호 확인" /> </div> {errors.confirmPassword && ( <div className="flex flex-row"> <span className="w-3/12"></span> <p className="text-red-500 text-sm">{String(errors.confirmPassword.message)}</p> </div> )} </div> <div className="mb-6 flex flex-col"> <div className="flex flex-row"> <label className="w-3/12 block text-sm font-medium text-gray-700 mb-2"> 생일 </label> <input type="date" {...register("birthDate", { required: "생일을 입력하세요." })} className={`w-9/12 px-4 py-2 border ${errors.birthDate ? "border-red-500" : "border-gray-300"} rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500`} /> </div> {errors.birthDate && ( <div className="flex flex-row"> <span className="w-3/12"></span> <p className="text-red-500 text-sm">{String(errors.birthDate.message)}</p> </div> )} </div> <div className="mb-6 flex flex-col"> <div className="flex flex-row"> <label className="w-3/12 block text-sm font-medium text-gray-700 mb-2"> 이메일 </label> <input type="email" {...register("email", { required: "이메일을 입력하세요." })} className={`w-9/12 px-4 py-2 border ${errors.email ? "border-red-500" : "border-gray-300"} rounded-md shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500`} placeholder="이메일" /> </div> {errors.email && ( <div className="flex flex-row"> <span className="w-3/12"></span> <p className="text-red-500 text-sm">{String(errors.email.message)}</p> </div> )} </div> <div className="my-3"> <p> 이미 회원가입이 되어있습니까?{" "} <Link to={"/login"} className="text-indigo-700 hover:text-indigo-900" > 로그인 </Link> </p> </div> <div className="flex justify-center"> <button type="submit" className={`w-full py-2 px-4 ${isLoading ? "bg-gray-500" : "bg-blue-500"} text-white font-semibold rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`} disabled={isLoading} > {isLoading ? "회원 가입중..." : "회원 가입"} </button> </div> </div> </div> </Form> {actionData?.alertMessage && ( <DialogConfirmComponent alertMessage={actionData.alertMessage} isDialogOpen={isDialogOpen} setIsDialogOpen={setIsDialogOpen} redirectURL={redirectURL} /> )} </> ); }; export default SignupComponent;
설명:
- 폼 제출 및 유효성 검사: react-hook-form을 사용해 입력값에 대해 유효성 검사를 하고, 문제가 없을 경우 submit 함수로 데이터를 전송합니다.
- 에러 메시지 처리: 서버에서 반환된 에러는 해당 필드에 자동으로 표시됩니다.
- 성공 시 리다이렉트: 회원가입 성공 시 로그인 페이지로 리다이렉트하도록 설정합니다.
추가 알림 팝업 메시창 - shadcn 으로 팝업구현
DialogConfirmComponent.tsx
import { Dialog, DialogHeader, DialogFooter, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { useNavigate } from "react-router-dom"; // navigator 사용을 위해 추가 interface DialogConfirmComponentProps { isDialogOpen: boolean; setIsDialogOpen: React.Dispatch<React.SetStateAction<boolean>>; alertMessage: string; redirectURL?: string; } const DialogConfirmComponent: React.FC<DialogConfirmComponentProps> = ({ isDialogOpen, setIsDialogOpen, alertMessage, redirectURL }) => { const navigate = useNavigate(); const buttonConfirm = () => { setIsDialogOpen(false); if (redirectURL) { navigate(redirectURL); // 페이지 이동 } }; return ( <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <DialogContent> <DialogHeader> <DialogTitle className="text-lg font-bold text-rose-700">알림</DialogTitle> <DialogDescription className="mt-3 text-xl">{alertMessage}</DialogDescription> </DialogHeader> <DialogFooter className="mt-4"> <Button variant="ghost" onClick={buttonConfirm} className="bg-slate-500 text-white hover:bg-slate-700 hover:text-white" > 확인 </Button> </DialogFooter> </DialogContent> </Dialog> ); }; export default DialogConfirmComponent;
스프링 부트 백엔드 설정
1) User
package net.macaronics.springboot.webapp.entity; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import net.macaronics.springboot.webapp.dto.user.UserUpdateFormDTO; import net.macaronics.springboot.webapp.enums.Role; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Entity @Getter @Setter @Table(name = "users") @Builder @AllArgsConstructor @NoArgsConstructor public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; @Column(nullable = false, unique = true) private String username; private String password; private String name; @Column(unique = true) private String email; private LocalDate birthDate; @Enumerated(EnumType.STRING) private Role role; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) // 삭제된 자식 엔티티 제거 private List<Todo> todos = new ArrayList<>(); @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List<Post> posts = new ArrayList<>(); public User(String username, LocalDate birthDate) { this.password = "$2a$10$8VKmqNwV0x/bQEN8Z54w7uUpLPTGeHgoJR73dyH2S6ZxoHkVkxGSm"; this.username = username; this.birthDate = birthDate; } // 더티 체킹 업데이트 public static User updateUser(User user, UserUpdateFormDTO updateFormDTO) { user.setPassword(updateFormDTO.getPassword()); // 암호화 로직 추가 고려 user.setBirthDate(updateFormDTO.getBirthDate()); user.setRole(updateFormDTO.getRole()); return user; } }
2)UserRegisterFormDTO
/** POST => http://localhost:8080/api/users/signup * 3. 사용자 생성 (회원가입) 메소드 * @param registerFormDTO * @param bindingResult * @return * @throws Exception */ @PostMapping("/signup") @Operation(summary = "사용자 생성", description = "새 사용자를 등록합니다.") public ResponseEntity<?> createUser(@Valid @RequestBody UserRegisterFormDTO registerFormDTO, BindingResult bindingResult) throws Exception { log.info("회원 가입 처리 : {}", registerFormDTO.toString()); // 입력 검증 오류 처리 if (bindingResult.hasErrors()) { throw new MethodArgumentNotValidException(null, bindingResult); } // 중복 사용자 확인 if (userService.findByUsername(registerFormDTO.getUsername()) != null) { // 이미 존재하는 아이디일 경우 bindingResult.rejectValue("username", "error.username", "이미 사용 중인 아이디입니다."); throw new MethodArgumentNotValidException(null, bindingResult); } // 이메일 중복 확인 if (userService.existsByEmail(registerFormDTO.getEmail())) { // 이메일 중복 확인 bindingResult.rejectValue("email", "error.email", "이미 사용 중인 이메일입니다."); throw new MethodArgumentNotValidException(null, bindingResult); } // DTO를 User 객체로 변환 후 저장 //User user = UserRegisterFormDTO.toCreateUser(registerFormDTO); User user=userMapper.ofUser(registerFormDTO); User savedUser = userService.saveUser(user); // 사용자 저장 // 생성된 사용자의 URI 생성 URI location = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(ApiUserController.class).getUserById(savedUser.getId())).toUri(); UserResponse userResponse = addUserLinks(UserResponse.of(savedUser)); return ResponseEntity.created(location) .body(ResponseDTO.builder() .code(1) .message("success") .data(userResponse) .build() ); }
3) GlobalExceptionHandler
package net.macaronics.springboot.webapp.exception; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import lombok.extern.slf4j.Slf4j; import net.macaronics.springboot.webapp.dto.ResponseDTO; @ControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 모든 예외 처리 * @param ex * @param request * @return */ @ExceptionHandler(Exception.class) public final ResponseEntity<?> handleAllExceptions(Exception ex, WebRequest request){ log.error("Unhandled exception occurred", ex); ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(), request.getDescription(false)); ResponseDTO<?> response = ResponseDTO.builder() .code(-1) .message("Internal Server Error") .data(errorDetails) .errorCode("INTERNAL_SERVER_ERROR") .build(); return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } /** * 공통 404 예외 처리: NotFoundException * @param ex * @param request * @return */ @ExceptionHandler(ResourceNotFoundException.class) public final ResponseEntity<?> handleNotFoundException(ResourceNotFoundException ex, WebRequest request){ log.warn("Resource not found exception: {}", ex.getMessage()); ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), ex.getMessage(), request.getDescription(false)); ResponseDTO<?> response = ResponseDTO.builder() .code(-1) .message("Resource Not Found") .data(errorDetails) .errorCode("NOT_FOUND") .build(); return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); } /** * bindingResult.hasErrors() 에러시 반환 처리한다 * 유효성 체크 에러 처리 * @param ex * @param request * @return */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException ex, WebRequest request){ List<String> errors = ex.getBindingResult() .getAllErrors() .stream() .map(DefaultMessageSourceResolvable::getDefaultMessage) .collect(Collectors.toList()); ErrorDetails errorDetails = new ErrorDetails(LocalDateTime.now(), "Validation Failed", request.getDescription(false)); log.warn("Validation failed: {}", errorDetails.getMessage()); ResponseDTO<?> response = ResponseDTO.builder() .code(-1) .message(errors.get(0)) .data(errorDetails) .errorCode("VALIDATION_ERROR") .errorField(ex.getFieldError().getField()) .build(); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } // 기타 특정 예외 핸들러 추가 가능 }
댓글 ( 0)
댓글 남기기