diff --git a/infra/dev/db/docker-compose.yml b/infra/dev/db/docker-compose.yml new file mode 100644 index 00000000..9c089542 --- /dev/null +++ b/infra/dev/db/docker-compose.yml @@ -0,0 +1,11 @@ +services: + mysql: + image: mysql + ports: + - 3306:3306 + environment: + MYSQL_ROOT_PASSWORD: test + redis: + image: redis + ports: + - 6379:6379 \ No newline at end of file diff --git a/src/main/java/hyundai/softeer/orange/admin/entity/Admin.java b/src/main/java/hyundai/softeer/orange/admin/entity/Admin.java index d97c3f69..8f3e53d5 100644 --- a/src/main/java/hyundai/softeer/orange/admin/entity/Admin.java +++ b/src/main/java/hyundai/softeer/orange/admin/entity/Admin.java @@ -14,7 +14,7 @@ @NoArgsConstructor public class Admin { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** diff --git a/src/main/java/hyundai/softeer/orange/comment/dto/WriteCommentCountDto.java b/src/main/java/hyundai/softeer/orange/comment/dto/WriteCommentCountDto.java new file mode 100644 index 00000000..f430d710 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/comment/dto/WriteCommentCountDto.java @@ -0,0 +1,6 @@ +package hyundai.softeer.orange.comment.dto; + +public interface WriteCommentCountDto { + Long getEventUserId(); + Long getCount(); +} diff --git a/src/main/java/hyundai/softeer/orange/comment/repository/CommentRepository.java b/src/main/java/hyundai/softeer/orange/comment/repository/CommentRepository.java index f2da290f..37bc5125 100644 --- a/src/main/java/hyundai/softeer/orange/comment/repository/CommentRepository.java +++ b/src/main/java/hyundai/softeer/orange/comment/repository/CommentRepository.java @@ -1,5 +1,6 @@ package hyundai.softeer.orange.comment.repository; +import hyundai.softeer.orange.comment.dto.WriteCommentCountDto; import hyundai.softeer.orange.comment.entity.Comment; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -26,4 +27,13 @@ public interface CommentRepository extends JpaRepository { countProjection = "c.id", // 어떤 값으로 count 셀건지 지정. 지정 안하면 count(c.*)가 되어 문제 발생. nativeQuery = true) Page findAllByEventId(@Param("eventId") String eventId, Pageable pageable); + + // 이름 매핑 필요. 필드 이름과 직접 매핑. + @Query(value = "SELECT c.event_user_id as eventUserId, COUNT(c.event_user_id) as count " + + "FROM comment c " + + "JOIN event_frame ef ON c.event_frame_id = ef.id " + + "JOIN event_metadata e ON ef.id = e.event_frame_id " + + "WHERE e.id = :eventRawId " + + "GROUP BY c.event_user_id " , nativeQuery = true) + List countPerEventUserByEventId(@Param("eventRawId") Long eventRawId); } diff --git a/src/main/java/hyundai/softeer/orange/common/ErrorCode.java b/src/main/java/hyundai/softeer/orange/common/ErrorCode.java index cc9967aa..1673c945 100644 --- a/src/main/java/hyundai/softeer/orange/common/ErrorCode.java +++ b/src/main/java/hyundai/softeer/orange/common/ErrorCode.java @@ -31,6 +31,7 @@ public enum ErrorCode { USER_NOT_FOUND(HttpStatus.NOT_FOUND, false, "사용자를 찾을 수 없습니다."), SHORT_URL_NOT_FOUND(HttpStatus.NOT_FOUND, false, "단축 URL을 찾을 수 없습니다."), FCFS_EVENT_NOT_FOUND(HttpStatus.NOT_FOUND, false, "선착순 이벤트를 찾을 수 없습니다."), + DRAW_EVENT_NOT_FOUND(HttpStatus.NOT_FOUND, false, "추첨 이벤트를 찾을 수 없습니다."), EVENT_FRAME_NOT_FOUND(HttpStatus.NOT_FOUND, false, "이벤트 프레임을 찾을 수 없습니다."), EVENT_USER_NOT_FOUND(HttpStatus.NOT_FOUND, false, "이벤트 사용자를 찾을 수 없습니다."), COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, false, "기대평을 찾을 수 없습니다."), diff --git a/src/main/java/hyundai/softeer/orange/common/GlobalExceptionHandler.java b/src/main/java/hyundai/softeer/orange/common/GlobalExceptionHandler.java index b5aa736d..406085ab 100644 --- a/src/main/java/hyundai/softeer/orange/common/GlobalExceptionHandler.java +++ b/src/main/java/hyundai/softeer/orange/common/GlobalExceptionHandler.java @@ -8,9 +8,11 @@ import hyundai.softeer.orange.event.url.exception.UrlException; import hyundai.softeer.orange.eventuser.exception.EventUserException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -18,10 +20,13 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import java.util.HashMap; +import java.util.Locale; import java.util.Map; @RestControllerAdvice public class GlobalExceptionHandler { + @Autowired + private MessageSource messageSource; @ExceptionHandler(MethodArgumentNotValidException.class) // 요청의 유효성 검사 실패 시 @ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request로 응답 반환 @@ -39,10 +44,15 @@ public ResponseEntity> handleInValidRequestException(MethodA // TODO: messages.properties에 예외 메시지 커스터마이징할 수 있게 방법 찾아보기 @ExceptionHandler(MethodArgumentTypeMismatchException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - public ResponseEntity> handleInValidRequestException(MethodArgumentTypeMismatchException e) { - Map errors = new HashMap<>(); - errors.put(e.getName(), e.getLocalizedMessage()); - return ResponseEntity.badRequest().body(errors); + public Map handleInValidRequestException(MethodArgumentTypeMismatchException e) { + String code = e.getErrorCode(); + String fieldName = e.getName(); + Locale locale = LocaleContextHolder.getLocale(); // 현재 스레드의 로케일 정보를 가져온다. + String errorMessage = messageSource.getMessage(code, null, locale); // 국제화 된 메시지를 가져온다. + + Map error = new HashMap<>(); + error.put(fieldName, errorMessage); + return error; } @ExceptionHandler({CommentException.class, AdminException.class, EventUserException.class, FcfsEventException.class, UrlException.class, InternalServerException.class}) diff --git a/src/main/java/hyundai/softeer/orange/event/draw/DrawEventConfig.java b/src/main/java/hyundai/softeer/orange/event/draw/DrawEventConfig.java new file mode 100644 index 00000000..9730affa --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/DrawEventConfig.java @@ -0,0 +1,36 @@ +package hyundai.softeer.orange.event.draw; + +import hyundai.softeer.orange.event.draw.component.score.actionHandler.ActionHandler; +import hyundai.softeer.orange.event.draw.enums.DrawEventAction; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Configuration +public class DrawEventConfig { + private static final String NamePattern = "(.+?)_ActionHandler"; + + // actionHandler을 Action 타입으로 가져올 수 있도록 설정 + @Bean(name = "actionHandlerMap") + public Map actionHandlerMap(Map handlers) { + Map actionHandlerMap = new HashMap<>(); + Pattern pattern = Pattern.compile(NamePattern); + + for(var entry: handlers.entrySet()) { + Matcher matcher = pattern.matcher(entry.getKey()); + if (!matcher.find()) throw new RuntimeException("no matched action"); + + String key = matcher.group(1); + ActionHandler actionHandler = entry.getValue(); + + actionHandlerMap.put(DrawEventAction.valueOf(key), actionHandler); + } + // 런타임에 변경 못하게 제한 + return Collections.unmodifiableMap(actionHandlerMap); + } +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/component/picker/AccSumBasedWinnerPicker.java b/src/main/java/hyundai/softeer/orange/event/draw/component/picker/AccSumBasedWinnerPicker.java new file mode 100644 index 00000000..29c48737 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/component/picker/AccSumBasedWinnerPicker.java @@ -0,0 +1,124 @@ +package hyundai.softeer.orange.event.draw.component.picker; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Primary +@Component +public class AccSumBasedWinnerPicker implements WinnerPicker { + + @Override + public List pick(List items, long count) { + long maxPickCount = Math.min(items.size(), count); + // TODO: 둘 중 하나를 선택하는 최적 조건 분석하기. + if (count - maxPickCount <= 10 || // 두 값 차이가 작을 때 + ((double) count / maxPickCount) <= 1.1 ) { + return pickMany(items, maxPickCount); + } else { + return pickManyUsingSet(items, maxPickCount); + } + } + + protected List pickMany(List targets, long count) { + List pickedTargets = new ArrayList<>(); + // 추첨에 참여하는 객체들이 존재하는 set + Set targetSet = new HashSet<>(targets); + + for(int i = 0; i < count; i++) { + RandomItem[] items = getAccumulatedItems(targetSet); + long bound = items[items.length - 1].score; + long targetScore = new Random().nextLong(1, bound + 1); + int pickedIdx = binarySearch(items, targetScore); + PickTarget pickedTarget = items[pickedIdx].target; + + targetSet.remove(pickedTarget); + pickedTargets.add(pickedTarget); + } + + return pickedTargets; + } + + /** + * set 자료구조를 이용하여 가중합을 1번만 계산하는 타겟 선택 방식 + * @param targets 선택 대상이 되는 배열 + * @param count + * @return + */ + protected List pickManyUsingSet(List targets, long count) { + // 가중합 배열 + RandomItem[] items = getAccumulatedItems(targets); + List pickedTargets = new ArrayList<>(); + long bound = items[items.length - 1].score; + // 이미 선택된 대상이 존재하는 공간 + Set pickedIdxSet = new HashSet<>(); + + for(int i = 0; i < count; i++) { + int pickedIdx; + do { + long targetScore = new Random().nextLong(1, bound + 1); + pickedIdx = binarySearch(items, targetScore); + }while(pickedIdxSet.contains(pickedIdx)); + // 방문했다고 마킹 + pickedIdxSet.add(pickedIdx); + pickedTargets.add(targets.get(pickedIdx)); + } + + return pickedTargets; + } + + + + /** + * 누적합 배열을 반환한다 + * @param targets 추첨 대상들 + * @return 누적합 형태로 표현된 RandomItem 배열 + */ + protected RandomItem[] getAccumulatedItems(Collection targets) { + RandomItem[] items = new RandomItem[targets.size()]; + long score = 0; + + int idx = 0; + for (PickTarget target : targets) { + score += target.score(); + items[idx++] = new RandomItem(target, score); + } + + return items; + } + + /** + * 이진 탐색을 이용하여 누적합 배열에서 대상 인덱스를 얻는다. + * @param items 누적합 배열 + * @param target 대상 점수 + * @return 선택된 인덱스 값 + */ + protected int binarySearch(RandomItem[] items, long target) { + int low = 0; + int high = items.length - 1; + + while(low <= high) { + // >>>은 비트열 오른쪽으로 이동, 빈 공간은 0으로 채움 + // 2로 나누는 로직을 >>>으로 처리 + int mid = (low + high) >>> 1; + + if(items[mid].score > target) { + high = mid - 1; + } else if(items[mid].score < target) { + low = mid + 1; + } else { + break; + } + } + + return low; + } + + /** + * 가중합을 기록해두는 객체 + * @param target 추첨 대상 + * @param score 누적합 점수 + */ + protected record RandomItem(PickTarget target, long score) {} +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/component/picker/PickTarget.java b/src/main/java/hyundai/softeer/orange/event/draw/component/picker/PickTarget.java new file mode 100644 index 00000000..d0ac08a1 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/component/picker/PickTarget.java @@ -0,0 +1,3 @@ +package hyundai.softeer.orange.event.draw.component.picker; + +public record PickTarget(Long key, long score) {} \ No newline at end of file diff --git a/src/main/java/hyundai/softeer/orange/event/draw/component/picker/WinnerPicker.java b/src/main/java/hyundai/softeer/orange/event/draw/component/picker/WinnerPicker.java new file mode 100644 index 00000000..cc784695 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/component/picker/WinnerPicker.java @@ -0,0 +1,10 @@ +package hyundai.softeer.orange.event.draw.component.picker; + +import java.util.List; + +/** + * "추첨" 작업을 진행하는 서비스. 여러 추첨 방식이 존재할 수 있으므로, 추첨 과정은 별도 인터페이스로 분리. + */ +public interface WinnerPicker { + List pick(List items,long count); +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/component/score/ScoreCalculator.java b/src/main/java/hyundai/softeer/orange/event/draw/component/score/ScoreCalculator.java new file mode 100644 index 00000000..c259950e --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/component/score/ScoreCalculator.java @@ -0,0 +1,29 @@ +package hyundai.softeer.orange.event.draw.component.score; + +import hyundai.softeer.orange.event.draw.component.score.actionHandler.ActionHandler; +import hyundai.softeer.orange.event.draw.entity.DrawEventScorePolicy; +import hyundai.softeer.orange.event.draw.enums.DrawEventAction; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 특정 추첨 이벤트에 대한 점수를 계산하는 클래스. 저장은 안한다. + */ +@RequiredArgsConstructor +@Component +public class ScoreCalculator { + private final Map handlerMap; + + public Map calculate(long eventId, List policies) { + Map scoreMap = new HashMap<>(); + for (var policy : policies) { + ActionHandler handler = handlerMap.get(policy.getAction()); + handler.handle(scoreMap, eventId, policy.getScore()); + } + return scoreMap; + } +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ActionHandler.java b/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ActionHandler.java new file mode 100644 index 00000000..10dcc03c --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ActionHandler.java @@ -0,0 +1,12 @@ +package hyundai.softeer.orange.event.draw.component.score.actionHandler; + +import java.util.Map; + +/** + * 채점 정책에 대한 동작을 처리한다. + *

component 이름은 action 이름 + ActionHandler 형식을 띄어야 한다.

+ *

액션 = {@link hyundai.softeer.orange.event.draw.enums.DrawEventAction}

+ */ +public interface ActionHandler { + void handle(Map scoreMap, long eventRawId, long score); +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ParticipateEventActionHandler.java b/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ParticipateEventActionHandler.java new file mode 100644 index 00000000..00deb304 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ParticipateEventActionHandler.java @@ -0,0 +1,22 @@ +package hyundai.softeer.orange.event.draw.component.score.actionHandler; + +import hyundai.softeer.orange.event.draw.repository.EventParticipationInfoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@RequiredArgsConstructor +@Component("ParticipateEvent_ActionHandler") +public class ParticipateEventActionHandler implements ActionHandler { + private final EventParticipationInfoRepository repo; + + @Override + public void handle(Map scoreMap, long eventRawId, long score) { + var participateCounts = repo.countPerEventUserByEventId(eventRawId); + for (var p : participateCounts) { + long beforeScore = scoreMap.getOrDefault(p.getEventUserId(),0L); + scoreMap.put(p.getEventUserId(), beforeScore + p.getCount() * score); + } + } +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/WriteCommentActionHandler.java b/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/WriteCommentActionHandler.java new file mode 100644 index 00000000..b0e38939 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/WriteCommentActionHandler.java @@ -0,0 +1,22 @@ +package hyundai.softeer.orange.event.draw.component.score.actionHandler; + +import hyundai.softeer.orange.comment.repository.CommentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@RequiredArgsConstructor +@Component("WriteComment_ActionHandler") +public class WriteCommentActionHandler implements ActionHandler { + private final CommentRepository repo; + + @Override + public void handle(Map scoreMap, long eventRawId, long score) { + var commentCounts = repo.countPerEventUserByEventId(eventRawId); + for (var c : commentCounts) { + long beforeScore = scoreMap.getOrDefault(c.getEventUserId(), 0L); + scoreMap.put(c.getEventUserId(), beforeScore + c.getCount() * score); + } + } +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/dto/DrawEventWinningInfoBulkInsertDto.java b/src/main/java/hyundai/softeer/orange/event/draw/dto/DrawEventWinningInfoBulkInsertDto.java new file mode 100644 index 00000000..1e7333b4 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/dto/DrawEventWinningInfoBulkInsertDto.java @@ -0,0 +1,18 @@ +package hyundai.softeer.orange.event.draw.dto; + +import lombok.Getter; + +@Getter +public class DrawEventWinningInfoBulkInsertDto { + private long eventUserId; + private long ranking; + private long drawEventId; + + public static DrawEventWinningInfoBulkInsertDto of(long eventUserId, long ranking, long drawEventId) { + var dto = new DrawEventWinningInfoBulkInsertDto(); + dto.eventUserId = eventUserId; + dto.ranking = ranking; + dto.drawEventId = drawEventId; + return dto; + } +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/dto/EventParticipateCountDto.java b/src/main/java/hyundai/softeer/orange/event/draw/dto/EventParticipateCountDto.java new file mode 100644 index 00000000..ad16f5e2 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/dto/EventParticipateCountDto.java @@ -0,0 +1,6 @@ +package hyundai.softeer.orange.event.draw.dto; + +public interface EventParticipateCountDto { + long getEventUserId(); + long getCount(); +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/enums/DrawEventAction.java b/src/main/java/hyundai/softeer/orange/event/draw/enums/DrawEventAction.java index d17cada9..6404c2dd 100644 --- a/src/main/java/hyundai/softeer/orange/event/draw/enums/DrawEventAction.java +++ b/src/main/java/hyundai/softeer/orange/event/draw/enums/DrawEventAction.java @@ -1,6 +1,11 @@ package hyundai.softeer.orange.event.draw.enums; +/** + * 점수 계산에 반영될 수 있는 작업 목록. 대응되는 handler이 등록되어야 한다. + * + *

ActionHandler: {@link hyundai.softeer.orange.event.draw.component.score.actionHandler.ActionHandler}

+ */ public enum DrawEventAction { - WRITE_COMMENT, - PARTICIPATE_EVENT + WriteComment, + ParticipateEvent } diff --git a/src/main/java/hyundai/softeer/orange/event/draw/exception/DrawEventException.java b/src/main/java/hyundai/softeer/orange/event/draw/exception/DrawEventException.java new file mode 100644 index 00000000..ab9f8389 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/exception/DrawEventException.java @@ -0,0 +1,10 @@ +package hyundai.softeer.orange.event.draw.exception; + +import hyundai.softeer.orange.common.BaseException; +import hyundai.softeer.orange.common.ErrorCode; + +public class DrawEventException extends BaseException { + public DrawEventException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepository.java b/src/main/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepository.java new file mode 100644 index 00000000..e0967667 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepository.java @@ -0,0 +1,9 @@ +package hyundai.softeer.orange.event.draw.repository; + +import hyundai.softeer.orange.event.draw.dto.DrawEventWinningInfoBulkInsertDto; + +import java.util.List; + +public interface CustomDrawEventWinningInfoRepository { + void insertMany(List targets); +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepositoryImpl.java b/src/main/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepositoryImpl.java new file mode 100644 index 00000000..a8460b2e --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepositoryImpl.java @@ -0,0 +1,40 @@ +package hyundai.softeer.orange.event.draw.repository; + +import hyundai.softeer.orange.event.draw.dto.DrawEventWinningInfoBulkInsertDto; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class CustomDrawEventWinningInfoRepositoryImpl implements CustomDrawEventWinningInfoRepository { + private final JdbcTemplate jdbcTemplate; + + @Override + public void insertMany(List targets) { + String sql = "insert into draw_event_winning_info (event_user_id, ranking, draw_event_id) VALUES (?, ?, ?)"; + + jdbcTemplate.batchUpdate( + sql, + new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + var target = targets.get(i); + ps.setLong(1, target.getEventUserId()); + ps.setLong(2, target.getRanking()); + ps.setLong(3, target.getDrawEventId()); + } + + @Override + public int getBatchSize() { + return targets.size(); + } + } + ); + } +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventRepository.java b/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventRepository.java index 6ba0d852..e97a507b 100644 --- a/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventRepository.java +++ b/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventRepository.java @@ -1,11 +1,15 @@ package hyundai.softeer.orange.event.draw.repository; -import hyundai.softeer.orange.event.common.entity.EventMetadata; import hyundai.softeer.orange.event.draw.entity.DrawEvent; +import io.lettuce.core.dynamic.annotation.Param; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository -public interface DrawEventRepository extends JpaRepository { } +public interface DrawEventRepository extends JpaRepository { + @Query(value = "SELECT d FROM DrawEvent d WHERE d.eventMetadata.eventId = :eventId") + Optional findByEventId(@Param("eventId") String eventId); +} diff --git a/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventWinningInfoRepository.java b/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventWinningInfoRepository.java index d56779ef..fe9ac841 100644 --- a/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventWinningInfoRepository.java +++ b/src/main/java/hyundai/softeer/orange/event/draw/repository/DrawEventWinningInfoRepository.java @@ -4,6 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -@Repository -public interface DrawEventWinningInfoRepository extends JpaRepository { +public interface DrawEventWinningInfoRepository extends JpaRepository, CustomDrawEventWinningInfoRepository { } diff --git a/src/main/java/hyundai/softeer/orange/event/draw/repository/EventParticipationInfoRepository.java b/src/main/java/hyundai/softeer/orange/event/draw/repository/EventParticipationInfoRepository.java index 19043aec..f74c5daa 100644 --- a/src/main/java/hyundai/softeer/orange/event/draw/repository/EventParticipationInfoRepository.java +++ b/src/main/java/hyundai/softeer/orange/event/draw/repository/EventParticipationInfoRepository.java @@ -1,9 +1,18 @@ package hyundai.softeer.orange.event.draw.repository; +import hyundai.softeer.orange.event.draw.dto.EventParticipateCountDto; import hyundai.softeer.orange.event.draw.entity.EventParticipationInfo; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface EventParticipationInfoRepository extends JpaRepository { + @Query(value = "SELECT event_user_id as eventUserId, COUNT(event_user_id) as count " + + "FROM event_partication_info " + + "WHERE draw_event_id = :eventRawId " + + "GROUP BY event_user_id", nativeQuery = true) + List countPerEventUserByEventId(Long eventRawId); } diff --git a/src/main/java/hyundai/softeer/orange/event/draw/service/DrawEventService.java b/src/main/java/hyundai/softeer/orange/event/draw/service/DrawEventService.java new file mode 100644 index 00000000..f099d4e3 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/event/draw/service/DrawEventService.java @@ -0,0 +1,96 @@ +package hyundai.softeer.orange.event.draw.service; + +import hyundai.softeer.orange.common.ErrorCode; +import hyundai.softeer.orange.event.draw.component.picker.PickTarget; +import hyundai.softeer.orange.event.draw.component.score.ScoreCalculator; +import hyundai.softeer.orange.event.draw.dto.DrawEventWinningInfoBulkInsertDto; +import hyundai.softeer.orange.event.draw.entity.DrawEvent; +import hyundai.softeer.orange.event.draw.entity.DrawEventMetadata; +import hyundai.softeer.orange.event.draw.entity.DrawEventScorePolicy; +import hyundai.softeer.orange.event.draw.exception.DrawEventException; +import hyundai.softeer.orange.event.draw.repository.DrawEventRepository; +import hyundai.softeer.orange.event.draw.component.picker.WinnerPicker; +import hyundai.softeer.orange.event.draw.repository.DrawEventWinningInfoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * 추첨 이벤트를 다루는 서비스 + */ +@RequiredArgsConstructor +@Service +public class DrawEventService { + private final DrawEventRepository deRepository; + private final DrawEventWinningInfoRepository deWinningInfoRepository; + private final WinnerPicker picker; + private final ScoreCalculator calculator; + + /** + * eventId에 대한 추첨을 진행하는 메서드 + * @param drawEventId draw event의 id 값. + */ + @Transactional + @Async + public void draw(Long drawEventId) { + // 채점 & 추첨 과정 분리하는 것도 좋을것 같다. + DrawEvent drawEvent = deRepository.findById(drawEventId) + .orElseThrow(() -> new DrawEventException(ErrorCode.DRAW_EVENT_NOT_FOUND)); + + // draw event의 primary key id 값 + long drawEventRawId = drawEvent.getId(); + + // 점수 계산. 추후 추첨 과정과 분리될 수도 있음. + List policies = drawEvent.getPolicyList(); + var userScoreMap = calculator.calculate(drawEventRawId, policies); + + // 추첨 타겟 리스트 생성 + List targets = userScoreMap.entrySet().stream() + .map(it -> new PickTarget(it.getKey(), it.getValue())).toList(); + + // 몇 등이 몇명이나 있는지 적혀 있는 정보. 등급끼리 정렬해서 1 ~ n 등 순서로 정렬 + // 확률 높은 사람이 손해보면 안됨 + List metadataList = drawEvent.getMetadataList(); + metadataList.sort(Comparator.comparing(DrawEventMetadata::getGrade)); + + // 총 당첨 인원 설정 + long pickCount = metadataList.stream() + .mapToLong(DrawEventMetadata::getCount).sum(); + + // 당첨된 인원 구하기 + var pickedTargets = picker.pick(targets, pickCount); + + // 이하 영역은 여러 작업을 동시에 수행할 수도 있음 + // ex) 인원 등록 / 상품 문자 발송 ... + + // 인원 등록을 위한 작업 + List insertTargets = new ArrayList<>(); + int mdIdx = -1; + long remain = 0; + long grade = -1; + DrawEventMetadata metadata = null; + + for(var target : pickedTargets) { + if(remain <= 0) { + mdIdx++; + metadata = metadataList.get(mdIdx); + grade = metadata.getGrade(); + remain = metadata.getCount(); + } + + insertTargets.add(DrawEventWinningInfoBulkInsertDto.of( + target.key(), + grade, + drawEventRawId + )); + remain--; + } + + deWinningInfoRepository.insertMany(insertTargets); + } +} diff --git a/src/main/java/hyundai/softeer/orange/eventuser/dto/EventUserScoreDto.java b/src/main/java/hyundai/softeer/orange/eventuser/dto/EventUserScoreDto.java new file mode 100644 index 00000000..a6f17255 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/eventuser/dto/EventUserScoreDto.java @@ -0,0 +1,3 @@ +package hyundai.softeer.orange.eventuser.dto; + +public record EventUserScoreDto(Long userId, Long score) {} diff --git a/src/main/java/hyundai/softeer/orange/eventuser/repository/CustomEventUserRepository.java b/src/main/java/hyundai/softeer/orange/eventuser/repository/CustomEventUserRepository.java new file mode 100644 index 00000000..fbb4eae7 --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/eventuser/repository/CustomEventUserRepository.java @@ -0,0 +1,10 @@ +package hyundai.softeer.orange.eventuser.repository; + +import hyundai.softeer.orange.eventuser.dto.EventUserScoreDto; + +import java.util.Collection; +import java.util.List; + +public interface CustomEventUserRepository { + void updateScoreMany(List dto); +} diff --git a/src/main/java/hyundai/softeer/orange/eventuser/repository/CustomEventUserRepositoryImpl.java b/src/main/java/hyundai/softeer/orange/eventuser/repository/CustomEventUserRepositoryImpl.java new file mode 100644 index 00000000..6172335c --- /dev/null +++ b/src/main/java/hyundai/softeer/orange/eventuser/repository/CustomEventUserRepositoryImpl.java @@ -0,0 +1,36 @@ +package hyundai.softeer.orange.eventuser.repository; + +import hyundai.softeer.orange.eventuser.dto.EventUserScoreDto; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +@RequiredArgsConstructor +@Repository +public class CustomEventUserRepositoryImpl implements CustomEventUserRepository { + private final JdbcTemplate jdbcTemplate; + + @Override + public void updateScoreMany(List userScores) { + String sql = "UPDATE event_user SET score = ? WHERE id = ?"; + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + EventUserScoreDto userScore = userScores.get(i); + ps.setLong(1, userScore.score()); + ps.setLong(2, userScore.userId()); + } + + @Override + public int getBatchSize() { + return userScores.size(); + } + }); + } +} diff --git a/src/main/java/hyundai/softeer/orange/eventuser/repository/EventUserRepository.java b/src/main/java/hyundai/softeer/orange/eventuser/repository/EventUserRepository.java index 4311ab6a..01bd4038 100644 --- a/src/main/java/hyundai/softeer/orange/eventuser/repository/EventUserRepository.java +++ b/src/main/java/hyundai/softeer/orange/eventuser/repository/EventUserRepository.java @@ -1,5 +1,6 @@ package hyundai.softeer.orange.eventuser.repository; +import hyundai.softeer.orange.eventuser.dto.EventUserScoreDto; import hyundai.softeer.orange.eventuser.entity.EventUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -10,14 +11,20 @@ import java.util.Optional; @Repository -public interface EventUserRepository extends JpaRepository { +public interface EventUserRepository extends JpaRepository, CustomEventUserRepository { Optional findByUserNameAndPhoneNumber(String userName, String phoneNumber); Optional findByUserId(String userId); - @Query("select eu from EventUser eu where eu.userId in :userIds") + @Query("SELECT eu FROM EventUser eu WHERE eu.userId IN :userIds") List findAllByUserId(@Param("userIds") List userIds); boolean existsByPhoneNumber(String phoneNumber); + + @Query(value = "SELECT u.id as userId, u.score as score FROM event_user u " + + "JOIN event_frame ef ON ef.id = u.event_frame_id " + + "JOIN event_metadata e ON e.event_frame_id = ef.id " + + "WHERE e.id = :rawEventId", nativeQuery = true) + List findAllUserScoreByDrawEventId(@Param("rawEventId") long rawEventId); } diff --git a/src/test/java/hyundai/softeer/orange/comment/repository/CommentRepositoryTest.java b/src/test/java/hyundai/softeer/orange/comment/repository/CommentRepositoryTest.java new file mode 100644 index 00000000..f719c267 --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/comment/repository/CommentRepositoryTest.java @@ -0,0 +1,48 @@ +package hyundai.softeer.orange.comment.repository; + +import hyundai.softeer.orange.comment.dto.WriteCommentCountDto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.jdbc.Sql; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +// 매 테스트마다 초기화하는 코드 찾아봐야 할듯? +@Sql(value = "classpath:sql/CommentRepositoryTest.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@DataJpaTest(showSql = false) +@TestPropertySource(locations = "classpath:application-test.yml") +class CommentRepositoryTest { + @Autowired + CommentRepository commentRepository; + + + @DisplayName("존재하는 대상 이벤트에 대해 작성된 댓글이 있다면 유저 별로 개수를 구해 반환") + @Test + void getCountOfCommentPerUserIfCommentExist() { + List counts = commentRepository.countPerEventUserByEventId(1L); + + assertThat(counts).hasSize(3); + assertThat(counts.get(0).getCount()).isEqualTo(3); + assertThat(counts.get(1).getCount()).isEqualTo(6); + assertThat(counts.get(2).getCount()).isEqualTo(2); + } + + @DisplayName("존재하지 않는 대상 이벤트는 빈 배열 반환") + @Test + void getCountOfCommentPerUserIfEventNotExist() { + List counts = commentRepository.countPerEventUserByEventId(3L); + assertThat(counts).hasSize(0); + } + + @DisplayName("존재해도 댓글 없으면 빈 배열 반환") + @Test + void getCountOfCommentPerUserIfEventExistButNoComment() { + List counts = commentRepository.countPerEventUserByEventId(2L); + assertThat(counts).hasSize(0); + } +} \ No newline at end of file diff --git a/src/test/java/hyundai/softeer/orange/event/draw/component/picker/AccSumBasedWinnerPickerTest.java b/src/test/java/hyundai/softeer/orange/event/draw/component/picker/AccSumBasedWinnerPickerTest.java new file mode 100644 index 00000000..7a005846 --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/event/draw/component/picker/AccSumBasedWinnerPickerTest.java @@ -0,0 +1,109 @@ +package hyundai.softeer.orange.event.draw.component.picker; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +class AccSumBasedWinnerPickerTest { + + @DisplayName("정상적으로 N개의 서로 다른 아이템을 뽑는지 검사") + @Test + public void test_pickMany() { + AccSumBasedWinnerPicker picker = new AccSumBasedWinnerPicker(); + List pickTargets = new ArrayList<>(); + + Random random = new Random(); + for (int i = 0; i < 10000; i++) { + pickTargets.add(new PickTarget((long)i, random.nextInt(1, 200))); + } + + int pickCount = 1000; + long startTime = System.currentTimeMillis(); + // pickMany와 pickMany를 바꿔가면서 테스트 필요. + var picked = picker.pickMany(pickTargets, pickCount); + long endTime = System.currentTimeMillis(); + log.info("time lapse: {}", (endTime - startTime)); + + var distinctPicked = picked.stream().map(PickTarget::key).collect(Collectors.toSet()); + assertThat(distinctPicked).hasSize(1000); + } + + @DisplayName("정상적으로 N개의 서로 다른 아이템을 뽑는지 검사") + @Test + public void test_pickManyUsingSet() { + AccSumBasedWinnerPicker picker = new AccSumBasedWinnerPicker(); + List pickTargets = new ArrayList<>(); + + Random random = new Random(); + for (int i = 0; i < 10000; i++) { + pickTargets.add(new PickTarget((long)i, random.nextInt(1, 200))); + } + + int pickCount = 1000; + long startTime = System.currentTimeMillis(); + // pickMany와 pickMany를 바꿔가면서 테스트 필요. + var picked = picker.pickManyUsingSet(pickTargets, pickCount); + long endTime = System.currentTimeMillis(); + log.info("time lapse: {}", (endTime - startTime)); + + var distinctPicked = picked.stream().map(PickTarget::key).collect(Collectors.toSet()); + assertThat(distinctPicked).hasSize(1000); + } + + @DisplayName("binary search가 제대로 동작하는지 테스트") + @Test + public void test_binarySearch() { + AccSumBasedWinnerPicker picker = new AccSumBasedWinnerPicker(); + AccSumBasedWinnerPicker.RandomItem[] items = new AccSumBasedWinnerPicker.RandomItem[5]; + + var target = new PickTarget(0L, 0L); + + items[0] = new AccSumBasedWinnerPicker.RandomItem(target, 5); + items[1] = new AccSumBasedWinnerPicker.RandomItem(target, 10); + items[2] = new AccSumBasedWinnerPicker.RandomItem(target, 16); + items[3] = new AccSumBasedWinnerPicker.RandomItem(target, 21); + items[4] = new AccSumBasedWinnerPicker.RandomItem(target, 33); + + // 1 ~ 5는 item 0에, 6 ~ 10은 item 1에 존재 + int idx = picker.binarySearch(items, 1); + assertThat(idx).isEqualTo(0); + int idx2 = picker.binarySearch(items, 5); + assertThat(idx2).isEqualTo(0); + int idx3 = picker.binarySearch(items, 6); + assertThat(idx3).isEqualTo(1); + int idx4 = picker.binarySearch(items, 10); + assertThat(idx4).isEqualTo(1); + int idx5 = picker.binarySearch(items, 11); + assertThat(idx5).isEqualTo(2); + int idx6 = picker.binarySearch(items, 33); + assertThat(idx6).isEqualTo(4); + } + + @DisplayName("정상적으로 누적합을 만드는지 검사") + @Test + public void test_getAccumulatedItems() { + + List pickTargets = new ArrayList<>(); + + for (int i = 1; i < 5; i++) { + pickTargets.add(new PickTarget((long)i, i)); + } + AccSumBasedWinnerPicker picker = new AccSumBasedWinnerPicker(); + + var randItems = picker.getAccumulatedItems(pickTargets); + + assertThat(randItems).hasSize(4); + assertThat(randItems[0].score()).isEqualTo(1); + assertThat(randItems[1].score()).isEqualTo(3); + assertThat(randItems[2].score()).isEqualTo(6); + assertThat(randItems[3].score()).isEqualTo(10); + } +} \ No newline at end of file diff --git a/src/test/java/hyundai/softeer/orange/event/draw/component/score/ScoreCalculatorTest.java b/src/test/java/hyundai/softeer/orange/event/draw/component/score/ScoreCalculatorTest.java new file mode 100644 index 00000000..1426c1a7 --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/event/draw/component/score/ScoreCalculatorTest.java @@ -0,0 +1,84 @@ +package hyundai.softeer.orange.event.draw.component.score; + +import hyundai.softeer.orange.comment.dto.WriteCommentCountDto; +import hyundai.softeer.orange.comment.repository.CommentRepository; +import hyundai.softeer.orange.event.draw.component.score.actionHandler.ActionHandler; +import hyundai.softeer.orange.event.draw.component.score.actionHandler.ParticipateEventActionHandler; +import hyundai.softeer.orange.event.draw.component.score.actionHandler.WriteCommentActionHandler; +import hyundai.softeer.orange.event.draw.dto.EventParticipateCountDto; +import hyundai.softeer.orange.event.draw.entity.DrawEventScorePolicy; +import hyundai.softeer.orange.event.draw.enums.DrawEventAction; +import hyundai.softeer.orange.event.draw.repository.EventParticipationInfoRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ScoreCalculatorTest { + @DisplayName("유저의 점수 기록을 계산하여 반환. 제대로 계산하는지 검사") + @Test + void calculateScore() { + // 유저 참여 정보 mocking + EventParticipateCountDto dto1 = mock(EventParticipateCountDto.class); + when(dto1.getEventUserId()).thenReturn(1L); + when(dto1.getCount()).thenReturn(3L); + EventParticipateCountDto dto2 = mock(EventParticipateCountDto.class); + when(dto2.getEventUserId()).thenReturn(2L); + when(dto2.getCount()).thenReturn(4L); + EventParticipateCountDto dto3 = mock(EventParticipateCountDto.class); + when(dto3.getEventUserId()).thenReturn(3L); + when(dto3.getCount()).thenReturn(5L); + + var mockRepo = mock(EventParticipationInfoRepository.class); + when(mockRepo.countPerEventUserByEventId(anyLong())) + .thenReturn(List.of(dto1, dto2, dto3)); + + var epiHandler = new ParticipateEventActionHandler(mockRepo); + + WriteCommentCountDto dto4 = mock(WriteCommentCountDto.class); + when(dto4.getEventUserId()).thenReturn(2L); + when(dto4.getCount()).thenReturn(10L); + WriteCommentCountDto dto5 = mock(WriteCommentCountDto.class); + when(dto5.getEventUserId()).thenReturn(3L); + when(dto5.getCount()).thenReturn(6L); + WriteCommentCountDto dto6 = mock(WriteCommentCountDto.class); + when(dto6.getEventUserId()).thenReturn(4L); + when(dto6.getCount()).thenReturn(1L); + + var mockRepo2 = mock(CommentRepository.class); + when(mockRepo2.countPerEventUserByEventId(anyLong())) + .thenReturn(List.of(dto4, dto5, dto6)); + + var comHandler = new WriteCommentActionHandler(mockRepo2); + + + Map handlerMap = new HashMap<>(); + handlerMap.put(DrawEventAction.ParticipateEvent, epiHandler); + handlerMap.put(DrawEventAction.WriteComment, comHandler); + + ScoreCalculator scoreCalculator = new ScoreCalculator(handlerMap); + List policies = List.of( + DrawEventScorePolicy.of(DrawEventAction.ParticipateEvent, 5, null), + DrawEventScorePolicy.of(DrawEventAction.WriteComment, 3, null) + ); + + var resultMap = scoreCalculator.calculate(anyLong(), policies); + assertThat(resultMap).isNotNull(); + + assertThat(resultMap.size()).isEqualTo(4); + assertThat(resultMap.get(1L)).isEqualTo(15L); // 3 * 5 = 15 + assertThat(resultMap.get(2L)).isEqualTo(50L); // 4 * 5 + 10 * 3 = 50 + assertThat(resultMap.get(3L)).isEqualTo(43L); // 5 * 5 + 6 * 3 = 43 + assertThat(resultMap.get(4L)).isEqualTo(3L); // 1 * 3 = 3 + } +} \ No newline at end of file diff --git a/src/test/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ParticipateEventActionHandlerTest.java b/src/test/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ParticipateEventActionHandlerTest.java new file mode 100644 index 00000000..ce291622 --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/ParticipateEventActionHandlerTest.java @@ -0,0 +1,46 @@ +package hyundai.softeer.orange.event.draw.component.score.actionHandler; + +import hyundai.softeer.orange.event.draw.dto.EventParticipateCountDto; +import hyundai.softeer.orange.event.draw.repository.EventParticipationInfoRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ParticipateEventActionHandlerTest { + @DisplayName("사용자에 대한 점수 * 참여 수 설정") + @Test + void checkUserScoreXCountSet() { + EventParticipateCountDto dto1 = mock(EventParticipateCountDto.class); + when(dto1.getEventUserId()).thenReturn(1L); + when(dto1.getCount()).thenReturn(10L); + EventParticipateCountDto dto2 = mock(EventParticipateCountDto.class); + when(dto2.getEventUserId()).thenReturn(2L); + when(dto2.getCount()).thenReturn(3L); + EventParticipateCountDto dto3 = mock(EventParticipateCountDto.class); + when(dto3.getEventUserId()).thenReturn(3L); + when(dto3.getCount()).thenReturn(5L); + + var mockRepo = mock(EventParticipationInfoRepository.class); + when(mockRepo.countPerEventUserByEventId(anyLong())) + .thenReturn(List.of(dto1, dto2, dto3)); + + ParticipateEventActionHandler handler = new ParticipateEventActionHandler(mockRepo); + Map scoreMap = new HashMap<>(); + + long score = 3L; + handler.handle(scoreMap, anyLong(), score); + + assertThat(scoreMap).hasSize(3); + assertThat(scoreMap.get(1L)).isEqualTo(score * 10L); + assertThat(scoreMap.get(2L)).isEqualTo(score * 3L); + assertThat(scoreMap.get(3L)).isEqualTo(score * 5L); + } +} \ No newline at end of file diff --git a/src/test/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/WriteCommentActionHandlerTest.java b/src/test/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/WriteCommentActionHandlerTest.java new file mode 100644 index 00000000..c6d9f56a --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/event/draw/component/score/actionHandler/WriteCommentActionHandlerTest.java @@ -0,0 +1,46 @@ +package hyundai.softeer.orange.event.draw.component.score.actionHandler; + +import hyundai.softeer.orange.comment.dto.WriteCommentCountDto; +import hyundai.softeer.orange.comment.repository.CommentRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WriteCommentActionHandlerTest { + @DisplayName("사용자가 작성한 댓글 수 * 참여 수 설정") + @Test + void checkUserScoreXCountSet() { + WriteCommentCountDto dto1 = mock(WriteCommentCountDto.class); + when(dto1.getEventUserId()).thenReturn(1L); + when(dto1.getCount()).thenReturn(10L); + WriteCommentCountDto dto2 = mock(WriteCommentCountDto.class); + when(dto2.getEventUserId()).thenReturn(2L); + when(dto2.getCount()).thenReturn(3L); + WriteCommentCountDto dto3 = mock(WriteCommentCountDto.class); + when(dto3.getEventUserId()).thenReturn(3L); + when(dto3.getCount()).thenReturn(5L); + + var mockRepo = mock(CommentRepository.class); + when(mockRepo.countPerEventUserByEventId(anyLong())) + .thenReturn(List.of(dto1, dto2, dto3)); + + WriteCommentActionHandler handler = new WriteCommentActionHandler(mockRepo); + Map scoreMap = new HashMap<>(); + + long score = 3L; + handler.handle(scoreMap, anyLong(), score); + + assertThat(scoreMap).hasSize(3); + assertThat(scoreMap.get(1L)).isEqualTo(score * 10L); + assertThat(scoreMap.get(2L)).isEqualTo(score * 3L); + assertThat(scoreMap.get(3L)).isEqualTo(score * 5L); + } +} \ No newline at end of file diff --git a/src/test/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepositoryImplTest.java b/src/test/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepositoryImplTest.java new file mode 100644 index 00000000..be6d8e79 --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/event/draw/repository/CustomDrawEventWinningInfoRepositoryImplTest.java @@ -0,0 +1,50 @@ +package hyundai.softeer.orange.event.draw.repository; + +import hyundai.softeer.orange.event.common.entity.EventFrame; +import hyundai.softeer.orange.event.common.repository.EventFrameRepository; +import hyundai.softeer.orange.event.draw.dto.DrawEventWinningInfoBulkInsertDto; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.jdbc.Sql; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +@Sql(value="classpath:sql/CustomDrawEventWinningInfoRepositoryImplTest.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@DataJpaTest(showSql = false) +@TestPropertySource(locations = "classpath:application-test.yml") +class CustomDrawEventWinningInfoRepositoryImplTest { + @Autowired // DrawEventWinningInfoRepository로 접근 가능해야 함 + private DrawEventWinningInfoRepository repo; + + @DisplayName("제대로 데이터를 삽입하는지 확인") + @Test + void testBulkInsertWork() { + // Executing prepared SQL statement [insert into draw_event_winning_info (event_user_id, ranking, draw_event_id) VALUES (?, ?, ?)] + + List targets = List.of( + DrawEventWinningInfoBulkInsertDto.of(1, 1, 1), + DrawEventWinningInfoBulkInsertDto.of(2, 1, 1), + DrawEventWinningInfoBulkInsertDto.of(3, 2, 1), + DrawEventWinningInfoBulkInsertDto.of(4, 2, 1), + DrawEventWinningInfoBulkInsertDto.of(5, 2, 1), + DrawEventWinningInfoBulkInsertDto.of(6, 2, 1), + DrawEventWinningInfoBulkInsertDto.of(1, 1, 2), + DrawEventWinningInfoBulkInsertDto.of(2, 1, 2), + DrawEventWinningInfoBulkInsertDto.of(3, 2, 2), + DrawEventWinningInfoBulkInsertDto.of(4, 2, 2), + DrawEventWinningInfoBulkInsertDto.of(5, 2, 2), + DrawEventWinningInfoBulkInsertDto.of(6, 2, 2) + ); + repo.insertMany(targets); + var events = repo.findAll(); + assertThat(events).hasSize(12); + } +} \ No newline at end of file diff --git a/src/test/java/hyundai/softeer/orange/event/draw/repository/EventParticipationInfoRepositoryTest.java b/src/test/java/hyundai/softeer/orange/event/draw/repository/EventParticipationInfoRepositoryTest.java new file mode 100644 index 00000000..9bb4b028 --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/event/draw/repository/EventParticipationInfoRepositoryTest.java @@ -0,0 +1,52 @@ +package hyundai.softeer.orange.event.draw.repository; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.jdbc.Sql; + +import static org.assertj.core.api.Assertions.assertThat; + +@Sql(value = "classpath:sql/EventParticipationInfoRepositoryTest.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS) +@DataJpaTest(showSql = false) +@TestPropertySource(locations = "classpath:application-test.yml") +class EventParticipationInfoRepositoryTest { + @Autowired + EventParticipationInfoRepository epiRepository; + + @DisplayName("존재하는 draw 이벤트는 참여자 정보 반환") + @Test + void getParticipationCountPerUserIfDrawEventExist() { + var participationCounts = epiRepository.countPerEventUserByEventId(1L); + participationCounts.sort((a,b) -> (int) (a.getEventUserId() - b.getEventUserId())); + assertThat(participationCounts).hasSize(3); + assertThat(participationCounts.get(0).getCount()).isEqualTo(3); + assertThat(participationCounts.get(1).getCount()).isEqualTo(6); + assertThat(participationCounts.get(2).getCount()).isEqualTo(2); + } + + @DisplayName("존재하지 않는 draw 이벤트는 빈 배열 반환") + @Test + void getEmptyArrIfDrawEventNotExist() { + var participationCounts = epiRepository.countPerEventUserByEventId(10L); + assertThat(participationCounts).isEmpty(); + } + + @DisplayName("draw 이벤트 존재해도 유저 없으면 안함") + @Test + void getEmptyArrIfDrawEventExistButUserNotExist() { + var participationCounts = epiRepository.countPerEventUserByEventId(2L); + assertThat(participationCounts).isEmpty(); + } + + + @DisplayName("draw 이벤트 존재해도 유저 없으면 안함") + @Test + void getEmptyArrIfDrawEventAndUserExistButNoParticipation() { + var participationCounts = epiRepository.countPerEventUserByEventId(2L); + assertThat(participationCounts).isEmpty(); + } +} \ No newline at end of file diff --git a/src/test/java/hyundai/softeer/orange/event/draw/service/DrawEventServiceTest.java b/src/test/java/hyundai/softeer/orange/event/draw/service/DrawEventServiceTest.java new file mode 100644 index 00000000..aae210e9 --- /dev/null +++ b/src/test/java/hyundai/softeer/orange/event/draw/service/DrawEventServiceTest.java @@ -0,0 +1,106 @@ +package hyundai.softeer.orange.event.draw.service; + +import hyundai.softeer.orange.event.draw.component.picker.PickTarget; +import hyundai.softeer.orange.event.draw.component.picker.WinnerPicker; +import hyundai.softeer.orange.event.draw.component.score.ScoreCalculator; +import hyundai.softeer.orange.event.draw.dto.DrawEventWinningInfoBulkInsertDto; +import hyundai.softeer.orange.event.draw.entity.DrawEvent; +import hyundai.softeer.orange.event.draw.entity.DrawEventMetadata; +import hyundai.softeer.orange.event.draw.repository.DrawEventRepository; +import hyundai.softeer.orange.event.draw.repository.DrawEventWinningInfoRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +class DrawEventServiceTest { + + private static final Logger log = LoggerFactory.getLogger(DrawEventServiceTest.class); + + @DisplayName("대응되는 이벤트가 존재하지 않으면 예외 반환") + @Test + void throwExceptionIfDrawEventNotFound() { + var deRepository = mock(DrawEventRepository.class); + when(deRepository.findById(anyLong())).thenReturn(Optional.empty()); + + var deService = new DrawEventService(deRepository, null, null, null); + + assertThatThrownBy(() -> { + deService.draw(0L); + }); + } + + @DisplayName("대응되는 이벤트가 존재하면 작업 수행") + @Test + void drawEvent() { + // draw event 처리 + var drawEvent = mock(DrawEvent.class); + + // 정확한 인원 수를 추첨하는지 + when(drawEvent.getMetadataList()).thenReturn(new ArrayList<>(List.of( + // 합 = 5 + DrawEventMetadata.of(3L, 2L, null, null), + DrawEventMetadata.of(1L, 1L, null, null), + DrawEventMetadata.of(2L, 2L, null, null) + ))); + when(drawEvent.getPolicyList()).thenReturn(List.of()); + + var deRepository = mock(DrawEventRepository.class); + when(deRepository.findById(anyLong())).thenReturn(Optional.of(drawEvent)); + + // repository 모킹. saveMany 호출하는지 검사 필요 + var deWinningInfoRepository = mock(DrawEventWinningInfoRepository.class); + + // 점수 채점기 모킹 + var calculator = mock(ScoreCalculator.class); + when(calculator.calculate(anyLong(), anyList())).thenReturn(Map.of(1L, 1L, 2L, 10L, 3L,5L)); + + // 추첨기 모킹 + var picker = mock(WinnerPicker.class); + when(picker.pick(anyList(), anyLong())).thenReturn(new ArrayList<>( + List.of( + new PickTarget(2L, 1L), // 1 + new PickTarget(1L, 2L), // 2 + new PickTarget(3L, 3L), // 2 + new PickTarget(5L, 1L), // 3 + new PickTarget(4L, 2L) // 3 + ))); + + var deService = new DrawEventService(deRepository, deWinningInfoRepository, picker, calculator); + + deService.draw(0L); + + ArgumentCaptor> ac = ArgumentCaptor.forClass(List.class); + + verify(picker, times(1)).pick(anyList(), eq(5L)); + verify(deWinningInfoRepository, times(1)).insertMany(ac.capture()); + + var list = ac.getValue(); + assertThat(list).hasSize(5); + assertThat(list.get(0).getEventUserId()).isEqualTo(2L); + assertThat(list.get(1).getEventUserId()).isEqualTo(1L); + assertThat(list.get(2).getEventUserId()).isEqualTo(3L); + assertThat(list.get(3).getEventUserId()).isEqualTo(5L); + assertThat(list.get(4).getEventUserId()).isEqualTo(4L); + for(var v: list) { + log.info("ranking {}", v.getRanking()); + } + assertThat(list.get(0).getRanking()).isEqualTo(1L); + assertThat(list.get(1).getRanking()).isEqualTo(2L); + assertThat(list.get(2).getRanking()).isEqualTo(2L); + assertThat(list.get(3).getRanking()).isEqualTo(3L); + assertThat(list.get(4).getRanking()).isEqualTo(3L); + } +} \ No newline at end of file diff --git a/src/test/resources/sql/CommentRepositoryTest.sql b/src/test/resources/sql/CommentRepositoryTest.sql new file mode 100644 index 00000000..e1e03438 --- /dev/null +++ b/src/test/resources/sql/CommentRepositoryTest.sql @@ -0,0 +1,26 @@ +INSERT INTO event_frame(name) VALUES ('test1'); +INSERT INTO event_frame(name) VALUES ('test2'); + +INSERT INTO event_metadata(event_type, event_frame_id, event_id) VALUES (1, 1, 'HD_240808_001'); +INSERT INTO event_metadata(event_type, event_frame_id, event_id) VALUES (0, 2, 'HD_240808_002'); + +INSERT INTO event_user(score, event_frame_id, user_id) VALUES (0, 1, 'user1'); +INSERT INTO event_user(score, event_frame_id, user_id) VALUES (0, 1, 'user2'); +INSERT INTO event_user(score, event_frame_id, user_id) VALUES (0, 1, 'user3'); + +-- 3 comments for user1 +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 1); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 1); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 1); + +-- 6 comments for user2 +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 2); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 2); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 2); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 2); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 2); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 2); + +-- 2 comments for user3 +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 3); +INSERT INTO comment(event_frame_id, event_user_id) VALUES (1, 3); diff --git a/src/test/resources/sql/CustomDrawEventWinningInfoRepositoryImplTest.sql b/src/test/resources/sql/CustomDrawEventWinningInfoRepositoryImplTest.sql new file mode 100644 index 00000000..b29f2b9f --- /dev/null +++ b/src/test/resources/sql/CustomDrawEventWinningInfoRepositoryImplTest.sql @@ -0,0 +1,31 @@ +INSERT INTO event_frame(name) VALUES ('test'); + +INSERT INTO event_metadata(event_type, event_frame_id, event_id) +VALUES (1, 1, 'HD_240808_001'); + +INSERT INTO draw_event(event_metadata_id) +VALUES (1); + +INSERT INTO event_metadata(event_type, event_frame_id, event_id) +VALUES (1, 1, 'HD_240808_002'); + +INSERT INTO draw_event(event_metadata_id) +VALUES (2); + +INSERT INTO event_user(score, event_frame_id, user_id) +VALUES (0, 1, 'user1'); + +INSERT INTO event_user(score, event_frame_id, user_id) +VALUES (0, 1, 'user2'); + +INSERT INTO event_user(score, event_frame_id, user_id) +VALUES (0, 1, 'user3'); + +INSERT INTO event_user(score, event_frame_id, user_id) +VALUES (0, 1, 'user4'); + +INSERT INTO event_user(score, event_frame_id, user_id) +VALUES (0, 1, 'user5'); + +INSERT INTO event_user(score, event_frame_id, user_id) +VALUES (0, 1, 'user6'); \ No newline at end of file diff --git a/src/test/resources/sql/EventParticipationInfoRepositoryTest.sql b/src/test/resources/sql/EventParticipationInfoRepositoryTest.sql new file mode 100644 index 00000000..6ac6126a --- /dev/null +++ b/src/test/resources/sql/EventParticipationInfoRepositoryTest.sql @@ -0,0 +1,28 @@ +INSERT INTO event_frame(name) VALUES ('test1'); +INSERT INTO event_frame(name) VALUES ('test2'); +INSERT INTO event_frame(name) VALUES ('test3'); + +INSERT INTO event_metadata(event_type, event_frame_id, event_id) VALUES (1, 1, 'HD_240808_001'); +INSERT INTO event_metadata(event_type, event_frame_id, event_id) VALUES (1, 2, 'HD_240808_002'); +INSERT INTO event_metadata(event_type, event_frame_id, event_id) VALUES (1, 3, 'HD_240808_003'); + +INSERT INTO draw_event(event_metadata_id) VALUES(1); + +INSERT INTO event_user(score, event_frame_id, user_id) VALUES (0, 1, 'user1'); +INSERT INTO event_user(score, event_frame_id, user_id) VALUES (0, 1, 'user2'); +INSERT INTO event_user(score, event_frame_id, user_id) VALUES (0, 1, 'user3'); +INSERT INTO event_user(score, event_frame_id, user_id) VALUES (0, 3, 'user4'); + +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,1); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,1); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,1); + +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,2); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,2); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,2); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,2); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,2); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,2); + +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,3); +INSERT INTO event_partication_info(draw_event_id, event_user_id) VALUES (1,3);