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);