React

 

최신 트렌드 적용한 

React Router v6.4 , react-hook-form, axiosInstance, typescript, tailwind를 활용한 회원가입 폼 전송 처리 개발방법

 

 

소스 :

https://github.com/braverokmc79/springboot-restful-web

 

 

설치

1) vite 로 리액트 + typescript,  프로젝트를 생성한다.

https://ko.vitejs.dev/guide/

 

2) tailwind  설정

https://tailwindcss.com/

 

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 사용

  1. 라우트 설정
// 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 으로 팝업구현

 

https://ui.shadcn.com/

 

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);
    }
    
    // 기타 특정 예외 핸들러 추가 가능
    
    
    
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

지렁이도 밟으면 꿈틀한다 , 아무리 보잘것없고 약한 사람이라도 너무 업신여김을 당하면 반항한다는 말.

댓글 ( 0)

댓글 남기기

작성