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 4e2c37f..b786930 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
@@ -1,19 +1,70 @@
package com.cmc.suppin.event.crawl.controller;
+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.security.reslover.Account;
+import com.cmc.suppin.global.security.reslover.CurrentAccount;
+import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+import java.util.List;
+
@RestController
@Slf4j
@RequiredArgsConstructor
@Validated
@Tag(name = "Event-Comments", description = "Crawling Comments 관련 API")
-@RequestMapping("/api/v1/comments")
+@RequestMapping("/api/v1/event/comments")
public class CommentApi {
+ private final CommentService commentService;
+
+ @GetMapping("/list")
+ @Operation(summary = "크롤링된 전체 댓글 조회 API",
+ description = "주어진 이벤트 ID와 URL의 댓글을 페이지네이션하여 이벤트의 endDate 전에 작성된 댓글들만 조회합니다.
" +
+ "Request: eventId: 조회할 이벤트의 ID, url: 댓글을 조회할 유튜브 URL, page: 조회할 페이지 번호 (1부터 시작), " +
+ "size: 한 페이지당 댓글 수, Authorization: JWT 토큰을 포함한 인증 헤더
" +
+ "Response: totalCommentCount: 전체 댓글 수, participantCount: 현재 페이지에서 가져온 댓글 수, crawlTime: 댓글 조회(크롤링) 요청 시간, comments: 각 댓글의 상세 정보 배열" +
+ "author: 댓글 작성자, commentText: 댓글 내용, commentDate: 댓글 작성 시간")
+ public ResponseEntity> getComments(
+ @RequestParam Long eventId,
+ @RequestParam String url,
+ @RequestParam int page,
+ @RequestParam int size,
+ @CurrentAccount Account account) {
+ CommentResponseDTO.CrawledCommentListDTO comments = commentService.getComments(eventId, url, page, size, account.userId());
+ return ResponseEntity.ok(ApiResponse.of(comments));
+ }
+
+ @GetMapping("/draft-winners")
+ @Operation(summary = "조건별 당첨자 추첨 API", description = "주어진 조건에 따라 이벤트의 당첨자를 추첨합니다.")
+ public ResponseEntity> drawWinners(
+ @RequestParam Long eventId,
+ @RequestParam String startDate,
+ @RequestParam String endDate,
+ @RequestParam int winnerCount,
+ @RequestParam List keywords,
+ @CurrentAccount Account account) {
+ CommentResponseDTO.WinnerResponseDTO winners = commentService.drawWinners(eventId, startDate, endDate, winnerCount, keywords, account.userId());
+ return ResponseEntity.ok(ApiResponse.of(winners));
+ }
+ @GetMapping("/winners/keywordFiltering")
+ @Operation(summary = "키워드별 당첨자 조회 API", description = "주어진 키워드에 따라 1차 랜덤 추첨된 당첨자 중에서 키워드가 포함된 당첨자들을 조회합니다.")
+ public ResponseEntity>> getWinnersByKeyword(
+ @RequestParam Long eventId,
+ @RequestParam String keyword,
+ @CurrentAccount Account account) {
+ List filteredWinners = commentService.getCommentsByKeyword(eventId, keyword, account.userId());
+ return ResponseEntity.ok(ApiResponse.of(filteredWinners));
+ }
}
diff --git a/src/main/java/com/cmc/suppin/event/crawl/controller/CrawlApi.java b/src/main/java/com/cmc/suppin/event/crawl/controller/CrawlApi.java
index 3ff2d6d..9acadaf 100644
--- a/src/main/java/com/cmc/suppin/event/crawl/controller/CrawlApi.java
+++ b/src/main/java/com/cmc/suppin/event/crawl/controller/CrawlApi.java
@@ -11,10 +11,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
@RestController
@Slf4j
@@ -26,13 +23,44 @@ public class CrawlApi {
private final CrawlService crawlService;
- // 유튜브 크롤링
- @GetMapping("/crawling/comments")
- @Operation(summary = "유튜브 댓글 크롤링 API", description = "주어진 URL의 유튜브 댓글을 크롤링하고 DB에 저장합니다.")
- public ResponseEntity> crawlYoutubeComments(@RequestParam String url, @RequestParam Long eventId, @CurrentAccount Account account) {
- crawlService.crawlYoutubeComments(url, eventId, account.userId());
- return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS));
+ // 크롤링 URL 중복 검증
+ @GetMapping("/comments/checkUrl")
+ @Operation(summary = "크롤링 중복 검증 API",
+ description = "주어진 URL과 eventId로 중복된 댓글 수집 이력이 있는지 확인합니다.
" +
+ "Request: url: 중복 검증할 URL, eventId: 중복 검증할 이벤트 ID, Authorization: JWT 토큰을 포함한 인증 헤더
" +
+ "Response: 중복된 댓글 수집 이력이 있을 경우 message 출력, 없을 경우 null")
+ public ResponseEntity> checkExistingComments(@RequestParam String url, @RequestParam Long eventId, @CurrentAccount Account account) {
+ String message = crawlService.checkExistingComments(url, eventId, account.userId());
+ if (message != null) {
+ return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS, message));
+ }
+ return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS, "수집 이력이 없습니다."));
}
+ // 유튜브 댓글 크롤링(DB 저장)
+ @PostMapping("/crawling/comments")
+ @Operation(summary = "유튜브 댓글 크롤링 API",
+ description = "주어진 URL의 유튜브 댓글을 크롤링하여 DB에 저장합니다.
" +
+ "Request: url: 크롤링할 URL, eventId: 댓글을 수집할 eventId, forceUpdate: 댓글을 강제로 업데이트할지 여부(Boolean), Authorization: JWT 토큰을 포함한 인증 헤더
" +
+ "forceUpdate 입력 값이 false일 때 설명
" +
+ "- DB에 기존 댓글이 존재하는 경우: 크롤링을 중지하고 예외를 던집니다.
" +
+ "- DB에 기존 댓글이 존재하지 않는 경우: 새로운 댓글을 크롤링하고 이를 DB에 저장합니다.")
+ public ResponseEntity> crawlYoutubeComments(@RequestParam String url, @RequestParam Long eventId, @RequestParam boolean forceUpdate, @CurrentAccount Account account) {
+ crawlService.crawlYoutubeComments(url, eventId, account.userId(), forceUpdate);
+ return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS, "댓글 수집이 완료되었습니다."));
+ }
+
+// @GetMapping("/count")
+// @Operation(summary = "크롤링된 전체 댓글 수 조회 API", description = "주어진 이벤트 ID와 URL의 댓글 수를 조회합니다.
" +
+// "Request: eventId: 조회할 이벤트의 ID, url: 댓글을 조회할 URL, Authorization: JWT 토큰을 포함한 인증 헤더
" +
+// "Response: 댓글 수")
+// public ResponseEntity> getCommentsCount(
+// @RequestParam Long eventId,
+// @RequestParam String url,
+// @CurrentAccount Account account) {
+// int count = commentService.getCommentsCount(eventId, url, account.userId());
+// return ResponseEntity.ok(ApiResponse.of(CommentResponseDTO.CommentCountsDTO(count)));
+// }
+
// TODO: 인스타그램 게시글 크롤링
}
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
new file mode 100644
index 0000000..c3b1028
--- /dev/null
+++ b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentRequestDTO.java
@@ -0,0 +1,34 @@
+package com.cmc.suppin.event.crawl.controller.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+public class CommentRequestDTO {
+
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class CommentListRequestDTO {
+ private Long eventId;
+ private String url;
+ private int page;
+ private int size;
+ }
+
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class WinnerRequestDTO {
+ private Long eventId;
+ private String startDate;
+ private String endDate;
+ private int winnerCount;
+ 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
new file mode 100644
index 0000000..30862ca
--- /dev/null
+++ b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java
@@ -0,0 +1,43 @@
+package com.cmc.suppin.event.crawl.controller.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+public class CommentResponseDTO {
+
+ @Builder
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class CrawledCommentListDTO {
+ private int totalCommentCount;
+ private int participantCount;
+ private String crawlTime;
+ private List comments;
+ }
+
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class CommentDetailDTO {
+ private String author;
+ private String commentText;
+ private String commentDate;
+ }
+
+ @Getter
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @Builder
+ public static class WinnerResponseDTO {
+ private int winnerCount;
+ private String startDate;
+ private String endDate;
+ private List winners;
+ }
+}
diff --git a/src/main/java/com/cmc/suppin/event/crawl/converter/CommentConverter.java b/src/main/java/com/cmc/suppin/event/crawl/converter/CommentConverter.java
index 8dd1cdc..fb09af0 100644
--- a/src/main/java/com/cmc/suppin/event/crawl/converter/CommentConverter.java
+++ b/src/main/java/com/cmc/suppin/event/crawl/converter/CommentConverter.java
@@ -1,9 +1,13 @@
package com.cmc.suppin.event.crawl.converter;
+import com.cmc.suppin.event.crawl.controller.dto.CommentResponseDTO;
import com.cmc.suppin.event.crawl.domain.Comment;
import com.cmc.suppin.event.events.domain.Event;
import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.stream.Collectors;
public class CommentConverter {
@@ -16,5 +20,39 @@ public static Comment toCommentEntity(String author, String text, LocalDateTime
.event(event)
.build();
}
+
+ public static CommentResponseDTO.CommentDetailDTO toCommentDetailDTO(Comment comment) {
+ return CommentResponseDTO.CommentDetailDTO.builder()
+ .author(comment.getAuthor())
+ .commentText(comment.getCommentText())
+ .commentDate(comment.getCommentDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")))
+ .build();
+ }
+
+ public static CommentResponseDTO.CrawledCommentListDTO toCommentListDTO(List comments, String crawlTime, int totalComments) {
+ List commentDetailDTOS = comments.stream()
+ .map(CommentConverter::toCommentDetailDTO)
+ .collect(Collectors.toList());
+
+ return CommentResponseDTO.CrawledCommentListDTO.builder()
+ .totalCommentCount(totalComments)
+ .participantCount(commentDetailDTOS.size())
+ .crawlTime(crawlTime)
+ .comments(commentDetailDTOS)
+ .build();
+ }
+
+ public static CommentResponseDTO.WinnerResponseDTO toWinnerResponseDTO(List winners, int winnerCount, String startDate, String endDate) {
+ List winnerDetails = winners.stream()
+ .map(CommentConverter::toCommentDetailDTO)
+ .collect(Collectors.toList());
+
+ return CommentResponseDTO.WinnerResponseDTO.builder()
+ .winnerCount(winnerCount)
+ .startDate(startDate)
+ .endDate(endDate)
+ .winners(winnerDetails)
+ .build();
+ }
}
diff --git a/src/main/java/com/cmc/suppin/event/crawl/domain/Comment.java b/src/main/java/com/cmc/suppin/event/crawl/domain/Comment.java
index d2fc8d5..8ab7ad4 100644
--- a/src/main/java/com/cmc/suppin/event/crawl/domain/Comment.java
+++ b/src/main/java/com/cmc/suppin/event/crawl/domain/Comment.java
@@ -6,6 +6,8 @@
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
+import java.time.LocalDateTime;
+
@Entity
@Getter
@@ -34,6 +36,6 @@ public class Comment extends BaseDateTimeEntity {
private String commentText;
@Column(nullable = false)
- private String commentDate;
+ private LocalDateTime commentDate;
}
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 ffd5b5b..0896ba0 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
@@ -1,12 +1,27 @@
package com.cmc.suppin.event.crawl.domain.repository;
import com.cmc.suppin.event.crawl.domain.Comment;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.time.LocalDateTime;
import java.util.List;
public interface CommentRepository extends JpaRepository {
List findByEventId(Long eventId);
- List findByVideoUrl(String videoUrl);
+ List findByUrl(String url);
+
+ void deleteByUrlAndEventId(String url, Long eventId);
+
+ Page findByEventIdAndUrlAndCommentDateBefore(Long eventId, String url, LocalDateTime endDate, Pageable pageable);
+
+ List findByUrlAndEventId(String url, Long eventId);
+
+ int countByEventIdAndUrl(Long eventId, String url);
+
+ List findByEventIdAndCommentDateBetween(Long eventId, LocalDateTime start, LocalDateTime end);
+
+ List findByEventIdAndCommentTextContaining(Long eventId, String keyword);
}
diff --git a/src/main/java/com/cmc/suppin/event/crawl/exception/CrawlErrorCode.java b/src/main/java/com/cmc/suppin/event/crawl/exception/CrawlErrorCode.java
new file mode 100644
index 0000000..4c92aba
--- /dev/null
+++ b/src/main/java/com/cmc/suppin/event/crawl/exception/CrawlErrorCode.java
@@ -0,0 +1,28 @@
+package com.cmc.suppin.event.crawl.exception;
+
+import com.cmc.suppin.global.exception.BaseErrorCode;
+import com.cmc.suppin.global.response.ErrorResponse;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+public enum CrawlErrorCode implements BaseErrorCode {
+
+ DUPLICATE_URL("crawl-404/01", HttpStatus.CONFLICT, "이미 수집된 댓글이 있는 URL입니다."),
+ CRAWL_FAILED("crawl-500/01", HttpStatus.INTERNAL_SERVER_ERROR, "크롤링에 실패했습니다.");
+
+ private final String code;
+ private final HttpStatus status;
+ private final String message;
+
+ CrawlErrorCode(String code, HttpStatus status, String message) {
+ this.code = code;
+ this.status = status;
+ this.message = message;
+ }
+
+ @Override
+ public ErrorResponse getErrorResponse() {
+ return ErrorResponse.of(code, message);
+ }
+}
diff --git a/src/main/java/com/cmc/suppin/event/crawl/exception/CrawlException.java b/src/main/java/com/cmc/suppin/event/crawl/exception/CrawlException.java
new file mode 100644
index 0000000..d3169a6
--- /dev/null
+++ b/src/main/java/com/cmc/suppin/event/crawl/exception/CrawlException.java
@@ -0,0 +1,13 @@
+package com.cmc.suppin.event.crawl.exception;
+
+import com.cmc.suppin.global.exception.BaseErrorCode;
+import com.cmc.suppin.global.exception.CustomException;
+import lombok.Getter;
+
+@Getter
+public class CrawlException extends CustomException {
+
+ public CrawlException(BaseErrorCode errorCode) {
+ super(errorCode);
+ }
+}
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
new file mode 100644
index 0000000..d3cf147
--- /dev/null
+++ b/src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java
@@ -0,0 +1,111 @@
+package com.cmc.suppin.event.crawl.service;
+
+import com.cmc.suppin.event.crawl.controller.dto.CommentResponseDTO;
+import com.cmc.suppin.event.crawl.converter.CommentConverter;
+import com.cmc.suppin.event.crawl.domain.Comment;
+import com.cmc.suppin.event.crawl.domain.repository.CommentRepository;
+import com.cmc.suppin.event.events.domain.Event;
+import com.cmc.suppin.event.events.domain.repository.EventRepository;
+import com.cmc.suppin.global.enums.UserStatus;
+import com.cmc.suppin.member.domain.Member;
+import com.cmc.suppin.member.domain.repository.MemberRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+@Transactional
+public class CommentService {
+
+ private final CommentRepository commentRepository;
+ private final EventRepository eventRepository;
+ private final MemberRepository memberRepository;
+
+ // 크롤링된 댓글 조회
+ public CommentResponseDTO.CrawledCommentListDTO getComments(Long eventId, String url, int page, int size, 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"));
+
+ Pageable pageable = PageRequest.of(page - 1, size, Sort.by("commentDate").descending());
+ Page comments = commentRepository.findByEventIdAndUrlAndCommentDateBefore(eventId, url, event.getEndDate(), pageable);
+
+ int totalComments = commentRepository.countByEventIdAndUrl(eventId, url);
+
+ String crawlTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
+
+ return CommentConverter.toCommentListDTO(comments.getContent(), crawlTime, totalComments);
+ }
+
+ // 당첨자 조건별 랜덤 추첨
+ public CommentResponseDTO.WinnerResponseDTO drawWinners(Long eventId, String startDate, String endDate, int winnerCount, List keywords, 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"));
+
+ // String을 LocalDate로 변환하고 LocalDateTime으로 변환
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ LocalDate start = LocalDate.parse(startDate, dateFormatter);
+ LocalDate end = LocalDate.parse(endDate, dateFormatter);
+
+ // LocalDateTime으로 변환
+ LocalDateTime startDateTime = start.atStartOfDay();
+ LocalDateTime endDateTime = end.atTime(LocalTime.MAX);
+
+ // 당첨자 추첨 로직
+ List comments = commentRepository.findByEventIdAndCommentDateBetween(event.getId(), startDateTime, endDateTime);
+
+ // 키워드 필터링(OR 로직)
+ List filteredComments = comments.stream()
+ .filter(comment -> keywords.stream().anyMatch(keyword -> comment.getCommentText().contains(keyword)))
+ .collect(Collectors.toList());
+
+ // 랜덤 추첨
+ Collections.shuffle(filteredComments);
+ List winners = filteredComments.stream().limit(winnerCount).collect(Collectors.toList());
+
+ return CommentConverter.toWinnerResponseDTO(winners, winnerCount, startDate, endDate);
+ }
+
+
+ // 키워드별 댓글 조회
+ public List getCommentsByKeyword(Long eventId, String keyword, 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"));
+
+ List comments = commentRepository.findByEventIdAndCommentTextContaining(eventId, keyword);
+
+ return comments.stream()
+ .map(CommentConverter::toCommentDetailDTO)
+ .collect(Collectors.toList());
+ }
+
+ public List filterWinnersByKeyword(List winners, String keyword) {
+ return winners.stream()
+ .filter(winner -> winner.getCommentText().contains(keyword))
+ .collect(Collectors.toList());
+ }
+
+}
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 3981280..597f62c 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
@@ -4,6 +4,8 @@
import com.cmc.suppin.event.crawl.converter.DateConverter;
import com.cmc.suppin.event.crawl.domain.Comment;
import com.cmc.suppin.event.crawl.domain.repository.CommentRepository;
+import com.cmc.suppin.event.crawl.exception.CrawlErrorCode;
+import com.cmc.suppin.event.crawl.exception.CrawlException;
import com.cmc.suppin.event.events.domain.Event;
import com.cmc.suppin.event.events.domain.repository.EventRepository;
import com.cmc.suppin.global.enums.UserStatus;
@@ -24,6 +26,7 @@
import java.time.LocalDateTime;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
@Service
@@ -36,13 +39,43 @@ public class CrawlService {
private final EventRepository eventRepository;
private final MemberRepository memberRepository;
- public void crawlYoutubeComments(String url, Long eventId, String userId) {
+ public String checkExistingComments(String url, 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"));
+ List existingComments = commentRepository.findByUrlAndEventId(url, eventId);
+ if (!existingComments.isEmpty()) {
+ LocalDateTime firstCommentDate = existingComments.get(0).getCreatedAt();
+ return "동일한 URL의 댓글을 " + firstCommentDate.toLocalDate() + " 일자에 수집한 이력이 있습니다.";
+ }
+
+ return null;
+ }
+
+ public void crawlYoutubeComments(String url, Long eventId, String userId, boolean forceUpdate) {
+ 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"));
+
+ if (forceUpdate) {
+ // 기존 댓글 삭제
+ commentRepository.deleteByUrlAndEventId(url, eventId);
+ } else {
+ // 기존 댓글이 존재하는 경우: 크롤링을 중지하고 예외를 던집니다.
+ // 기존 댓글이 존재하지 않는 경우: 새로운 댓글을 크롤링하고 이를 DB에 저장합니다.
+
+ List existingComments = commentRepository.findByUrlAndEventId(url, eventId);
+ if (!existingComments.isEmpty()) {
+ throw new CrawlException(CrawlErrorCode.DUPLICATE_URL);
+ }
+ }
+
+ // 크롤링 코드 실행 (생략)
System.setProperty("webdriver.chrome.driver", "src/main/resources/drivers/chromedriver");
ChromeOptions options = new ChromeOptions();
@@ -51,6 +84,11 @@ public void crawlYoutubeComments(String url, Long eventId, String userId) {
options.addArguments("--no-sandbox");
options.addArguments("--disable-dev-shm-usage");
options.addArguments("--remote-allow-origins=*");
+ options.addArguments("--window-size=1920,1080");
+ options.addArguments("--disable-extensions");
+ options.addArguments("--disable-infobars");
+ options.addArguments("--disable-browser-side-navigation");
+ options.addArguments("--disable-software-rasterizer");
WebDriver driver = new ChromeDriver(options);
driver.get(url);
@@ -58,13 +96,14 @@ public void crawlYoutubeComments(String url, Long eventId, String userId) {
Set uniqueComments = new HashSet<>();
try {
- Thread.sleep(5000);
+ Thread.sleep(5000); // 초기 로딩 대기
- long endTime = System.currentTimeMillis() + 120000;
+ long endTime = System.currentTimeMillis() + 240000; // 스크롤 시간 조정 (필요에 따라 조정)
JavascriptExecutor jsExecutor = (JavascriptExecutor) driver;
while (System.currentTimeMillis() < endTime) {
jsExecutor.executeScript("window.scrollTo(0, document.documentElement.scrollHeight);");
+
Thread.sleep(1000);
String pageSource = driver.getPageSource();
diff --git a/src/main/java/com/cmc/suppin/global/security/config/WebMvcConfig.java b/src/main/java/com/cmc/suppin/global/security/config/WebMvcConfig.java
index 213fc00..73ea984 100644
--- a/src/main/java/com/cmc/suppin/global/security/config/WebMvcConfig.java
+++ b/src/main/java/com/cmc/suppin/global/security/config/WebMvcConfig.java
@@ -24,7 +24,8 @@ public void addArgumentResolvers(List resolvers)
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
- .allowedOrigins(getAllowOrigins())
+ .allowedOriginPatterns("*") // TODO: 2024-08-07 개발용으로 모든 도메인 허용, 운영 시 아래 주석 해제
+// .allowedOrigins(getAllowOrigins())
.allowedHeaders("Authorization", "Cache-Control", "Content-Type")
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowCredentials(true);
diff --git a/src/main/java/com/cmc/suppin/global/exception/SecurityErrorCode.java b/src/main/java/com/cmc/suppin/global/security/exception/SecurityErrorCode.java
similarity index 93%
rename from src/main/java/com/cmc/suppin/global/exception/SecurityErrorCode.java
rename to src/main/java/com/cmc/suppin/global/security/exception/SecurityErrorCode.java
index dc4e3d1..dae305a 100644
--- a/src/main/java/com/cmc/suppin/global/exception/SecurityErrorCode.java
+++ b/src/main/java/com/cmc/suppin/global/security/exception/SecurityErrorCode.java
@@ -1,5 +1,6 @@
-package com.cmc.suppin.global.exception;
+package com.cmc.suppin.global.security.exception;
+import com.cmc.suppin.global.exception.BaseErrorCode;
import com.cmc.suppin.global.response.ErrorResponse;
import lombok.Getter;
import org.springframework.http.HttpStatus;
diff --git a/src/main/java/com/cmc/suppin/global/security/jwt/JwtAccessDeniedHandler.java b/src/main/java/com/cmc/suppin/global/security/jwt/JwtAccessDeniedHandler.java
index bf16017..777bcca 100644
--- a/src/main/java/com/cmc/suppin/global/security/jwt/JwtAccessDeniedHandler.java
+++ b/src/main/java/com/cmc/suppin/global/security/jwt/JwtAccessDeniedHandler.java
@@ -1,6 +1,6 @@
package com.cmc.suppin.global.security.jwt;
-import com.cmc.suppin.global.exception.SecurityErrorCode;
+import com.cmc.suppin.global.security.exception.SecurityErrorCode;
import com.cmc.suppin.global.security.util.HttpResponseUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
diff --git a/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationEntryPoint.java
index 7c7c1b0..5d35bd8 100644
--- a/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationEntryPoint.java
+++ b/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationEntryPoint.java
@@ -1,6 +1,6 @@
package com.cmc.suppin.global.security.jwt;
-import com.cmc.suppin.global.exception.SecurityErrorCode;
+import com.cmc.suppin.global.security.exception.SecurityErrorCode;
import com.cmc.suppin.global.security.util.HttpResponseUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
diff --git a/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationFilter.java b/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationFilter.java
index 05d86a2..c6cfd15 100644
--- a/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationFilter.java
+++ b/src/main/java/com/cmc/suppin/global/security/jwt/JwtAuthenticationFilter.java
@@ -1,6 +1,6 @@
package com.cmc.suppin.global.security.jwt;
-import com.cmc.suppin.global.exception.SecurityErrorCode;
+import com.cmc.suppin.global.security.exception.SecurityErrorCode;
import com.cmc.suppin.global.security.util.HttpResponseUtil;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
diff --git a/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java b/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java
index 61d2708..6e852bb 100644
--- a/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java
+++ b/src/main/java/com/cmc/suppin/global/security/service/MemberDetailsService.java
@@ -16,8 +16,8 @@
import java.util.ArrayList;
import java.util.List;
-import static com.cmc.suppin.global.exception.MemberErrorCode.MEMBER_ALREADY_DELETED;
-import static com.cmc.suppin.global.exception.MemberErrorCode.MEMBER_NOT_FOUND;
+import static com.cmc.suppin.member.exception.MemberErrorCode.MEMBER_ALREADY_DELETED;
+import static com.cmc.suppin.member.exception.MemberErrorCode.MEMBER_NOT_FOUND;
@Service
@RequiredArgsConstructor
diff --git a/src/main/java/com/cmc/suppin/global/security/util/SecurityUtil.java b/src/main/java/com/cmc/suppin/global/security/util/SecurityUtil.java
index a523ea9..1b291be 100644
--- a/src/main/java/com/cmc/suppin/global/security/util/SecurityUtil.java
+++ b/src/main/java/com/cmc/suppin/global/security/util/SecurityUtil.java
@@ -1,6 +1,6 @@
package com.cmc.suppin.global.security.util;
-import com.cmc.suppin.global.exception.SecurityErrorCode;
+import com.cmc.suppin.global.security.exception.SecurityErrorCode;
import com.cmc.suppin.global.security.exception.SecurityException;
import com.cmc.suppin.global.security.reslover.Account;
import com.cmc.suppin.global.security.user.UserDetailsImpl;
diff --git a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java
index 43b99b8..e0170e9 100644
--- a/src/main/java/com/cmc/suppin/member/controller/MemberApi.java
+++ b/src/main/java/com/cmc/suppin/member/controller/MemberApi.java
@@ -30,7 +30,7 @@ public class MemberApi {
// 회원가입
@PostMapping("/join")
- @Operation(summary = "회원가입 API", description = "request 파라미터 : id, password, name, phone, email")
+ @Operation(summary = "회원가입 API", description = "Request: termsAgree, userId, password, name, phone, email, userType, verificationCode")
public ResponseEntity> join(@RequestBody @Valid MemberRequestDTO.JoinDTO request) {
Member member = memberService.join(request);
diff --git a/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java b/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java
index 201b575..15bd279 100644
--- a/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java
+++ b/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java
@@ -39,6 +39,7 @@ public static MemberResponseDTO.JoinResultDTO toJoinResultDTO(Member member) {
.userId(member.getUserId())
.name(member.getName())
.email(member.getEmail())
+ .phoneNumber(member.getPhoneNumber())
.createdAt(LocalDateTime.now())
.build();
}
diff --git a/src/main/java/com/cmc/suppin/member/domain/Member.java b/src/main/java/com/cmc/suppin/member/domain/Member.java
index d972b99..8fd23dd 100644
--- a/src/main/java/com/cmc/suppin/member/domain/Member.java
+++ b/src/main/java/com/cmc/suppin/member/domain/Member.java
@@ -41,9 +41,7 @@ public class Member extends BaseDateTimeEntity {
@Column(columnDefinition = "VARCHAR(13)", nullable = false)
private String phoneNumber;
-
- private Boolean termsAgree;
-
+
@Enumerated(EnumType.STRING)
private UserRole role;
diff --git a/src/main/java/com/cmc/suppin/global/exception/MemberErrorCode.java b/src/main/java/com/cmc/suppin/member/exception/MemberErrorCode.java
similarity index 93%
rename from src/main/java/com/cmc/suppin/global/exception/MemberErrorCode.java
rename to src/main/java/com/cmc/suppin/member/exception/MemberErrorCode.java
index a525134..603fa0c 100644
--- a/src/main/java/com/cmc/suppin/global/exception/MemberErrorCode.java
+++ b/src/main/java/com/cmc/suppin/member/exception/MemberErrorCode.java
@@ -1,5 +1,6 @@
-package com.cmc.suppin.global.exception;
+package com.cmc.suppin.member.exception;
+import com.cmc.suppin.global.exception.BaseErrorCode;
import com.cmc.suppin.global.response.ErrorResponse;
import lombok.Getter;
import org.springframework.http.HttpStatus;
diff --git a/src/main/java/com/cmc/suppin/member/service/MemberService.java b/src/main/java/com/cmc/suppin/member/service/MemberService.java
index f1455a6..d020f74 100644
--- a/src/main/java/com/cmc/suppin/member/service/MemberService.java
+++ b/src/main/java/com/cmc/suppin/member/service/MemberService.java
@@ -2,7 +2,6 @@
import com.cmc.suppin.global.config.MailConfig;
import com.cmc.suppin.global.enums.UserStatus;
-import com.cmc.suppin.global.exception.MemberErrorCode;
import com.cmc.suppin.global.security.jwt.JwtTokenProvider;
import com.cmc.suppin.global.security.user.UserDetailsImpl;
import com.cmc.suppin.member.controller.dto.MemberRequestDTO;
@@ -13,6 +12,7 @@
import com.cmc.suppin.member.domain.TermsAgree;
import com.cmc.suppin.member.domain.repository.EmailVerificationTokenRepository;
import com.cmc.suppin.member.domain.repository.MemberRepository;
+import com.cmc.suppin.member.exception.MemberErrorCode;
import com.cmc.suppin.member.exception.MemberException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -26,6 +26,7 @@
import java.security.SecureRandom;
import java.time.LocalDateTime;
+import java.util.ArrayList;
import java.util.Optional;
@Service
@@ -67,6 +68,10 @@ public Member join(MemberRequestDTO.JoinDTO request) {
member.setStatus(UserStatus.ACTIVE);
// 약관 동의 항목 처리
+ // termsAgreeList 초기화
+ if (member.getTermsAgreeList() == null) {
+ member.setTermsAgreeList(new ArrayList<>());
+ }
TermsAgree termsAgree = memberConverter.toTermsAgreeEntity(request.getTermsAgree(), member);
member.addTermsAgree(termsAgree);