Skip to content

✨ [Feat] 마이페이지/전체메뉴 - 회원정보조회/수정/회원탈퇴/로그아웃 기능 #66

@dahyoon

Description

@dahyoon

📝 개요

✅ To-Do

마이페이지

  • 1 회원 정보 조회
    • 1.1 회원 정보 조회 API 호출
    • 1.2 회원 정보(이름, 이메일) 표시 (회원 정보 수정 성공 시 변경된 회원 정보 반영되도록 설정)
  • 2 회원 정보 수정
    • 2.1 이름 변경
      • 2.1.1 이름 입력값 유효성 검사
        • 2.1.1.1 기존 이름과 동일 X
        • 2.1.1.2 올바른 형식: 한글, 영문, 2~20자
    • 2.2 비밀번호 변경
      • 2.2.1 비밀번호 변경 항목 표시 대상: 이메일 계정만 (카카오 및 구글 계정은 미표시)
      • 2.2.2 기존 비밀번호 검증
      • 2.2.3 비밀번호 입력값 유효성 검사
        • 2.2.3.1 기존 비밀번호와 동일 X
        • 2.2.3.2 올바른 형식: 영문 대·소문자, 숫자, 특수문자를 각각 포함한 8자 이상
        • 2.2.3.3 새 비밀번호, 비밀번호 일치 여부
    • 2.3 프로필사진 변경
      • 2.3.1 새 파일로 변경
      • 2.3.2 기본 이미지로 변경
    • 2.4 회원 정보 수정 성공/실패 시 안내 메세지 표시 (Toast)
  • 3 회원 탈퇴
    • 3.1 이메일 계정
      • 3.1.1 기존 비밀번호 검증
      • 3.1.2 회원 탈퇴 API 호출
    • 3.2 카카오 계정
    • 3.3 구글 계정

전체 메뉴

  • 1 회원 정보 조회
    • 1.1 로그인 했을 경우
      • 1.1.1 이메일 계정: 프로필사진, 이름 및 이메일 표시
      • 1.1.2 카카오 계정: 프로필사진, 이름 표시
      • 1.1.3 구글 계정: 프로필사진, 이름 및 이메일 표시
    • 1.2 비회원
      • 1.2.1 로고, "안전한 거래의 시작, Light House" 문구 표시
  • 2 로그아웃 버튼 로그인한 회원에게만 표시

📸 스크린샷

마이페이지 (/mypage)

  • 회원 정보 조회/표시, 프로필사진 수정, 회원 정보 (이름, 비밀번호)수정, 회원 탈퇴
image image image image image

전체 메뉴 (/mainMenu)

  • 상단 프로필 영역
image

Trouble Shooting

CORS 설정

  • 문제: 회원정보 변경 요청 시, 이름/비밀번호는 CORS 통과 되는데 프로필사진 변경은 CORS에 걸림
Image
  • 원인:

    • 파일 업로드 요청에서만 CORS가 막히는 이유는 multipart/form-data 요청Spring Security 필터 체인의 처리 순서에 있음
  • 설명:

    • 파일 업로드 요청의 특성: multipart/form-data는 일반 JSON 요청과 다른 Content-Type을 가짐
    • Spring Security 필터 순서: CORS 필터가 Security 필터 체인에서 부적절한 위치에 있을 수 있음
    • OPTIONS 요청 처리: 브라우저가 파일 업로드 전에 보내는 preflight OPTIONS 요청이 제대로 처리되지 않을 수 있음
  • 해결 방법:

    • CorsConfigurationSource 분리: CORS 설정을 별도 Bean으로 분리하여 더 명확하게 관리
    • 헤더 명시적 설정: multipart/form-data 요청에 필요한 헤더들을 명시적으로 허용
    • OPTIONS 요청 강화: 모든 경로에서 OPTIONS 요청을 허용하도록 변경
    • 필터 순서 명시: CORS 필터를 명시적으로 필터 체인에 추가
    • preflight 캐시: maxAge 설정으로 preflight 요청 최적화
package com.lighthouse.security.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Arrays;

@Slf4j
@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = {"com.lighthouse.security"})
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

   @Value("${FRONT_ORIGIN}")
   private String frontOrigin;

   @Bean
   public PasswordEncoder passwordEncoder() {
       return new BCryptPasswordEncoder();
   }

   @Bean
   public AuthenticationManager authenticationManager() throws Exception {
       return super.authenticationManager();
   }

   @Bean
   public CorsConfigurationSource corsConfigurationSource() {
       CorsConfiguration configuration = new CorsConfiguration();
       configuration.setAllowCredentials(true);
       configuration.addAllowedOrigin(frontOrigin);
       configuration.addAllowedHeader("*");
       configuration.addAllowedMethod("*");
       // multipart/form-data 요청을 위한 추가 설정 - 프로필사진 업로드 시 사용
       configuration.setAllowedHeaders(Arrays.asList(
               "Authorization", "Content-Type", "Content-Disposition", "Content-Length",
               "X-Requested-With", "accept", "Origin", "Access-Control-Request-Method",
               "Access-Control-Request-Headers"
       ));
       configuration.setExposedHeaders(Arrays.asList(
               "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials"
       ));
       configuration.setMaxAge(3600L); // preflight 캐시 시간 설정 (1시간)

       UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
       source.registerCorsConfiguration("/**", configuration);
       return source;
   }

   @Bean
   public CorsFilter corsFilter() {
       return new CorsFilter(corsConfigurationSource());
   }

   // 접근 제한 무시 경로 설정 – resource
   @Override
   public void configure(WebSecurity web) {
       web.ignoring().antMatchers("/assets/**", "/*", "/api/member/**",
               "/swagger-ui.html", "/webjars/**", "/swagger-resources/**", "/v2/api-docs");
   }

   @Override
   public void configure(HttpSecurity http) throws Exception {
       http.httpBasic().disable()
               .csrf().disable()
               .formLogin().disable()
               .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
               .and()
               .cors().configurationSource(corsConfigurationSource()); // CorsConfigurationSource 명시적 지정

       http
               .authorizeRequests() // 경로별 접근 권한 설정
               .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 모든 OPTIONS 요청 허용
               .antMatchers(HttpMethod.PUT, "/api/member").authenticated()
               .anyRequest().permitAll();
   }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions