diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/application/dto/response/MyPageResponse.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/application/dto/response/MyPageResponse.java new file mode 100644 index 0000000..5ae7686 --- /dev/null +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/application/dto/response/MyPageResponse.java @@ -0,0 +1,12 @@ +package com.whereyouad.WhereYouAd.domains.user.application.dto.response; + +public record MyPageResponse( + Long userId, + String email, + String name, + String profileImageUrl, + String phoneNumber, + boolean isEmailVerified, + String providerType +) { +} diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/application/mapper/UserConverter.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/application/mapper/UserConverter.java index 197f017..a2a6e6c 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/application/mapper/UserConverter.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/application/mapper/UserConverter.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.domains.user.application.mapper; +import com.whereyouad.WhereYouAd.domains.user.application.dto.response.MyPageResponse; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse; import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus; import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User; @@ -32,4 +33,16 @@ public static OAuth2UserInfo toOAuth2UserInfo(User user, OAuth2Response oAuth2Re .provider(oAuth2Response.getProvider()) .build(); } + + public static MyPageResponse toMyPageResponse(User user, String provider) { + + return new MyPageResponse(user.getId(), + user.getEmail(), + user.getName(), + user.getProfileImageUrl(), + user.getPhoneNumber(), + user.isEmailVerified(), + provider + ); + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/constant/Provider.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/constant/Provider.java index 9805402..f2edbe9 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/constant/Provider.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/constant/Provider.java @@ -10,6 +10,7 @@ @Getter @RequiredArgsConstructor public enum Provider { + EMAIL("email", "이메일"), GOOGLE("google", "구글"), NAVER("naver", "네이버"), KAKAO("kakao", "카카오"); diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java index c9fa01e..31073c6 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/UserService.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.domains.user.domain.service; +import com.whereyouad.WhereYouAd.domains.user.application.dto.response.MyPageResponse; import com.whereyouad.WhereYouAd.domains.user.exception.handler.UserHandler; import com.whereyouad.WhereYouAd.domains.user.exception.code.UserErrorCode; import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus; @@ -10,6 +11,7 @@ import com.whereyouad.WhereYouAd.domains.user.persistence.repository.UserRepository; import com.whereyouad.WhereYouAd.global.utils.RedisUtil; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -85,4 +87,21 @@ public void passwordReset(String email, String password) { redisUtil.deleteData("VERIFIED:" + email); } + + /** + * 마이페이지 조회 + * 파라미터 추가: userId 외에 'provider'(로그인 유형) 도 받기 + * 캐시 키 수정: key = "#userId + ':' + #provider" + * 같은 유저(userId=1)라도 '구글'로 로그인했을 때와 '이메일'로 로그인했을 때 + * 응답 데이터(MyPageResponse의 provider 필드)가 다르므로 캐시를 구분해야 합니다. + * 예) user:profile::1:GOOGLE / user:profile::1:EMAIL 로 따로 저장됨. + */ + @Cacheable(value = "user:profile", key = "#userId + ':' + #provider", unless = "#result == null") + @Transactional(readOnly = true) + public MyPageResponse getMyPage(Long userId, String provider) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); + + return UserConverter.toMyPageResponse(user, provider); + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java index 729653e..dec243b 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/UserController.java @@ -4,6 +4,7 @@ import com.whereyouad.WhereYouAd.domains.user.application.dto.request.SmsRequest; import com.whereyouad.WhereYouAd.domains.user.application.dto.request.PwdResetRequest; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse; +import com.whereyouad.WhereYouAd.domains.user.application.dto.response.MyPageResponse; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SmsResponse; import com.whereyouad.WhereYouAd.domains.user.domain.service.EmailService; import com.whereyouad.WhereYouAd.domains.user.domain.service.SmsService; @@ -12,13 +13,12 @@ import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse; import com.whereyouad.WhereYouAd.domains.user.presentation.docs.UserControllerDocs; import com.whereyouad.WhereYouAd.global.response.DataResponse; +import com.whereyouad.WhereYouAd.global.security.jwt.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -84,4 +84,17 @@ public ResponseEntity> resetPassword(@RequestBody @Valid Pw DataResponse.from("비밀번호 변경이 완료되었습니다.") ); } + + @GetMapping("/my") + public ResponseEntity> getMyPage(@AuthenticationPrincipal CustomUserDetails userDetails) { + + MyPageResponse response = userService.getMyPage( + userDetails.getUserId(), + userDetails.getProvider().name() + ); + + return ResponseEntity.ok( + DataResponse.from(response) + ); + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java b/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java index ce462ed..d879959 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/user/presentation/docs/UserControllerDocs.java @@ -5,14 +5,17 @@ import com.whereyouad.WhereYouAd.domains.user.application.dto.request.PwdResetRequest; import com.whereyouad.WhereYouAd.domains.user.application.dto.request.SignUpRequest; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse; +import com.whereyouad.WhereYouAd.domains.user.application.dto.response.MyPageResponse; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SmsResponse; import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse; import com.whereyouad.WhereYouAd.global.response.DataResponse; +import com.whereyouad.WhereYouAd.global.security.jwt.CustomUserDetails; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.RequestBody; public interface UserControllerDocs { @@ -85,4 +88,15 @@ public ResponseEntity> sendSms( }) public ResponseEntity> verifySms( @RequestBody @Valid SmsRequest.SmsVerifyRequest request); + + @Operation( + summary = "마이페이지 API", + description = "Authorization : Bearer \\ 을 헤더로 받아 현재 로그인한 회원의 정보를 조회합니다.\n\n" + + "회원 DB id,이메일, 이름, 프로필 이미지 URL, 전화번호, 이메일 인증 여부(true / false), 로그인 Provider(EMAIL, KAKAO, NAVER, GOOGLE) 를 반환합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "404_1", description = "해당 사용자 존재하지 않음") + }) + public ResponseEntity> getMyPage(@AuthenticationPrincipal CustomUserDetails userDetails); } diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/config/RedisConfig.java b/src/main/java/com/whereyouad/WhereYouAd/global/config/RedisConfig.java new file mode 100644 index 0000000..1c9f6bf --- /dev/null +++ b/src/main/java/com/whereyouad/WhereYouAd/global/config/RedisConfig.java @@ -0,0 +1,36 @@ +package com.whereyouad.WhereYouAd.global.config; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class RedisConfig { + + @Bean + public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { + // Redis 캐시 설정 정의 + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() + // Key 직렬화: String ("user:profile::1" 처럼 보기 좋게 저장) + .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + // Value 직렬화: JSON (자바 객체 -> JSON 변환 저장) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) + // TTL 설정: 데이터 유효 시간 (30분) + .entryTtl(Duration.ofMinutes(30)) + // null 데이터는 캐싱하지 않음 + .disableCachingNullValues(); + + return RedisCacheManager.builder(connectionFactory) + .cacheDefaults(config) + .build(); + } +} diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.java b/src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.java index 267ee37..f1d9ab3 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.java +++ b/src/main/java/com/whereyouad/WhereYouAd/global/security/SecurityConfig.java @@ -47,6 +47,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ) .authorizeHttpRequests(auth -> auth .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() //swagger 접근 허용 + .requestMatchers("/api/users/my").authenticated() //마이페이지는 인증 필요 .requestMatchers("/api/users/**", "/api/auth/**").permitAll() //로그인, 회원가입, 이메일 인증 접근 허용 .anyRequest().authenticated() //이외 접근은 인증 필요 ) diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetails.java b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetails.java index 196adb8..b5ff7ff 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetails.java +++ b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetails.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.global.security.jwt; +import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider; import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus; import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User; import lombok.Getter; @@ -10,13 +11,13 @@ import java.util.Collection; import java.util.Collections; -import java.util.List; @Getter @RequiredArgsConstructor public class CustomUserDetails implements UserDetails { private final User user; + private final Provider provider; // 일단 User 엔티티에 권한 구분(ADMIN / USER) 가 없기도 하고, // 팀장, 멤버등의 역할 구분은 2차 MVP 에서 진행한다. diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetailsService.java b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetailsService.java index 21f2578..8f6f013 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetailsService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/CustomUserDetailsService.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.global.security.jwt; +import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider; import com.whereyouad.WhereYouAd.domains.user.exception.code.AuthErrorCode; import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User; import com.whereyouad.WhereYouAd.domains.user.persistence.repository.UserRepository; @@ -22,6 +23,6 @@ public UserDetails loadUserByUsername(String email) { User user = userRepository.findUserByEmail(email) .orElseThrow(() -> new AppException(AuthErrorCode.USER_NOT_FOUND)); - return new CustomUserDetails(user); + return new CustomUserDetails(user, Provider.EMAIL); } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java index 317bade..11a0bf6 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtAuthenticationFilter.java @@ -1,6 +1,7 @@ package com.whereyouad.WhereYouAd.global.security.jwt; import com.fasterxml.jackson.databind.ObjectMapper; +import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider; import com.whereyouad.WhereYouAd.domains.user.exception.code.AuthErrorCode; import com.whereyouad.WhereYouAd.global.response.ErrorResponse; import io.jsonwebtoken.ExpiredJwtException; @@ -12,7 +13,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -44,13 +44,19 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse //토큰에서 email 값 추출 String email = jwtTokenProvider.getSubject(token); + String providerStr = jwtTokenProvider.getProvider(token); //& email 값으로 DB 내 해당 email 로 가입한 회원 존재하는지 확인 - UserDetails userDetails = customUserDetailService.loadUserByUsername(email); + CustomUserDetails userDetails = (CustomUserDetails) customUserDetailService.loadUserByUsername(email); + + CustomUserDetails finalUserDetails = new CustomUserDetails( + userDetails.getUser(), + Provider.valueOf(providerStr) + ); //Spring Security 가 인식 가능한 인증 객체(Authentication) 생성 //이미 인증된 상태에서 Security 가 인식 가능하게 만드는 것 임으로 비밀번호(credentials) 필드는 null UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + new UsernamePasswordAuthenticationToken(finalUserDetails, null, finalUserDetails.getAuthorities()); //SecurityContextHolder 에 인증 객체 저장 SecurityContextHolder.getContext().setAuthentication(authentication); diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtTokenProvider.java b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtTokenProvider.java index da40559..a0e3dd8 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/whereyouad/WhereYouAd/global/security/jwt/JwtTokenProvider.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.global.security.jwt; +import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider; import com.whereyouad.WhereYouAd.global.security.jwt.dto.TokenResponse; import com.whereyouad.WhereYouAd.global.security.oauth2.dto.CustomOAuth2User; import io.jsonwebtoken.*; @@ -22,6 +23,7 @@ public class JwtTokenProvider { //JWT 토큰 내 권한 정보를 담을 때 사용하는 key 값 private static final String AUTHORITIES_KEY = "auth"; + private static final String PROVIDER_KEY = "provider"; //HTTP 헤더에 붙일 타입(Bearer {token}) private static final String BEARER_TYPE = "Bearer"; //AccessToken 만료 시간 @@ -43,17 +45,26 @@ public TokenResponse generateToken(Authentication authentication) { // 로그인 종류에 따라 이메일(식별자) 추출 String email; + String provider; + Object principal = authentication.getPrincipal(); if (principal instanceof CustomOAuth2User) { + CustomOAuth2User oAuth2User = (CustomOAuth2User) principal; // 소셜 로그인: CustomOAuth2User에서 이메일 추출 - email = ((CustomOAuth2User) principal).getEmail(); + email = oAuth2User.getEmail(); + + provider = oAuth2User.getProvider().name(); } else if (principal instanceof CustomUserDetails) { + CustomUserDetails userDetails = (CustomUserDetails) principal; // 일반 로그인: UserDetails의 username(email) 추출 - email = ((CustomUserDetails) principal).getUsername(); + email = userDetails.getUsername(); + + provider = userDetails.getProvider().name(); } else { // 그 외의 경우 (기본값) email = authentication.getName(); + provider = Provider.EMAIL.name(); } //사용자 권한(ROLE_USER) 가져와서 문자열로 반환 String authorities = authentication.getAuthorities().stream() @@ -67,6 +78,7 @@ public TokenResponse generateToken(Authentication authentication) { String accessToken = Jwts.builder() .setSubject(email) // Payload "sub": 유저의 이메일(ID) .claim(AUTHORITIES_KEY, authorities) // Payload "auth": "ROLE_USER" + .claim(PROVIDER_KEY, provider) .setExpiration(accessTokenExpireIn) // Payload "exp": 만료 시간 .signWith(key, SignatureAlgorithm.HS512) // Header "alg": HS512 알고리즘으로 서명 .compact(); @@ -118,4 +130,15 @@ private Claims parseClaims(String accessToken) { return e.getClaims(); } } + + public String getProvider(String token) { + Claims claims = parseClaims(token); + String provider = claims.get(PROVIDER_KEY, String.class); + + if (provider == null || provider.isEmpty()) { + return Provider.EMAIL.name(); + } + + return provider; + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/CustomOAuth2User.java b/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/CustomOAuth2User.java index 8a0f8a3..39b093e 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/CustomOAuth2User.java +++ b/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/dto/CustomOAuth2User.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.global.security.oauth2.dto; +import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.OAuth2User; @@ -12,6 +13,7 @@ public class CustomOAuth2User implements OAuth2User { private final OAuth2UserInfo authUserDTO; + private final Provider provider; @Override public Map getAttributes() { @@ -43,4 +45,8 @@ public String getProviderId() { public String getEmail() { return authUserDTO.getEmail(); } + + public Provider getProvider() { + return provider; + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/service/CustomOAuth2UserService.java b/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/service/CustomOAuth2UserService.java index 42e1386..7ebb1da 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/service/CustomOAuth2UserService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/global/security/oauth2/service/CustomOAuth2UserService.java @@ -97,6 +97,6 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic // 공통: OAuth2UserInfo 생성 및 반환 OAuth2UserInfo authUserDTO = UserConverter.toOAuth2UserInfo(user, oAuth2Response); - return new CustomOAuth2User(authUserDTO); + return new CustomOAuth2User(authUserDTO, provider); } }