From 4ba9d669ce8feb80860779ad966dd60de800703d Mon Sep 17 00:00:00 2001 From: hyun Date: Sat, 5 Apr 2025 13:01:09 +0900 Subject: [PATCH 1/3] feat: subscribe interest keyword --- .../controller/SubscriberController.java | 8 ++--- .../dto/request/SubscribeRequestDto.java | 10 ++++++ .../domain/subscriber/entity/Interest.java | 31 +++++++++++++++++++ .../domain/subscriber/entity/Subscriber.java | 24 ++++++++++++-- .../subscriber/service/SubscriberService.java | 4 ++- .../service/impl/SubscriberServiceImpl.java | 7 ++--- 6 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/example/userservice/domain/subscriber/entity/Interest.java diff --git a/src/main/java/com/example/userservice/domain/subscriber/controller/SubscriberController.java b/src/main/java/com/example/userservice/domain/subscriber/controller/SubscriberController.java index 216873c..07aea61 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/controller/SubscriberController.java +++ b/src/main/java/com/example/userservice/domain/subscriber/controller/SubscriberController.java @@ -1,16 +1,16 @@ package com.example.userservice.domain.subscriber.controller; -import com.example.userservice.domain.member.dto.response.CreateMemberResponseDto; import com.example.userservice.domain.subscriber.dto.request.SubscribeRequestDto; import com.example.userservice.domain.subscriber.service.SubscriberService; -import com.example.userservice.global.common.CommonResDto; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; + @RestController @RequiredArgsConstructor @Slf4j @@ -20,8 +20,8 @@ public class SubscriberController { private final SubscriberService subscriberService; @PostMapping("/subscribe") - public ResponseEntity subscribe(@RequestBody SubscribeRequestDto subscribeRequestDto) { - subscriberService.subscribe(subscribeRequestDto.getEmail()); + public ResponseEntity subscribe(@Valid @RequestBody SubscribeRequestDto subscribeRequestDto) { + subscriberService.subscribe(subscribeRequestDto.getEmail(), subscribeRequestDto.getKeywords()); return ResponseEntity.status(HttpStatus.CREATED).body("구독 완료: " + subscribeRequestDto.getEmail()); } diff --git a/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java b/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java index be5aff5..3b3333c 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java +++ b/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java @@ -3,8 +3,18 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.List; + @NoArgsConstructor @Getter public class SubscribeRequestDto { private String email; + + @Size(min=1, max = 3, message = "You must provide 1 to 3 keywords.") + private List< + @NotBlank(message = "Each keyword must not be blank.") + @Size(min = 2, max = 20, message = "Keyword must be between 2 and 20 characters.") + String> keywords; } diff --git a/src/main/java/com/example/userservice/domain/subscriber/entity/Interest.java b/src/main/java/com/example/userservice/domain/subscriber/entity/Interest.java new file mode 100644 index 0000000..98a9e48 --- /dev/null +++ b/src/main/java/com/example/userservice/domain/subscriber/entity/Interest.java @@ -0,0 +1,31 @@ +package com.example.userservice.domain.subscriber.entity; + +import com.example.userservice.global.entity.BaseTimeEntity; +import lombok.*; + +import javax.persistence.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Interest extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String keyword; + + @ManyToOne + @JoinColumn(name = "subscriber_id") + private Subscriber subscriber; + + public Interest(String keyword) { + this.keyword = keyword; + } + + public void setSubscriber(Subscriber subscriber) { + this.subscriber = subscriber; + } +} diff --git a/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java b/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java index 764e02c..4403369 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java +++ b/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java @@ -2,11 +2,11 @@ import com.example.userservice.global.entity.BaseTimeEntity; import lombok.*; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; import javax.persistence.*; -import java.util.Objects; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -27,12 +27,30 @@ public class Subscriber extends BaseTimeEntity { @Column(nullable = false) private boolean subscribed = true; // 구독 상태 (true: 구독 중, false: 구독 취소) + @OneToMany(mappedBy = "subscriber", cascade = CascadeType.ALL, orphanRemoval = true) + private List interests = new ArrayList<>(); + public Subscriber(String email) { this.email = email; } + public Subscriber(String email, List keywords) { + this.email = email; + + keywords.stream() + .distinct() + .map(Interest::new) + .forEach(this::add); + } + public void unsubscribe() { this.subscribed = false; } + // 연관관계 메서드 + public void add(Interest interest) { + interests.add(interest); + interest.setSubscriber(this); + } + } diff --git a/src/main/java/com/example/userservice/domain/subscriber/service/SubscriberService.java b/src/main/java/com/example/userservice/domain/subscriber/service/SubscriberService.java index 4fb9476..45e9f6c 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/service/SubscriberService.java +++ b/src/main/java/com/example/userservice/domain/subscriber/service/SubscriberService.java @@ -1,6 +1,8 @@ package com.example.userservice.domain.subscriber.service; +import java.util.List; + public interface SubscriberService { - void subscribe(String email); + void subscribe(String email, List keywords); void unsubscribe(String email); } diff --git a/src/main/java/com/example/userservice/domain/subscriber/service/impl/SubscriberServiceImpl.java b/src/main/java/com/example/userservice/domain/subscriber/service/impl/SubscriberServiceImpl.java index 0475660..5075a49 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/service/impl/SubscriberServiceImpl.java +++ b/src/main/java/com/example/userservice/domain/subscriber/service/impl/SubscriberServiceImpl.java @@ -1,6 +1,5 @@ package com.example.userservice.domain.subscriber.service.impl; -import com.example.userservice.domain.member.dto.request.SignUpRequestDto; import com.example.userservice.domain.subscriber.entity.Subscriber; import com.example.userservice.domain.subscriber.repository.SubscriberRepository; import com.example.userservice.domain.subscriber.service.SubscriberService; @@ -25,15 +24,15 @@ public class SubscriberServiceImpl implements SubscriberService { @Override @Transactional - public void subscribe(String email) { + public void subscribe(String email, List keywords) { // 이메일 중복 체크 subscriberRepository.findByEmail(email).ifPresent(subscriber -> { throw new IllegalArgumentException("이미 구독된 이메일입니다."); }); emailVerifyCheck(email); - // 새 구독자 저장 - Subscriber subscriber = new Subscriber(email); + // 관심 키워드와 함께 새 구독자 저장 + Subscriber subscriber = new Subscriber(email, keywords); subscriberRepository.save(subscriber); } From 64a16ed17f41dbb79d35ff711281ae59b51b0e0e Mon Sep 17 00:00:00 2001 From: hyun Date: Sat, 5 Apr 2025 13:58:11 +0900 Subject: [PATCH 2/3] modify: apply custom keywords validator --- .../dto/request/SubscribeRequestDto.java | 23 ++++++---- .../dto/validator/KeywordsValidator.java | 42 +++++++++++++++++++ .../dto/validator/ValidKeywords.java | 23 ++++++++++ .../domain/subscriber/entity/Subscriber.java | 1 - 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java create mode 100644 src/main/java/com/example/userservice/domain/subscriber/dto/validator/ValidKeywords.java diff --git a/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java b/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java index 3b3333c..b1ddae7 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java +++ b/src/main/java/com/example/userservice/domain/subscriber/dto/request/SubscribeRequestDto.java @@ -1,20 +1,29 @@ package com.example.userservice.domain.subscriber.dto.request; +import com.example.userservice.domain.subscriber.dto.validator.ValidKeywords; import lombok.Getter; import lombok.NoArgsConstructor; +import org.apache.commons.lang.StringUtils; -import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import java.util.List; +import java.util.stream.Collectors; @NoArgsConstructor -@Getter public class SubscribeRequestDto { + @Getter private String email; - @Size(min=1, max = 3, message = "You must provide 1 to 3 keywords.") - private List< - @NotBlank(message = "Each keyword must not be blank.") - @Size(min = 2, max = 20, message = "Keyword must be between 2 and 20 characters.") - String> keywords; + @ValidKeywords(minSize = 1, maxSize = 3, keywordMinLength = 2, keywordMaxLength = 20, + message = "Keywords must be 1~3, each 2~20 characters and unique.") + private List keywords; + + // 중복 제거 + public List getKeywords() { + return keywords.stream() + .map(String::trim) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java b/src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java new file mode 100644 index 0000000..63c2ea0 --- /dev/null +++ b/src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java @@ -0,0 +1,42 @@ +package com.example.userservice.domain.subscriber.dto.validator; + +import org.apache.commons.lang.StringUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.List; +import java.util.stream.Collectors; + +public class KeywordsValidator implements ConstraintValidator> { + + private int minSize; + private int maxSize; + private int keywordMinLength; + private int keywordMaxLength; + + @Override + public void initialize(ValidKeywords constraintAnnotation) { + this.minSize = constraintAnnotation.minSize(); + this.maxSize = constraintAnnotation.maxSize(); + this.keywordMinLength = constraintAnnotation.keywordMinLength(); + this.keywordMaxLength = constraintAnnotation.keywordMaxLength(); + } + + @Override + public boolean isValid(List value, ConstraintValidatorContext constraintValidatorContext) { + if (value == null) return false; + + List processed = value.stream() + .map(String::trim) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); + + if (processed.size() < minSize || processed.size() > maxSize) { + return false; + } + + return processed.stream() + .allMatch(s -> s.length() >= keywordMinLength && s.length() <= keywordMaxLength); + } +} diff --git a/src/main/java/com/example/userservice/domain/subscriber/dto/validator/ValidKeywords.java b/src/main/java/com/example/userservice/domain/subscriber/dto/validator/ValidKeywords.java new file mode 100644 index 0000000..01e2dad --- /dev/null +++ b/src/main/java/com/example/userservice/domain/subscriber/dto/validator/ValidKeywords.java @@ -0,0 +1,23 @@ +package com.example.userservice.domain.subscriber.dto.validator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = KeywordsValidator.class) +public @interface ValidKeywords { + + String message() default "Invalid keywords."; + Class[] groups() default {}; + Class[] payload() default {}; + + int minSize() default 1; + int maxSize() default 3; + int keywordMinLength() default 1; + int keywordMaxLength() default 200; +} diff --git a/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java b/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java index 4403369..6597293 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java +++ b/src/main/java/com/example/userservice/domain/subscriber/entity/Subscriber.java @@ -38,7 +38,6 @@ public Subscriber(String email, List keywords) { this.email = email; keywords.stream() - .distinct() .map(Interest::new) .forEach(this::add); } From cb2b4c9b653d8df3addc523def757a277d3bf877 Mon Sep 17 00:00:00 2001 From: hyun Date: Sun, 6 Apr 2025 11:10:17 +0900 Subject: [PATCH 3/3] feat: add special character verfication --- .../subscriber/dto/validator/KeywordsValidator.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java b/src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java index 63c2ea0..e675282 100644 --- a/src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java +++ b/src/main/java/com/example/userservice/domain/subscriber/dto/validator/KeywordsValidator.java @@ -5,10 +5,13 @@ import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class KeywordsValidator implements ConstraintValidator> { + private static final Pattern VALID_KEYWORD_PATTERN = Pattern.compile("^[a-zA-Z0-9가-힣 ]+$"); + private int minSize; private int maxSize; private int keywordMinLength; @@ -26,7 +29,7 @@ public void initialize(ValidKeywords constraintAnnotation) { public boolean isValid(List value, ConstraintValidatorContext constraintValidatorContext) { if (value == null) return false; - List processed = value.stream() + List processed = value.stream() .map(String::trim) .filter(StringUtils::isNotBlank) .distinct() @@ -37,6 +40,8 @@ public boolean isValid(List value, ConstraintValidatorContext constraint } return processed.stream() - .allMatch(s -> s.length() >= keywordMinLength && s.length() <= keywordMaxLength); + .allMatch(s -> s.length() >= keywordMinLength + && s.length() <= keywordMaxLength + && VALID_KEYWORD_PATTERN.matcher(s).matches()); } }