Skip to content

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

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions