diff --git a/src/main/java/ita/tinybite/domain/party/controller/PartyController.java b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java index 6baaa25..4db4ce2 100644 --- a/src/main/java/ita/tinybite/domain/party/controller/PartyController.java +++ b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java @@ -16,6 +16,7 @@ import ita.tinybite.domain.party.dto.response.PartyListResponse; import ita.tinybite.domain.party.entity.PartyParticipant; import ita.tinybite.domain.party.enums.PartyCategory; +import ita.tinybite.domain.party.service.PartySearchService; import ita.tinybite.domain.party.service.PartyService; import ita.tinybite.global.response.APIResponse; import jakarta.validation.Valid; @@ -34,7 +35,7 @@ public class PartyController { private final PartyService partyService; - private final JwtTokenProvider jwtTokenProvider; + private final PartySearchService partySearchService; @Operation( @@ -459,6 +460,43 @@ public APIResponse getParty( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size ) { - return APIResponse.success(partyService.searchParty(q, category, page, size)); + return APIResponse.success(partySearchService.searchParty(q, category, page, size)); + } + + @Operation( + summary = "최근 검색어 조회", + description = """ + 검색 돋보기 클릭 시 보이는 최근 검색어를 조회합니다.
+ 한 번에 20개가 조회됩니다. + """ + ) + @GetMapping("/search/log") + public APIResponse> getRecentLog() { + return APIResponse.success(partySearchService.getLog()); + } + + @Operation( + summary = "특정 최근 검색어 삭제", + description = """ + 최근 검색어에서 특정 검색어를 삭제합니다.
+ 이때 검색어에 대한 Id값은 없고, 최근 검색어 자체를 keyword에 넣어주시면 됩니다. + """ + ) + @DeleteMapping("/search/log/{keyword}") + public APIResponse deleteRecentLog(@PathVariable String keyword) { + partySearchService.deleteLog(keyword); + return APIResponse.success(); + } + + @Operation( + summary = "모든 최근 검색어 삭제", + description = """ + 특정 유저에 대한 모든 최근 검색어를 삭제합니다. + """ + ) + @DeleteMapping("/search/log") + public APIResponse deleteRecentLogAll() { + partySearchService.deleteAllLog(); + return APIResponse.success(); } } \ No newline at end of file diff --git a/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java b/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java index 16a0087..0faf300 100644 --- a/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java +++ b/src/main/java/ita/tinybite/domain/party/repository/PartyParticipantRepository.java @@ -80,4 +80,7 @@ List findActivePartiesByUserIdExcludingHost( @Param("userId") Long userId, @Param("partyStatus") PartyStatus partyStatus, @Param("participantStatus") ParticipantStatus participantStatus - );} \ No newline at end of file + ); + + int countByPartyIdAndStatusAndUserIdNot(Long partyId, ParticipantStatus participantStatus, Long userId); +} \ No newline at end of file diff --git a/src/main/java/ita/tinybite/domain/party/service/PartySearchService.java b/src/main/java/ita/tinybite/domain/party/service/PartySearchService.java new file mode 100644 index 0000000..0cf87c2 --- /dev/null +++ b/src/main/java/ita/tinybite/domain/party/service/PartySearchService.java @@ -0,0 +1,86 @@ +package ita.tinybite.domain.party.service; + +import ita.tinybite.domain.auth.service.SecurityProvider; +import ita.tinybite.domain.party.dto.request.PartyQueryListResponse; +import ita.tinybite.domain.party.dto.response.PartyCardResponse; +import ita.tinybite.domain.party.entity.Party; +import ita.tinybite.domain.party.enums.ParticipantStatus; +import ita.tinybite.domain.party.enums.PartyCategory; +import ita.tinybite.domain.party.repository.PartyParticipantRepository; +import ita.tinybite.domain.party.repository.PartyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class PartySearchService { + + private final PartyRepository partyRepository; + private final PartyParticipantRepository participantRepository; + private final StringRedisTemplate redisTemplate; + private final SecurityProvider securityProvider; + + + private static final String KEY_PREFIX = "recent_search:"; + private String key(Long userId) { + return KEY_PREFIX + userId; + } + + // 파티 검색 조회 + public PartyQueryListResponse searchParty(String q, PartyCategory category, int page, int size) { + Long userId = securityProvider.getCurrentUser().getUserId(); + + // recent_search:{userId} + String key = key(userId); + + redisTemplate.opsForZSet().remove(key, q); + redisTemplate.opsForZSet().add(key, q, System.currentTimeMillis()); + + Pageable pageable = PageRequest.of(page, size); + + // category가 없을 시에는 ALL로 처리 + Page result = (category == null || category == PartyCategory.ALL) + ? partyRepository.findByTitleContaining(q, pageable) + : partyRepository.findByTitleContainingAndCategory(q, category, pageable); + + List partyCardResponseList = result.stream() + .map(party -> { + int currentParticipants = participantRepository + .countByPartyIdAndStatus(party.getId(), ParticipantStatus.APPROVED); + return PartyCardResponse.from(party, currentParticipants); + }) + .toList(); + + return PartyQueryListResponse.builder() + .parties(partyCardResponseList) + .hasNext(result.hasNext()) + .build(); + } + + + // 최근 검색어 20개 조회 + public List getLog() { + Long userId = securityProvider.getCurrentUser().getUserId(); + return redisTemplate.opsForZSet() + .reverseRange(key(userId), 0, 19) + .stream().toList(); + } + + public void deleteLog(String keyword) { + Long userId = securityProvider.getCurrentUser().getUserId(); + redisTemplate.opsForZSet().remove(key(userId), keyword); + } + + public void deleteAllLog() { + Long userId = securityProvider.getCurrentUser().getUserId(); + redisTemplate.delete(key(userId)); + } +} diff --git a/src/main/java/ita/tinybite/domain/party/service/PartyService.java b/src/main/java/ita/tinybite/domain/party/service/PartyService.java index cfd5232..08b863b 100644 --- a/src/main/java/ita/tinybite/domain/party/service/PartyService.java +++ b/src/main/java/ita/tinybite/domain/party/service/PartyService.java @@ -334,7 +334,6 @@ private PartyDetailResponse convertToDetailResponse(Party party, double distance .build(); } - @Transactional public void updateParty(Long partyId, Long userId, PartyUpdateRequest request) { Party party = partyRepository.findById(partyId) .orElseThrow(() -> new IllegalArgumentException("파티를 찾을 수 없습니다")); @@ -344,30 +343,33 @@ public void updateParty(Long partyId, Long userId, PartyUpdateRequest request) { throw new IllegalStateException("파티장만 수정할 수 있습니다"); } - // 승인된 파티원 확인 - boolean hasApprovedParticipants = party.getCurrentParticipants() > 1; + // 호스트 제외한 승인된 파티원 수 확인 + int approvedParticipantsExcludingHost = participantRepository + .countByPartyIdAndStatusAndUserIdNot( + partyId, + ParticipantStatus.APPROVED, + userId + ); - if (hasApprovedParticipants) { - // 승인된 파티원이 있는 경우: 설명과 이미지만 수정 가능 + if (approvedParticipantsExcludingHost > 0) { + // 다른 승인된 파티원이 있는 경우: 설명과 이미지만 수정 가능 party.updateLimitedFields( request.getDescription(), request.getImages() ); } else { - // 승인된 파티원이 없는 경우: 모든 항목 수정 가능 + // 승인된 파티원이 없는 경우, 호스트 혼자인 경우: 모든 항목 수정 가능 party.updateAllFields( request.getTitle(), request.getTotalPrice(), request.getMaxParticipants(), - getPickUpLocationIfExists(request,party), -// new PickupLocation(request.getPickupLocation()), + getPickUpLocationIfExists(request, party), request.getProductLink(), request.getDescription(), request.getImages() ); } } - private PickupLocation getPickUpLocationIfExists(PartyUpdateRequest request, Party currentParty) { if (request.getPickupLocation() == null) { return currentParty.getPickupLocation(); @@ -668,28 +670,6 @@ private String formatDistanceIfExists(Double distance) { return distance!= null? DistanceCalculator.formatDistance(distance):null; } - // 파티 검색 조회 - public PartyQueryListResponse searchParty(String q, PartyCategory category, int page, int size) { - Pageable pageable = PageRequest.of(page, size); - // category가 없을 시에는 ALL로 처리 - Page result = (category == null || category == PartyCategory.ALL) - ? partyRepository.findByTitleContaining(q, pageable) - : partyRepository.findByTitleContainingAndCategory(q, category, pageable); - - - List partyCardResponseList = result.stream() - .map(party -> { - int currentParticipants = participantRepository - .countByPartyIdAndStatus(party.getId(), ParticipantStatus.APPROVED); - return PartyCardResponse.from(party, currentParticipants); - }) - .toList(); - - return PartyQueryListResponse.builder() - .parties(partyCardResponseList) - .hasNext(result.hasNext()) - .build(); - } } diff --git a/src/main/java/ita/tinybite/global/sms/service/SmsAuthService.java b/src/main/java/ita/tinybite/global/sms/service/SmsAuthService.java index b1cdf79..53a4664 100644 --- a/src/main/java/ita/tinybite/global/sms/service/SmsAuthService.java +++ b/src/main/java/ita/tinybite/global/sms/service/SmsAuthService.java @@ -27,6 +27,10 @@ public SmsAuthService(SmsService smsService, RedisTemplate redis this.authCodeGenerator = AuthCodeGenerator.getInstance(); } + private static final String KEY_PREFIX = "sms:"; + private String key(String phone) { + return KEY_PREFIX + phone; + } /** * 1. 인증코드 생성
* 2. 주어진 폰번호로 인증코드 전송
@@ -37,7 +41,7 @@ public void send(String phone) { String smsAuthCode = authCodeGenerator.getAuthCode(); smsService.send(phone.replaceAll("-", ""), smsAuthCode); - redisTemplate.opsForValue().set(phone, smsAuthCode, EXPIRE_TIME, TimeUnit.MILLISECONDS); + redisTemplate.opsForValue().set(key(phone), smsAuthCode, EXPIRE_TIME, TimeUnit.MILLISECONDS); } /** @@ -48,7 +52,7 @@ public void send(String phone) { public void check(CheckReqDto req) { validatePhoneNumber(req.phone()); - String authCode = redisTemplate.opsForValue().get(req.phone()); + String authCode = redisTemplate.opsForValue().get(key(req.phone())); if(authCode == null) throw BusinessException.of(AuthErrorCode.EXPIRED_AUTH_CODE);