diff --git a/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java b/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java index 6708558..2523fc3 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java +++ b/src/main/java/com/hyetaekon/hyetaekon/answer/entity/Answer.java @@ -60,7 +60,7 @@ public void suspend() { public String getDisplayContent() { if (this.deletedAt != null) { - return "삭제된 답변입니다."; + return "사용자가 삭제한 답변입니다."; } else if (this.suspendAt != null) { return "관리자에 의해 삭제된 답변입니다."; } diff --git a/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java b/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java index 6ffc1f6..cd14dba 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/bookmark/service/BookmarkService.java @@ -5,16 +5,17 @@ import com.hyetaekon.hyetaekon.common.exception.GlobalException; import com.hyetaekon.hyetaekon.publicservice.entity.PublicService; import com.hyetaekon.hyetaekon.publicservice.repository.PublicServiceRepository; -import com.hyetaekon.hyetaekon.publicservice.service.PublicServiceHandler; -import com.hyetaekon.hyetaekon.publicservice.service.mongodb.ServiceMatchedHandler; import com.hyetaekon.hyetaekon.user.entity.User; import com.hyetaekon.hyetaekon.user.repository.UserRepository; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import static com.hyetaekon.hyetaekon.common.exception.ErrorCode.*; +@Slf4j @Service @Transactional @RequiredArgsConstructor @@ -22,7 +23,6 @@ public class BookmarkService { private final BookmarkRepository bookmarkRepository; private final UserRepository userRepository; private final PublicServiceRepository publicServiceRepository; - private final ServiceMatchedHandler serviceMatchedHandler; public void addBookmark(String serviceId, Long userId) { User user = userRepository.findById(userId) @@ -46,8 +46,6 @@ public void addBookmark(String serviceId, Long userId) { // 북마크 수 증가 publicService.increaseBookmarkCount(); publicServiceRepository.save(publicService); - // 캐시 무효화 추가 - serviceMatchedHandler.refreshMatchedServicesCache(userId); } @Transactional @@ -61,7 +59,7 @@ public void removeBookmark(String serviceId, Long userId) { PublicService publicService = bookmark.getPublicService(); publicService.decreaseBookmarkCount(); publicServiceRepository.save(publicService); - // 캐시 무효화 추가 - serviceMatchedHandler.refreshMatchedServicesCache(userId); } + + } diff --git a/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Comment.java b/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Comment.java index 4f34fd4..43c6486 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Comment.java +++ b/src/main/java/com/hyetaekon/hyetaekon/comment/entity/Comment.java @@ -58,7 +58,7 @@ public void suspend() { public String getDisplayContent() { if (this.deletedAt != null) { - return "삭제된 댓글입니다."; + return "사용자가 삭제한 댓글입니다."; } else if (this.suspendAt != null) { return "관리자에 의해 삭제된 댓글입니다."; } diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityConfig.java b/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityConfig.java index 719bb1e..ef1b20c 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityConfig.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/config/SecurityConfig.java @@ -74,6 +74,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public CorsConfigurationSource corsConfigurationSource() { org.springframework.web.cors.CorsConfiguration configuration = new org.springframework.web.cors.CorsConfiguration(); configuration.addAllowedOrigin("http://localhost:3000"); // 개발 환경 + configuration.addAllowedOrigin("https://hyetaekon.netlify.app"); configuration.addAllowedOrigin("https://hyetaek-on.co.kr"); // 혜택온 도메인 configuration.addAllowedOrigin("https://www.hyetaek-on.co.kr"); configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용 diff --git a/src/main/java/com/hyetaekon/hyetaekon/common/jwt/JwtAuthenticationFilter.java b/src/main/java/com/hyetaekon/hyetaekon/common/jwt/JwtAuthenticationFilter.java index a53a431..2ca50cb 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/common/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/hyetaekon/hyetaekon/common/jwt/JwtAuthenticationFilter.java @@ -43,20 +43,42 @@ protected void doFilterInternal( // access token이 있고, BEARER로 시작한다면 if (authHeader != null && authHeader.startsWith(BEARER)) { - String token = authHeader.substring(BEARER.length()); - // 토큰 검증 - if (jwtTokenProvider.validateToken(token)) { - // 유효한 토큰: 유저 정보 가져옴 - Authentication authentication = jwtTokenProvider.getAuthentication(token); - - // 관리자 API 접근 시 추가 검증 - if (isAdminRequest && !hasAdminRole(authentication.getAuthorities())) { - log.warn("관리자 권한 없이 관리자 리소스에 접근 시도: {}", requestURI); - sendAccessDeniedResponse(response); - return; + String token = authHeader.substring(BEARER.length()).trim(); // trim 추가 + + try { + // 토큰 검증 + if (jwtTokenProvider.validateToken(token)) { + // 유효한 토큰: 유저 정보 가져옴 + Authentication authentication = jwtTokenProvider.getAuthentication(token); + + // 관리자 API 접근 시 추가 검증 + if (isAdminRequest && !hasAdminRole(authentication.getAuthorities())) { + log.warn("관리자 권한 없이 관리자 리소스에 접근 시도: {}", requestURI); + sendAccessDeniedResponse(response); + return; + } + + SecurityContextHolder.getContext().setAuthentication(authentication); + } else { + // 토큰이 유효하지 않은 경우 - 관리자 API는 차단, 일반 API는 진행 + if (isAdminRequest) { + log.warn("유효하지 않은 토큰으로 관리자 리소스 접근 시도: {}", requestURI); + sendUnauthorizedResponse(response); + return; + } + // 일반 API는 인증 없이도 접근 가능한 경우가 있으므로 계속 진행 } + } catch (Exception e) { + // 토큰 처리 중 예외 발생 + log.error("JWT 토큰 처리 중 오류 발생 - URI: {}, Error: {}", requestURI, e.getMessage()); - SecurityContextHolder.getContext().setAuthentication(authentication); + if (isAdminRequest) { + // 관리자 API는 예외 발생 시 차단 + sendUnauthorizedResponse(response); + return; + } + // 일반 API는 인증 실패로 처리하고 서비스 계속 진행 (SecurityContext 비워둠) + SecurityContextHolder.clearContext(); } } else if (isAdminRequest) { // 관리자 API 접근 시 토큰 없으면 Unauthorized 응답 diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostDetailResponseDto.java b/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostDetailResponseDto.java index 6335aa6..dd558bd 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostDetailResponseDto.java +++ b/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostDetailResponseDto.java @@ -24,6 +24,7 @@ public class PostDetailResponseDto { private String urlTitle; private String urlPath; private String tags; - private List imageUrls; // ✅ 이미지 URL 리스트 추가 + private List images; private boolean recommended; // 현재 로그인한 사용자의 추천 여부 + } diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostImageResponseDto.java b/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostImageResponseDto.java new file mode 100644 index 0000000..b7973c0 --- /dev/null +++ b/src/main/java/com/hyetaekon/hyetaekon/post/dto/PostImageResponseDto.java @@ -0,0 +1,13 @@ +package com.hyetaekon.hyetaekon.post.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PostImageResponseDto { + private Long imageId; // 이미지 ID + private String imageUrl; // 이미지 URL +} diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java b/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java index 26d1f78..1be58ae 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java +++ b/src/main/java/com/hyetaekon/hyetaekon/post/entity/Post.java @@ -126,7 +126,7 @@ public void suspend() { public String getDisplayContent() { if (this.deletedAt != null) { - return "삭제된 게시글입니다."; + return "사용자가 삭제한 게시글입니다."; } else if (this.suspendAt != null) { return "관리자에 의해 삭제된 게시글입니다."; } diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostImageMapper.java b/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostImageMapper.java index d795d9c..fb6695f 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostImageMapper.java +++ b/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostImageMapper.java @@ -1,8 +1,10 @@ package com.hyetaekon.hyetaekon.post.mapper; +import com.hyetaekon.hyetaekon.post.dto.PostImageResponseDto; import com.hyetaekon.hyetaekon.post.entity.Post; import com.hyetaekon.hyetaekon.post.entity.PostImage; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.ReportingPolicy; import java.util.Collections; @@ -13,6 +15,22 @@ @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) public interface PostImageMapper { + // PostImage를 PostImageResponseDto로 변환 + @Mapping(source = "id", target = "imageId") + PostImageResponseDto toResponseDto(PostImage postImage); + + // List 변환 + default List toResponseDtoList(List postImages) { + if (postImages == null || postImages.isEmpty()) { + return Collections.emptyList(); + } + + return postImages.stream() + .filter(img -> img.getDeletedAt() == null) + .map(this::toResponseDto) + .collect(Collectors.toList()); + } + // 게시글 이미지 변환 default PostImage toPostImage(String url, Post post) { if (url == null || post == null) { @@ -35,16 +53,4 @@ default List toEntityList(List uploadedUrls, Post post) { .collect(Collectors.toList()); } - // List → List 변환 - default List toImageUrls(List postImages) { - if (postImages == null || postImages.isEmpty()) { - return Collections.emptyList(); - } - - return postImages.stream() - .filter(img -> img.getDeletedAt() == null) - .map(PostImage::getImageUrl) - .collect(Collectors.toList()); - } - } diff --git a/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostMapper.java b/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostMapper.java index 6d5d86d..7c696ef 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostMapper.java +++ b/src/main/java/com/hyetaekon/hyetaekon/post/mapper/PostMapper.java @@ -40,7 +40,7 @@ public interface PostMapper { @Mapping(target = "content", expression = "java(post.getDisplayContent())") @Mapping(source = "postType.koreanName", target = "postType") @Mapping(target = "recommended", constant = "false") - @Mapping(source = "postImages", target = "imageUrls") + @Mapping(source = "postImages", target = "images") PostDetailResponseDto toPostDetailDto(Post post); } diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/ServiceMatchedController.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/ServiceMatchedController.java index fe59802..ee9ab37 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/ServiceMatchedController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/controller/mongodb/ServiceMatchedController.java @@ -1,7 +1,6 @@ package com.hyetaekon.hyetaekon.publicservice.controller.mongodb; import com.hyetaekon.hyetaekon.common.jwt.CustomUserDetails; -import com.hyetaekon.hyetaekon.common.util.AuthenticateUser; import com.hyetaekon.hyetaekon.publicservice.dto.PublicServiceListResponseDto; import com.hyetaekon.hyetaekon.publicservice.service.mongodb.ServiceMatchedHandler; import jakarta.validation.constraints.Max; diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/CacheType.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/CacheType.java index e09d57d..2c054a9 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/CacheType.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/entity/CacheType.java @@ -8,7 +8,6 @@ public enum CacheType { SERVICE_AUTOCOMPLETE("serviceAutocomplete", 2, 1200), // 자동완성 캐시 FILTER_OPTIONS("filterOptions", 24, 50), // 필터 옵션 캐시 (하루 유지) - MATCHED_SERVICES("matchedServices", 2, 100), // 맞춤 서비스 캐시 SERVICE_BASIC_INFO("serviceBasicInfo", 12, 1000); // 서비스 기본 정보 캐시 private final String cacheName; diff --git a/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/mongodb/ServiceMatchedHandler.java b/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/mongodb/ServiceMatchedHandler.java index 499d2b8..6dbf262 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/mongodb/ServiceMatchedHandler.java +++ b/src/main/java/com/hyetaekon/hyetaekon/publicservice/service/mongodb/ServiceMatchedHandler.java @@ -13,8 +13,6 @@ import com.hyetaekon.hyetaekon.userInterest.repository.UserInterestRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -40,7 +38,6 @@ public class ServiceMatchedHandler { /** * 사용자 맞춤 공공서비스 추천 - 사용자 정보 및 검색 기록 기반 */ - @Cacheable(value = "matchedServices", key = "#userId", unless = "#result.isEmpty()") public List getPersonalizedServices(Long userId, int size) { // 사용자 정보 조회 User user = userRepository.findById(userId) @@ -89,8 +86,4 @@ public List getPersonalizedServices(Long userId, i .collect(Collectors.toList()); } - @CacheEvict(value = "matchedServices", key = "#userId") - public void refreshMatchedServicesCache(Long userId) { - // 캐시 갱신 메서드 - } } diff --git a/src/main/java/com/hyetaekon/hyetaekon/searchHistory/Service/SearchHistoryService.java b/src/main/java/com/hyetaekon/hyetaekon/searchHistory/Service/SearchHistoryService.java index b96c4b5..6d0f43b 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/searchHistory/Service/SearchHistoryService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/searchHistory/Service/SearchHistoryService.java @@ -17,7 +17,6 @@ @Service @RequiredArgsConstructor public class SearchHistoryService { - private final RedisTemplate redisTemplate; // Redis 키 관련 상수 diff --git a/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java b/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java index 40af042..b10db73 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/user/controller/UserController.java @@ -49,7 +49,7 @@ public ResponseEntity registerUser(@RequestBody @Valid Us return ResponseEntity.status(HttpStatus.CREATED).body(userSignUpResponseDto); } - // 회원 정보 조회 api + // 개인 정보 조회 api @GetMapping("/users/me") public ResponseEntity getMyInfo(@AuthenticationPrincipal CustomUserDetails userDetails) { Long userId = userDetails.getId(); @@ -57,6 +57,7 @@ public ResponseEntity getMyInfo(@AuthenticationPrincipal Custom return ResponseEntity.ok(userInfo); } + // 타회원 정보 조회 @GetMapping("/users/{userId}") public ResponseEntity getUserById(@PathVariable Long userId) { UserResponseDto user = userService.getUserById(userId); @@ -64,7 +65,7 @@ public ResponseEntity getUserById(@PathVariable Long userId) { } - // 회원 정보 수정 api + // 개인 정보 수정 api @PutMapping("/users/me/profile") public ResponseEntity updateMyProfile( @AuthenticationPrincipal CustomUserDetails userDetails, @@ -86,18 +87,6 @@ public ResponseEntity updateMyPassword( } // 회원 탈퇴 - /*@DeleteMapping("/users/me") - public ResponseEntity deleteUser( - @AuthenticationPrincipal CustomUserDetails customUserDetails, - @RequestBody UserDeleteRequestDto deleteRequestDto, - @CookieValue(name = "refreshToken", required = false) String refreshToken, - @RequestHeader("Authorization") String authHeader - ) { - String accessToken = authHeader.replace("Bearer ", ""); - userService.deleteUser(customUserDetails.getId(), deleteRequestDto.getDeleteReason(), accessToken, refreshToken); - - return ResponseEntity.noContent().build(); - }*/ @DeleteMapping("/users/me") public ResponseEntity deleteUser( @AuthenticationPrincipal CustomUserDetails userDetails, @@ -105,29 +94,26 @@ public ResponseEntity deleteUser( @CookieValue(name = "refreshToken", required = false) String refreshToken, @RequestHeader("Authorization") String authHeader ) { - log.debug("회원 탈퇴 요청 - 인증 객체: {}", userDetails); + String accessToken = authHeader.replace("Bearer ", ""); + // 인증 객체가 null인 경우 토큰에서 직접 사용자 정보 추출 if (userDetails == null) { - // 인증 객체가 null일 경우 토큰에서 직접 정보 추출 try { - String token = authHeader.replace("Bearer ", ""); - Claims claims = jwtTokenParser.parseClaims(token); + Claims claims = jwtTokenParser.parseClaims(accessToken); String realId = claims.getSubject(); User user = userRepository.findByRealIdAndDeletedAtIsNull(realId) .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_REAL_ID)); - userService.deleteUser(user.getId(), deleteRequestDto.getDeleteReason(), token, refreshToken); - return ResponseEntity.noContent().build(); + userService.deleteUser(user.getId(), deleteRequestDto.getDeleteReason(), accessToken, refreshToken); } catch (Exception e) { - log.error("회원 탈퇴 처리 실패: {}", e.getMessage()); throw new GlobalException(ErrorCode.DELETE_USER_DENIED); } + } else { + // 정상적인 인증 객체가 있는 경우 + userService.deleteUser(userDetails.getId(), deleteRequestDto.getDeleteReason(), accessToken, refreshToken); } - // 기존 로직 - String accessToken = authHeader.replace("Bearer ", ""); - userService.deleteUser(userDetails.getId(), deleteRequestDto.getDeleteReason(), accessToken, refreshToken); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/com/hyetaekon/hyetaekon/userInterest/controller/UserInterestController.java b/src/main/java/com/hyetaekon/hyetaekon/userInterest/controller/UserInterestController.java index 3de8529..3b64267 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/userInterest/controller/UserInterestController.java +++ b/src/main/java/com/hyetaekon/hyetaekon/userInterest/controller/UserInterestController.java @@ -24,7 +24,7 @@ public class UserInterestController { private final UserInterestService userInterestService; - // 선택할 키워드 목록 조회 + // 선택할 관심사 목록 조회 @GetMapping public ResponseEntity getAvailableInterests() { // Enum에서 카테고리별 displayName 값 추출 @@ -36,7 +36,7 @@ public ResponseEntity getAvailableInterests() { return ResponseEntity.ok(new CategorizedInterestsResponseDto(categorizedInterests)); } - // 모든 관심사와 사용자 선택 여부 조회 + // 선택한 관심사 목록 조회 @GetMapping("/me") public ResponseEntity getMyInterestsWithSelection( @AuthenticationPrincipal CustomUserDetails userDetails) { diff --git a/src/main/java/com/hyetaekon/hyetaekon/userInterest/entity/UserInterest.java b/src/main/java/com/hyetaekon/hyetaekon/userInterest/entity/UserInterest.java index fb696c2..b7ace16 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/userInterest/entity/UserInterest.java +++ b/src/main/java/com/hyetaekon/hyetaekon/userInterest/entity/UserInterest.java @@ -17,7 +17,7 @@ public class UserInterest { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; diff --git a/src/main/java/com/hyetaekon/hyetaekon/userInterest/service/UserInterestService.java b/src/main/java/com/hyetaekon/hyetaekon/userInterest/service/UserInterestService.java index ff459c2..32ca2d9 100644 --- a/src/main/java/com/hyetaekon/hyetaekon/userInterest/service/UserInterestService.java +++ b/src/main/java/com/hyetaekon/hyetaekon/userInterest/service/UserInterestService.java @@ -78,7 +78,7 @@ public void saveUserInterests(Long userId, List selectedInterests) { } } - User user = userRepository.findByIdAndDeletedAtIsNull(userId) + User user = userRepository.findByIdAndDeletedAtIsNullWithInterests(userId) .orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID)); // 기존 관심사 제거