From 16608843c2f904d8fb34a1a2153b46e30b1113ca Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Wed, 7 Aug 2024 22:06:52 +0900 Subject: [PATCH 01/18] =?UTF-8?q?Feat:=20=ED=81=AC=EB=A1=A4=EB=A7=81=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C,?= =?UTF-8?q?=20datetime=EC=9C=BC=EB=A1=9C=20=EB=B3=80=ED=99=98=EB=90=98?= =?UTF-8?q?=EC=96=B4=20=EC=A0=80=EC=9E=A5=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crawl/converter/CommentConverter.java | 7 ++- .../event/crawl/converter/DateConverter.java | 47 +++++++++++++++++++ .../event/crawl/service/CrawlService.java | 13 ++--- 3 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/cmc/suppin/event/crawl/converter/DateConverter.java 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 2c50b9d..8dd1cdc 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 @@ -3,15 +3,18 @@ import com.cmc.suppin.event.crawl.domain.Comment; import com.cmc.suppin.event.events.domain.Event; +import java.time.LocalDateTime; + public class CommentConverter { - public static Comment toCommentEntity(String author, String text, String time, String url, Event event) { + public static Comment toCommentEntity(String author, String text, LocalDateTime commentDate, String url, Event event) { return Comment.builder() .author(author) .commentText(text) - .commentDate(time) + .commentDate(commentDate) .url(url) .event(event) .build(); } } + diff --git a/src/main/java/com/cmc/suppin/event/crawl/converter/DateConverter.java b/src/main/java/com/cmc/suppin/event/crawl/converter/DateConverter.java new file mode 100644 index 0000000..62883c4 --- /dev/null +++ b/src/main/java/com/cmc/suppin/event/crawl/converter/DateConverter.java @@ -0,0 +1,47 @@ +package com.cmc.suppin.event.crawl.converter; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DateConverter { + + public static LocalDateTime convertRelativeTime(String relativeTime) { + LocalDateTime now = LocalDateTime.now(); + if (relativeTime.contains("일 전")) { + int days = extractNumber(relativeTime); + return now.minusDays(days); + } else if (relativeTime.contains("시간 전")) { + int hours = extractNumber(relativeTime); + return now.minusHours(hours); + } else if (relativeTime.contains("분 전")) { + int minutes = extractNumber(relativeTime); + return now.minusMinutes(minutes); + } else if (relativeTime.contains("주 전")) { + int weeks = extractNumber(relativeTime); + return now.minusWeeks(weeks); + } else if (relativeTime.contains("개월 전")) { + int months = extractNumber(relativeTime); + return now.minusMonths(months); + } else if (relativeTime.contains("년 전")) { + int years = extractNumber(relativeTime); + return now.minusYears(years); + } + return now; + } + + private static int extractNumber(String relativeTime) { + Pattern pattern = Pattern.compile("\\d+"); + Matcher matcher = pattern.matcher(relativeTime); + if (matcher.find()) { + return Integer.parseInt(matcher.group()); + } + return 0; + } + + public static String formatLocalDateTime(LocalDateTime dateTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + return dateTime.format(formatter); + } +} 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 07073c2..3981280 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 @@ -1,6 +1,7 @@ package com.cmc.suppin.event.crawl.service; import com.cmc.suppin.event.crawl.converter.CommentConverter; +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.events.domain.Event; @@ -21,6 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @@ -35,18 +37,12 @@ public class CrawlService { private final MemberRepository memberRepository; public void crawlYoutubeComments(String url, Long eventId, String userId) { - log.info("Start crawling comments for URL: {} by user: {}", url, userId); - Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED) .orElseThrow(() -> new IllegalArgumentException("Member not found")); - log.info("Member found: {}", member.getId()); - Event event = eventRepository.findByIdAndMemberId(eventId, member.getId()) .orElseThrow(() -> new IllegalArgumentException("Event not found")); - log.info("Event found: {}", event.getId()); - System.setProperty("webdriver.chrome.driver", "src/main/resources/drivers/chromedriver"); ChromeOptions options = new ChromeOptions(); @@ -84,9 +80,9 @@ public void crawlYoutubeComments(String url, Long eventId, String userId) { uniqueComments.add(text); // 엔티티 저장 - Comment comment = CommentConverter.toCommentEntity(author, text, time, url, event); + LocalDateTime actualCommentDate = DateConverter.convertRelativeTime(time); + Comment comment = CommentConverter.toCommentEntity(author, text, actualCommentDate, url, event); commentRepository.save(comment); - log.info("Comment saved: {}", comment.getId()); } } } @@ -97,3 +93,4 @@ public void crawlYoutubeComments(String url, Long eventId, String userId) { } } } + From 338e8ee9a1a1b58c026376629abe22267da0bf1e Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Wed, 7 Aug 2024 22:54:00 +0900 Subject: [PATCH 02/18] =?UTF-8?q?Refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EC=95=BD=EA=B4=80=EB=8F=99=EC=9D=98=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9C=A0=EC=A0=80=EC=9C=A0=ED=98=95=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/dto/MemberRequestDTO.java | 23 +++++++++++++ .../member/converter/MemberConverter.java | 14 +++++++- .../com/cmc/suppin/member/domain/Member.java | 18 +++++----- .../cmc/suppin/member/domain/TermsAgree.java | 34 +++++++++++++++++++ .../suppin/member/service/MemberService.java | 9 +++-- 5 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/cmc/suppin/member/domain/TermsAgree.java diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java index 871ddd2..379ca53 100644 --- a/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberRequestDTO.java @@ -3,6 +3,7 @@ import jakarta.persistence.Id; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; import lombok.AllArgsConstructor; import lombok.Getter; @@ -14,6 +15,9 @@ public class MemberRequestDTO { @NoArgsConstructor @AllArgsConstructor public static class JoinDTO { + + private TermsAgreeDTO termsAgree; + @NotBlank(message = "아이디를 입력해주세요") @Id private String userId; @@ -32,8 +36,27 @@ public static class JoinDTO { @NotBlank(message = "휴대폰 번호를 입력해주세요") private String phone; + private String userType; + @NotBlank(message = "이메일 인증번호를 입력해주세요") private String verificationCode; + + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class TermsAgreeDTO { + @NotNull(message = "14세 이상 동의 여부를 선택해주세요") + private Boolean ageOver14Agree; + + @NotNull(message = "서비스 이용 동의 여부를 선택해주세요") + private Boolean serviceUseAgree; + + @NotNull(message = "개인정보 수집 및 이용 동의 여부를 선택해주세요") + private Boolean personalInfoAgree; + + private Boolean marketingAgree; // 선택 사항 } @Getter 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 21727da..201b575 100644 --- a/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java +++ b/src/main/java/com/cmc/suppin/member/converter/MemberConverter.java @@ -3,6 +3,7 @@ import com.cmc.suppin.member.controller.dto.MemberRequestDTO; import com.cmc.suppin.member.controller.dto.MemberResponseDTO; import com.cmc.suppin.member.domain.Member; +import com.cmc.suppin.member.domain.TermsAgree; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -11,13 +12,24 @@ @Component public class MemberConverter { - public Member toEntity(MemberRequestDTO.JoinDTO request, PasswordEncoder encoder) { + public Member toMemberEntity(MemberRequestDTO.JoinDTO request, PasswordEncoder encoder) { return Member.builder() .userId(request.getUserId()) .name(request.getName()) .password(encoder.encode(request.getPassword())) .email(request.getEmail()) .phoneNumber(request.getPhone()) + .userType(request.getUserType()) + .build(); + } + + public TermsAgree toTermsAgreeEntity(MemberRequestDTO.TermsAgreeDTO termsAgreeDTO, Member member) { + return TermsAgree.builder() + .ageOver14Agree(termsAgreeDTO.getAgeOver14Agree()) + .serviceUseAgree(termsAgreeDTO.getServiceUseAgree()) + .personalInfoAgree(termsAgreeDTO.getPersonalInfoAgree()) + .marketingAgree(termsAgreeDTO.getMarketingAgree()) + .member(member) .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 87f0118..d972b99 100644 --- a/src/main/java/com/cmc/suppin/member/domain/Member.java +++ b/src/main/java/com/cmc/suppin/member/domain/Member.java @@ -24,7 +24,7 @@ public class Member extends BaseDateTimeEntity { @Column(name = "member_id") private Long id; - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List eventList = new ArrayList<>(); @Column(columnDefinition = "VARCHAR(30)", nullable = false) @@ -50,13 +50,15 @@ public class Member extends BaseDateTimeEntity { @Enumerated(EnumType.STRING) private UserStatus status; - // 추가된 생성자 - public Member(String name, String password, String email, String phoneNumber, UserRole role) { - this.name = name; - this.password = password; - this.email = email; - this.phoneNumber = phoneNumber; - this.role = role; + @Column(columnDefinition = "VARCHAR(50)", nullable = false) + private String userType; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List termsAgreeList = new ArrayList<>(); + + public void addTermsAgree(TermsAgree termsAgree) { + termsAgree.setMember(this); + this.termsAgreeList.add(termsAgree); } public boolean isDeleted() { diff --git a/src/main/java/com/cmc/suppin/member/domain/TermsAgree.java b/src/main/java/com/cmc/suppin/member/domain/TermsAgree.java new file mode 100644 index 0000000..9d9a9c7 --- /dev/null +++ b/src/main/java/com/cmc/suppin/member/domain/TermsAgree.java @@ -0,0 +1,34 @@ +package com.cmc.suppin.member.domain; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TermsAgree { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false) + private Boolean ageOver14Agree; + + @Column(nullable = false) + private Boolean serviceUseAgree; + + @Column(nullable = false) + private Boolean personalInfoAgree; + + @Column(nullable = true) + private Boolean marketingAgree; +} + 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 6940584..f1455a6 100644 --- a/src/main/java/com/cmc/suppin/member/service/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/MemberService.java @@ -10,6 +10,7 @@ import com.cmc.suppin.member.converter.MemberConverter; import com.cmc.suppin.member.domain.EmailVerificationToken; import com.cmc.suppin.member.domain.Member; +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.MemberException; @@ -62,9 +63,13 @@ public Member join(MemberRequestDTO.JoinDTO request) { } // DTO를 Entity로 변환 - Member member = memberConverter.toEntity(request, passwordEncoder); + Member member = memberConverter.toMemberEntity(request, passwordEncoder); member.setStatus(UserStatus.ACTIVE); + // 약관 동의 항목 처리 + TermsAgree termsAgree = memberConverter.toTermsAgreeEntity(request.getTermsAgree(), member); + member.addTermsAgree(termsAgree); + // 회원 정보 저장 memberRepository.save(member); @@ -167,7 +172,7 @@ private void saveVerificationToken(String email, String code) { emailVerificationTokenRepository.deleteByEmail(email); emailVerificationTokenRepository.save(verificationToken); } - + private Member getMember(Long memberId) { return memberRepository.findByIdAndStatusNot(memberId, UserStatus.DELETED) From 0bb7706da0a7ea0963d66e544944d19b61aff305 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Wed, 7 Aug 2024 22:54:56 +0900 Subject: [PATCH 03/18] =?UTF-8?q?Chore:=20BaseDateTimeEntity=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/cmc/suppin/SuppinApplication.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/cmc/suppin/SuppinApplication.java b/src/main/java/com/cmc/suppin/SuppinApplication.java index 90c0eed..995c389 100644 --- a/src/main/java/com/cmc/suppin/SuppinApplication.java +++ b/src/main/java/com/cmc/suppin/SuppinApplication.java @@ -2,12 +2,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class SuppinApplication { - public static void main(String[] args) { - SpringApplication.run(SuppinApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(SuppinApplication.class, args); + } } From e09b5ece289ec208e640b80179aa52a7095aa6b3 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Wed, 7 Aug 2024 23:04:35 +0900 Subject: [PATCH 04/18] =?UTF-8?q?Fix:=20=EA=B0=9C=EB=B0=9C=EC=9A=A9,=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20Origin=20=EA=B2=BD=EB=A1=9C=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9(=EC=B6=94=ED=9B=84=20=EB=B3=80=EA=B2=BD=20=ED=95=84?= =?UTF-8?q?=EC=9A=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cmc/suppin/global/security/config/WebMvcConfig.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..040cbe9 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()) + .allowedOrigins("*") // TODO: 2024-08-07 개발용으로 모든 도메인 허용, 운영 시 아래 주석 해제 +// .allowedOrigins(getAllowOrigins()) .allowedHeaders("Authorization", "Cache-Control", "Content-Type") .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH") .allowCredentials(true); From 3ec5f8e82fa42153bf6055c15084bd7f70b76131 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Wed, 7 Aug 2024 23:05:49 +0900 Subject: [PATCH 05/18] =?UTF-8?q?Fix:=20Comment=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=BB=AC=EB=9F=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/cmc/suppin/event/crawl/domain/Comment.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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; } From 9a65eb73edc648b923b9ef76005770b07099f4d5 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Wed, 7 Aug 2024 23:06:14 +0900 Subject: [PATCH 06/18] =?UTF-8?q?Feat:=20=ED=81=AC=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/crawl/exception/CrawlException.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/cmc/suppin/event/crawl/exception/CrawlException.java 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); + } +} From 85b69841a1dd035e545134f07018ef3f32347463 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 01:01:56 +0900 Subject: [PATCH 07/18] Fix: allowedOrigins -> allowedOriginPatterens --- .../com/cmc/suppin/global/security/config/WebMvcConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 040cbe9..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,7 @@ public void addArgumentResolvers(List resolvers) @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("*") // TODO: 2024-08-07 개발용으로 모든 도메인 허용, 운영 시 아래 주석 해제 + .allowedOriginPatterns("*") // TODO: 2024-08-07 개발용으로 모든 도메인 허용, 운영 시 아래 주석 해제 // .allowedOrigins(getAllowOrigins()) .allowedHeaders("Authorization", "Cache-Control", "Content-Type") .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH") From a86b400458428aa7677b2341ef725e4c9139bb7b Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 01:02:59 +0900 Subject: [PATCH 08/18] =?UTF-8?q?Fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cmc/suppin/member/controller/MemberApi.java | 2 +- .../com/cmc/suppin/member/converter/MemberConverter.java | 1 + src/main/java/com/cmc/suppin/member/domain/Member.java | 4 +--- .../java/com/cmc/suppin/member/service/MemberService.java | 5 +++++ 4 files changed, 8 insertions(+), 4 deletions(-) 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/member/service/MemberService.java b/src/main/java/com/cmc/suppin/member/service/MemberService.java index f1455a6..536ca95 100644 --- a/src/main/java/com/cmc/suppin/member/service/MemberService.java +++ b/src/main/java/com/cmc/suppin/member/service/MemberService.java @@ -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); From 32da7fb4d6f76d8409b1be0289e01c1cf01bfb33 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 04:39:11 +0900 Subject: [PATCH 09/18] =?UTF-8?q?Refactor:=20ChromeOption=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=A6=9D=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/crawl/controller/CrawlApi.java | 6 ++--- .../event/crawl/service/CrawlService.java | 25 ++++++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) 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..cdcfee8 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 @@ -29,9 +29,9 @@ public class CrawlApi { // 유튜브 크롤링 @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)); + public ResponseEntity> crawlYoutubeComments(@RequestParam String url, @RequestParam Long eventId, @CurrentAccount Account account) { + String message = crawlService.crawlYoutubeComments(url, eventId, account.userId()); + return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS, message)); } // TODO: 인스타그램 게시글 크롤링 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..4e8e37e 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 @@ -24,6 +24,7 @@ import java.time.LocalDateTime; import java.util.HashSet; +import java.util.List; import java.util.Set; @Service @@ -36,13 +37,23 @@ public class CrawlService { private final EventRepository eventRepository; private final MemberRepository memberRepository; - public void crawlYoutubeComments(String url, Long eventId, String userId) { + public String crawlYoutubeComments(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.findByUrl(url); + if (!existingComments.isEmpty()) { + LocalDateTime firstCommentDate = existingComments.get(0).getCreatedAt(); + return "이미 " + firstCommentDate.toLocalDate() + " 일자에 수집했던 댓글이 있습니다."; + } + + // 기존 댓글 삭제 + commentRepository.deleteByUrl(url); + System.setProperty("webdriver.chrome.driver", "src/main/resources/drivers/chromedriver"); ChromeOptions options = new ChromeOptions(); @@ -51,6 +62,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 +74,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(); @@ -91,6 +108,8 @@ public void crawlYoutubeComments(String url, Long eventId, String userId) { } finally { driver.quit(); } + + return "댓글 수집이 완료되었습니다."; } } From 6c1d2d1898536e3ffcb06db0061f9c1b8a6772f8 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 04:39:41 +0900 Subject: [PATCH 10/18] =?UTF-8?q?Feat:=20=ED=81=AC=EB=A1=A4=EB=A7=81?= =?UTF-8?q?=EB=90=9C=20=EB=8C=93=EA=B8=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/crawl/controller/CommentApi.java | 28 ++++++++++- .../controller/dto/CommentRequestDTO.java | 20 ++++++++ .../controller/dto/CommentResponseDTO.java | 31 ++++++++++++ .../crawl/converter/CommentConverter.java | 24 ++++++++++ .../domain/repository/CommentRepository.java | 9 +++- .../event/crawl/service/CommentService.java | 48 +++++++++++++++++++ 6 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentRequestDTO.java create mode 100644 src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java create mode 100644 src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java 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..670b443 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,10 +1,19 @@ 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; @RestController @@ -12,8 +21,25 @@ @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: 조회할 페이지 번호 (0부터 시작), " + + "size: 한 페이지당 댓글 수, Authorization: JWT 토큰을 포함한 인증 헤더
" + + "Response: 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)); + } } 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..c98dd54 --- /dev/null +++ b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentRequestDTO.java @@ -0,0 +1,20 @@ +package com.cmc.suppin.event.crawl.controller.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class CommentRequestDTO { + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CommentListRequestDTO { + private Long eventId; + private String url; + private int page; + private int size; + } +} 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..f9bb4e8 --- /dev/null +++ b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java @@ -0,0 +1,31 @@ +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 participantCount; + private String crawlTime; + private List comments; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class CommentDetailDTO { + private String author; + private String commentText; + private String commentDate; + } +} 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..415d7ec 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,25 @@ 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) { + List commentDetailDTOS = comments.stream() + .map(CommentConverter::toCommentDetailDTO) + .collect(Collectors.toList()); + + return CommentResponseDTO.CrawledCommentListDTO.builder() + .participantCount(commentDetailDTOS.size()) + .crawlTime(crawlTime) + .comments(commentDetailDTOS) + .build(); + } } 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..f80353d 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,19 @@ 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 deleteByUrl(String url); + + Page findByEventIdAndUrlAndCommentDateBefore(Long eventId, String url, LocalDateTime endDate, Pageable pageable); } 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..fbe9a1b --- /dev/null +++ b/src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java @@ -0,0 +1,48 @@ +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.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@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, size, Sort.by("commentDate").descending()); + Page comments = commentRepository.findByEventIdAndUrlAndCommentDateBefore(eventId, url, event.getEndDate(), pageable); + + String crawlTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")); + + return CommentConverter.toCommentListDTO(comments.getContent(), crawlTime); + } +} From 6cc3594ac897133e441bdfccc67c82629e178c55 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 05:32:41 +0900 Subject: [PATCH 11/18] =?UTF-8?q?Feat:=20=EC=82=AC=EC=A0=84=20URL=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/crawl/controller/CrawlApi.java | 45 ++++++++++++++---- .../domain/repository/CommentRepository.java | 4 +- .../event/crawl/exception/CrawlErrorCode.java | 28 +++++++++++ .../event/crawl/service/CrawlService.java | 46 +++++++++++++------ .../service/MemberDetailsService.java | 4 +- 5 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/cmc/suppin/event/crawl/exception/CrawlErrorCode.java 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 cdcfee8..4dc7de7 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,41 @@ 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) { - String message = crawlService.crawlYoutubeComments(url, eventId, account.userId()); - return ResponseEntity.ok(ApiResponse.of(ResponseCode.SUCCESS, message)); + // 크롤링 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 토큰을 포함한 인증 헤더") + 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/domain/repository/CommentRepository.java b/src/main/java/com/cmc/suppin/event/crawl/domain/repository/CommentRepository.java index f80353d..3098913 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 @@ -13,7 +13,9 @@ public interface CommentRepository extends JpaRepository { List findByUrl(String url); - void deleteByUrl(String url); + void deleteByUrlAndEventId(String url, Long eventId); Page findByEventIdAndUrlAndCommentDateBefore(Long eventId, String url, LocalDateTime endDate, Pageable pageable); + + List findByUrlAndEventId(String url, Long eventId); } 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/service/CrawlService.java b/src/main/java/com/cmc/suppin/event/crawl/service/CrawlService.java index 4e8e37e..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; @@ -37,23 +39,43 @@ public class CrawlService { private final EventRepository eventRepository; private final MemberRepository memberRepository; - public String 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.findByUrl(url); + List existingComments = commentRepository.findByUrlAndEventId(url, eventId); if (!existingComments.isEmpty()) { LocalDateTime firstCommentDate = existingComments.get(0).getCreatedAt(); - return "이미 " + firstCommentDate.toLocalDate() + " 일자에 수집했던 댓글이 있습니다."; + return "동일한 URL의 댓글을 " + firstCommentDate.toLocalDate() + " 일자에 수집한 이력이 있습니다."; } - // 기존 댓글 삭제 - commentRepository.deleteByUrl(url); + 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(); @@ -62,11 +84,11 @@ public String 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"); // 소프트웨어 래스터라이저 비활성화 + 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); @@ -108,8 +130,6 @@ public String crawlYoutubeComments(String url, Long eventId, String userId) { } finally { driver.quit(); } - - return "댓글 수집이 완료되었습니다."; } } 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 From 6c7877e22dc452f6962379d5b3f353abe30f508c Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 05:33:30 +0900 Subject: [PATCH 12/18] =?UTF-8?q?Chore:=20ErrorCode=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/{ => security}/exception/SecurityErrorCode.java | 3 ++- .../cmc/suppin/global/security/jwt/JwtAccessDeniedHandler.java | 2 +- .../global/security/jwt/JwtAuthenticationEntryPoint.java | 2 +- .../suppin/global/security/jwt/JwtAuthenticationFilter.java | 2 +- .../java/com/cmc/suppin/global/security/util/SecurityUtil.java | 2 +- .../suppin/{global => member}/exception/MemberErrorCode.java | 3 ++- src/main/java/com/cmc/suppin/member/service/MemberService.java | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) rename src/main/java/com/cmc/suppin/global/{ => security}/exception/SecurityErrorCode.java (93%) rename src/main/java/com/cmc/suppin/{global => member}/exception/MemberErrorCode.java (93%) 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/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/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 536ca95..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; From d611c78d3aa55d9e61bcd5119533d274a319f784 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 05:43:47 +0900 Subject: [PATCH 13/18] =?UTF-8?q?Feat:=20=ED=81=AC=EB=A1=A4=EB=A7=81=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C,?= =?UTF-8?q?=20=EC=A0=84=EC=B2=B4=20=EB=8C=93=EA=B8=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/cmc/suppin/event/crawl/controller/CommentApi.java | 2 +- .../suppin/event/crawl/controller/dto/CommentResponseDTO.java | 1 + .../cmc/suppin/event/crawl/converter/CommentConverter.java | 3 ++- .../event/crawl/domain/repository/CommentRepository.java | 2 ++ .../com/cmc/suppin/event/crawl/service/CommentService.java | 4 +++- 5 files changed, 9 insertions(+), 3 deletions(-) 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 670b443..aca894a 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 @@ -31,7 +31,7 @@ public class CommentApi { description = "주어진 이벤트 ID와 URL의 댓글을 페이지네이션하여 이벤트의 endDate 전에 작성된 댓글들만 조회합니다.

" + "Request: eventId: 조회할 이벤트의 ID, url: 댓글을 조회할 유튜브 URL, page: 조회할 페이지 번호 (0부터 시작), " + "size: 한 페이지당 댓글 수, Authorization: JWT 토큰을 포함한 인증 헤더
" + - "Response: participantCount: 현재 페이지에서 가져온 댓글 수, crawlTime: 댓글 조회(크롤링) 요청 시간, comments: 각 댓글의 상세 정보 배열" + + "Response: totalCommentCount: 전체 댓글 수, participantCount: 현재 페이지에서 가져온 댓글 수, crawlTime: 댓글 조회(크롤링) 요청 시간, comments: 각 댓글의 상세 정보 배열" + "author: 댓글 작성자, commentText: 댓글 내용, commentDate: 댓글 작성 시간") public ResponseEntity> getComments( @RequestParam Long eventId, 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 f9bb4e8..6978646 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 @@ -14,6 +14,7 @@ public class CommentResponseDTO { @NoArgsConstructor @AllArgsConstructor public static class CrawledCommentListDTO { + private int totalCommentCount; private int participantCount; private String crawlTime; private List comments; 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 415d7ec..4ea30f1 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 @@ -29,12 +29,13 @@ public static CommentResponseDTO.CommentDetailDTO toCommentDetailDTO(Comment com .build(); } - public static CommentResponseDTO.CrawledCommentListDTO toCommentListDTO(List comments, String crawlTime) { + 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) 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 3098913..a1fd831 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 @@ -18,4 +18,6 @@ public interface CommentRepository extends JpaRepository { Page findByEventIdAndUrlAndCommentDateBefore(Long eventId, String url, LocalDateTime endDate, Pageable pageable); List findByUrlAndEventId(String url, Long eventId); + + int countByEventIdAndUrl(Long eventId, String url); } 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 fbe9a1b..3f6f9be 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 @@ -41,8 +41,10 @@ public CommentResponseDTO.CrawledCommentListDTO getComments(Long eventId, String Pageable pageable = PageRequest.of(page, 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); + return CommentConverter.toCommentListDTO(comments.getContent(), crawlTime, totalComments); } } From b0a67ad2779ac80af7b0eeb0c6bba8d71eec018e Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 05:44:48 +0900 Subject: [PATCH 14/18] =?UTF-8?q?Fix:=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=8B=9C=EC=9E=91=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B2=88=ED=98=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cmc/suppin/event/crawl/controller/CommentApi.java | 2 +- .../java/com/cmc/suppin/event/crawl/service/CommentService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 aca894a..7aa361c 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 @@ -29,7 +29,7 @@ public class CommentApi { @GetMapping("/list") @Operation(summary = "크롤링된 전체 댓글 조회 API", description = "주어진 이벤트 ID와 URL의 댓글을 페이지네이션하여 이벤트의 endDate 전에 작성된 댓글들만 조회합니다.

" + - "Request: eventId: 조회할 이벤트의 ID, url: 댓글을 조회할 유튜브 URL, page: 조회할 페이지 번호 (0부터 시작), " + + "Request: eventId: 조회할 이벤트의 ID, url: 댓글을 조회할 유튜브 URL, page: 조회할 페이지 번호 (1부터 시작), " + "size: 한 페이지당 댓글 수, Authorization: JWT 토큰을 포함한 인증 헤더
" + "Response: totalCommentCount: 전체 댓글 수, participantCount: 현재 페이지에서 가져온 댓글 수, crawlTime: 댓글 조회(크롤링) 요청 시간, comments: 각 댓글의 상세 정보 배열" + "author: 댓글 작성자, commentText: 댓글 내용, commentDate: 댓글 작성 시간") 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 3f6f9be..1dcad23 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 @@ -38,7 +38,7 @@ public CommentResponseDTO.CrawledCommentListDTO getComments(Long eventId, String Event event = eventRepository.findByIdAndMemberId(eventId, member.getId()) .orElseThrow(() -> new IllegalArgumentException("Event not found")); - Pageable pageable = PageRequest.of(page, size, Sort.by("commentDate").descending()); + 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); From 8d5ef9a67009f2b9fb04704dd1f9b2a935a72804 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 05:50:33 +0900 Subject: [PATCH 15/18] =?UTF-8?q?Docs:=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/cmc/suppin/event/crawl/controller/CrawlApi.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 4dc7de7..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 @@ -41,7 +41,10 @@ public ResponseEntity> checkExistingComments(@RequestParam S @PostMapping("/crawling/comments") @Operation(summary = "유튜브 댓글 크롤링 API", description = "주어진 URL의 유튜브 댓글을 크롤링하여 DB에 저장합니다.

" + - "Request: url: 크롤링할 URL, eventId: 댓글을 수집할 eventId, forceUpdate: 댓글을 강제로 업데이트할지 여부(Boolean), Authorization: JWT 토큰을 포함한 인증 헤더") + "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, "댓글 수집이 완료되었습니다.")); From daf2b22451f2b1b199e6c1dcf7cecebe34c0fb5d Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 07:09:28 +0900 Subject: [PATCH 16/18] =?UTF-8?q?Feat:=20=EC=A1=B0=EA=B1=B4=EB=B3=84=20?= =?UTF-8?q?=EB=8B=B9=EC=B2=A8=EC=9E=90=20=EB=9E=9C=EB=8D=A4=20=EC=B6=94?= =?UTF-8?q?=EC=B2=A8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/crawl/controller/CommentApi.java | 15 +++++++ .../controller/dto/CommentRequestDTO.java | 14 +++++++ .../controller/dto/CommentResponseDTO.java | 10 +++++ .../domain/repository/CommentRepository.java | 3 ++ .../event/crawl/service/CommentService.java | 41 +++++++++++++++++++ 5 files changed, 83 insertions(+) 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 7aa361c..4791acc 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 @@ -16,6 +16,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController @Slf4j @RequiredArgsConstructor @@ -42,4 +44,17 @@ public ResponseEntity> get 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) { + List winners = commentService.drawWinners(eventId, startDate, endDate, winnerCount, keywords, account.userId()); + return ResponseEntity.ok(ApiResponse.of(winners)); + } } 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 c98dd54..c3b1028 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 @@ -5,6 +5,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.List; + public class CommentRequestDTO { @Getter @@ -17,4 +19,16 @@ public static class CommentListRequestDTO { 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 index 6978646..8ca3739 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 @@ -29,4 +29,14 @@ public static class CommentDetailDTO { private String commentText; private String commentDate; } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class WinnerResponseDTO { + private String author; + private String commentText; + private String 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 a1fd831..e4a46ee 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 @@ -20,4 +20,7 @@ public interface CommentRepository extends JpaRepository { List findByUrlAndEventId(String url, Long eventId); int countByEventIdAndUrl(Long eventId, String url); + + + List findByEventIdAndCommentDateBetween(Long eventId, LocalDateTime start, LocalDateTime end); } 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 1dcad23..2caeab2 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 @@ -18,8 +18,13 @@ 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 @@ -31,6 +36,7 @@ public class CommentService { 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")); @@ -47,4 +53,39 @@ public CommentResponseDTO.CrawledCommentListDTO getComments(Long eventId, String return CommentConverter.toCommentListDTO(comments.getContent(), crawlTime, totalComments); } + + // 당첨자 조건별 랜덤 추첨 + public List 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 winners.stream() + .map(comment -> new CommentResponseDTO.WinnerResponseDTO(comment.getAuthor(), comment.getCommentText(), comment.getCommentDate().toString())) + .collect(Collectors.toList()); + } + } From cb3707b70f45d746cb9065d6bee5df1b848d0703 Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 07:51:50 +0900 Subject: [PATCH 17/18] =?UTF-8?q?Feat:=20=ED=82=A4=EC=9B=8C=EB=93=9C?= =?UTF-8?q?=EB=B3=84=20=EB=8B=B9=EC=B2=A8=EC=9E=90=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/crawl/controller/CommentApi.java | 12 ++++++++++- .../controller/dto/CommentResponseDTO.java | 8 +++++++ .../crawl/converter/CommentConverter.java | 8 +++++++ .../domain/repository/CommentRepository.java | 3 ++- .../event/crawl/service/CommentService.java | 21 +++++++++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) 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 4791acc..6cad824 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 @@ -46,7 +46,7 @@ public ResponseEntity> get } @GetMapping("/draft-winners") - @Operation(summary = "조건별 당첨자 추첨 API", description = "주어진 조건에 따라 이벤트의 당첨자를 추첨합니다.") + @Operation(summary = "조건별 당첨자 추첨 API", description = "주어진 조건에 따라 이벤트의 당첨자를 추첨합니다. 키워드 조건은 OR 조건으로 적용됩니다.") public ResponseEntity>> drawWinners( @RequestParam Long eventId, @RequestParam String startDate, @@ -57,4 +57,14 @@ public ResponseEntity>> d List winners = commentService.drawWinners(eventId, startDate, endDate, winnerCount, keywords, account.userId()); return ResponseEntity.ok(ApiResponse.of(winners)); } + + @GetMapping("/winners/keywordFiltering") + @Operation(summary = "키워드별 당첨자 조회 API", description = "주어진 키워드에 따라 1차 랜덤 추첨된 당첨자 중에서 키워드가 포함된 당첨자들을 조회합니다. 해당 API에서 요청 키워드 갯수는 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/dto/CommentResponseDTO.java b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java index 8ca3739..676ba00 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 @@ -39,4 +39,12 @@ public static class WinnerResponseDTO { private String commentText; private String commentDate; } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class KeywordFilteredWinnerResponseDTO { + 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 4ea30f1..ff972e3 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 @@ -41,5 +41,13 @@ public static CommentResponseDTO.CrawledCommentListDTO toCommentListDTO(List { 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/service/CommentService.java b/src/main/java/com/cmc/suppin/event/crawl/service/CommentService.java index 2caeab2..f3b1230 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 @@ -88,4 +88,25 @@ public List drawWinners(Long eventId, Stri .collect(Collectors.toList()); } + // 키워드별 댓글 조회 + 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::toWinnerResponseDTO) + .collect(Collectors.toList()); + } + + public List filterWinnersByKeyword(List winners, String keyword) { + return winners.stream() + .filter(winner -> winner.getCommentText().contains(keyword)) + .collect(Collectors.toList()); + } + } From cadb5130658c4c60a61ba6231856f177982d23cf Mon Sep 17 00:00:00 2001 From: yxhwxn Date: Thu, 8 Aug 2024 08:06:01 +0900 Subject: [PATCH 18/18] =?UTF-8?q?Feat:=20=EB=8B=B9=EC=B2=A8=EC=9E=90=20?= =?UTF-8?q?=EC=B6=94=EC=B2=A8=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C,=20=EC=9D=91=EB=8B=B5=20dto=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../suppin/event/crawl/controller/CommentApi.java | 12 ++++++------ .../crawl/controller/dto/CommentResponseDTO.java | 15 ++++----------- .../event/crawl/converter/CommentConverter.java | 13 +++++++++---- .../event/crawl/service/CommentService.java | 13 ++++++------- 4 files changed, 25 insertions(+), 28 deletions(-) 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 6cad824..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 @@ -46,25 +46,25 @@ public ResponseEntity> get } @GetMapping("/draft-winners") - @Operation(summary = "조건별 당첨자 추첨 API", description = "주어진 조건에 따라 이벤트의 당첨자를 추첨합니다. 키워드 조건은 OR 조건으로 적용됩니다.") - public ResponseEntity>> drawWinners( + @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) { - List winners = commentService.drawWinners(eventId, startDate, endDate, winnerCount, keywords, account.userId()); + 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차 랜덤 추첨된 당첨자 중에서 키워드가 포함된 당첨자들을 조회합니다. 해당 API에서 요청 키워드 갯수는 1개입니다.") - public ResponseEntity>> getWinnersByKeyword( + @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()); + 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/dto/CommentResponseDTO.java b/src/main/java/com/cmc/suppin/event/crawl/controller/dto/CommentResponseDTO.java index 676ba00..30862ca 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 @@ -35,16 +35,9 @@ public static class CommentDetailDTO { @AllArgsConstructor @Builder public static class WinnerResponseDTO { - private String author; - private String commentText; - private String commentDate; - } - - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - public static class KeywordFilteredWinnerResponseDTO { - private List winners; + 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 ff972e3..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 @@ -42,11 +42,16 @@ public static CommentResponseDTO.CrawledCommentListDTO toCommentListDTO(List winners, int winnerCount, String startDate, String endDate) { + List winnerDetails = winners.stream() + .map(CommentConverter::toCommentDetailDTO) + .collect(Collectors.toList()); + return CommentResponseDTO.WinnerResponseDTO.builder() - .author(comment.getAuthor()) - .commentText(comment.getCommentText()) - .commentDate(comment.getCommentDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))) + .winnerCount(winnerCount) + .startDate(startDate) + .endDate(endDate) + .winners(winnerDetails) .build(); } } 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 f3b1230..d3cf147 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 @@ -55,7 +55,7 @@ public CommentResponseDTO.CrawledCommentListDTO getComments(Long eventId, String } // 당첨자 조건별 랜덤 추첨 - public List drawWinners(Long eventId, String startDate, String endDate, int winnerCount, List keywords, String userId) { + 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")); @@ -83,13 +83,12 @@ public List drawWinners(Long eventId, Stri Collections.shuffle(filteredComments); List winners = filteredComments.stream().limit(winnerCount).collect(Collectors.toList()); - return winners.stream() - .map(comment -> new CommentResponseDTO.WinnerResponseDTO(comment.getAuthor(), comment.getCommentText(), comment.getCommentDate().toString())) - .collect(Collectors.toList()); + return CommentConverter.toWinnerResponseDTO(winners, winnerCount, startDate, endDate); } + // 키워드별 댓글 조회 - public List getCommentsByKeyword(Long eventId, String keyword, String userId) { + public List getCommentsByKeyword(Long eventId, String keyword, String userId) { Member member = memberRepository.findByUserIdAndStatusNot(userId, UserStatus.DELETED) .orElseThrow(() -> new IllegalArgumentException("Member not found")); @@ -99,11 +98,11 @@ public List getCommentsByKeyword(Long even List comments = commentRepository.findByEventIdAndCommentTextContaining(eventId, keyword); return comments.stream() - .map(CommentConverter::toWinnerResponseDTO) + .map(CommentConverter::toCommentDetailDTO) .collect(Collectors.toList()); } - public List filterWinnersByKeyword(List winners, String keyword) { + public List filterWinnersByKeyword(List winners, String keyword) { return winners.stream() .filter(winner -> winner.getCommentText().contains(keyword)) .collect(Collectors.toList());