diff --git a/.gitignore b/.gitignore index 91d8ad1..dea977d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ build/ !**/src/test/**/build/ /src/main/resources/firebase/ /src/main/resources/firebase/suppin-a5657-firebase-adminsdk-s75m9-d65cc88029.json +/src/main/resources/drivers/ + ### STS ### .apt_generated diff --git a/src/main/java/com/cmc/suppin/event/crawl/controller/CommentApi.java b/src/main/java/com/cmc/suppin/event/crawl/controller/CommentApi.java index 578071a..81353b3 100644 --- a/src/main/java/com/cmc/suppin/event/crawl/controller/CommentApi.java +++ b/src/main/java/com/cmc/suppin/event/crawl/controller/CommentApi.java @@ -4,6 +4,7 @@ import com.cmc.suppin.event.crawl.controller.dto.CommentResponseDTO; import com.cmc.suppin.event.crawl.service.CommentService; import com.cmc.suppin.global.response.ApiResponse; +import com.cmc.suppin.global.response.ResponseCode; import com.cmc.suppin.global.security.reslover.Account; import com.cmc.suppin.global.security.reslover.CurrentAccount; import io.swagger.v3.oas.annotations.Operation; @@ -44,7 +45,7 @@ public ResponseEntity> get } @PostMapping("/draft-winners") - @Operation(summary = "당첨자 랜덤 추첨 결과 리스트 조회 API(댓글 이벤트)", description = "주어진 조건에 따라 이벤트의 당첨자를 추첨합니다.") + @Operation(summary = "당첨자 랜덤 추첨 결과 조회 API(댓글 이벤트)", description = "주어진 조건에 따라 이벤트의 당첨자를 추첨합니다.") public ResponseEntity> drawWinners( @RequestBody @Valid CommentRequestDTO.WinnerRequestDTO request, @CurrentAccount Account account) { @@ -61,4 +62,11 @@ public ResponseEntity>> ge List filteredWinners = commentService.getCommentsByKeyword(eventId, keyword, account.userId()); return ResponseEntity.ok(ApiResponse.of(filteredWinners)); } + + @DeleteMapping("/") + @Operation(summary = "댓글 이벤트 당첨자 리스트 삭제 API(당첨자 재추첨 시, 기존 당첨자 리스트를 삭제한 후 진행해야 합니다.", description = "모든 당첨자들의 isWinner 값을 false로 변경합니다.") + public ResponseEntity> deleteWinners(@RequestParam("eventId") Long eventId, @CurrentAccount Account account) { + commentService.deleteWinners(eventId); + return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); + } } diff --git a/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentRequestDTO.java b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentRequestDTO.java index 6dd74de..14bd8a2 100644 --- a/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentRequestDTO.java +++ b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentRequestDTO.java @@ -1,6 +1,5 @@ package com.cmc.suppin.event.crawl.controller.dto; -import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -27,14 +26,10 @@ public static class CommentListRequestDTO { @Builder public static class WinnerRequestDTO { private Long eventId; - private int winnerCount; - private int minLength; - - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") + private Integer winnerCount; + private Integer minLength; private String startDate; - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String endDate; - private List keywords; } } diff --git a/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java index 2b3f462..895fd67 100644 --- a/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java +++ b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java @@ -42,6 +42,19 @@ public static class WinnerResponseDTO { private List winners; } + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CommentEventWinnersWithCriteria { + private Integer winnerCount; + private Integer minLength; + private String startDate; + private String endDate; + private List keywords; + private List winners; + } + @Getter @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/com/cmc/suppin/event/crawl/domain/repository/CommentRepository.java b/src/main/java/com/cmc/suppin/event/crawl/domain/repository/CommentRepository.java index 872a5f1..0912e57 100644 --- a/src/main/java/com/cmc/suppin/event/crawl/domain/repository/CommentRepository.java +++ b/src/main/java/com/cmc/suppin/event/crawl/domain/repository/CommentRepository.java @@ -26,4 +26,7 @@ public interface CommentRepository extends JpaRepository { List findByEventIdAndCommentTextContaining(Long eventId, String keyword); List findByEventIdAndIsWinnerTrue(Long eventId); + + + boolean existsByUrlAndEventId(String url, Long eventId); } diff --git a/src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java b/src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java index 0ca9d05..de3d477 100644 --- a/src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java +++ b/src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java @@ -48,7 +48,7 @@ public CommentResponseDTO.CrawledCommentListDTO getComments(Long eventId, String int totalComments = commentRepository.countByEventIdAndUrl(eventId, url); - String crawlTime = comments.isEmpty() ? "" : comments.getContent().get(0).getCrawlTime().format(DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm")); + String crawlTime = comments.isEmpty() ? "" : comments.getContent().get(0).getCrawlTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); return CommentConverter.toCommentListDTO(comments.getContent(), crawlTime, totalComments); } @@ -62,15 +62,35 @@ public CommentResponseDTO.WinnerResponseDTO drawWinners(CommentRequestDTO.Winner Event event = eventRepository.findByIdAndMemberId(request.getEventId(), member.getId()) .orElseThrow(() -> new IllegalArgumentException("Event not found")); - DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); - LocalDateTime startDateTime = LocalDateTime.parse(request.getStartDate(), dateTimeFormatter); - LocalDateTime endDateTime = LocalDateTime.parse(request.getEndDate(), dateTimeFormatter); + // 당첨자 선별 조건 Event 엔티티에 저장 + event.setSelectionCriteria(request); + eventRepository.save(event); - List comments = commentRepository.findByEventIdAndCommentDateBetween(event.getId(), startDateTime, endDateTime); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + LocalDateTime startDateTime = null; + LocalDateTime endDateTime = null; + + // 날짜 필터링을 위한 조건 설정 + if (request.getStartDate() != null && !request.getStartDate().isEmpty()) { + startDateTime = LocalDateTime.parse(request.getStartDate(), dateTimeFormatter); + } + if (request.getEndDate() != null && !request.getEndDate().isEmpty()) { + endDateTime = LocalDateTime.parse(request.getEndDate(), dateTimeFormatter); + } + + List comments; + + // 날짜 조건이 있을 경우에만 필터링, 없으면 전체 댓글을 조회 + if (startDateTime != null && endDateTime != null) { + comments = commentRepository.findByEventIdAndCommentDateBetween(event.getId(), startDateTime, endDateTime); + } else { + comments = commentRepository.findByEventId(event.getId()); + } // 키워드 필터링(OR 로직) 및 minLength 필터링 추가 List filteredComments = comments.stream() - .filter(comment -> request.getKeywords().stream().anyMatch(keyword -> comment.getCommentText().contains(keyword))) + .filter(comment -> request.getKeywords().isEmpty() || request.getKeywords().stream().anyMatch(keyword -> comment.getCommentText().contains(keyword))) .filter(comment -> comment.getCommentText().length() >= request.getMinLength()) .collect(Collectors.toList()); @@ -109,15 +129,37 @@ public List filterWinnersByKeyword(List getCommentEventWinners(Long eventId, String userId) { + public CommentResponseDTO.CommentEventWinnersWithCriteria getCommentEventWinnersWithCriteria(Long eventId, String userId) { Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED) .orElseThrow(() -> new IllegalArgumentException("Member not found")); + Event event = eventRepository.findByIdAndMemberId(eventId, member.getId()) + .orElseThrow(() -> new IllegalArgumentException("Event not found or does not belong to the user")); + List winners = commentRepository.findByEventIdAndIsWinnerTrue(eventId); - return winners.stream() + List winnerList = winners.stream() .map(CommentConverter::toCommentEventWinners) .collect(Collectors.toList()); + + return CommentResponseDTO.CommentEventWinnersWithCriteria.builder() + .winners(winnerList) + .winnerCount(event.getWinnerCount()) + .minLength(event.getMinLength()) + .startDate(event.getSelectionStartDate() != null ? event.getSelectionStartDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : null) + .endDate(event.getSelectionEndDate() != null ? event.getSelectionEndDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) : null) + .keywords(event.getKeywords()) + .build(); + } + + + public void deleteWinners(Long eventId) { + List comments = commentRepository.findByEventIdAndIsWinnerTrue(eventId); + + for (Comment comment : comments) { + comment.setIsWinner(false); + commentRepository.save(comment); + } } } diff --git a/src/main/java/com/cmc/suppin/event/crawl/service/CrawlService.java b/src/main/java/com/cmc/suppin/event/crawl/service/CrawlService.java index 4fa01be..015794b 100644 --- a/src/main/java/com/cmc/suppin/event/crawl/service/CrawlService.java +++ b/src/main/java/com/cmc/suppin/event/crawl/service/CrawlService.java @@ -27,10 +27,10 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; @Service @Slf4j @@ -48,7 +48,7 @@ public String checkExistingComments(String url, String userId) { List existingComments = commentRepository.findByUrl(url); if (!existingComments.isEmpty()) { - LocalDateTime firstCommentDate = existingComments.get(0).getCrawlTime(); + ZonedDateTime firstCommentDate = existingComments.get(0).getCrawlTime().atZone(ZoneId.of("Asia/Seoul")); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); return "동일한 URL의 댓글을 " + firstCommentDate.format(formatter) + " 일자에 수집한 이력이 있습니다."; } @@ -64,23 +64,17 @@ public CrawlResponseDTO.CrawlResultDTO crawlYoutubeComments(String url, Long eve .orElseThrow(() -> new IllegalArgumentException("Event not found")); if (forceUpdate) { - // 기존 댓글 삭제 commentRepository.deleteByUrlAndEventId(url, eventId); - // 삭제 후, 확인을 위한 로그 출력 또는 추가 검증 - List deletedComments = commentRepository.findByUrlAndEventId(url, eventId); - if (!deletedComments.isEmpty()) { + if (commentRepository.existsByUrlAndEventId(url, eventId)) { throw new RuntimeException("기존 댓글 삭제에 실패했습니다."); } } else { - // 기존 댓글이 존재하는 경우: 크롤링을 중지하고 예외를 던집니다. - List existingComments = commentRepository.findByUrlAndEventId(url, eventId); - if (!existingComments.isEmpty()) { + if (commentRepository.existsByUrlAndEventId(url, eventId)) { throw new CrawlException(CrawlErrorCode.DUPLICATE_URL); } } - // 크롤링 코드 실행 String chromeDriverPath = System.getenv("CHROME_DRIVER_PATH"); if (chromeDriverPath != null && !chromeDriverPath.isEmpty()) { System.setProperty("webdriver.chrome.driver", chromeDriverPath); @@ -99,8 +93,8 @@ public CrawlResponseDTO.CrawlResultDTO crawlYoutubeComments(String url, Long eve options.addArguments("--disable-infobars"); options.addArguments("--disable-browser-side-navigation"); options.addArguments("--disable-software-rasterizer"); - options.addArguments("--blink-settings=imagesEnabled=false"); // 이미지 로딩 비활성화 - options.setPageLoadStrategy(PageLoadStrategy.NORMAL); // 페이지 로드 전략 설정 + options.addArguments("--blink-settings=imagesEnabled=false"); + options.setPageLoadStrategy(PageLoadStrategy.NORMAL); WebDriver driver = new ChromeDriver(options); driver.get(url); @@ -108,27 +102,43 @@ public CrawlResponseDTO.CrawlResultDTO crawlYoutubeComments(String url, Long eve Set uniqueComments = new HashSet<>(); try { - Thread.sleep(5000); // 초기 로딩 대기 + Thread.sleep(5000); - long endTime = System.currentTimeMillis() + 600000; // 스크롤 시간을 10분으로 설정 (600,000ms) JavascriptExecutor jsExecutor = (JavascriptExecutor) driver; - int previousCommentCount = 0; - int currentCommentCount; + Queue heightQueue = new LinkedList<>(); + int maxQueueSize = 50; + int enqueueCount = 0; + int retryCount = 0; - while (System.currentTimeMillis() < endTime) { + while (true) { jsExecutor.executeScript("window.scrollTo(0, document.documentElement.scrollHeight);"); + Thread.sleep(100); - Thread.sleep(3000); // 3초 대기 + long newPageHeight = (long) jsExecutor.executeScript("return document.documentElement.scrollHeight"); + + if (enqueueCount > maxQueueSize) { + break; + } + + if (heightQueue.isEmpty()) { + heightQueue.offer(newPageHeight); + enqueueCount++; + } else { + if (heightQueue.peek().equals(newPageHeight)) { + heightQueue.offer(newPageHeight); + enqueueCount++; + } else { + heightQueue.clear(); + heightQueue.offer(newPageHeight); + enqueueCount = 1; + } + } String pageSource = driver.getPageSource(); Document doc = Jsoup.parse(pageSource); Elements comments = doc.select("ytd-comment-thread-renderer"); - currentCommentCount = comments.size(); - - LocalDateTime crawlTime = LocalDateTime.now(); - for (Element commentElement : comments) { String author = commentElement.select("#author-text span").text(); String text = commentElement.select("#content yt-attributed-string#content-text").text(); @@ -137,27 +147,33 @@ public CrawlResponseDTO.CrawlResultDTO crawlYoutubeComments(String url, Long eve if (!uniqueComments.contains(text)) { uniqueComments.add(text); - // 엔티티 저장 LocalDateTime actualCommentDate = DateConverter.convertRelativeTime(time); Comment comment = CommentConverter.toCommentEntity(author, text, actualCommentDate, url, event); - comment.setCrawlTime(crawlTime); + + // Set crawl time with timezone + comment.setCrawlTime(ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime()); commentRepository.save(comment); } } - // 더 이상 새로운 댓글이 없을 때, 크롤링 종료 - if (currentCommentCount == previousCommentCount) { - break; // 새로운 댓글이 로드되지 않으면 루프를 종료합니다. + if (comments.size() == uniqueComments.size()) { + if (retryCount >= 3) { + break; + } + retryCount++; + } else { + retryCount = 0; } - previousCommentCount = currentCommentCount; } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); e.printStackTrace(); } finally { driver.quit(); } + return CommentConverter.toCrawlResultDTO(LocalDateTime.now(), uniqueComments.size()); } - } + diff --git a/src/main/java/com/cmc/suppin/event/events/controller/EventApi.java b/src/main/java/com/cmc/suppin/event/events/controller/EventApi.java index 833902f..ce23807 100644 --- a/src/main/java/com/cmc/suppin/event/events/controller/EventApi.java +++ b/src/main/java/com/cmc/suppin/event/events/controller/EventApi.java @@ -74,22 +74,23 @@ public ResponseEntity> deleteEvent(@PathVariable("eventId") Lo } @GetMapping("/comment-winners") - @Operation(summary = "댓글 이벤트 당첨자 조회 API", description = "댓글 이벤트의 당첨자 리스트를 조회합니다.") - public ResponseEntity>> getCommentEventWinners( + @Operation(summary = "댓글 이벤트 당첨자 조회 API", description = "댓글 이벤트의 당첨자 리스트와 선별 조건을 조회합니다.") + public ResponseEntity> getCommentEventWinners( @RequestParam("eventId") Long eventId, @CurrentAccount Account account) { - List winners = commentService.getCommentEventWinners(eventId, account.userId()); - return ResponseEntity.ok(ApiResponse.of(winners)); + CommentResponseDTO.CommentEventWinnersWithCriteria winnersWithCriteria = commentService.getCommentEventWinnersWithCriteria(eventId, account.userId()); + return ResponseEntity.ok(ApiResponse.of(winnersWithCriteria)); } + @GetMapping("/survey-winners") - @Operation(summary = "설문 이벤트 당첨자 조회 API", description = "설문 이벤트의 당첨자 리스트를 조회합니다.") - public ResponseEntity>> getSurveyEventWinners( + @Operation(summary = "설문 이벤트 당첨자 조회 API", description = "설문 이벤트의 당첨자 리스트 및 선별 조건을 조회합니다.") + public ResponseEntity> getSurveyEventWinners( @RequestParam("surveyId") Long surveyId, @CurrentAccount Account account) { - List winners = surveyService.getSurveyEventWinners(surveyId, account.userId()); - return ResponseEntity.ok(ApiResponse.of(winners)); + SurveyResponseDTO.SurveyEventWinnersResponse response = surveyService.getSurveyEventWinners(surveyId, account.userId()); + return ResponseEntity.ok(ApiResponse.of(response)); } } diff --git a/src/main/java/com/cmc/suppin/event/events/controller/dto/EventRequestDTO.java b/src/main/java/com/cmc/suppin/event/events/controller/dto/EventRequestDTO.java index bafffd1..ffeb1cc 100644 --- a/src/main/java/com/cmc/suppin/event/events/controller/dto/EventRequestDTO.java +++ b/src/main/java/com/cmc/suppin/event/events/controller/dto/EventRequestDTO.java @@ -3,7 +3,6 @@ import com.cmc.suppin.global.enums.EventType; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -28,13 +27,10 @@ public static class CommentEventCreateDTO { private String url; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String startDate; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String endDate; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String announcementDate; } @@ -53,13 +49,10 @@ public static class SurveyEventCreateDTO { private String description; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String startDate; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String endDate; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String announcementDate; } @@ -75,13 +68,10 @@ public static class EventUpdateDTO { private String url; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String startDate; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String endDate; @NotEmpty - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String announcementDate; } } diff --git a/src/main/java/com/cmc/suppin/event/events/converter/EventConverter.java b/src/main/java/com/cmc/suppin/event/events/converter/EventConverter.java index 330e709..f6f3524 100644 --- a/src/main/java/com/cmc/suppin/event/events/converter/EventConverter.java +++ b/src/main/java/com/cmc/suppin/event/events/converter/EventConverter.java @@ -14,7 +14,7 @@ public class EventConverter { public static Event toCommentEventEntity(EventRequestDTO.CommentEventCreateDTO request, Member member) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); return Event.builder() .type(EventType.COMMENT) .title(request.getTitle()) @@ -28,7 +28,7 @@ public static Event toCommentEventEntity(EventRequestDTO.CommentEventCreateDTO r } public static Event toSurveyEventEntity(EventRequestDTO.SurveyEventCreateDTO request, Member member) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); return Event.builder() .type(EventType.SURVEY) .title(request.getTitle()) @@ -41,7 +41,7 @@ public static Event toSurveyEventEntity(EventRequestDTO.SurveyEventCreateDTO req } public static EventResponseDTO.CommentEventDetailDTO toEventDetailDTO(Event event) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); return EventResponseDTO.CommentEventDetailDTO.builder() .type(event.getType()) .title(event.getTitle()) @@ -53,7 +53,7 @@ public static EventResponseDTO.CommentEventDetailDTO toEventDetailDTO(Event even } public static EventResponseDTO.EventInfoDTO toEventInfoDTO(Event event) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); Optional url = Optional.empty(); if (event.getType() == EventType.COMMENT) { @@ -87,7 +87,7 @@ public static EventResponseDTO.EventInfoDTO toEventInfoDTO(Event event) { } public static Event toUpdatedEventEntity(EventRequestDTO.EventUpdateDTO request, Member member) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); Event.EventBuilder eventBuilder = Event.builder() .title(request.getTitle()) .description(request.getDescription()) diff --git a/src/main/java/com/cmc/suppin/event/events/domain/Event.java b/src/main/java/com/cmc/suppin/event/events/domain/Event.java index bd3e0e0..e3d1bd6 100644 --- a/src/main/java/com/cmc/suppin/event/events/domain/Event.java +++ b/src/main/java/com/cmc/suppin/event/events/domain/Event.java @@ -1,5 +1,6 @@ package com.cmc.suppin.event.events.domain; +import com.cmc.suppin.event.crawl.controller.dto.CommentRequestDTO; import com.cmc.suppin.event.crawl.domain.Comment; import com.cmc.suppin.event.survey.domain.Survey; import com.cmc.suppin.global.domain.BaseDateTimeEntity; @@ -11,6 +12,7 @@ import org.hibernate.annotations.DynamicInsert; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @@ -63,6 +65,24 @@ public class Event extends BaseDateTimeEntity { @Enumerated(EnumType.STRING) private EventStatus status; + // 당첨자 선별 조건 + @Column + private Integer winnerCount; + + @Column + private LocalDateTime selectionStartDate; + + @Column + private LocalDateTime selectionEndDate; + + @Column + private Integer minLength; + + @ElementCollection + @CollectionTable(name = "event_keywords", joinColumns = @JoinColumn(name = "event_id")) + @Column(name = "keyword") + private List keywords; + public void setMember(Member member) { this.member = member; member.getEventList().add(this); @@ -75,4 +95,51 @@ public void setStatus(EventStatus status) { public void setId(Long id) { this.id = id; } + + public void setWinnerCount(Integer winnerCount) { + this.winnerCount = winnerCount; + } + + public void setSelectionStartDate(LocalDateTime selectionStartDate) { + this.selectionStartDate = selectionStartDate; + } + + public void setSelectionEndDate(LocalDateTime selectionEndDate) { + this.selectionEndDate = selectionEndDate; + } + + public void setMinLength(Integer minLength) { + this.minLength = minLength; + } + + public void setKeywords(List keywords) { + this.keywords = keywords; + } + + public void setSelectionCriteria(CommentRequestDTO.WinnerRequestDTO request) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + this.winnerCount = request.getWinnerCount(); + + // startDate와 endDate가 빈 문자열("")이거나 null일 경우 null로 설정 + if (request.getStartDate() == null || request.getStartDate().isEmpty()) { + this.selectionStartDate = null; // 필터링을 하지 않도록 null로 설정 + } else { + this.selectionStartDate = LocalDateTime.parse(request.getStartDate(), formatter); + } + + if (request.getEndDate() == null || request.getEndDate().isEmpty()) { + this.selectionEndDate = null; // 필터링을 하지 않도록 null로 설정 + } else { + this.selectionEndDate = LocalDateTime.parse(request.getEndDate(), formatter); + } + + this.minLength = request.getMinLength(); + + // keywords가 빈 문자열만 있을 경우 처리 + this.keywords = (request.getKeywords() == null || request.getKeywords().isEmpty() || + (request.getKeywords().size() == 1 && request.getKeywords().get(0).isEmpty())) ? + new ArrayList<>() : request.getKeywords(); + } + } diff --git a/src/main/java/com/cmc/suppin/event/survey/controller/SurveyApi.java b/src/main/java/com/cmc/suppin/event/survey/controller/SurveyApi.java index fcf7a7c..627ff17 100644 --- a/src/main/java/com/cmc/suppin/event/survey/controller/SurveyApi.java +++ b/src/main/java/com/cmc/suppin/event/survey/controller/SurveyApi.java @@ -77,7 +77,7 @@ public ResponseEntity> getS } @PostMapping("/draft") - @Operation(summary = "당첨자 랜덤 추첨 결과 리스트 조회 API(설문 이벤트)", + @Operation(summary = "당첨자 랜덤 추첨 결과 조회 API(설문 이벤트)", description = "주관식 답변 중 조건을 설정하여 랜덤으로 당첨자를 추첨합니다. 추첨된 당첨자의 isWinner값이 True로 설정됩니다. " + "자세한 요청 및 응답 형식은 노션 API 문서를 참고해주세요.") public ResponseEntity> selectRandomWinners( @RequestBody @Valid SurveyRequestDTO.RandomSelectionRequestDTO request, @CurrentAccount Account account) { @@ -95,7 +95,7 @@ public ResponseEntity> getWinnerD } @DeleteMapping("/winners") - @Operation(summary = "당첨자 리스트 삭제 API(당첨자 재추첨 시, 기존 당첨자 리스트를 삭제 후 진행 해야합니다.)", description = "해당 설문조사의 모든 당첨자들의 isWinner 값을 false로 변경합니다.") + @Operation(summary = "설문 이벤트 당첨자 리스트 삭제 API(당첨자 재추첨 시, 기존 당첨자 리스트를 삭제 후 진행해야 합니다.)", description = "해당 설문조사의 모든 당첨자들의 isWinner 값을 false로 변경합니다.") public ResponseEntity> deleteWinners(@RequestParam("surveyId") Long surveyId) { surveyService.deleteWinners(surveyId); return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS)); diff --git a/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyRequestDTO.java b/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyRequestDTO.java index 97408f2..2b9ed97 100644 --- a/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyRequestDTO.java +++ b/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyRequestDTO.java @@ -4,7 +4,6 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -91,7 +90,6 @@ public static class AnswerDTO { @AllArgsConstructor @Builder public static class AnswerOptionDTO { - @NotNull private Long questionOptionId; } } @@ -109,10 +107,8 @@ public static class RandomSelectionRequestDTO { @NotNull private Integer winnerCount; @NotNull - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String startDate; @NotNull - @Pattern(regexp = "\\d{4}\\. \\d{2}\\. \\d{2} \\d{2}:\\d{2}", message = "날짜 형식은 yyyy. MM. dd HH:mm 이어야 합니다.") private String endDate; @NotNull private Integer minLength; diff --git a/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyResponseDTO.java b/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyResponseDTO.java index bd64639..4ef5d45 100644 --- a/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyResponseDTO.java +++ b/src/main/java/com/cmc/suppin/event/survey/controller/dto/SurveyResponseDTO.java @@ -151,4 +151,26 @@ public static class SurveyEventWinners { private String name; private List answers; } + + // 당첨자 선별 조건도 포함하여 반환하기 위한 DTO + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class SurveyEventWinnersResponse { + private SelectionCriteriaDTO selectionCriteria; + private List winners; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class SelectionCriteriaDTO { + private Integer winnerCount; + private LocalDateTime selectionStartDate; + private LocalDateTime selectionEndDate; + private Integer minLength; + private List keywords; + } + } } diff --git a/src/main/java/com/cmc/suppin/event/survey/converter/SurveyConverter.java b/src/main/java/com/cmc/suppin/event/survey/converter/SurveyConverter.java index f0cdd8a..3585f51 100644 --- a/src/main/java/com/cmc/suppin/event/survey/converter/SurveyConverter.java +++ b/src/main/java/com/cmc/suppin/event/survey/converter/SurveyConverter.java @@ -131,7 +131,7 @@ public static AnswerOption toAnswerOption(SurveyRequestDTO.SurveyAnswerDTO.Answe } public static SurveyResponseDTO.RandomSelectionResponseDTO.SelectionCriteriaDTO toSelectionCriteriaDTO(SurveyRequestDTO.RandomSelectionRequestDTO request) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); return SurveyResponseDTO.RandomSelectionResponseDTO.SelectionCriteriaDTO.builder() .winnerCount(request.getWinnerCount()) diff --git a/src/main/java/com/cmc/suppin/event/survey/service/SurveyService.java b/src/main/java/com/cmc/suppin/event/survey/service/SurveyService.java index 664b0d2..26fe64b 100644 --- a/src/main/java/com/cmc/suppin/event/survey/service/SurveyService.java +++ b/src/main/java/com/cmc/suppin/event/survey/service/SurveyService.java @@ -168,14 +168,25 @@ public SurveyResponseDTO.RandomSelectionResponseDTO selectRandomWinners(SurveyRe Question question = questionRepository.findByIdAndSurveyId(request.getQuestionId(), request.getSurveyId()) .orElseThrow(() -> new IllegalArgumentException("Question not found for the given survey")); + Event event = survey.getEvent(); + + // 선별 조건 Event 엔티티에 저장 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + event.setWinnerCount(request.getWinnerCount()); + event.setSelectionStartDate(LocalDateTime.parse(request.getStartDate(), formatter)); + event.setSelectionEndDate(LocalDateTime.parse(request.getEndDate(), formatter)); + event.setMinLength(request.getMinLength()); + event.setKeywords(request.getKeywords()); + + // Event 엔티티 업데이트 (저장) + eventRepository.save(event); + // 키워드를 OR 조건으로 연결 List keywordPatterns = request.getKeywords().stream() .map(keyword -> "%" + keyword.toLowerCase() + "%") .collect(Collectors.toList()); // 조건에 맞는 주관식 답변 조회 - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy. MM. dd HH:mm"); - List eligibleAnswers = answerCustomRepository.findEligibleAnswers( request.getQuestionId(), LocalDateTime.parse(request.getStartDate(), formatter), LocalDateTime.parse(request.getEndDate(), formatter), request.getMinLength(), request.getKeywords()); @@ -232,18 +243,35 @@ public void deleteWinners(Long surveyId) { } } - public List getSurveyEventWinners(Long surveyId, String userId) { + public SurveyResponseDTO.SurveyEventWinnersResponse getSurveyEventWinners(Long surveyId, String userId) { Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED) .orElseThrow(() -> new IllegalArgumentException("Member not found")); Survey survey = surveyRepository.findById(surveyId) .orElseThrow(() -> new IllegalArgumentException("Survey not found")); + Event event = survey.getEvent(); + + // 당첨자 리스트 가져오기 List winners = anonymousParticipantRepository.findBySurveyAndIsWinnerTrue(survey); - return winners.stream() + List winnerList = winners.stream() .map(SurveyConverter::toSurveyEventWinners) .collect(Collectors.toList()); + + // 선별 조건 가져오기 + SurveyResponseDTO.SurveyEventWinnersResponse.SelectionCriteriaDTO criteria = SurveyResponseDTO.SurveyEventWinnersResponse.SelectionCriteriaDTO.builder() + .winnerCount(event.getWinnerCount()) + .selectionStartDate(event.getSelectionStartDate()) + .selectionEndDate(event.getSelectionEndDate()) + .minLength(event.getMinLength()) + .keywords(event.getKeywords()) + .build(); + + return SurveyResponseDTO.SurveyEventWinnersResponse.builder() + .selectionCriteria(criteria) + .winners(winnerList) + .build(); } } diff --git a/src/main/resources/drivers/chromedriver b/src/main/resources/drivers/chromedriver deleted file mode 100755 index 03fea3d..0000000 Binary files a/src/main/resources/drivers/chromedriver and /dev/null differ diff --git a/src/test/java/com/cmc/suppin/SuppinApplicationTests.java b/src/test/java/com/cmc/suppin/SuppinApplicationTests.java new file mode 100644 index 0000000..0e18457 --- /dev/null +++ b/src/test/java/com/cmc/suppin/SuppinApplicationTests.java @@ -0,0 +1,13 @@ +package com.cmc.suppin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SuppinApplicationTests { + + @Test + void contextLoads() { + } + +}