Skip to content
Merged

Dev #85

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void suspend() {

public String getDisplayContent() {
if (this.deletedAt != null) {
return "μ‚­μ œλœ λ‹΅λ³€μž…λ‹ˆλ‹€.";
return "μ‚¬μš©μžκ°€ μ‚­μ œν•œ λ‹΅λ³€μž…λ‹ˆλ‹€.";
} else if (this.suspendAt != null) {
return "κ΄€λ¦¬μžμ— μ˜ν•΄ μ‚­μ œλœ λ‹΅λ³€μž…λ‹ˆλ‹€.";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@
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
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)
Expand All @@ -46,8 +46,6 @@ public void addBookmark(String serviceId, Long userId) {
// 뢁마크 수 증가
publicService.increaseBookmarkCount();
publicServiceRepository.save(publicService);
// μΊμ‹œ λ¬΄νš¨ν™” μΆ”κ°€
serviceMatchedHandler.refreshMatchedServicesCache(userId);
}

@Transactional
Expand All @@ -61,7 +59,7 @@ public void removeBookmark(String serviceId, Long userId) {
PublicService publicService = bookmark.getPublicService();
publicService.decreaseBookmarkCount();
publicServiceRepository.save(publicService);
// μΊμ‹œ λ¬΄νš¨ν™” μΆ”κ°€
serviceMatchedHandler.refreshMatchedServicesCache(userId);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public void suspend() {

public String getDisplayContent() {
if (this.deletedAt != null) {
return "μ‚­μ œλœ λŒ“κΈ€μž…λ‹ˆλ‹€.";
return "μ‚¬μš©μžκ°€ μ‚­μ œν•œ λŒ“κΈ€μž…λ‹ˆλ‹€.";
} else if (this.suspendAt != null) {
return "κ΄€λ¦¬μžμ— μ˜ν•΄ μ‚­μ œλœ λŒ“κΈ€μž…λ‹ˆλ‹€.";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 λ©”μ„œλ“œ ν—ˆμš©
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 응닡
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class PostDetailResponseDto {
private String urlTitle;
private String urlPath;
private String tags;
private List<String> imageUrls; // βœ… 이미지 URL 리슀트 μΆ”κ°€
private List<PostImageResponseDto> images;
private boolean recommended; // ν˜„μž¬ λ‘œκ·ΈμΈν•œ μ‚¬μš©μžμ˜ μΆ”μ²œ μ—¬λΆ€

}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public void suspend() {

public String getDisplayContent() {
if (this.deletedAt != null) {
return "μ‚­μ œλœ κ²Œμ‹œκΈ€μž…λ‹ˆλ‹€.";
return "μ‚¬μš©μžκ°€ μ‚­μ œν•œ κ²Œμ‹œκΈ€μž…λ‹ˆλ‹€.";
} else if (this.suspendAt != null) {
return "κ΄€λ¦¬μžμ— μ˜ν•΄ μ‚­μ œλœ κ²Œμ‹œκΈ€μž…λ‹ˆλ‹€.";
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<PostImageResponseDto> toResponseDtoList(List<PostImage> 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) {
Expand All @@ -35,16 +53,4 @@ default List<PostImage> toEntityList(List<String> uploadedUrls, Post post) {
.collect(Collectors.toList());
}

// List<PostImage> β†’ List<String> λ³€ν™˜
default List<String> toImageUrls(List<PostImage> postImages) {
if (postImages == null || postImages.isEmpty()) {
return Collections.emptyList();
}

return postImages.stream()
.filter(img -> img.getDeletedAt() == null)
.map(PostImage::getImageUrl)
.collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,7 +38,6 @@ public class ServiceMatchedHandler {
/**
* μ‚¬μš©μž 맞좀 κ³΅κ³΅μ„œλΉ„μŠ€ μΆ”μ²œ - μ‚¬μš©μž 정보 및 검색 기둝 기반
*/
@Cacheable(value = "matchedServices", key = "#userId", unless = "#result.isEmpty()")
public List<PublicServiceListResponseDto> getPersonalizedServices(Long userId, int size) {
// μ‚¬μš©μž 정보 쑰회
User user = userRepository.findById(userId)
Expand Down Expand Up @@ -89,8 +86,4 @@ public List<PublicServiceListResponseDto> getPersonalizedServices(Long userId, i
.collect(Collectors.toList());
}

@CacheEvict(value = "matchedServices", key = "#userId")
public void refreshMatchedServicesCache(Long userId) {
// μΊμ‹œ κ°±μ‹  λ©”μ„œλ“œ
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
@Service
@RequiredArgsConstructor
public class SearchHistoryService {

private final RedisTemplate<String, String> redisTemplate;

// Redis ν‚€ κ΄€λ ¨ μƒμˆ˜
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,23 @@ public ResponseEntity<UserSignUpResponseDto> registerUser(@RequestBody @Valid Us
return ResponseEntity.status(HttpStatus.CREATED).body(userSignUpResponseDto);
}

// νšŒμ› 정보 쑰회 api
// 개인 정보 쑰회 api
@GetMapping("/users/me")
public ResponseEntity<UserResponseDto> getMyInfo(@AuthenticationPrincipal CustomUserDetails userDetails) {
Long userId = userDetails.getId();
UserResponseDto userInfo = userService.getMyInfo(userId);
return ResponseEntity.ok(userInfo);
}

// νƒ€νšŒμ› 정보 쑰회
@GetMapping("/users/{userId}")
public ResponseEntity<UserResponseDto> getUserById(@PathVariable Long userId) {
UserResponseDto user = userService.getUserById(userId);
return ResponseEntity.ok(user);
}


// νšŒμ› 정보 μˆ˜μ • api
// 개인 정보 μˆ˜μ • api
@PutMapping("/users/me/profile")
public ResponseEntity<UserResponseDto> updateMyProfile(
@AuthenticationPrincipal CustomUserDetails userDetails,
Expand All @@ -86,48 +87,33 @@ public ResponseEntity<Void> updateMyPassword(
}

// νšŒμ› νƒˆν‡΄
/*@DeleteMapping("/users/me")
public ResponseEntity<Void> 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<Void> deleteUser(
@AuthenticationPrincipal CustomUserDetails userDetails,
@RequestBody UserDeleteRequestDto deleteRequestDto,
@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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class UserInterestController {

private final UserInterestService userInterestService;

// 선택할 ν‚€μ›Œλ“œ λͺ©λ‘ 쑰회
// 선택할 관심사 λͺ©λ‘ 쑰회
@GetMapping
public ResponseEntity<CategorizedInterestsResponseDto> getAvailableInterests() {
// Enumμ—μ„œ μΉ΄ν…Œκ³ λ¦¬λ³„ displayName κ°’ μΆ”μΆœ
Expand All @@ -36,7 +36,7 @@ public ResponseEntity<CategorizedInterestsResponseDto> getAvailableInterests() {
return ResponseEntity.ok(new CategorizedInterestsResponseDto(categorizedInterests));
}

// λͺ¨λ“  관심사와 μ‚¬μš©μž 선택 μ—¬λΆ€ 쑰회
// μ„ νƒν•œ 관심사 λͺ©λ‘ 쑰회
@GetMapping("/me")
public ResponseEntity<CategorizedInterestsWithSelectionDto> getMyInterestsWithSelection(
@AuthenticationPrincipal CustomUserDetails userDetails) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public void saveUserInterests(Long userId, List<String> selectedInterests) {
}
}

User user = userRepository.findByIdAndDeletedAtIsNull(userId)
User user = userRepository.findByIdAndDeletedAtIsNullWithInterests(userId)
.orElseThrow(() -> new GlobalException(ErrorCode.USER_NOT_FOUND_BY_ID));

// κΈ°μ‘΄ 관심사 제거
Expand Down