diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..fc874ada --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,16 @@ +language: "ko-KR" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: false + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: false + base_branches: + - "develop" +chat: + auto_reply: true diff --git a/.github/workflows/Dev_CD.yml b/.github/workflows/Dev_CD.yml index 8718cc23..e9a4e830 100644 --- a/.github/workflows/Dev_CD.yml +++ b/.github/workflows/Dev_CD.yml @@ -1,6 +1,7 @@ name: dev-cd on: + workflow_dispatch: push: branches: - "develop" @@ -10,10 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout Develop Branch + - name: Checkout Branch uses: actions/checkout@v4 - with: - ref: "develop" - name: Setting dev-secret.yml run: | @@ -66,7 +65,18 @@ jobs: uses: actions/download-artifact@v4 with: name: deploy-scripts - path: ~/app/scripts + path: ~/app/scripts/ - name: Replace application to latest run: sudo sh ~/app/scripts/replace-new-version.sh + + - name: Health Check + run: sh ~/app/scripts/health-check.sh + + - name: Send Discord Alert on Failure + if: failure() + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\":\":x: [DEV] 배포 실패! 확인이 필요합니다.\n링크: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ + ${{ secrets.DISCORD_WEB_HOOK }} diff --git a/.github/workflows/Prod_CD.yml b/.github/workflows/Prod_CD.yml index 20e0c517..cb10ee58 100644 --- a/.github/workflows/Prod_CD.yml +++ b/.github/workflows/Prod_CD.yml @@ -1,6 +1,7 @@ name: prod-cd on: + workflow_dispatch: push: branches: - "main" @@ -12,8 +13,6 @@ jobs: steps: - name: Checkout Develop Branch uses: actions/checkout@v4 - with: - ref: "main" - name: Setting prod-secret.yml run: | @@ -66,7 +65,18 @@ jobs: uses: actions/download-artifact@v4 with: name: deploy-scripts - path: ~/app/scripts + path: ~/app/scripts/ - name: Replace application to latest run: sudo sh ~/app/scripts/replace-new-version.sh + + - name: Health Check + run: sh ~/app/scripts/health-check.sh + + - name: Send Discord Alert on Failure + if: failure() + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\":\":warning: :warning: [PROD] 배포 실패! :warning: :warning: \n링크: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \ + ${{ secrets.DISCORD_WEB_HOOK }} diff --git a/scripts/dev/health-check.sh b/scripts/dev/health-check.sh new file mode 100644 index 00000000..96fd66fc --- /dev/null +++ b/scripts/dev/health-check.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# 최대 반복 횟수 +MAX_RETRIES=60 + +# 성공 상태 코드와 요청 URL +SUCCESS_STATUS=200 +HEALTH_CHECK_URL="http://localhost:8083/monitoring/health" + +# 반복 시작 +i=1 +while [ "$i" -le "$MAX_RETRIES" ]; do + # HTTP 요청 보내기 + RESPONSE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_CHECK_URL") + echo "[TRY $i] StatusCode : $RESPONSE_STATUS " + # 상태 코드 확인 + if [ "$RESPONSE_STATUS" -eq "$SUCCESS_STATUS" ]; then + echo "Success: Received $SUCCESS_STATUS status code on attempt $i." + exit 0 + fi + + # 2초 대기 + sleep 2 + + # 반복 변수 증가 + i=$((i + 1)) +done + +# 실패 메시지 +echo "Failure: Did not receive $SUCCESS_STATUS status code within $MAX_RETRIES attempts." +exit 1 diff --git a/scripts/prod/health-check.sh b/scripts/prod/health-check.sh new file mode 100644 index 00000000..96fd66fc --- /dev/null +++ b/scripts/prod/health-check.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# 최대 반복 횟수 +MAX_RETRIES=60 + +# 성공 상태 코드와 요청 URL +SUCCESS_STATUS=200 +HEALTH_CHECK_URL="http://localhost:8083/monitoring/health" + +# 반복 시작 +i=1 +while [ "$i" -le "$MAX_RETRIES" ]; do + # HTTP 요청 보내기 + RESPONSE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_CHECK_URL") + echo "[TRY $i] StatusCode : $RESPONSE_STATUS " + # 상태 코드 확인 + if [ "$RESPONSE_STATUS" -eq "$SUCCESS_STATUS" ]; then + echo "Success: Received $SUCCESS_STATUS status code on attempt $i." + exit 0 + fi + + # 2초 대기 + sleep 2 + + # 반복 변수 증가 + i=$((i + 1)) +done + +# 실패 메시지 +echo "Failure: Did not receive $SUCCESS_STATUS status code within $MAX_RETRIES attempts." +exit 1 diff --git a/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java index 5f5c952f..0597deca 100644 --- a/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java +++ b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java @@ -2,6 +2,7 @@ import com.debatetimer.exception.custom.DTInitializationException; import com.debatetimer.exception.errorcode.InitializationErrorCode; +import io.micrometer.core.annotation.Timed; import java.util.Arrays; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -34,6 +35,7 @@ private JDA initializeJda(String token) { } } + @Timed(value = "discord.send_error_message") public void sendErrorMessage(Throwable throwable) { TextChannel channel = jda.getTextChannelById(properties.getChannelId()); String errorMessage = throwable.toString(); diff --git a/src/main/java/com/debatetimer/client/oauth/OAuthClient.java b/src/main/java/com/debatetimer/client/oauth/OAuthClient.java index ed10dcf4..973f99c9 100644 --- a/src/main/java/com/debatetimer/client/oauth/OAuthClient.java +++ b/src/main/java/com/debatetimer/client/oauth/OAuthClient.java @@ -5,6 +5,7 @@ import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.OAuthToken; import com.debatetimer.exception.handler.OAuthClientErrorHandler; +import io.micrometer.core.annotation.Timed; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; @@ -24,6 +25,7 @@ public OAuthClient(RestClient.Builder restClientBuilder, OAuthProperties oauthPr this.oauthProperties = oauthProperties; } + @Timed(value = "google.request_token") public OAuthToken requestToken(MemberCreateRequest request) { return restClient.post() .uri("https://oauth2.googleapis.com/token") @@ -34,6 +36,7 @@ public OAuthToken requestToken(MemberCreateRequest request) { .body(OAuthToken.class); } + @Timed(value = "google.request_member_info") public MemberInfo requestMemberInfo(OAuthToken response) { return restClient.get() .uri("https://www.googleapis.com/oauth2/v3/userinfo") diff --git a/src/main/java/com/debatetimer/config/ErrorDecoderConfig.java b/src/main/java/com/debatetimer/config/ErrorDecoderConfig.java new file mode 100644 index 00000000..5fee2c04 --- /dev/null +++ b/src/main/java/com/debatetimer/config/ErrorDecoderConfig.java @@ -0,0 +1,33 @@ +package com.debatetimer.config; + +import com.debatetimer.exception.decoder.H2ErrorDecoder; +import com.debatetimer.exception.decoder.MySqlErrorDecoder; +import com.debatetimer.exception.decoder.RepositoryErrorDecoder; +import lombok.NoArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class ErrorDecoderConfig { + + @Profile({"dev", "prod"}) + @Configuration + public static class MySqlErrorDecoderConfig { + + @Bean + public RepositoryErrorDecoder mySqlErrorDecoder() { + return new MySqlErrorDecoder(); + } + } + + @Profile({"test", "local"}) + @Configuration + public static class H2ErrorDecoderConfig { + + @Bean + public RepositoryErrorDecoder h2ErrorDecoder() { + return new H2ErrorDecoder(); + } + } +} diff --git a/src/main/java/com/debatetimer/config/MonitoringConfig.java b/src/main/java/com/debatetimer/config/MonitoringConfig.java new file mode 100644 index 00000000..b3bced6d --- /dev/null +++ b/src/main/java/com/debatetimer/config/MonitoringConfig.java @@ -0,0 +1,17 @@ +package com.debatetimer.config; + +import io.micrometer.core.aop.TimedAspect; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MonitoringConfig { + + @Bean + @ConditionalOnMissingBean(TimedAspect.class) + public TimedAspect timedAspect(MeterRegistry registry) { + return new TimedAspect(registry); + } +} diff --git a/src/main/java/com/debatetimer/controller/poll/PollController.java b/src/main/java/com/debatetimer/controller/poll/PollController.java new file mode 100644 index 00000000..88da0830 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/poll/PollController.java @@ -0,0 +1,49 @@ +package com.debatetimer.controller.poll; + +import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.poll.response.PollCreateResponse; +import com.debatetimer.dto.poll.response.PollInfoResponse; +import com.debatetimer.service.poll.PollService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class PollController { + + private final PollService pollService; + + @PostMapping("/api/polls/{tableId}") + @ResponseStatus(HttpStatus.CREATED) + public PollCreateResponse createPoll( + @AuthMember Member member, + @PathVariable(name = "tableId") long tableId + ) { + return pollService.create(tableId, member); + } + + @GetMapping("/api/polls/{pollId}") + @ResponseStatus(HttpStatus.OK) + public PollInfoResponse getPollInfo( + @AuthMember Member member, + @PathVariable(name = "pollId") long pollId + ) { + return pollService.getPollInfo(pollId, member); + } + + @PatchMapping("/api/polls/{pollId}") + @ResponseStatus(HttpStatus.OK) + public PollInfoResponse finishPoll( + @AuthMember Member member, + @PathVariable(name = "pollId") long pollId + ) { + return pollService.finishPoll(pollId, member); + } +} diff --git a/src/main/java/com/debatetimer/controller/poll/VoteController.java b/src/main/java/com/debatetimer/controller/poll/VoteController.java new file mode 100644 index 00000000..64f53365 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/poll/VoteController.java @@ -0,0 +1,37 @@ +package com.debatetimer.controller.poll; + +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.service.poll.VoteService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class VoteController { + + private final VoteService voteService; + + @GetMapping("/api/polls/{pollId}/votes") + @ResponseStatus(HttpStatus.OK) + public VoterPollInfoResponse getVotersPollInfo(@PathVariable long pollId) { + return voteService.getVoterPollInfo(pollId); + } + + @PostMapping("/api/polls/{pollId}/votes") + @ResponseStatus(HttpStatus.CREATED) + public VoteCreateResponse votePoll( + @PathVariable long pollId, + @RequestBody @Valid VoteRequest voteRequest + ) { + return voteService.vote(pollId, voteRequest); + } +} diff --git a/src/main/java/com/debatetimer/domain/Stance.java b/src/main/java/com/debatetimer/domain/Stance.java deleted file mode 100644 index bc9dc559..00000000 --- a/src/main/java/com/debatetimer/domain/Stance.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.debatetimer.domain; - -public enum Stance { - - PROS, - CONS, - NEUTRAL, -} diff --git a/src/main/java/com/debatetimer/domain/customize/Agenda.java b/src/main/java/com/debatetimer/domain/customize/Agenda.java new file mode 100644 index 00000000..6e0b3571 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/Agenda.java @@ -0,0 +1,28 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import lombok.Getter; + +@Getter +public class Agenda { + + public static final int AGENDA_MAX_LENGTH = 255; + + private final String value; + + public Agenda(String value) { + validate(value); + this.value = value; + } + + private void validate(String value) { + if (value == null) { + return; + } + + if (value.length() > AGENDA_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_AGENDA_LENGTH); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/Bell.java b/src/main/java/com/debatetimer/domain/customize/Bell.java new file mode 100644 index 00000000..4766bb31 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/Bell.java @@ -0,0 +1,29 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import lombok.Getter; + +@Getter +public class Bell { + + public static final int MAX_BELL_COUNT = 3; + + private final BellType type; + private final int time; + private final int count; + + public Bell(BellType type, int time, int count) { + type.validateTime(time); + validateCount(count); + this.type = type; + this.time = time; + this.count = count; + } + + private void validateCount(int count) { + if (count <= 0 || count > MAX_BELL_COUNT) { + throw new DTClientErrorException(ClientErrorCode.INVALID_BELL_COUNT); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/BellType.java b/src/main/java/com/debatetimer/domain/customize/BellType.java new file mode 100644 index 00000000..e90dc35d --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/BellType.java @@ -0,0 +1,25 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.function.IntPredicate; + +public enum BellType { + + AFTER_START(time -> time >= 0), + BEFORE_END(time -> time >= 0), + AFTER_END(time -> time <= 0), + ; + + private final IntPredicate timeValidator; + + BellType(IntPredicate timeValidator) { + this.timeValidator = timeValidator; + } + + public void validateTime(int time) { + if (!timeValidator.test(time)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_BELL_TIME); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java index ff487570..dfb7fe31 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java @@ -2,113 +2,74 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.TableType; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; -import lombok.AccessLevel; import lombok.Getter; -import lombok.NoArgsConstructor; -@Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) public class CustomizeTable { - private static final String TABLE_NAME_REGEX = "^[\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\s]+$"; - private static final String TEAM_NAME_REGEX = "^[\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\s]+$"; - public static final int TABLE_NAME_MAX_LENGTH = 20; - public static final int TEAM_NAME_MAX_LENGTH = 8; - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; - - @NotNull - private String name; - - private String agenda; - - @NotBlank - private String prosTeamName; - - @NotBlank - private String consTeamName; - - private boolean warningBell; - private boolean finishBell; - - @NotNull - private LocalDateTime usedAt; + private final Long id; + private final Member member; + private final TableName name; + private final Agenda agenda; + private final TeamName prosTeamName; + private final TeamName consTeamName; + private final boolean warningBell; + private final boolean finishBell; + private final LocalDateTime usedAt; public CustomizeTable( + Long id, Member member, String name, String agenda, + String prosTeamName, + String consTeamName, boolean warningBell, boolean finishBell, - String prosTeamName, - String consTeamName + LocalDateTime usedAt ) { - validateTableName(name); - validateTeamName(prosTeamName); - validateTeamName(consTeamName); + this.id = id; this.member = member; - this.name = name; - this.agenda = agenda; - this.prosTeamName = prosTeamName; - this.consTeamName = consTeamName; + this.name = new TableName(name); + this.agenda = new Agenda(agenda); + this.prosTeamName = new TeamName(prosTeamName); + this.consTeamName = new TeamName(consTeamName); this.warningBell = warningBell; this.finishBell = finishBell; - this.usedAt = LocalDateTime.now(); + this.usedAt = usedAt; + } + + public CustomizeTable( + Member member, + String name, + String agenda, + String prosTeamName, + String consTeamName, + boolean warningBell, + boolean finishBell, + LocalDateTime usedAt + ) { + this(null, member, name, agenda, prosTeamName, consTeamName, warningBell, finishBell, usedAt); } - public void updateTable(CustomizeTable renewTable) { - this.name = renewTable.getName(); - this.agenda = renewTable.getAgenda(); - this.prosTeamName = renewTable.getProsTeamName(); - this.consTeamName = renewTable.getConsTeamName(); - this.warningBell = renewTable.isWarningBell(); - this.finishBell = renewTable.isFinishBell(); - this.usedAt = LocalDateTime.now(); + public TableType getType() { + return TableType.CUSTOMIZE; } - public void updateUsedAt() { - this.usedAt = LocalDateTime.now(); + public String getProsTeamName() { + return prosTeamName.getValue(); } - private void validateTableName(String name) { - if (name.length() > TABLE_NAME_MAX_LENGTH) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_LENGTH); - } - if (!name.matches(TABLE_NAME_REGEX)) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_FORM); - } + public String getConsTeamName() { + return consTeamName.getValue(); } - private void validateTeamName(String teamName) { - if (teamName.length() > TEAM_NAME_MAX_LENGTH) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TEAM_NAME_LENGTH); - } - if (!teamName.matches(TEAM_NAME_REGEX)) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TEAM_NAME_FORM); - } + public String getName() { + return name.getValue(); } - public TableType getType() { - return TableType.CUSTOMIZE; + public String getAgenda() { + return agenda.getValue(); } } diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java index c580713d..498c9179 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java @@ -1,125 +1,44 @@ package com.debatetimer.domain.customize; -import com.debatetimer.domain.Stance; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CustomizeTimeBox { +import java.util.List; +import org.springframework.lang.Nullable; + +public abstract class CustomizeTimeBox { public static final int SPEECH_TYPE_MAX_LENGTH = 10; - public static final int TIME_MULTIPLIER = 2; public static final int SPEAKER_MAX_LENGTH = 5; - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "table_id") - private CustomizeTable customizeTable; - - private int sequence; - - @NotNull - @Enumerated(EnumType.STRING) - private Stance stance; - - private int time; - private String speaker; - - @NotBlank - private String speechType; - - @NotNull - @Enumerated(value = EnumType.STRING) - private CustomizeBoxType boxType; - - private Integer timePerTeam; - private Integer timePerSpeaking; + private final Stance stance; - public CustomizeTimeBox( - CustomizeTable customizeTable, - int sequence, - Stance stance, - String speechType, - CustomizeBoxType boxType, - Integer time, - String speaker - ) { - validateNotTimeBasedType(boxType); - validateSpeechType(speechType); - validateSpeaker(speaker); - validateSequence(sequence); - validateTime(time); + private final String speechType; - this.customizeTable = customizeTable; - this.sequence = sequence; - this.stance = stance; - this.time = time; - this.speaker = initializeSpeaker(speaker); - this.speechType = speechType; - this.boxType = boxType; - } + @Nullable + private final String speaker; - public CustomizeTimeBox( - CustomizeTable customizeTable, - int sequence, - Stance stance, - String speechType, - CustomizeBoxType boxType, - Integer timePerTeam, - Integer timePerSpeaking, - String speaker - ) { - validateTimeBasedTimes(timePerTeam, timePerSpeaking); - validateTimeBasedType(boxType); + protected CustomizeTimeBox(Stance stance, String speechType, @Nullable String speaker) { + validateStance(stance); validateSpeechType(speechType); validateSpeaker(speaker); - validateSequence(sequence); - validateTime(convertToTime(timePerTeam)); - this.sequence = sequence; this.stance = stance; - this.time = convertToTime(timePerTeam); - this.speaker = initializeSpeaker(speaker); - this.customizeTable = customizeTable; this.speechType = speechType; - this.boxType = boxType; - this.timePerTeam = timePerTeam; - this.timePerSpeaking = timePerSpeaking; + this.speaker = speaker; } - private static int convertToTime(Integer timePerTeam) { - if (timePerTeam == null) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + private void validateStance(Stance stance) { + if (stance == null || !isValidStance(stance)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); } - return timePerTeam * TIME_MULTIPLIER; } + protected abstract boolean isValidStance(Stance stance); - private String initializeSpeaker(String speaker) { - if (speaker == null || speaker.isBlank()) { - return null; + private void validateSpeechType(String speechType) { + if (speechType == null || speechType.isBlank() || speechType.length() > SPEECH_TYPE_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH); } - return speaker; } private void validateSpeaker(String speaker) { @@ -128,51 +47,29 @@ private void validateSpeaker(String speaker) { } } - private void validateSequence(int sequence) { - if (sequence <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE); - } + public final Stance getStance() { + return stance; } - private void validateTime(int time) { - if (time <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); - } + public final String getSpeechType() { + return speechType; } - private void validateTime(Integer time) { - if (time == null || time <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); - } + @Nullable + public final String getSpeaker() { + return speaker; } - private void validateTimeBasedTimes(Integer timePerTeam, Integer timePerSpeaking) { - validateTime(timePerTeam); - if (timePerSpeaking == null) { - return; - } + public abstract CustomizeBoxType getBoxType(); - validateTime(timePerSpeaking); - if (timePerTeam < timePerSpeaking) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME); - } - } + @Nullable + public abstract Integer getTime(); - private void validateTimeBasedType(CustomizeBoxType boxType) { - if (boxType.isNotTimeBased()) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); - } - } + @Nullable + public abstract Integer getTimePerTeam(); - private void validateNotTimeBasedType(CustomizeBoxType boxType) { - if (boxType.isTimeBased()) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); - } - } + @Nullable + public abstract Integer getTimePerSpeaking(); - private void validateSpeechType(String speechType) { - if (speechType.length() > SPEECH_TYPE_MAX_LENGTH) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH); - } - } + public abstract List getBells(); } diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBoxes.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBoxes.java deleted file mode 100644 index 7be35232..00000000 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBoxes.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.debatetimer.domain.customize; - -import java.util.Comparator; -import java.util.List; -import lombok.Getter; - -@Getter -public class CustomizeTimeBoxes { - - private static final Comparator TIME_BOX_COMPARATOR = Comparator - .comparing(CustomizeTimeBox::getSequence); - - private final List timeBoxes; - - public CustomizeTimeBoxes(List timeBoxes) { - this.timeBoxes = timeBoxes.stream() - .sorted(TIME_BOX_COMPARATOR) - .toList(); - } -} diff --git a/src/main/java/com/debatetimer/domain/customize/NormalTimeBox.java b/src/main/java/com/debatetimer/domain/customize/NormalTimeBox.java new file mode 100644 index 00000000..4e413aad --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/NormalTimeBox.java @@ -0,0 +1,57 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.List; +import org.springframework.lang.Nullable; + +public final class NormalTimeBox extends CustomizeTimeBox { + + private final int time; + private final List bells; + + public NormalTimeBox(Stance stance, String speechType, @Nullable String speaker, Integer time, List bells) { + super(stance, speechType, speaker); + validateTime(time); + this.time = time; + this.bells = bells; + } + + private void validateTime(Integer time) { + if (time == null || time <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + @Override + protected boolean isValidStance(Stance stance) { + return true; + } + + @Override + public CustomizeBoxType getBoxType() { + return CustomizeBoxType.NORMAL; + } + + @Override + public Integer getTime() { + return time; + } + + @Nullable + @Override + public Integer getTimePerTeam() { + return null; + } + + @Nullable + @Override + public Integer getTimePerSpeaking() { + return null; + } + + @Override + public List getBells() { + return bells; + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/Stance.java b/src/main/java/com/debatetimer/domain/customize/Stance.java new file mode 100644 index 00000000..0df12c9d --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/Stance.java @@ -0,0 +1,13 @@ +package com.debatetimer.domain.customize; + +public enum Stance { + + PROS, + CONS, + NEUTRAL, + ; + + public boolean isNeutralStance() { + return this == NEUTRAL; + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/TableName.java b/src/main/java/com/debatetimer/domain/customize/TableName.java new file mode 100644 index 00000000..4cbcfe33 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/TableName.java @@ -0,0 +1,28 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import lombok.Getter; + +@Getter +public class TableName { + + private static final String NAME_REGEX = "^[\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\s]+$"; + public static final int NAME_MAX_LENGTH = 20; + + private final String value; + + public TableName(String value) { + validate(value); + this.value = value; + } + + private void validate(String name) { + if (name.length() > NAME_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_LENGTH); + } + if (!name.matches(NAME_REGEX)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_FORM); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/TeamName.java b/src/main/java/com/debatetimer/domain/customize/TeamName.java new file mode 100644 index 00000000..352eca26 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/TeamName.java @@ -0,0 +1,28 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import lombok.Getter; + +@Getter +public class TeamName { + + private static final String NAME_REGEX = "^[\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\s]+$"; + public static final int NAME_MAX_LENGTH = 8; + + private final String value; + + public TeamName(String value) { + validate(value); + this.value = value; + } + + private void validate(String teamName) { + if (teamName.isBlank() || teamName.length() > NAME_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TEAM_NAME_LENGTH); + } + if (!teamName.matches(NAME_REGEX)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TEAM_NAME_FORM); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/TimeBasedTimeBox.java b/src/main/java/com/debatetimer/domain/customize/TimeBasedTimeBox.java new file mode 100644 index 00000000..40018cab --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/TimeBasedTimeBox.java @@ -0,0 +1,79 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.Collections; +import java.util.List; +import org.springframework.lang.Nullable; + +public class TimeBasedTimeBox extends CustomizeTimeBox { + + private final int timePerTeam; + + @Nullable + private final Integer timePerSpeaking; + + public TimeBasedTimeBox(Stance stance, + String speechType, + @Nullable String speaker, + Integer timePerTeam, + @Nullable Integer timePerSpeaking) { + super(stance, speechType, speaker); + + validateTimes(timePerTeam, timePerSpeaking); + this.timePerTeam = timePerTeam; + this.timePerSpeaking = timePerSpeaking; + } + + private void validateTimes(Integer timePerTeam, Integer timePerSpeaking) { + validateTimePerTeam(timePerTeam); + validateTimePerSpeaking(timePerSpeaking); + if (timePerSpeaking != null && timePerTeam < timePerSpeaking) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME); + } + } + + private void validateTimePerTeam(Integer time) { + if (time == null || time <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + private void validateTimePerSpeaking(Integer timePerSpeaking) { + if (timePerSpeaking != null && timePerSpeaking <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + @Override + protected boolean isValidStance(Stance stance) { + return stance.isNeutralStance(); + } + + @Override + public CustomizeBoxType getBoxType() { + return CustomizeBoxType.TIME_BASED; + } + + @Override + @Nullable + public Integer getTime() { + return null; + } + + @Override + public Integer getTimePerTeam() { + return timePerTeam; + } + + @Override + @Nullable + public Integer getTimePerSpeaking() { + return timePerSpeaking; + } + + @Override + public List getBells() { + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/debatetimer/domain/member/Member.java b/src/main/java/com/debatetimer/domain/member/Member.java index 2299c385..923d17c5 100644 --- a/src/main/java/com/debatetimer/domain/member/Member.java +++ b/src/main/java/com/debatetimer/domain/member/Member.java @@ -1,6 +1,6 @@ package com.debatetimer.domain.member; -import com.debatetimer.domain.BaseTimeEntity; +import com.debatetimer.entity.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; diff --git a/src/main/java/com/debatetimer/domain/poll/ParticipantName.java b/src/main/java/com/debatetimer/domain/poll/ParticipantName.java new file mode 100644 index 00000000..86c012a2 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/poll/ParticipantName.java @@ -0,0 +1,22 @@ +package com.debatetimer.domain.poll; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import lombok.Getter; + +@Getter +public class ParticipantName { + + private final String value; + + public ParticipantName(String value) { + validateName(value); + this.value = value; + } + + private void validateName(String value) { + if (value == null || value.isBlank()) { + throw new DTClientErrorException(ClientErrorCode.INVALID_POLL_PARTICIPANT_NAME); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/poll/ParticipateCode.java b/src/main/java/com/debatetimer/domain/poll/ParticipateCode.java new file mode 100644 index 00000000..29c323a2 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/poll/ParticipateCode.java @@ -0,0 +1,22 @@ +package com.debatetimer.domain.poll; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import lombok.Getter; + +@Getter +public class ParticipateCode { + + private final String value; + + public ParticipateCode(String value) { + validateName(value); + this.value = value; + } + + private void validateName(String value) { + if (value == null || value.isBlank()) { + throw new DTClientErrorException(ClientErrorCode.INVALID_POLL_PARTICIPANT_CODE); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/poll/Poll.java b/src/main/java/com/debatetimer/domain/poll/Poll.java new file mode 100644 index 00000000..b3b23663 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/poll/Poll.java @@ -0,0 +1,33 @@ +package com.debatetimer.domain.poll; + +import com.debatetimer.domain.customize.Agenda; +import com.debatetimer.domain.customize.TeamName; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class Poll { + + private final Long id; + private final long tableId; + private final long memberId; + private final PollStatus status; + private final TeamName prosTeamName; + private final TeamName consTeamName; + private final Agenda agenda; + + public Poll(long tableId, long memberId, String prosTeamName, String consTeamName, String agenda) { + this(null, tableId, memberId, PollStatus.PROGRESS, + new TeamName(prosTeamName), new TeamName(consTeamName), new Agenda(agenda)); + } + + public Poll(Long id, long tableId, long memberId, PollStatus status, + String prosTeamName, String consTeamName, String agenda) { + this(id, tableId, memberId, status, new TeamName(prosTeamName), new TeamName(consTeamName), new Agenda(agenda)); + } + + public boolean isProgress() { + return status.isProgress(); + } +} diff --git a/src/main/java/com/debatetimer/domain/poll/PollStatus.java b/src/main/java/com/debatetimer/domain/poll/PollStatus.java new file mode 100644 index 00000000..98379b68 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/poll/PollStatus.java @@ -0,0 +1,12 @@ +package com.debatetimer.domain.poll; + +public enum PollStatus { + + PROGRESS, + DONE, + ; + + public boolean isProgress() { + return this == PROGRESS; + } +} diff --git a/src/main/java/com/debatetimer/domain/poll/Vote.java b/src/main/java/com/debatetimer/domain/poll/Vote.java new file mode 100644 index 00000000..6d14cf5f --- /dev/null +++ b/src/main/java/com/debatetimer/domain/poll/Vote.java @@ -0,0 +1,23 @@ +package com.debatetimer.domain.poll; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class Vote { + + private final Long id; + private final long pollId; + private final VoteTeam team; + private final ParticipantName name; + private final ParticipateCode code; + + public Vote(long pollId, VoteTeam team, String name, String code) { + this(null, pollId, team, name, code); + } + + public Vote(Long id, long pollId, VoteTeam team, String name, String code) { + this(id, pollId, team, new ParticipantName(name), new ParticipateCode(code)); + } +} diff --git a/src/main/java/com/debatetimer/domain/poll/VoteInfo.java b/src/main/java/com/debatetimer/domain/poll/VoteInfo.java new file mode 100644 index 00000000..36734ebb --- /dev/null +++ b/src/main/java/com/debatetimer/domain/poll/VoteInfo.java @@ -0,0 +1,19 @@ +package com.debatetimer.domain.poll; + +import lombok.Getter; + +@Getter +public class VoteInfo { + + private final long pollId; + private final long totalCount; + private final long prosCount; + private final long consCount; + + public VoteInfo(long pollId, long prosCount, long consCount) { + this.pollId = pollId; + this.totalCount = prosCount + consCount; + this.prosCount = prosCount; + this.consCount = consCount; + } +} diff --git a/src/main/java/com/debatetimer/domain/poll/VoteTeam.java b/src/main/java/com/debatetimer/domain/poll/VoteTeam.java new file mode 100644 index 00000000..d9648937 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/poll/VoteTeam.java @@ -0,0 +1,8 @@ +package com.debatetimer.domain.poll; + +public enum VoteTeam { + + PROS, + CONS, + ; +} diff --git a/src/main/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepository.java new file mode 100644 index 00000000..2c2e96e7 --- /dev/null +++ b/src/main/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepository.java @@ -0,0 +1,111 @@ +package com.debatetimer.domainrepository.customize; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.member.Member; +import com.debatetimer.entity.customize.BellEntity; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; +import com.debatetimer.repository.customize.BellRepository; +import com.debatetimer.repository.customize.CustomizeTableRepository; +import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +public class CustomizeTableDomainRepository { + + private final CustomizeTableRepository tableRepository; + private final CustomizeTimeBoxRepository timeBoxRepository; + private final BellRepository bellRepository; + + @Transactional + public CustomizeTable save(CustomizeTable table, List timeBoxes) { + CustomizeTableEntity savedTableEntity = tableRepository.save(new CustomizeTableEntity(table)); + saveTimeBoxes(savedTableEntity, timeBoxes); + + return savedTableEntity.toDomain(); + } + + private void saveTimeBoxes(CustomizeTableEntity tableEntity, List timeBoxes) { + IntStream.range(0, timeBoxes.size()) + .forEach(i -> saveTimeBox(tableEntity, timeBoxes.get(i), i + 1)); + } + + private void saveTimeBox(CustomizeTableEntity tableEntity, CustomizeTimeBox timeBox, int sequence) { + CustomizeTimeBoxEntity timeBoxEntity = timeBoxRepository.save( + new CustomizeTimeBoxEntity(tableEntity, timeBox, sequence)); + timeBox.getBells() + .forEach(bell -> bellRepository.save(new BellEntity(timeBoxEntity, bell))); + } + + @Transactional(readOnly = true) + public CustomizeTable getByIdAndMember(long tableId, Member member) { + return tableRepository.getByIdAndMember(tableId, member) + .toDomain(); + } + + @Transactional(readOnly = true) + public List getCustomizeTimeBoxes(long tableId, Member member) { + CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member); + List timeBoxEntityList = timeBoxRepository.findAllByCustomizeTable(tableEntity); + List bellEntityList = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntityList); + return toCustomizeTimeBoxes(timeBoxEntityList, bellEntityList); + } + + private List toCustomizeTimeBoxes( + List timeBoxEntities, + List bellEntities + ) { + Map> timeBoxIdToBellEntities = bellEntities.stream() + .collect(Collectors.groupingBy(bellEntity -> bellEntity.getCustomizeTimeBox().getId())); + return timeBoxEntities.stream() + .sorted(Comparator.comparing(CustomizeTimeBoxEntity::getSequence)) + .map(timeBoxEntity -> toTimeBox( + timeBoxEntity, + timeBoxIdToBellEntities.getOrDefault(timeBoxEntity.getId(), Collections.emptyList())) + ).toList(); + } + + private CustomizeTimeBox toTimeBox(CustomizeTimeBoxEntity timeBoxEntity, List bellEntities) { + return bellEntities.stream() + .map(BellEntity::toDomain) + .collect(Collectors.collectingAndThen(Collectors.toList(), timeBoxEntity::toDomain)); + } + + @Transactional + public CustomizeTable update(CustomizeTable table, long tableId, Member member, List timeBoxes) { + CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member); + tableEntity.updateTable(table); + + bellRepository.deleteAllByTable(tableEntity.getId()); + timeBoxRepository.deleteAllByTable(tableEntity.getId()); + + saveTimeBoxes(tableEntity, timeBoxes); + return tableEntity.toDomain(); + } + + @Transactional + public CustomizeTable updateUsedAt(long tableId, Member member) { + CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member); + tableEntity.updateUsedAt(); + return tableEntity.toDomain(); + } + + @Transactional + public void delete(long tableId, Member member) { + CustomizeTableEntity table = tableRepository.getByIdAndMember(tableId, member); + + bellRepository.deleteAllByTable(table.getId()); + timeBoxRepository.deleteAllByTable(table.getId()); + tableRepository.delete(table); + } +} diff --git a/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java new file mode 100644 index 00000000..e35ceca4 --- /dev/null +++ b/src/main/java/com/debatetimer/domainrepository/poll/PollDomainRepository.java @@ -0,0 +1,41 @@ +package com.debatetimer.domainrepository.poll; + +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.repository.poll.PollRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +public class PollDomainRepository { + + private final PollRepository pollRepository; + + @Transactional + public Poll create(Poll poll) { + PollEntity pollEntity = new PollEntity(poll); + return pollRepository.save(pollEntity) + .toDomain(); + } + + @Transactional(readOnly = true) + public Poll getByIdAndMemberId(long id, long memberId) { + return pollRepository.getByIdAndMemberId(id, memberId) + .toDomain(); + } + + @Transactional(readOnly = true) + public Poll getById(long id) { + return pollRepository.getById(id) + .toDomain(); + } + + @Transactional + public Poll finishPoll(long pollId, long memberId) { + PollEntity pollEntity = pollRepository.getByIdAndMemberId(pollId, memberId); + pollEntity.updateToDone(); + return pollEntity.toDomain(); + } +} diff --git a/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java new file mode 100644 index 00000000..dc79f01f --- /dev/null +++ b/src/main/java/com/debatetimer/domainrepository/poll/VoteDomainRepository.java @@ -0,0 +1,59 @@ +package com.debatetimer.domainrepository.poll; + +import com.debatetimer.domain.poll.ParticipateCode; +import com.debatetimer.domain.poll.Vote; +import com.debatetimer.domain.poll.VoteInfo; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.entity.poll.VoteEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.decoder.RepositoryErrorDecoder; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.poll.PollRepository; +import com.debatetimer.repository.poll.VoteRepository; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class VoteDomainRepository { + + private final PollRepository pollRepository; + private final VoteRepository voteRepository; + private final RepositoryErrorDecoder errorDecoder; + + public VoteInfo findVoteInfoByPollId(long pollId) { + List pollVotes = voteRepository.findAllByPollId(pollId); + return countVotes(pollId, pollVotes); + } + + private VoteInfo countVotes(long pollId, List voteEntities) { + Map teamCount = voteEntities.stream() + .collect(Collectors.groupingBy(VoteEntity::getTeam, Collectors.counting())); + long prosCount = teamCount.getOrDefault(VoteTeam.PROS, 0L); + long consCount = teamCount.getOrDefault(VoteTeam.CONS, 0L); + return new VoteInfo(pollId, prosCount, consCount); + } + + public boolean isExists(long pollId, ParticipateCode code) { + return voteRepository.existsByPollIdAndParticipateCode(pollId, code.getValue()); + } + + public Vote save(Vote vote) { + try { + PollEntity pollEntity = pollRepository.getById(vote.getPollId()); + VoteEntity voteEntity = new VoteEntity(vote, pollEntity); + return voteRepository.save(voteEntity) + .toDomain(); + } catch (DataIntegrityViolationException exception) { + if (errorDecoder.isUniqueConstraintViolation(exception)) { + throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT); + } + throw exception; + } + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/request/BellRequest.java b/src/main/java/com/debatetimer/dto/customize/request/BellRequest.java new file mode 100644 index 00000000..5142fe8c --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/request/BellRequest.java @@ -0,0 +1,17 @@ +package com.debatetimer.dto.customize.request; + +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.BellType; +import jakarta.validation.constraints.NotNull; + +public record BellRequest( + @NotNull + BellType type, + int time, + int count +) { + + public Bell toDomain() { + return new Bell(type, time, count); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java index e0eb509e..b6ab52e1 100644 --- a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java @@ -1,25 +1,23 @@ package com.debatetimer.dto.customize.request; import com.debatetimer.domain.customize.CustomizeTable; -import com.debatetimer.domain.customize.CustomizeTimeBoxes; +import com.debatetimer.domain.customize.CustomizeTimeBox; import com.debatetimer.domain.member.Member; import jakarta.validation.Valid; import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; public record CustomizeTableCreateRequest( @Valid CustomizeTableInfoCreateRequest info, @Valid List table ) { - public CustomizeTable toTable(Member member) { - return info.toTable(member); + public List toTimeBoxes() { + return table.stream() + .map(CustomizeTimeBoxCreateRequest::toDomain) + .toList(); } - public CustomizeTimeBoxes toTimeBoxes(CustomizeTable customizeTable) { - return IntStream.range(0, table.size()) - .mapToObj(i -> table.get(i).toTimeBox(customizeTable, i + 1)) - .collect(Collectors.collectingAndThen(Collectors.toList(), CustomizeTimeBoxes::new)); + public CustomizeTable toTable(Member member) { + return info.toTable(member); } } diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java index c83a7d5d..d6e6ecd1 100644 --- a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java @@ -4,6 +4,7 @@ import com.debatetimer.domain.member.Member; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; public record CustomizeTableInfoCreateRequest( @NotBlank @@ -23,6 +24,7 @@ public record CustomizeTableInfoCreateRequest( ) { public CustomizeTable toTable(Member member) { - return new CustomizeTable(member, name, agenda, warningBell, finishBell, prosTeamName, consTeamName); + return new CustomizeTable(member, name, agenda, prosTeamName, consTeamName, warningBell, finishBell, + LocalDateTime.now()); } } diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java index d5c6d320..0507ad3b 100644 --- a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java @@ -1,11 +1,18 @@ package com.debatetimer.dto.customize.request; -import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.Bell; import com.debatetimer.domain.customize.CustomizeBoxType; import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.customize.NormalTimeBox; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.domain.customize.TimeBasedTimeBox; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; import org.springframework.lang.Nullable; public record CustomizeTimeBoxCreateRequest( @@ -21,6 +28,9 @@ public record CustomizeTimeBoxCreateRequest( @Nullable Integer time, + @Nullable + List bell, + @Nullable Integer timePerTeam, @@ -31,11 +41,28 @@ public record CustomizeTimeBoxCreateRequest( String speaker ) { - public CustomizeTimeBox toTimeBox(CustomizeTable customizeTable, int sequence) { + public CustomizeTimeBoxEntity toTimeBox(CustomizeTable customizeTable, int sequence) { if (boxType.isTimeBased()) { - return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, timePerTeam, - timePerSpeaking, speaker); + return new CustomizeTimeBoxEntity(new CustomizeTableEntity(customizeTable), sequence, stance, speechType, + boxType, timePerTeam, timePerSpeaking, speaker); + } + return new CustomizeTimeBoxEntity(new CustomizeTableEntity(customizeTable), sequence, stance, speechType, boxType, + time, speaker); + } + + public CustomizeTimeBox toDomain() { + if (boxType.isTimeBased()) { + return new TimeBasedTimeBox(stance, speechType, speaker, timePerTeam, timePerSpeaking); + } + return new NormalTimeBox(stance, speechType, speaker, time, toBells(bell)); + } + + private List toBells(List bellRequests) { + if (bellRequests == null) { + return Collections.emptyList(); } - return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, time, speaker); + return bellRequests.stream() + .map(BellRequest::toDomain) + .toList(); } } diff --git a/src/main/java/com/debatetimer/dto/customize/response/BellResponse.java b/src/main/java/com/debatetimer/dto/customize/response/BellResponse.java new file mode 100644 index 00000000..080782d5 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/response/BellResponse.java @@ -0,0 +1,15 @@ +package com.debatetimer.dto.customize.response; + +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.BellType; + +public record BellResponse( + BellType type, + int time, + int count +) { + + public BellResponse(Bell bell) { + this(bell.getType(), bell.getTime(), bell.getCount()); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java index d074287d..366939d6 100644 --- a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java @@ -2,24 +2,18 @@ import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.customize.CustomizeTimeBox; -import com.debatetimer.domain.customize.CustomizeTimeBoxes; import java.util.List; public record CustomizeTableResponse(long id, CustomizeTableInfoResponse info, List table) { - public CustomizeTableResponse( - CustomizeTable customizeTable, - CustomizeTimeBoxes customizeTimeBoxes - ) { - this( - customizeTable.getId(), + public CustomizeTableResponse(CustomizeTable customizeTable, + List customizeTimeBoxes) { + this(customizeTable.getId(), new CustomizeTableInfoResponse(customizeTable), - toTimeBoxResponses(customizeTimeBoxes) - ); + toTimeBoxResponses(customizeTimeBoxes)); } - private static List toTimeBoxResponses(CustomizeTimeBoxes timeBoxes) { - List customizeTimeBoxes = timeBoxes.getTimeBoxes(); + private static List toTimeBoxResponses(List customizeTimeBoxes) { return customizeTimeBoxes .stream() .map(CustomizeTimeBoxResponse::new) diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java index 113bd23e..5bb6bb7a 100644 --- a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java @@ -1,35 +1,62 @@ package com.debatetimer.dto.customize.response; -import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.Bell; import com.debatetimer.domain.customize.CustomizeBoxType; import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; +import java.util.List; public record CustomizeTimeBoxResponse( Stance stance, String speechType, CustomizeBoxType boxType, Integer time, + List bell, Integer timePerTeam, Integer timePerSpeaking, String speaker ) { - public CustomizeTimeBoxResponse(CustomizeTimeBox customizeTimeBox) { + public CustomizeTimeBoxResponse(CustomizeTimeBoxEntity customizeTimeBox) { this( customizeTimeBox.getStance(), customizeTimeBox.getSpeechType(), customizeTimeBox.getBoxType(), convertTime(customizeTimeBox), + null, customizeTimeBox.getTimePerTeam(), customizeTimeBox.getTimePerSpeaking(), customizeTimeBox.getSpeaker() ); } - private static Integer convertTime(CustomizeTimeBox customizeTimeBox) { + private static Integer convertTime(CustomizeTimeBoxEntity customizeTimeBox) { if (customizeTimeBox.getBoxType() == CustomizeBoxType.TIME_BASED) { return null; } return customizeTimeBox.getTime(); } + + public CustomizeTimeBoxResponse(CustomizeTimeBox customizeTimeBox) { + this( + customizeTimeBox.getStance(), + customizeTimeBox.getSpeechType(), + customizeTimeBox.getBoxType(), + customizeTimeBox.getTime(), + toResponses(customizeTimeBox.getBells()), + customizeTimeBox.getTimePerTeam(), + customizeTimeBox.getTimePerSpeaking(), + customizeTimeBox.getSpeaker() + ); + } + + private static List toResponses(List bells) { + if (bells.isEmpty()) { + return null; + } + return bells.stream() + .map(BellResponse::new) + .toList(); + } } diff --git a/src/main/java/com/debatetimer/dto/member/TableResponse.java b/src/main/java/com/debatetimer/dto/member/TableResponse.java index fe066734..e4c31f7a 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponse.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponse.java @@ -4,12 +4,12 @@ public record TableResponse(long id, String name, TableType type, String agenda) { - public TableResponse(CustomizeTable debateTable) { + public TableResponse(CustomizeTable customizeTable) { this( - debateTable.getId(), - debateTable.getName(), - debateTable.getType(), - debateTable.getAgenda() + customizeTable.getId(), + customizeTable.getName(), + customizeTable.getType(), + customizeTable.getAgenda() ); } } diff --git a/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java b/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java new file mode 100644 index 00000000..ccf22242 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/request/VoteRequest.java @@ -0,0 +1,13 @@ +package com.debatetimer.dto.poll.request; + +import com.debatetimer.domain.poll.VoteTeam; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record VoteRequest( + @NotBlank String name, + @NotBlank String participateCode, + @NotNull VoteTeam team +) { + +} diff --git a/src/main/java/com/debatetimer/dto/poll/response/PollCreateResponse.java b/src/main/java/com/debatetimer/dto/poll/response/PollCreateResponse.java new file mode 100644 index 00000000..462ec3d5 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/response/PollCreateResponse.java @@ -0,0 +1,16 @@ +package com.debatetimer.dto.poll.response; + +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.PollStatus; + +public record PollCreateResponse( + long id, + PollStatus status, + String prosTeamName, + String consTeamName +) { + + public PollCreateResponse(Poll poll) { + this(poll.getId(), poll.getStatus(), poll.getProsTeamName().getValue(), poll.getConsTeamName().getValue()); + } +} diff --git a/src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java b/src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java new file mode 100644 index 00000000..69f3f6ae --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/response/PollInfoResponse.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.poll.response; + +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteInfo; + +public record PollInfoResponse( + long id, + PollStatus status, + String prosTeamName, + String consTeamName, + long totalCount, + long prosCount, + long consCount +) { + + public PollInfoResponse(Poll poll, VoteInfo voteInfo) { + this( + poll.getId(), + poll.getStatus(), + poll.getProsTeamName().getValue(), + poll.getConsTeamName().getValue(), + voteInfo.getTotalCount(), + voteInfo.getProsCount(), + voteInfo.getConsCount() + ); + } +} diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java new file mode 100644 index 00000000..07bf2f2a --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/response/VoteCreateResponse.java @@ -0,0 +1,16 @@ +package com.debatetimer.dto.poll.response; + +import com.debatetimer.domain.poll.Vote; +import com.debatetimer.domain.poll.VoteTeam; + +public record VoteCreateResponse( + long id, + String name, + String participateCode, + VoteTeam team +) { + + public VoteCreateResponse(Vote vote) { + this(vote.getId(), vote.getName().getValue(), vote.getCode().getValue(), vote.getTeam()); + } +} diff --git a/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java b/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java new file mode 100644 index 00000000..acf86030 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/poll/response/VoterPollInfoResponse.java @@ -0,0 +1,31 @@ +package com.debatetimer.dto.poll.response; + +import com.debatetimer.domain.poll.ParticipateCode; +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteInfo; + +public record VoterPollInfoResponse( + long id, + PollStatus status, + String prosTeamName, + String consTeamName, + String participateCode, + long totalCount, + long prosCount, + long consCount +) { + + public VoterPollInfoResponse(Poll poll, VoteInfo voteInfo, ParticipateCode code) { + this( + poll.getId(), + poll.getStatus(), + poll.getProsTeamName().getValue(), + poll.getConsTeamName().getValue(), + code.getValue(), + voteInfo.getTotalCount(), + voteInfo.getProsCount(), + voteInfo.getConsCount() + ); + } +} diff --git a/src/main/java/com/debatetimer/domain/BaseTimeEntity.java b/src/main/java/com/debatetimer/entity/BaseTimeEntity.java similarity index 95% rename from src/main/java/com/debatetimer/domain/BaseTimeEntity.java rename to src/main/java/com/debatetimer/entity/BaseTimeEntity.java index ea8677cc..e9405201 100644 --- a/src/main/java/com/debatetimer/domain/BaseTimeEntity.java +++ b/src/main/java/com/debatetimer/entity/BaseTimeEntity.java @@ -1,4 +1,4 @@ -package com.debatetimer.domain; +package com.debatetimer.entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; diff --git a/src/main/java/com/debatetimer/entity/customize/BellEntity.java b/src/main/java/com/debatetimer/entity/customize/BellEntity.java new file mode 100644 index 00000000..81c33816 --- /dev/null +++ b/src/main/java/com/debatetimer/entity/customize/BellEntity.java @@ -0,0 +1,85 @@ +package com.debatetimer.entity.customize; + +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.BellType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "bell") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BellEntity { + + public static final int MAX_BELL_COUNT = 3; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customize_time_box_id") + private CustomizeTimeBoxEntity customizeTimeBox; + + @Column(name = "bell_type") + @NotNull + @Enumerated(value = EnumType.STRING) + private BellType type; + + @Column(name = "bell_time") + private int time; + private int count; + + public BellEntity(CustomizeTimeBoxEntity customizeTimeBox, BellType type, int time, int count) { + validateTime(time); + validateCount(count); + + this.customizeTimeBox = customizeTimeBox; + this.type = type; + this.time = time; + this.count = count; + } + + public BellEntity(CustomizeTimeBoxEntity customizeTimeBox, Bell bell) { + this.customizeTimeBox = customizeTimeBox; + this.type = bell.getType(); + this.time = bell.getTime(); + this.count = bell.getCount(); + } + + private void validateTime(int time) { + if (time < 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_BELL_TIME); + } + } + + private void validateCount(int count) { + if (count <= 0 || count > MAX_BELL_COUNT) { + throw new DTClientErrorException(ClientErrorCode.INVALID_BELL_COUNT); + } + } + + public Bell toDomain() { + return new Bell(type, time, count); + } + + public boolean isContained(CustomizeTimeBoxEntity timeBox) { + return this.customizeTimeBox.getId().equals(timeBox.getId()); + } +} diff --git a/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java b/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java new file mode 100644 index 00000000..ff24334e --- /dev/null +++ b/src/main/java/com/debatetimer/entity/customize/CustomizeTableEntity.java @@ -0,0 +1,97 @@ +package com.debatetimer.entity.customize; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.TableType; +import com.debatetimer.entity.BaseTimeEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "customize_table") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CustomizeTableEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @NotBlank + private String name; + + private String agenda; + + @NotBlank + private String prosTeamName; + + @NotBlank + private String consTeamName; + + private boolean warningBell; + private boolean finishBell; + + @NotNull + private LocalDateTime usedAt; + + public CustomizeTableEntity(CustomizeTable customizeTable) { + this.id = customizeTable.getId(); + this.member = customizeTable.getMember(); + this.name = customizeTable.getName(); + this.agenda = customizeTable.getAgenda(); + this.prosTeamName = customizeTable.getProsTeamName(); + this.consTeamName = customizeTable.getConsTeamName(); + this.warningBell = customizeTable.isWarningBell(); + this.finishBell = customizeTable.isFinishBell(); + this.usedAt = LocalDateTime.now(); + } + + public CustomizeTable toDomain() { + return new CustomizeTable( + id, + member, + name, + agenda, + prosTeamName, + consTeamName, + warningBell, + finishBell, + usedAt + ); + } + + public void updateTable(CustomizeTable renewTable) { + this.name = renewTable.getName(); + this.agenda = renewTable.getAgenda(); + this.prosTeamName = renewTable.getProsTeamName(); + this.consTeamName = renewTable.getConsTeamName(); + this.warningBell = renewTable.isWarningBell(); + this.finishBell = renewTable.isFinishBell(); + this.usedAt = LocalDateTime.now(); + } + + public void updateUsedAt() { + this.usedAt = LocalDateTime.now(); + } + + public TableType getType() { + return TableType.CUSTOMIZE; + } +} diff --git a/src/main/java/com/debatetimer/entity/customize/CustomizeTimeBoxEntity.java b/src/main/java/com/debatetimer/entity/customize/CustomizeTimeBoxEntity.java new file mode 100644 index 00000000..09dc6f4c --- /dev/null +++ b/src/main/java/com/debatetimer/entity/customize/CustomizeTimeBoxEntity.java @@ -0,0 +1,205 @@ +package com.debatetimer.entity.customize; + +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.customize.NormalTimeBox; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.domain.customize.TimeBasedTimeBox; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "customize_time_box") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CustomizeTimeBoxEntity { + + public static final int SPEECH_TYPE_MAX_LENGTH = 10; + public static final int TIME_MULTIPLIER = 2; + public static final int SPEAKER_MAX_LENGTH = 5; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "table_id") + private CustomizeTableEntity customizeTable; + + private int sequence; + + @NotNull + @Enumerated(EnumType.STRING) + private Stance stance; + + private Integer time; + private String speaker; + + @NotBlank + private String speechType; + + @NotNull + @Enumerated(value = EnumType.STRING) + private CustomizeBoxType boxType; + + private Integer timePerTeam; + private Integer timePerSpeaking; + + public CustomizeTimeBoxEntity( + CustomizeTableEntity customizeTableEntity, + int sequence, + Stance stance, + String speechType, + CustomizeBoxType boxType, + Integer time, + String speaker + ) { + validateNotTimeBasedType(boxType); + validateSpeechType(speechType); + validateSpeaker(speaker); + validateSequence(sequence); + validateTime(time); + + this.customizeTable = customizeTableEntity; + this.sequence = sequence; + this.stance = stance; + this.time = time; + this.speaker = initializeSpeaker(speaker); + this.speechType = speechType; + this.boxType = boxType; + } + + public CustomizeTimeBoxEntity( + CustomizeTableEntity customizeTableEntity, + int sequence, + Stance stance, + String speechType, + CustomizeBoxType boxType, + Integer timePerTeam, + Integer timePerSpeaking, + String speaker + ) { + validateTimeBasedTimes(timePerTeam, timePerSpeaking); + validateTimeBasedType(boxType); + validateSpeechType(speechType); + validateSpeaker(speaker); + validateSequence(sequence); + validateTime(convertToTime(timePerTeam)); + + this.sequence = sequence; + this.stance = stance; + this.time = convertToTime(timePerTeam); + this.speaker = initializeSpeaker(speaker); + this.customizeTable = customizeTableEntity; + this.speechType = speechType; + this.boxType = boxType; + this.timePerTeam = timePerTeam; + this.timePerSpeaking = timePerSpeaking; + } + + public CustomizeTimeBoxEntity(CustomizeTableEntity customizeTable, CustomizeTimeBox timeBox, int sequence) { + this.customizeTable = customizeTable; + this.sequence = sequence; + this.stance = timeBox.getStance(); + this.time = timeBox.getTime() == null ? timeBox.getTimePerTeam() * 2 : timeBox.getTime(); // TODO : Nullable 의논 + this.speaker = timeBox.getSpeaker(); + this.speechType = timeBox.getSpeechType(); + this.boxType = timeBox.getBoxType(); + this.timePerTeam = timeBox.getTimePerTeam(); + this.timePerSpeaking = timeBox.getTimePerSpeaking(); + } + + private static int convertToTime(Integer timePerTeam) { + if (timePerTeam == null) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + return timePerTeam * TIME_MULTIPLIER; + } + + + private String initializeSpeaker(String speaker) { + if (speaker == null || speaker.isBlank()) { + return null; + } + return speaker; + } + + private void validateSpeaker(String speaker) { + if (speaker != null && speaker.length() > SPEAKER_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH); + } + } + + private void validateSequence(int sequence) { + if (sequence <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE); + } + } + + private void validateTime(int time) { + if (time <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + private void validateTime(Integer time) { + if (time == null || time <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + private void validateTimeBasedTimes(Integer timePerTeam, Integer timePerSpeaking) { + validateTime(timePerTeam); + if (timePerSpeaking == null) { + return; + } + + validateTime(timePerSpeaking); + if (timePerTeam < timePerSpeaking) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME); + } + } + + private void validateTimeBasedType(CustomizeBoxType boxType) { + if (boxType.isNotTimeBased()) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + } + + private void validateNotTimeBasedType(CustomizeBoxType boxType) { + if (boxType.isTimeBased()) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + } + + private void validateSpeechType(String speechType) { + if (speechType.length() > SPEECH_TYPE_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH); + } + } + + public CustomizeTimeBox toDomain(List bells) { + if (boxType.isTimeBased()) { + return new TimeBasedTimeBox(stance, speechType, speaker, timePerTeam, timePerSpeaking); + } + return new NormalTimeBox(stance, speechType, speaker, time, bells); + } +} diff --git a/src/main/java/com/debatetimer/entity/poll/PollEntity.java b/src/main/java/com/debatetimer/entity/poll/PollEntity.java new file mode 100644 index 00000000..04e8ac28 --- /dev/null +++ b/src/main/java/com/debatetimer/entity/poll/PollEntity.java @@ -0,0 +1,65 @@ +package com.debatetimer.entity.poll; + +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.entity.BaseTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "poll") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PollEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "table_id") + private long tableId; + + @Column(name = "member_id") + private long memberId; + + @NotNull + @Enumerated(EnumType.STRING) + private PollStatus status; + + @NotBlank + private String prosTeamName; + + @NotBlank + private String consTeamName; + + private String agenda; + + public PollEntity(Poll poll) { + this.id = poll.getId(); + this.tableId = poll.getTableId(); + this.memberId = poll.getMemberId(); + this.status = poll.getStatus(); + this.prosTeamName = poll.getProsTeamName().getValue(); + this.consTeamName = poll.getConsTeamName().getValue(); + this.agenda = poll.getAgenda().getValue(); + } + + public void updateToDone() { + this.status = PollStatus.DONE; + } + + public Poll toDomain() { + return new Poll(id, tableId, memberId, status, prosTeamName, consTeamName, agenda); + } +} diff --git a/src/main/java/com/debatetimer/entity/poll/VoteEntity.java b/src/main/java/com/debatetimer/entity/poll/VoteEntity.java new file mode 100644 index 00000000..0b0f26c7 --- /dev/null +++ b/src/main/java/com/debatetimer/entity/poll/VoteEntity.java @@ -0,0 +1,60 @@ +package com.debatetimer.entity.poll; + +import com.debatetimer.domain.poll.Vote; +import com.debatetimer.domain.poll.VoteTeam; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "vote", uniqueConstraints = { + @UniqueConstraint(columnNames = {"poll_id", "participate_code"}) +}) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class VoteEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "poll_id") + private PollEntity poll; + + @NotNull + @Enumerated(EnumType.STRING) + private VoteTeam team; + + @NotBlank + private String name; + + @NotBlank + @Column(name = "participate_code") + private String participateCode; + + public VoteEntity(Vote vote, PollEntity pollEntity) { + this(vote.getId(), pollEntity, vote.getTeam(), vote.getName().getValue(), vote.getCode().getValue()); + } + + public Vote toDomain() { + return new Vote(id, poll.getId(), team, name, participateCode); + } +} diff --git a/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java new file mode 100644 index 00000000..42ea2660 --- /dev/null +++ b/src/main/java/com/debatetimer/exception/decoder/H2ErrorDecoder.java @@ -0,0 +1,22 @@ +package com.debatetimer.exception.decoder; + +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.dao.DataIntegrityViolationException; + +public class H2ErrorDecoder implements RepositoryErrorDecoder { + + protected static final String UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE = "23505"; + + @Override + public boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof ConstraintViolationException cve) { + String sqlState = cve.getSQLException().getSQLState(); + return UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE.equals(sqlState); + } + cause = cause.getCause(); + } + return false; + } +} diff --git a/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java new file mode 100644 index 00000000..f7ddf38c --- /dev/null +++ b/src/main/java/com/debatetimer/exception/decoder/MySqlErrorDecoder.java @@ -0,0 +1,27 @@ +package com.debatetimer.exception.decoder; + +import java.sql.SQLException; +import org.hibernate.exception.ConstraintViolationException; +import org.springframework.dao.DataIntegrityViolationException; + +public class MySqlErrorDecoder implements RepositoryErrorDecoder { + + protected static final String MYSQL_UNIQUE_VIOLATION = "23000"; + protected static final int MYSQL_DUP_ERROR_CODE = 1062; + + @Override + public boolean isUniqueConstraintViolation(DataIntegrityViolationException e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof ConstraintViolationException cve) { + SQLException sqlEx = cve.getSQLException(); + String sqlState = sqlEx.getSQLState(); + int errorCode = sqlEx.getErrorCode(); + return MYSQL_UNIQUE_VIOLATION.equals(sqlState) + && MYSQL_DUP_ERROR_CODE == errorCode; + } + cause = cause.getCause(); + } + return false; + } +} diff --git a/src/main/java/com/debatetimer/exception/decoder/RepositoryErrorDecoder.java b/src/main/java/com/debatetimer/exception/decoder/RepositoryErrorDecoder.java new file mode 100644 index 00000000..8d71e834 --- /dev/null +++ b/src/main/java/com/debatetimer/exception/decoder/RepositoryErrorDecoder.java @@ -0,0 +1,8 @@ +package com.debatetimer.exception.decoder; + +import org.springframework.dao.DataIntegrityViolationException; + +public interface RepositoryErrorDecoder { + + boolean isUniqueConstraintViolation(DataIntegrityViolationException exception); +} diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index cd341416..695cbb0c 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -1,7 +1,10 @@ package com.debatetimer.exception.errorcode; -import com.debatetimer.domain.customize.CustomizeTable; -import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.customize.Agenda; +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.TableName; +import com.debatetimer.domain.customize.TeamName; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -10,7 +13,7 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_TABLE_NAME_LENGTH( HttpStatus.BAD_REQUEST, - "테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(CustomizeTable.TABLE_NAME_MAX_LENGTH) + "테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(TableName.NAME_MAX_LENGTH) ), INVALID_TABLE_NAME_FORM( HttpStatus.BAD_REQUEST, @@ -26,23 +29,33 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_TIME_BASED_TIME(HttpStatus.BAD_REQUEST, "팀 발언 시간은 개인 발언 시간보다 길어야합니다"), INVALID_TIME_BOX_SPEECH_TYPE_LENGTH( HttpStatus.BAD_REQUEST, - "발언 유형 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH) + "발언 유형 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTimeBoxEntity.SPEECH_TYPE_MAX_LENGTH) ), INVALID_TIME_BOX_SPEAKER_LENGTH( HttpStatus.BAD_REQUEST, - "발언자 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTimeBox.SPEAKER_MAX_LENGTH) + "발언자 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTimeBoxEntity.SPEAKER_MAX_LENGTH) ), INVALID_TEAM_NAME_LENGTH( HttpStatus.BAD_REQUEST, - "팀 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTable.TEAM_NAME_MAX_LENGTH) + "팀 이름은 1자 이상 %d자 이하여야 합니다.".formatted(TeamName.NAME_MAX_LENGTH) ), INVALID_TEAM_NAME_FORM( HttpStatus.BAD_REQUEST, "팀 이름에 이모지를 넣을 수 없습니다" ), + INVALID_AGENDA_LENGTH( + HttpStatus.BAD_REQUEST, + "토론 주제는 1자 이상 %d자 이하여야 합니다.".formatted(Agenda.AGENDA_MAX_LENGTH) + ), + + INVALID_POLL_PARTICIPANT_NAME(HttpStatus.BAD_REQUEST, "잘못된 투표자 이름입니다"), + INVALID_POLL_PARTICIPANT_CODE(HttpStatus.BAD_REQUEST, "잘못된 투표참여 코드입니다"), + ALREADY_DONE_POLL(HttpStatus.BAD_REQUEST, "이미 완료된 투표 입니다"), + ALREADY_VOTED_PARTICIPANT(HttpStatus.BAD_REQUEST, "이미 참여한 투표자 입니다"), TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "토론 테이블을 찾을 수 없습니다."), NOT_TABLE_OWNER(HttpStatus.UNAUTHORIZED, "테이블을 소유한 회원이 아닙니다."), + POLL_NOT_FOUND(HttpStatus.NOT_FOUND, "투표를 찾을 수 없습니다."), UNAUTHORIZED_MEMBER(HttpStatus.UNAUTHORIZED, "접근 권한이 없습니다"), EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "토큰 기한이 만료되었습니다"), @@ -59,6 +72,9 @@ public enum ClientErrorCode implements ResponseErrorCode { ALREADY_DISCONNECTED(HttpStatus.BAD_REQUEST, "이미 클라이언트에서 요청이 종료되었습니다."), NO_COOKIE_FOUND(HttpStatus.BAD_REQUEST, "필수 쿠키 값이 존재하지 않습니다."), FILE_UPLOAD_ERROR(HttpStatus.BAD_REQUEST, "파일 업로드에 실패했습니다."), + + INVALID_BELL_TIME(HttpStatus.BAD_REQUEST, "벨 시간은 0 이상의 정수여야 합니다."), + INVALID_BELL_COUNT(HttpStatus.BAD_REQUEST, "벨 카운트는 1 이상 %d 이하의 정수여야 합니다.".formatted(Bell.MAX_BELL_COUNT)), ; private final HttpStatus status; diff --git a/src/main/java/com/debatetimer/repository/customize/BellRepository.java b/src/main/java/com/debatetimer/repository/customize/BellRepository.java new file mode 100644 index 00000000..8653266b --- /dev/null +++ b/src/main/java/com/debatetimer/repository/customize/BellRepository.java @@ -0,0 +1,19 @@ +package com.debatetimer.repository.customize; + +import com.debatetimer.entity.customize.BellEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; +import java.util.List; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; + +public interface BellRepository extends Repository { + + BellEntity save(BellEntity bell); + + @Query("DELETE FROM BellEntity b WHERE b.customizeTimeBox.customizeTable.id = :tableId") + @Modifying(clearAutomatically = true, flushAutomatically = true) + void deleteAllByTable(long tableId); + + List findAllByCustomizeTimeBoxIn(List timeBoxes); +} diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java index d92536a0..bf00c3c8 100644 --- a/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java @@ -1,27 +1,27 @@ package com.debatetimer.repository.customize; -import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; -import jakarta.persistence.LockModeType; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; import java.util.List; import java.util.Optional; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; -public interface CustomizeTableRepository extends Repository { +public interface CustomizeTableRepository extends Repository { - CustomizeTable save(CustomizeTable customizeTable); + CustomizeTableEntity save(CustomizeTableEntity customizeTableEntity); - Optional findById(long id); + Optional findById(long id); - List findAllByMember(Member member); + List findAllByMember(Member member); - Optional findByIdAndMember(long tableId, Member member); + Optional findByIdAndMember(long tableId, Member member); - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT c FROM CustomizeTable c WHERE c.id = :id AND c.member = :member") - Optional findByIdAndMemberWithLock(long id, Member member); + default CustomizeTableEntity getByIdAndMember(long tableId, Member member) { + return findByIdAndMember(tableId, member) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); + } - void delete(CustomizeTable table); + void delete(CustomizeTableEntity table); } diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java index 8f20829f..1a0e524d 100644 --- a/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java @@ -1,34 +1,19 @@ package com.debatetimer.repository.customize; -import com.debatetimer.domain.customize.CustomizeTable; -import com.debatetimer.domain.customize.CustomizeTimeBox; -import com.debatetimer.domain.customize.CustomizeTimeBoxes; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; import java.util.List; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.Repository; -import org.springframework.transaction.annotation.Transactional; -public interface CustomizeTimeBoxRepository extends Repository { +public interface CustomizeTimeBoxRepository extends Repository { - CustomizeTimeBox save(CustomizeTimeBox timeBox); + CustomizeTimeBoxEntity save(CustomizeTimeBoxEntity timeBox); - @Transactional - default List saveAll(List timeBoxes) { - return timeBoxes.stream() - .map(this::save) - .toList(); - } + List findAllByCustomizeTable(CustomizeTableEntity table); - List findAllByCustomizeTable(CustomizeTable table); - - default CustomizeTimeBoxes findTableTimeBoxes(CustomizeTable table) { - List timeBoxes = findAllByCustomizeTable(table); - return new CustomizeTimeBoxes(timeBoxes); - } - - @Query("DELETE FROM CustomizeTimeBox ctb WHERE ctb IN :timeBoxes") + @Query("DELETE FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId") @Modifying(clearAutomatically = true, flushAutomatically = true) - @Transactional - void deleteAll(List timeBoxes); + void deleteAllByTable(long tableId); } diff --git a/src/main/java/com/debatetimer/repository/poll/PollRepository.java b/src/main/java/com/debatetimer/repository/poll/PollRepository.java new file mode 100644 index 00000000..22c6db56 --- /dev/null +++ b/src/main/java/com/debatetimer/repository/poll/PollRepository.java @@ -0,0 +1,22 @@ +package com.debatetimer.repository.poll; + +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PollRepository extends JpaRepository { + + Optional findByIdAndMemberId(long id, long memberId); + + default PollEntity getById(long id) { + return findById(id) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); + } + + default PollEntity getByIdAndMemberId(long id, long memberId) { + return findByIdAndMemberId(id, memberId) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND)); + } +} diff --git a/src/main/java/com/debatetimer/repository/poll/VoteRepository.java b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java new file mode 100644 index 00000000..5f211efd --- /dev/null +++ b/src/main/java/com/debatetimer/repository/poll/VoteRepository.java @@ -0,0 +1,12 @@ +package com.debatetimer.repository.poll; + +import com.debatetimer.entity.poll.VoteEntity; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VoteRepository extends JpaRepository { + + List findAllByPollId(long pollId); + + boolean existsByPollIdAndParticipateCode(long pollId, String participateCode); +} diff --git a/src/main/java/com/debatetimer/service/customize/CustomizeService.java b/src/main/java/com/debatetimer/service/customize/CustomizeService.java index bc568eeb..1670036f 100644 --- a/src/main/java/com/debatetimer/service/customize/CustomizeService.java +++ b/src/main/java/com/debatetimer/service/customize/CustomizeService.java @@ -2,14 +2,10 @@ import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.customize.CustomizeTimeBox; -import com.debatetimer.domain.customize.CustomizeTimeBoxes; import com.debatetimer.domain.member.Member; +import com.debatetimer.domainrepository.customize.CustomizeTableDomainRepository; import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; import com.debatetimer.dto.customize.response.CustomizeTableResponse; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; -import com.debatetimer.repository.customize.CustomizeTableRepository; -import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -19,23 +15,20 @@ @RequiredArgsConstructor public class CustomizeService { - private final CustomizeTableRepository tableRepository; - private final CustomizeTimeBoxRepository timeBoxRepository; + private final CustomizeTableDomainRepository customizeTableDomainRepository; @Transactional public CustomizeTableResponse save(CustomizeTableCreateRequest tableCreateRequest, Member member) { CustomizeTable table = tableCreateRequest.toTable(member); - CustomizeTable savedTable = tableRepository.save(table); - - CustomizeTimeBoxes savedCustomizeTimeBoxes = saveTimeBoxes(tableCreateRequest, savedTable); - return new CustomizeTableResponse(savedTable, savedCustomizeTimeBoxes); + List timeBoxes = tableCreateRequest.toTimeBoxes(); + CustomizeTable savedTable = customizeTableDomainRepository.save(table, timeBoxes); + return new CustomizeTableResponse(savedTable, timeBoxes); } @Transactional(readOnly = true) public CustomizeTableResponse findTable(long tableId, Member member) { - CustomizeTable table = tableRepository.findByIdAndMember(tableId, member) - .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); - CustomizeTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + CustomizeTable table = customizeTableDomainRepository.getByIdAndMember(tableId, member); + List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes(tableId, member); return new CustomizeTableResponse(table, timeBoxes); } @@ -45,42 +38,22 @@ public CustomizeTableResponse updateTable( long tableId, Member member ) { - CustomizeTable existingTable = tableRepository.findByIdAndMemberWithLock(tableId, member) - .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); - CustomizeTable renewedTable = tableCreateRequest.toTable(member); - existingTable.updateTable(renewedTable); + CustomizeTable table = tableCreateRequest.toTable(member); + List timeBoxes = tableCreateRequest.toTimeBoxes(); - CustomizeTimeBoxes customizeTimeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); - timeBoxRepository.deleteAll(customizeTimeBoxes.getTimeBoxes()); - CustomizeTimeBoxes savedCustomizeTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); - return new CustomizeTableResponse(existingTable, savedCustomizeTimeBoxes); + CustomizeTable updatedTable = customizeTableDomainRepository.update(table, tableId, member, timeBoxes); + return new CustomizeTableResponse(updatedTable, timeBoxes); } @Transactional public CustomizeTableResponse updateUsedAt(long tableId, Member member) { - CustomizeTable table = tableRepository.findByIdAndMember(tableId, member) - .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); - CustomizeTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); - table.updateUsedAt(); - + CustomizeTable table = customizeTableDomainRepository.updateUsedAt(tableId, member); + List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes(tableId, member); return new CustomizeTableResponse(table, timeBoxes); } @Transactional public void deleteTable(long tableId, Member member) { - CustomizeTable table = tableRepository.findByIdAndMember(tableId, member) - .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); - CustomizeTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); - timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); - tableRepository.delete(table); - } - - private CustomizeTimeBoxes saveTimeBoxes( - CustomizeTableCreateRequest tableCreateRequest, - CustomizeTable table - ) { - CustomizeTimeBoxes customizeTimeBoxes = tableCreateRequest.toTimeBoxes(table); - List savedTimeBoxes = timeBoxRepository.saveAll(customizeTimeBoxes.getTimeBoxes()); - return new CustomizeTimeBoxes(savedTimeBoxes); + customizeTableDomainRepository.delete(tableId, member); } } diff --git a/src/main/java/com/debatetimer/service/member/MemberService.java b/src/main/java/com/debatetimer/service/member/MemberService.java index f5634e8b..88aba887 100644 --- a/src/main/java/com/debatetimer/service/member/MemberService.java +++ b/src/main/java/com/debatetimer/service/member/MemberService.java @@ -1,13 +1,13 @@ package com.debatetimer.service.member; -import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.MemberCreateResponse; import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; +import com.debatetimer.entity.customize.CustomizeTableEntity; import com.debatetimer.repository.customize.CustomizeTableRepository; import com.debatetimer.repository.member.MemberRepository; -import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,8 +22,10 @@ public class MemberService { @Transactional(readOnly = true) public TableResponses getTables(long memberId) { Member member = memberRepository.getById(memberId); - List memberTables = customizeTableRepository.findAllByMember(member); - return TableResponses.from(memberTables); + return customizeTableRepository.findAllByMember(member) + .stream() + .map(CustomizeTableEntity::toDomain) + .collect(Collectors.collectingAndThen(Collectors.toList(), TableResponses::from)); } @Transactional diff --git a/src/main/java/com/debatetimer/service/poll/PollService.java b/src/main/java/com/debatetimer/service/poll/PollService.java new file mode 100644 index 00000000..cfa021b9 --- /dev/null +++ b/src/main/java/com/debatetimer/service/poll/PollService.java @@ -0,0 +1,46 @@ +package com.debatetimer.service.poll; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.VoteInfo; +import com.debatetimer.domainrepository.customize.CustomizeTableDomainRepository; +import com.debatetimer.domainrepository.poll.PollDomainRepository; +import com.debatetimer.domainrepository.poll.VoteDomainRepository; +import com.debatetimer.dto.poll.response.PollCreateResponse; +import com.debatetimer.dto.poll.response.PollInfoResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PollService { + + private final CustomizeTableDomainRepository customizeTableDomainRepository; + private final PollDomainRepository pollDomainRepository; + private final VoteDomainRepository voteDomainRepository; + + @Transactional + public PollCreateResponse create(long tableId, Member member) { + CustomizeTable table = customizeTableDomainRepository.getByIdAndMember(tableId, member); + Poll poll = new Poll(table.getId(), member.getId(), table.getProsTeamName(), + table.getConsTeamName(), table.getAgenda()); + Poll savedPoll = pollDomainRepository.create(poll); + return new PollCreateResponse(savedPoll); + } + + @Transactional(readOnly = true) + public PollInfoResponse getPollInfo(long pollId, Member member) { + Poll poll = pollDomainRepository.getByIdAndMemberId(pollId, member.getId()); + VoteInfo voteInfo = voteDomainRepository.findVoteInfoByPollId(pollId); + return new PollInfoResponse(poll, voteInfo); + } + + @Transactional + public PollInfoResponse finishPoll(long pollId, Member member) { + Poll poll = pollDomainRepository.finishPoll(pollId, member.getId()); + VoteInfo voteInfo = voteDomainRepository.findVoteInfoByPollId(pollId); + return new PollInfoResponse(poll, voteInfo); + } +} diff --git a/src/main/java/com/debatetimer/service/poll/VoteService.java b/src/main/java/com/debatetimer/service/poll/VoteService.java new file mode 100644 index 00000000..8c485907 --- /dev/null +++ b/src/main/java/com/debatetimer/service/poll/VoteService.java @@ -0,0 +1,56 @@ +package com.debatetimer.service.poll; + +import com.debatetimer.domain.poll.ParticipateCode; +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.Vote; +import com.debatetimer.domain.poll.VoteInfo; +import com.debatetimer.domainrepository.poll.PollDomainRepository; +import com.debatetimer.domainrepository.poll.VoteDomainRepository; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class VoteService { + + private final VoteDomainRepository voteDomainRepository; + private final PollDomainRepository pollDomainRepository; + + @Transactional + public VoteCreateResponse vote(long pollId, VoteRequest voteRequest) { + validateProgressPoll(pollId); + validateAlreadyVoted(pollId, voteRequest.participateCode()); + Vote vote = new Vote(pollId, voteRequest.team(), voteRequest.name(), voteRequest.participateCode()); + Vote savedVote = voteDomainRepository.save(vote); + return new VoteCreateResponse(savedVote); + } + + private void validateProgressPoll(long pollId) { + Poll poll = pollDomainRepository.getById(pollId); + if (!poll.isProgress()) { + throw new DTClientErrorException(ClientErrorCode.ALREADY_DONE_POLL); + } + } + + private void validateAlreadyVoted(long pollId, String participateCode) { + ParticipateCode code = new ParticipateCode(participateCode); + if (voteDomainRepository.isExists(pollId, code)) { + throw new DTClientErrorException(ClientErrorCode.ALREADY_VOTED_PARTICIPANT); + } + } + + @Transactional(readOnly = true) + public VoterPollInfoResponse getVoterPollInfo(long pollId) { + Poll poll = pollDomainRepository.getById(pollId); + VoteInfo voteInfo = voteDomainRepository.findVoteInfoByPollId(pollId); + ParticipateCode code = new ParticipateCode(UUID.randomUUID().toString()); + return new VoterPollInfoResponse(poll, voteInfo, code); + } +} diff --git a/src/main/resources/db/migration/V10__add_bell_table.sql b/src/main/resources/db/migration/V10__add_bell_table.sql new file mode 100644 index 00000000..b6e4c5cf --- /dev/null +++ b/src/main/resources/db/migration/V10__add_bell_table.sql @@ -0,0 +1,13 @@ +create table bell +( + id bigint auto_increment, + bell_time bigint not null, + count bigint not null, + customize_time_box_id bigint not null, + primary key (id) +); + +alter table bell + add constraint bell_to_customize_time_box + foreign key (customize_time_box_id) + references customize_time_box(id); diff --git a/src/main/resources/db/migration/V11__add_memberId_into_poll.sql b/src/main/resources/db/migration/V11__add_memberId_into_poll.sql new file mode 100644 index 00000000..08772417 --- /dev/null +++ b/src/main/resources/db/migration/V11__add_memberId_into_poll.sql @@ -0,0 +1,2 @@ +ALTER TABLE poll + ADD COLUMN member_id BIGINT NOT NULL diff --git a/src/main/resources/db/migration/V12__add_participate_code_unique_constraint.sql b/src/main/resources/db/migration/V12__add_participate_code_unique_constraint.sql new file mode 100644 index 00000000..315fd427 --- /dev/null +++ b/src/main/resources/db/migration/V12__add_participate_code_unique_constraint.sql @@ -0,0 +1,5 @@ +ALTER TABLE vote + CHANGE participant_code participate_code VARCHAR (255) NOT NULL; + +ALTER TABLE vote + ADD CONSTRAINT uq_vote_poll_participate UNIQUE (poll_id, participate_code); diff --git a/src/main/resources/db/migration/V13__add_bell_type_into_bell.sql b/src/main/resources/db/migration/V13__add_bell_type_into_bell.sql new file mode 100644 index 00000000..e2f1a133 --- /dev/null +++ b/src/main/resources/db/migration/V13__add_bell_type_into_bell.sql @@ -0,0 +1,2 @@ +ALTER TABLE bell + ADD COLUMN bell_type enum ('AFTER_START','BEFORE_END','AFTER_END') NOT NULL diff --git a/src/main/resources/db/migration/V9__create_poll_and_vote_table.sql b/src/main/resources/db/migration/V9__create_poll_and_vote_table.sql new file mode 100644 index 00000000..d5bf80ff --- /dev/null +++ b/src/main/resources/db/migration/V9__create_poll_and_vote_table.sql @@ -0,0 +1,25 @@ +CREATE TABLE poll +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + table_id BIGINT NOT NULL, + status ENUM ('PROGRESS','DONE') NOT NULL, + pros_team_name VARCHAR(255) NOT NULL, + cons_team_name VARCHAR(255) NOT NULL, + agenda VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE vote +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + poll_id BIGINT NOT NULL, + team ENUM ('CONS','PROS') NOT NULL, + name VARCHAR(255) NOT NULL, + participant_code VARCHAR(255) NOT NULL +); + +alter table vote + add constraint vote_to_poll + foreign key (poll_id) + references poll (id); diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 0ccdab51..504c5dcd 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -2,16 +2,22 @@ import com.debatetimer.DataBaseCleaner; import com.debatetimer.client.oauth.OAuthClient; -import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.BellType; import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.customize.request.BellRequest; import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTableInfoCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTimeBoxCreateRequest; -import com.debatetimer.fixture.CustomizeTableGenerator; -import com.debatetimer.fixture.CustomizeTimeBoxGenerator; +import com.debatetimer.dto.poll.request.VoteRequest; import com.debatetimer.fixture.HeaderGenerator; -import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.TokenGenerator; +import com.debatetimer.fixture.entity.CustomizeTableEntityGenerator; +import com.debatetimer.fixture.entity.CustomizeTimeBoxEntityGenerator; +import com.debatetimer.fixture.entity.MemberGenerator; +import com.debatetimer.fixture.entity.PollEntityGenerator; +import com.debatetimer.fixture.entity.VoteEntityGenerator; import com.debatetimer.repository.customize.CustomizeTableRepository; import com.navercorp.fixturemonkey.ArbitraryBuilder; import com.navercorp.fixturemonkey.FixtureMonkey; @@ -21,6 +27,7 @@ import io.restassured.filter.log.RequestLoggingFilter; import io.restassured.filter.log.ResponseLoggingFilter; import io.restassured.specification.RequestSpecification; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -39,10 +46,16 @@ public abstract class BaseControllerTest { protected MemberGenerator memberGenerator; @Autowired - protected CustomizeTableGenerator customizeTableGenerator; + protected CustomizeTableEntityGenerator customizeTableEntityGenerator; @Autowired - protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; + protected CustomizeTimeBoxEntityGenerator customizeTimeBoxEntityGenerator; + + @Autowired + protected PollEntityGenerator pollEntityGenerator; + + @Autowired + protected VoteEntityGenerator voteEntityGenerator; @Autowired protected HeaderGenerator headerGenerator; @@ -81,6 +94,13 @@ protected ArbitraryBuilder getCustomizeTableCreateR .set("table", getCustomizeTimeBoxCreateRequestBuilder().sampleList(2)); } + protected ArbitraryBuilder getVoteRequestBuilder() { + return fixtureMonkey.giveMeBuilder(VoteRequest.class) + .set("name", "콜리") + .set("team", VoteTeam.PROS) + .set("participateCode", UUID.randomUUID().toString()); + } + private ArbitraryBuilder getCustomizeTableInfoCreateRequestBuilder() { return fixtureMonkey.giveMeBuilder(CustomizeTableInfoCreateRequest.class) .set("name", "자유 테이블") @@ -97,8 +117,16 @@ private ArbitraryBuilder getCustomizeTimeBoxCreat .set("speechType", "입론1") .set("boxType", CustomizeBoxType.NORMAL) .set("time", 120) + .set("bell", getBellRequestBuilder().sampleList(2)) .set("timePerTeam", 60) .set("timePerSpeaking", null) .set("speaker", "발언자"); } + + private ArbitraryBuilder getBellRequestBuilder() { + return fixtureMonkey.giveMeBuilder(BellRequest.class) + .set("type", BellType.AFTER_START) + .set("time", 30) + .set("count", 1); + } } diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index 206c8cda..0a74a3cc 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -12,6 +12,8 @@ import com.debatetimer.service.auth.AuthService; import com.debatetimer.service.customize.CustomizeService; import com.debatetimer.service.member.MemberService; +import com.debatetimer.service.poll.PollService; +import com.debatetimer.service.poll.VoteService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; @@ -62,6 +64,12 @@ public abstract class BaseDocumentTest { @MockitoBean protected AuthService authService; + @MockitoBean + protected PollService pollService; + + @MockitoBean + protected VoteService voteService; + @MockitoBean protected AuthManager authManager; diff --git a/src/test/java/com/debatetimer/controller/Tag.java b/src/test/java/com/debatetimer/controller/Tag.java index c7dfa897..39a3c588 100644 --- a/src/test/java/com/debatetimer/controller/Tag.java +++ b/src/test/java/com/debatetimer/controller/Tag.java @@ -6,6 +6,7 @@ public enum Tag { PARLIAMENTARY_API("Parliamentary Table API"), TIME_BASED_API("Time Based Table API"), CUSTOMIZE_API("Customize Table API"), + POLL_API("Poll API"), ; private final String displayName; diff --git a/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java b/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java index 06493950..df0c91e2 100644 --- a/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java @@ -4,12 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.debatetimer.controller.BaseControllerTest; -import com.debatetimer.domain.Stance; import com.debatetimer.domain.customize.CustomizeBoxType; -import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.Stance; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; import com.debatetimer.exception.ErrorResponse; import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.fixture.NullAndEmptyAndBlankSource; @@ -154,9 +154,9 @@ class GetTable { @Test void 사용자_지정_테이블을_조회한다() { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); + customizeTimeBoxEntityGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableResponse response = given() @@ -180,7 +180,7 @@ class UpdateTable { @Test void 사용자_지정_토론_테이블을_업데이트한다() { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest renewTableRequest = getCustomizeTableCreateRequestBuilder() .set("table[1].speaker", null) @@ -201,7 +201,7 @@ class UpdateTable { @NullAndEmptyAndBlankSource void 사용자_지정_테이블을_업데이트할때_테이블_이름은_개행문자_외_다른_글자가_포함되야한다(String tableName) { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest request = getCustomizeTableCreateRequestBuilder() .set("info.name", tableName) @@ -218,7 +218,7 @@ class UpdateTable { @ParameterizedTest void 사용자_지정_테이블을_업데이트할때_테이블_주제는_null이_올_수_없다(String agenda) { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest request = getCustomizeTableCreateRequestBuilder() .set("info.agenda", agenda) @@ -235,7 +235,7 @@ class UpdateTable { @NullAndEmptyAndBlankSource void 사용자_지정_테이블을_업데이트할때_찬성팀_이름은_개행문자_외_다른_글자가_포함되야한다(String prosTeamName) { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest request = getCustomizeTableCreateRequestBuilder() .set("info.prosTeamName", prosTeamName) @@ -252,7 +252,7 @@ class UpdateTable { @NullAndEmptyAndBlankSource void 사용자_지정_테이블을_업데이트할때_반대팀_이름은_개행문자_외_다른_글자가_포함되야한다(String consTeamName) { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest request = getCustomizeTableCreateRequestBuilder() @@ -270,7 +270,7 @@ class UpdateTable { @ParameterizedTest void 사용자_지정_테이블을_업데이트할때_타임박스_입장은_null이_올_수_없다(Stance stance) { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest request = getCustomizeTableCreateRequestBuilder() .set("table[0].stance", stance) @@ -287,7 +287,7 @@ class UpdateTable { @NullAndEmptyAndBlankSource void 사용자_지정_테이블을_업데이트할때_타임박스_발언_유형은_개행문자_외_다른_글자가_포함되야한다(String speechType) { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest request = getCustomizeTableCreateRequestBuilder() .set("table[0].speechType", speechType) @@ -304,7 +304,7 @@ class UpdateTable { @ParameterizedTest void 사용자_지정_테이블을_업데이트할때_타임박스_타입은_null이_올_수_없다(CustomizeBoxType boxType) { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableCreateRequest request = getCustomizeTableCreateRequestBuilder() .set("table[0].boxType", boxType) @@ -320,7 +320,7 @@ class UpdateTable { private ValidatableResponse sendCustomizeTableUpdateRequest( CustomizeTableCreateRequest request, HttpStatus statusCode, - CustomizeTable table, + CustomizeTableEntity table, Headers headers ) { return given() @@ -339,9 +339,9 @@ class Debate { @Test void 사용자_지정_토론을_시작한다() { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); + customizeTimeBoxEntityGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableResponse response = given() @@ -366,9 +366,9 @@ class DeleteTable { @Test void 사용자_지정_토론_테이블을_삭제한다() { Member bito = memberGenerator.generate("default@gmail.com"); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); + customizeTimeBoxEntityGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() diff --git a/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java index 180afad7..e2613aa0 100644 --- a/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java @@ -18,11 +18,14 @@ import com.debatetimer.controller.RestDocumentationRequest; import com.debatetimer.controller.RestDocumentationResponse; import com.debatetimer.controller.Tag; -import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.BellType; import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.dto.customize.request.BellRequest; import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTableInfoCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTimeBoxCreateRequest; +import com.debatetimer.dto.customize.response.BellResponse; import com.debatetimer.dto.customize.response.CustomizeTableInfoResponse; import com.debatetimer.dto.customize.response.CustomizeTableResponse; import com.debatetimer.dto.customize.response.CustomizeTimeBoxResponse; @@ -68,6 +71,10 @@ class Save { fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)").optional(), + fieldWithPath("table[].bell").type(ARRAY).description("종소리 정보").optional(), + fieldWithPath("table[].bell[].type").type(STRING).description("종소리 종류"), + fieldWithPath("table[].bell[].time").type(NUMBER).description("종소리 울릴 시간(초)"), + fieldWithPath("table[].bell[].count").type(NUMBER).description("종소리 횟수"), fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() @@ -89,6 +96,10 @@ class Save { fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)").optional(), + fieldWithPath("table[].bell").type(ARRAY).description("종소리 정보").optional(), + fieldWithPath("table[].bell[].type").type(STRING).description("종소리 종류"), + fieldWithPath("table[].bell[].time").type(NUMBER).description("종소리 울릴 시간(초)"), + fieldWithPath("table[].bell[].count").type(NUMBER).description("종소리 횟수"), fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() @@ -101,13 +112,13 @@ class Save { "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); CustomizeTableResponse response = new CustomizeTableResponse( @@ -116,13 +127,13 @@ class Save { "찬성", "반대", true, true), List.of( new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); doReturn(response).when(customizeService).save(eq(request), any()); @@ -146,6 +157,7 @@ class Save { "INVALID_TABLE_NAME_LENGTH", "INVALID_TABLE_NAME_FORM", "INVALID_TABLE_TIME", + "INVALID_AGENDA_LENGTH", "INVALID_TIME_BOX_SEQUENCE", "INVALID_TIME_BOX_TIME", "INVALID_TIME_BOX_STANCE", @@ -159,13 +171,13 @@ class Save { "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); doThrow(new DTClientErrorException(errorCode)).when(customizeService).save(eq(request), any()); @@ -220,6 +232,10 @@ class GetTable { fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)").optional(), + fieldWithPath("table[].bell").type(ARRAY).description("종소리 정보").optional(), + fieldWithPath("table[].bell[].type").type(STRING).description("종소리 종류"), + fieldWithPath("table[].bell[].time").type(NUMBER).description("종소리 울릴 시간(초)"), + fieldWithPath("table[].bell[].count").type(NUMBER).description("종소리 횟수"), fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() @@ -234,13 +250,13 @@ class GetTable { "찬성", "반대", true, true), List.of( new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); doReturn(response).when(customizeService).findTable(eq(tableId), any()); @@ -310,6 +326,10 @@ class UpdateTable { fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)").optional(), + fieldWithPath("table[].bell").type(ARRAY).description("종소리 정보").optional(), + fieldWithPath("table[].bell[].type").type(STRING).description("종소리 종류"), + fieldWithPath("table[].bell[].time").type(NUMBER).description("종소리 울릴 시간(초)"), + fieldWithPath("table[].bell[].count").type(NUMBER).description("종소리 횟수"), fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() @@ -331,6 +351,10 @@ class UpdateTable { fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)").optional(), + fieldWithPath("table[].bell").type(ARRAY).description("종소리 정보").optional(), + fieldWithPath("table[].bell[].type").type(STRING).description("종소리 종류"), + fieldWithPath("table[].bell[].time").type(NUMBER).description("종소리 울릴 시간(초)"), + fieldWithPath("table[].bell[].count").type(NUMBER).description("종소리 횟수"), fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() @@ -344,13 +368,13 @@ class UpdateTable { "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); CustomizeTableResponse response = new CustomizeTableResponse( @@ -359,13 +383,13 @@ class UpdateTable { "찬성", "반대", true, true), List.of( new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); doReturn(response).when(customizeService).updateTable(eq(request), eq(tableId), any()); @@ -391,6 +415,7 @@ class UpdateTable { "INVALID_TABLE_NAME_LENGTH", "INVALID_TABLE_NAME_FORM", "INVALID_TABLE_TIME", + "INVALID_AGENDA_LENGTH", "INVALID_TIME_BOX_SEQUENCE", "INVALID_TIME_BOX_TIME", "INVALID_TIME_BOX_STANCE", @@ -405,13 +430,13 @@ class UpdateTable { "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); doThrow(new DTClientErrorException(errorCode)).when(customizeService) @@ -468,6 +493,10 @@ class Debate { fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)").optional(), + fieldWithPath("table[].bell").type(ARRAY).description("종소리 정보").optional(), + fieldWithPath("table[].bell[].type").type(STRING).description("종소리 종류"), + fieldWithPath("table[].bell[].time").type(NUMBER).description("종소리 울릴 시간(초)"), + fieldWithPath("table[].bell[].count").type(NUMBER).description("종소리 횟수"), fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() @@ -483,13 +512,13 @@ class Debate { "찬성", "반대", true, true), List.of( new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "콜리"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, - null, 360, 120, null), + null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, - null, 360, null, null) + null, null, 360, null, null) ) ); doReturn(response).when(customizeService).updateUsedAt(eq(tableId), any()); diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index b0a59bca..01a972e9 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -10,12 +10,14 @@ import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.OAuthToken; import com.debatetimer.dto.member.TableResponses; +import com.debatetimer.entity.customize.CustomizeTableEntity; import com.debatetimer.exception.ErrorResponse; import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.fixture.NullAndEmptyAndBlankSource; import io.restassured.http.ContentType; import io.restassured.http.Headers; import io.restassured.response.ValidatableResponse; +import java.time.LocalDateTime; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -29,9 +31,9 @@ class GetTables { @Test void 회원의_전체_토론_시간표를_조회한다() { Member member = memberGenerator.generate("default@gmail.com"); - customizeTableRepository.save(new CustomizeTable(member, "커스텀 테이블", "주제", false, false, - "찬성", "반대")); - + CustomizeTable table = new CustomizeTable(member, "커스텀 테이블", "주제", "찬성", "반대", false, false, + LocalDateTime.now()); + customizeTableRepository.save(new CustomizeTableEntity(table)); Headers headers = headerGenerator.generateAccessTokenHeader(member); TableResponses response = given() diff --git a/src/test/java/com/debatetimer/controller/poll/PollControllerTest.java b/src/test/java/com/debatetimer/controller/poll/PollControllerTest.java new file mode 100644 index 00000000..ee3fad9b --- /dev/null +++ b/src/test/java/com/debatetimer/controller/poll/PollControllerTest.java @@ -0,0 +1,93 @@ +package com.debatetimer.controller.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.controller.BaseControllerTest; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.response.PollInfoResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import io.restassured.http.ContentType; +import io.restassured.http.Headers; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class PollControllerTest extends BaseControllerTest { + + @Nested + class CreatePoll { + + @Test + void 선거를_생성할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + Headers headers = headerGenerator.generateAccessTokenHeader(member); + + given() + .contentType(ContentType.JSON) + .headers(headers) + .pathParam("tableId", table.getId()) + .when().post("/api/polls/{tableId}") + .then().statusCode(HttpStatus.CREATED.value()); + } + } + + @Nested + class GetPollInfo { + + @Test + void 선거정보를_읽을_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "비토"); + voteEntityGenerator.generate(pollEntity, VoteTeam.CONS, "커찬"); + Headers headers = headerGenerator.generateAccessTokenHeader(member); + + PollInfoResponse response = given() + .contentType(ContentType.JSON) + .headers(headers) + .pathParam("pollId", pollEntity.getId()) + .when().get("/api/polls/{pollId}") + .then().statusCode(HttpStatus.OK.value()) + .extract().as(PollInfoResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(pollEntity.getId()), + () -> assertThat(response.prosTeamName()).isEqualTo(pollEntity.getProsTeamName()), + () -> assertThat(response.consTeamName()).isEqualTo(pollEntity.getConsTeamName()), + () -> assertThat(response.status()).isEqualTo(pollEntity.getStatus()), + () -> assertThat(response.totalCount()).isEqualTo(3L), + () -> assertThat(response.prosCount()).isEqualTo(2L), + () -> assertThat(response.consCount()).isEqualTo(1L) + ); + } + } + + @Nested + class FinishPoll { + + @Test + void 선거정보를_완료상태로_변경한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + Headers headers = headerGenerator.generateAccessTokenHeader(member); + + PollInfoResponse response = given() + .contentType(ContentType.JSON) + .headers(headers) + .pathParam("pollId", pollEntity.getId()) + .when().patch("/api/polls/{pollId}") + .then().statusCode(HttpStatus.OK.value()) + .extract().as(PollInfoResponse.class); + + assertThat(response.status()).isEqualTo(PollStatus.DONE); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/poll/PollDocumentTest.java b/src/test/java/com/debatetimer/controller/poll/PollDocumentTest.java new file mode 100644 index 00000000..e0cd3b42 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/poll/PollDocumentTest.java @@ -0,0 +1,168 @@ +package com.debatetimer.controller.poll; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import com.debatetimer.controller.BaseDocumentTest; +import com.debatetimer.controller.RestDocumentationRequest; +import com.debatetimer.controller.RestDocumentationResponse; +import com.debatetimer.controller.Tag; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.dto.poll.response.PollCreateResponse; +import com.debatetimer.dto.poll.response.PollInfoResponse; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; + +public class PollDocumentTest extends BaseDocumentTest { + + @Nested + class CreatePoll { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.POLL_API) + .summary("선거 생성") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("선거 ID"), + fieldWithPath("status").type(STRING).description("선거 상태 - 진행중 : PROGRESS, 완료 : DONE"), + fieldWithPath("prosTeamName").type(STRING).description("찬성측 팀 이름"), + fieldWithPath("consTeamName").type(STRING).description("반대측 팀 이름") + ); + + @Test + void 선거_생성_성공() { + PollCreateResponse response = new PollCreateResponse(1l, PollStatus.PROGRESS, "찬성", "반대"); + doReturn(response).when(pollService).create(anyLong(), any(Member.class)); + + var document = document("poll/post", 201) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", 1l) + .when().post("/api/polls/{tableId}") + .then().statusCode(201); + } + } + + @Nested + class GetPollInfo { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.POLL_API) + .summary("선거 정보 조회") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("pollId").description("선거 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("선거 ID"), + fieldWithPath("status").type(STRING).description("선거 상태 - 진행중 : PROGRESS, 완료 : DONE"), + fieldWithPath("prosTeamName").type(STRING).description("찬성측 팀 이름"), + fieldWithPath("consTeamName").type(STRING).description("반대측 팀 이름"), + fieldWithPath("totalCount").type(NUMBER).description("전체 투표 수"), + fieldWithPath("prosCount").type(NUMBER).description("찬성 투표 수"), + fieldWithPath("consCount").type(NUMBER).description("반대 투표 수") + ); + + @Test + void 선거_정보_조회() { + PollInfoResponse response = new PollInfoResponse( + 1L, + PollStatus.PROGRESS, + "찬성", + "반대", + 3L, + 2L, + 1L + ); + doReturn(response).when(pollService).getPollInfo(anyLong(), any(Member.class)); + + var document = document("poll/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("pollId", 1l) + .when().get("/api/polls/{pollId}") + .then().statusCode(200); + } + } + + @Nested + class FinishPoll { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.POLL_API) + .summary("선거 완료") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("pollId").description("선거 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("선거 ID"), + fieldWithPath("status").type(STRING).description("선거 상태 - 진행중 : PROGRESS, 완료 : DONE"), + fieldWithPath("prosTeamName").type(STRING).description("찬성측 팀 이름"), + fieldWithPath("consTeamName").type(STRING).description("반대측 팀 이름"), + fieldWithPath("totalCount").type(NUMBER).description("전체 투표 수"), + fieldWithPath("prosCount").type(NUMBER).description("찬성 투표 수"), + fieldWithPath("consCount").type(NUMBER).description("반대 투표 수") + ); + + @Test + void 선거_완료() { + PollInfoResponse response = new PollInfoResponse( + 1L, + PollStatus.DONE, + "찬성", + "반대", + 3L, + 2L, + 1L + ); + doReturn(response).when(pollService).finishPoll(anyLong(), any(Member.class)); + + var document = document("poll/patch", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("pollId", 1l) + .when().patch("/api/polls/{pollId}") + .then().statusCode(200); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java new file mode 100644 index 00000000..19fe3fdd --- /dev/null +++ b/src/test/java/com/debatetimer/controller/poll/VoteControllerTest.java @@ -0,0 +1,173 @@ +package com.debatetimer.controller.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.controller.BaseControllerTest; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.fixture.NullAndEmptyAndBlankSource; +import io.restassured.http.ContentType; +import java.util.UUID; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.springframework.http.HttpStatus; + +class VoteControllerTest extends BaseControllerTest { + + @Nested + class GetVotersPollInfo { + + @Test + void 투표자가_선거정보를_조회할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "비토"); + voteEntityGenerator.generate(pollEntity, VoteTeam.CONS, "커찬"); + + VoterPollInfoResponse response = given() + .contentType(ContentType.JSON) + .pathParam("pollId", pollEntity.getId()) + .when().get("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.OK.value()) + .extract().as(VoterPollInfoResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(pollEntity.getId()), + () -> assertThat(response.prosTeamName()).isEqualTo(pollEntity.getProsTeamName()), + () -> assertThat(response.consTeamName()).isEqualTo(pollEntity.getConsTeamName()), + () -> assertThat(response.status()).isEqualTo(pollEntity.getStatus()), + () -> assertThat(response.totalCount()).isEqualTo(3L), + () -> assertThat(response.participateCode()).isNotBlank(), + () -> assertThat(response.prosCount()).isEqualTo(2L), + () -> assertThat(response.consCount()).isEqualTo(1L) + ); + } + } + + @Nested + class VotePoll { + + @Test + void 진행_중인_선거에_최초로_투표_할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + VoteRequest voteRequest = getVoteRequestBuilder().sample(); + + VoteCreateResponse response = given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.CREATED.value()) + .extract().as(VoteCreateResponse.class); + + assertAll( + () -> assertThat(response.name()).isEqualTo(voteRequest.name()), + () -> assertThat(response.participateCode()).isEqualTo(voteRequest.participateCode()), + () -> assertThat(response.team()).isEqualTo(voteRequest.team()) + ); + } + + @ParameterizedTest + @NullAndEmptyAndBlankSource + void 투표_시_이름은_널이거나_빈_문자열일_수_없다(String invalidName) { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("name", invalidName) + .sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void 투표_시_팀은_널일_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("team", null) + .sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @ParameterizedTest + @NullAndEmptyAndBlankSource + void 투표_시_참여코드는_널이거나_빈_문자열일_수_없다(String participateCode) { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("participateCode", participateCode) + .sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void 이미_참여한_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + String participatecode = UUID.randomUUID().toString(); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participatecode); + VoteRequest voteRequest = getVoteRequestBuilder() + .set("participateCode", participatecode) + .sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", pollEntity.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void 끝난_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity alreadyDonePoll = pollEntityGenerator.generate(table, PollStatus.DONE); + VoteRequest voteRequest = getVoteRequestBuilder().sample(); + + given() + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", alreadyDonePoll.getId()) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/poll/VoteDocumentTest.java b/src/test/java/com/debatetimer/controller/poll/VoteDocumentTest.java new file mode 100644 index 00000000..998c6989 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/poll/VoteDocumentTest.java @@ -0,0 +1,156 @@ +package com.debatetimer.controller.poll; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import com.debatetimer.controller.BaseDocumentTest; +import com.debatetimer.controller.RestDocumentationRequest; +import com.debatetimer.controller.RestDocumentationResponse; +import com.debatetimer.controller.Tag; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import io.restassured.http.ContentType; +import java.util.UUID; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class VoteDocumentTest extends BaseDocumentTest { + + @Nested + class GetVotersPollInfo { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.POLL_API) + .summary("투표자 - 선거 정보 조회") + .pathParameter( + parameterWithName("pollId").description("선거 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("선거 ID"), + fieldWithPath("status").type(STRING).description("선거 상태 - 진행중 : PROGRESS, 완료 : DONE"), + fieldWithPath("prosTeamName").type(STRING).description("찬성측 팀 이름"), + fieldWithPath("consTeamName").type(STRING).description("반대측 팀 이름"), + fieldWithPath("participateCode").type(STRING).description("참여 코드"), + fieldWithPath("totalCount").type(NUMBER).description("전체 투표 수"), + fieldWithPath("prosCount").type(NUMBER).description("찬성 투표 수"), + fieldWithPath("consCount").type(NUMBER).description("반대 투표 수") + ); + + @Test + void 투표자_선거_정보_조회() { + VoterPollInfoResponse response = new VoterPollInfoResponse( + 1L, + PollStatus.PROGRESS, + "찬성", + "반대", + UUID.randomUUID().toString(), + 3L, + 2L, + 1L + ); + doReturn(response).when(voteService).getVoterPollInfo(anyLong()); + + var document = document("vote/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .pathParam("pollId", 1l) + .when().get("/api/polls/{pollId}/votes") + .then().statusCode(200); + } + } + + @Nested + class VotePoll { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.POLL_API) + .summary("투표자 - 선거 투표") + .pathParameter( + parameterWithName("pollId").description("선거 ID") + ) + .requestBodyField( + fieldWithPath("name").type(STRING).description("투표자 이름"), + fieldWithPath("participateCode").type(STRING).description("투표 참여 코드"), + fieldWithPath("team").type(STRING).description("투표 팀") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("투표 ID"), + fieldWithPath("name").type(STRING).description("투표자 이름"), + fieldWithPath("participateCode").type(STRING).description("투표 참여 코드"), + fieldWithPath("team").type(STRING).description("투표 팀") + ); + + @Test + void 투표자_선거_정보_조회() { + VoteRequest voteRequest = new VoteRequest("콜리", UUID.randomUUID().toString(), VoteTeam.PROS); + VoteCreateResponse response = new VoteCreateResponse( + 1L, + voteRequest.name(), + voteRequest.participateCode(), + voteRequest.team() + ); + doReturn(response).when(voteService).vote(anyLong(), any()); + + var document = document("vote/post", 201) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", 1l) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(201); + } + + @EnumSource( + value = ClientErrorCode.class, + names = { + "ALREADY_DONE_POLL", + "ALREADY_VOTED_PARTICIPANT", + "INVALID_POLL_PARTICIPANT_CODE", + "INVALID_POLL_PARTICIPANT_NAME" + } + ) + @ParameterizedTest + void 투표자_투표_실패(ClientErrorCode clientErrorCode) { + VoteRequest voteRequest = new VoteRequest("콜리", UUID.randomUUID().toString(), VoteTeam.PROS); + doThrow(new DTClientErrorException(clientErrorCode)).when(voteService).vote(anyLong(), any()); + + var document = document("vote/post", clientErrorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .body(voteRequest) + .pathParam("pollId", 1l) + .when().post("/api/polls/{pollId}/votes") + .then().statusCode(clientErrorCode.getStatus().value()); + + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/AgendaTest.java b/src/test/java/com/debatetimer/domain/customize/AgendaTest.java new file mode 100644 index 00000000..37a2050b --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/AgendaTest.java @@ -0,0 +1,23 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class AgendaTest { + + @Nested + class Validate { + @Test + void 테이블_이름은_정해진_길이_이내여야_한다() { + String longAgenda = "f".repeat(Agenda.AGENDA_MAX_LENGTH + 1); + + assertThatThrownBy(() -> new Agenda(longAgenda)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_AGENDA_LENGTH.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/BellTest.java b/src/test/java/com/debatetimer/domain/customize/BellTest.java new file mode 100644 index 00000000..6ec93af3 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/BellTest.java @@ -0,0 +1,32 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class BellTest { + + @Nested + class Validate { + + @ValueSource(ints = {0, Bell.MAX_BELL_COUNT + 1}) + @ParameterizedTest + void 벨_횟수는_정해진_횟수_바깥일_경우_생성되지_않는다(int count) { + assertThatThrownBy(() -> new Bell(BellType.AFTER_START, 1, count)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_BELL_COUNT.getMessage()); + } + + @ValueSource(ints = {1, Bell.MAX_BELL_COUNT}) + @ParameterizedTest + void 벨_횟수는_정해진_횟수_이내여야_한다(int count) { + assertThatCode(() -> new Bell(BellType.AFTER_START, 1, count)) + .doesNotThrowAnyException(); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/BellTypeTest.java b/src/test/java/com/debatetimer/domain/customize/BellTypeTest.java new file mode 100644 index 00000000..8500d326 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/BellTypeTest.java @@ -0,0 +1,36 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class BellTypeTest { + + @Nested + class ValidateTime { + + @Test + void 벨_타입이_AFTER_START일때_시간은_0이상이어야_한다() { + assertThatThrownBy(() -> BellType.AFTER_START.validateTime(-1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_BELL_TIME.getMessage()); + } + + @Test + void 벨_타입이_BEFORE_END일때_시간은_0이상이어야_한다() { + assertThatThrownBy(() -> BellType.BEFORE_END.validateTime(-1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_BELL_TIME.getMessage()); + } + + @Test + void 벨_타입이_AFTER_END일때_시간은_0이하여야_한다() { + assertThatThrownBy(() -> BellType.AFTER_END.validateTime(1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_BELL_TIME.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java deleted file mode 100644 index c95d5dda..00000000 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.debatetimer.domain.customize; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.debatetimer.domain.member.Member; -import com.debatetimer.dto.member.TableType; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; -import java.time.LocalDateTime; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -class CustomizeTableTest { - - @Nested - class GetType { - - @Test - void 사용자_지정_테이블_타입을_반환한다() { - CustomizeTable customizeTable = new CustomizeTable(); - - assertThat(customizeTable.getType()).isEqualTo(TableType.CUSTOMIZE); - } - } - - @Nested - class ValidateTableName { - - @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfältig"}) - @ParameterizedTest - void 테이블_이름은_이모지를_제외한_글자만_가능하다(String name) { - Member member = new Member("default@gmail.com"); - assertThatCode(() -> new CustomizeTable(member, name, "agenda", true, true, "pros", "cons")) - .doesNotThrowAnyException(); - } - - @ValueSource(strings = {"a😀bc가다9", "🐥", "🥦"}) - @ParameterizedTest - void 테이블_이름에_이모지를_넣을_수_없다(String name) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new CustomizeTable(member, name, "agenda", true, true, "pros", "cons")) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); - } - - @Test - void 테이블_이름은_정해진_길이_이내여야_한다() { - Member member = new Member("default@gmail.com"); - String longTableName = "f".repeat(CustomizeTable.TABLE_NAME_MAX_LENGTH + 1); - - assertThatThrownBy(() -> new CustomizeTable(member, longTableName, "agenda", true, true, "pros", "cons")) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); - } - } - - @Nested - class ValidateTeamName { - - @Test - void 찬성_팀_이름은_정해진_길이_이내여야_한다() { - Member member = new Member("default@gmail.com"); - String longProsTeamName = "f".repeat(CustomizeTable.TEAM_NAME_MAX_LENGTH + 1); - - assertThatThrownBy( - () -> new CustomizeTable(member, "name", "agenda", true, true, longProsTeamName, "cons")) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); - } - - @Test - void 반대_팀_이름은_정해진_길이_이내여야_한다() { - Member member = new Member("default@gmail.com"); - String longConsTeamName = "f".repeat(CustomizeTable.TEAM_NAME_MAX_LENGTH + 1); - - assertThatThrownBy( - () -> new CustomizeTable(member, "name", "agenda", true, true, "pros", longConsTeamName)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); - } - - @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfäl"}) - @ParameterizedTest - void 찬성_팀_이름은_이모지를_제외한_글자만_가능하다(String prosName) { - Member member = new Member("default@gmail.com"); - assertThatCode(() -> new CustomizeTable(member, "name", "agenda", true, true, prosName, "cons")) - .doesNotThrowAnyException(); - } - - @ValueSource(strings = {"a😀가다9", "🐥", "🥦"}) - @ParameterizedTest - void 찬성_팀_이름에_이모지를_넣을_수_없다(String prosName) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new CustomizeTable(member, "name", "agenda", true, true, prosName, "cons")) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_FORM.getMessage()); - } - - @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfäl"}) - @ParameterizedTest - void 반대_팀_이름은_이모지를_제외한_글자만_가능하다(String consName) { - Member member = new Member("default@gmail.com"); - assertThatCode(() -> new CustomizeTable(member, "name", "agenda", true, true, "pros", consName)) - .doesNotThrowAnyException(); - } - - @ValueSource(strings = {"a😀가다9", "🐥", "🥦"}) - @ParameterizedTest - void 반대_팀_이름에_이모지를_넣을_수_없다(String consName) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new CustomizeTable(member, "name", "agenda", true, true, "pros", consName)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_FORM.getMessage()); - } - } - - @Nested - class UpdateUsedAt { - - @Test - void 테이블의_사용_시각을_업데이트한다() throws InterruptedException { - Member member = new Member("default@gmail.com"); - CustomizeTable table = new CustomizeTable(member, "tableName", "agenda", true, true, "찬성", "반대"); - LocalDateTime beforeUsedAt = table.getUsedAt(); - Thread.sleep(1); - - table.updateUsedAt(); - - assertThat(table.getUsedAt()).isAfter(beforeUsedAt); - } - } - - @Nested - class Update { - - @Test - void 테이블_정보를_업데이트_할_수_있다() { - Member member = new Member("default@gmail.com"); - CustomizeTable table = new CustomizeTable(member, "tableName", "agenda", true, true, "pros", "cons"); - CustomizeTable renewTable = new CustomizeTable(member, "newName", "newAgenda", false, false, "newPros", - "newCons"); - - table.updateTable(renewTable); - - assertAll( - () -> assertThat(table.getName()).isEqualTo("newName"), - () -> assertThat(table.getAgenda()).isEqualTo("newAgenda"), - () -> assertThat(table.getProsTeamName()).isEqualTo("newPros"), - () -> assertThat(table.getConsTeamName()).isEqualTo("newCons"), - () -> assertThat(table.isWarningBell()).isFalse(), - () -> assertThat(table.isFinishBell()).isFalse() - ); - } - - @Test - void 테이블_업데이트_할_때_사용_시간을_변경한다() throws InterruptedException { - Member member = new Member("default@gmail.com"); - CustomizeTable table = new CustomizeTable(member, "tableName", "agenda", true, true, "pros", "cons"); - CustomizeTable renewTable = new CustomizeTable(member, "newName", "newAgenda", false, false, "newPros", - "newCons"); - LocalDateTime beforeUsedAt = table.getUsedAt(); - Thread.sleep(1); - - table.updateTable(renewTable); - - assertThat(table.getUsedAt()).isAfter(beforeUsedAt); - } - } -} diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java index 50fb562b..cab0b3d7 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java @@ -1,181 +1,120 @@ package com.debatetimer.domain.customize; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.debatetimer.domain.Stance; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; class CustomizeTimeBoxTest { @Nested - class ValidateSequence { + class ValidateStance { - @ValueSource(ints = {0, -1}) - @ParameterizedTest - void 순서는_양수만_가능하다(int sequence) { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - - assertThatThrownBy(() -> new CustomizeTimeBox(table, sequence, Stance.NEUTRAL, "자유토론", - customizeBoxType, 120, 60, "발언자")) + @Test + void 발언_입장은_비어있을_수_없다() { + assertThatThrownBy(() -> new InheritedCustomizeTimeBox(null, "비토", "발언자")) .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_STANCE.getMessage()); } - } - - @Nested - class ValidateTime { - @ValueSource(ints = {0, -1}) - @ParameterizedTest - void 시간은_양수만_가능하다(int time) { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; - - assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.CONS, "자유토론", - customizeBoxType, time, 60, "발언자")) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + @Test + void 발언_입장은_유효한_값이어야_한다() { + assertThatCode(() -> new InheritedCustomizeTimeBox(Stance.PROS, "비토", "발언자")) + .doesNotThrowAnyException(); } } @Nested - class ValidateSpeaker { + class ValidateSpeechType { @Test - void 발언자_이름은_일정길이_이내로_허용된다() { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; - String speaker = "k".repeat(CustomizeTimeBox.SPEAKER_MAX_LENGTH + 1); + void 발언_종류는_특정_글자를_초과할_수_없다() { + String speechType = "a".repeat(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH + 1); - assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.CONS, "입론", - customizeBoxType, 120, speaker)) + assertThatThrownBy(() -> new InheritedCustomizeTimeBox(Stance.PROS, speechType, "비토")) .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH.getMessage()); + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH.getMessage()); } - @NullSource @ParameterizedTest - void 발언자는_빈_값이_허용된다(String speaker) { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; - - assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.CONS, "입론", - customizeBoxType, 120, speaker)) - .doesNotThrowAnyException(); + @NullAndEmptySource + @ValueSource(strings = {" ", "\n\t"}) + void 발언_종류는_비어있을_수_없다(String emptySpeechType) { + assertThatThrownBy(() -> new InheritedCustomizeTimeBox(Stance.PROS, emptySpeechType, "비토")) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH.getMessage()); } - @ValueSource(strings = {" ", " "}) - @ParameterizedTest - void 발언자는_공백이_입력되면_null로_저장된다(String speaker) { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; - - CustomizeTimeBox timeBox = new CustomizeTimeBox(table, 1, Stance.CONS, "입론", - customizeBoxType, 120, speaker); + @Test + void 발언_종류는_특정_글자_이내이어야_한다() { + String speechType = "a".repeat(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH); - assertThat(timeBox.getSpeaker()).isNull(); + assertThatCode(() -> new InheritedCustomizeTimeBox(Stance.PROS, speechType, "비토")) + .doesNotThrowAnyException(); } } @Nested - class ValidateCustomizeTime { - - @Test - void 자유토론_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - - assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", - customizeBoxType, 120, 60, "발언자") - ).doesNotThrowAnyException(); - } + class ValidateSpeaker { @Test - void 자유토론_타입이_개인_발언_시간과_팀_발언_시간을_입력하지_않을_경우_예외가_발생한다() { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; + void 발언자_이름은_특정_글자를_초과할_수_없다() { + String speaker = "a".repeat(CustomizeTimeBox.SPEAKER_MAX_LENGTH + 1); - assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 10, - "발언자")).isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + assertThatThrownBy(() -> new InheritedCustomizeTimeBox(Stance.PROS, "비토", speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH.getMessage()); } @Test - void 자유토론_타입이_아닌_타임박스가_개인_발언_시간과_팀_발언_시간을_입력할_경우_예외가_발생한다() { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType notTimeBasedBoxType = CustomizeBoxType.NORMAL; - - assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 120, 60, - "발언자")).isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + void 발언자_이름은_비어있을_수_있다() { + assertThatCode(() -> new InheritedCustomizeTimeBox(Stance.PROS, "비토", null)) + .doesNotThrowAnyException(); } + } - @Test - void 팀_발언_시간은_있으며_개인_발언_시간은_없을_수_있다() { - CustomizeTable table = new CustomizeTable(); - Integer timePerTeam = 60; - Integer timePerSpeaking = null; + static class InheritedCustomizeTimeBox extends CustomizeTimeBox { - assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); + protected InheritedCustomizeTimeBox(Stance stance, String speechType, String speaker) { + super(stance, speechType, speaker); } - @Test - void 개인_발언_시간은_팀_발언_시간보다_적거나_같아야_한다() { - CustomizeTable table = new CustomizeTable(); - int timePerTeam = 60; - int timePerSpeaking = 59; - - assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); + @Override + protected boolean isValidStance(Stance stance) { + return true; } - @Test - void 개인_발언_시간이_팀_발언_시간보다_길면_예외가_발생한다() { - CustomizeTable table = new CustomizeTable(); - int timePerTeam = 60; - int timePerSpeaking = 61; - - assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam, timePerSpeaking, "발언자")).isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); + @Override + public CustomizeBoxType getBoxType() { + return null; } - @Test - void 발언_유형의_길이는_일정_범위_이내여야_한다() { - CustomizeTable table = new CustomizeTable(); - String longSpeechType = "s".repeat(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH + 1); + @Override + public Integer getTime() { + return 0; + } - assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, longSpeechType, CustomizeBoxType.TIME_BASED, - 120, 60, "발언자")).isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH.getMessage()); + @Override + public Integer getTimePerTeam() { + return 0; } - } - @Nested - class getTime { + @Override + public Integer getTimePerSpeaking() { + return 0; + } - @Test - void 자유_토론_타임_박스의_시간은_팀_당_발언_시간의_배수이어야_한다() { - int timePerTeam = 300; - int timePerSpeaking = 120; - CustomizeTable table = new CustomizeTable(); - CustomizeTimeBox timeBasedTimeBox = new CustomizeTimeBox(table, 1, Stance.CONS, "자유 토론", - CustomizeBoxType.TIME_BASED, timePerTeam, timePerSpeaking, "콜리"); - - assertThat(timeBasedTimeBox.getTime()).isEqualTo(timePerTeam * CustomizeTimeBox.TIME_MULTIPLIER); + @Override + public List getBells() { + return Collections.emptyList(); } } } diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxesTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxesTest.java deleted file mode 100644 index f37f835f..00000000 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxesTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.debatetimer.domain.customize; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.debatetimer.domain.Stance; -import com.debatetimer.domain.member.Member; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class CustomizeTimeBoxesTest { - - @Nested - class SortedBySequence { - - @Test - void 타임박스의_순서에_따라_정렬된다() { - Member member = new Member("default@gmail.com"); - CustomizeTable testTable = new CustomizeTable(member, "토론 테이블", "주제", - true, true, "찬성", "반대"); - CustomizeTimeBox firstBox = new CustomizeTimeBox(testTable, 1, Stance.PROS, "입론", - CustomizeBoxType.NORMAL, 300, "콜리"); - CustomizeTimeBox secondBox = new CustomizeTimeBox(testTable, 2, Stance.PROS, "입론", - CustomizeBoxType.NORMAL, 300, "콜리2"); - List timeBoxes = new ArrayList<>(Arrays.asList(secondBox, firstBox)); - - CustomizeTimeBoxes actual = new CustomizeTimeBoxes(timeBoxes); - - assertThat(actual.getTimeBoxes()).containsExactly(firstBox, secondBox); - } - } -} diff --git a/src/test/java/com/debatetimer/domain/customize/NormalTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/NormalTimeBoxTest.java new file mode 100644 index 00000000..c70bf53a --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/NormalTimeBoxTest.java @@ -0,0 +1,43 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.Collections; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class NormalTimeBoxTest { + + @Nested + class ValidateTime { + + @Test + void 시간은_0보다_커야_한다() { + Integer time = 0; + + assertThatThrownBy(() -> new NormalTimeBox(Stance.PROS, "비토", null, time, Collections.emptyList())) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + + @Test + void 시간은_비어있지_않아야_한다() { + Integer time = null; + + assertThatThrownBy(() -> new NormalTimeBox(Stance.PROS, "비토", null, time, Collections.emptyList())) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + + @Test + void 시간은_양수여야_한다() { + Integer time = 1; + + assertThatCode(() -> new NormalTimeBox(Stance.PROS, "비토", null, time, Collections.emptyList())) + .doesNotThrowAnyException(); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/TableNameTest.java b/src/test/java/com/debatetimer/domain/customize/TableNameTest.java new file mode 100644 index 00000000..1d160e0c --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/TableNameTest.java @@ -0,0 +1,42 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TableNameTest { + + @Nested + class Validate { + + @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfältig"}) + @ParameterizedTest + void 테이블_이름은_이모지를_제외한_글자만_가능하다(String validName) { + assertThatCode(() -> new TableName(validName)) + .doesNotThrowAnyException(); + } + + @ValueSource(strings = {"a😀bc가다9", "🐥", "🥦"}) + @ParameterizedTest + void 테이블_이름에_이모지를_넣을_수_없다(String invalidName) { + assertThatThrownBy(() -> new TableName(invalidName)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); + } + + @Test + void 테이블_이름은_정해진_길이_이내여야_한다() { + String longTableName = "f".repeat(TableName.NAME_MAX_LENGTH + 1); + + assertThatThrownBy(() -> new TableName(longTableName)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/TeamNameTest.java b/src/test/java/com/debatetimer/domain/customize/TeamNameTest.java new file mode 100644 index 00000000..71bc434e --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/TeamNameTest.java @@ -0,0 +1,53 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class TeamNameTest { + + @Nested + class Validate { + + @Test + void 팀_이름은_정해진_길이_이내여야_한다() { + String longTeamName = "f".repeat(TeamName.NAME_MAX_LENGTH + 1); + + assertThatThrownBy( + () -> new TeamName(longTeamName)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); + } + + @ParameterizedTest + @EmptySource + @ValueSource(strings = {" "}) + void 팀_이름은_빈_값을_허용하지_않는다(String invalidTeamName) { + assertThatThrownBy(() -> new TeamName(invalidTeamName)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); + } + + @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfäl"}) + @ParameterizedTest + void 팀_이름은_이모지를_제외한_글자만_가능하다(String validTeamName) { + assertThatCode(() -> new TeamName(validTeamName)) + .doesNotThrowAnyException(); + } + + @ValueSource(strings = {"a😀가다9", "🐥", "🥦"}) + @ParameterizedTest + void 팀_이름에_이모지를_넣을_수_없다(String invalidTeamName) { + assertThatThrownBy(() -> new TeamName(invalidTeamName)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_FORM.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/TimeBasedTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/TimeBasedTimeBoxTest.java new file mode 100644 index 00000000..41d27641 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/TimeBasedTimeBoxTest.java @@ -0,0 +1,93 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class TimeBasedTimeBoxTest { + + @Nested + class ValidateStance { + + @Test + void 중립_스탠스가_아니면_예외가_발생한다() { + Stance stance = Stance.PROS; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(stance, "자유발언", "비토", 120, 60)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_STANCE.getMessage()); + } + + @Test + void 중립_스탠스면_예외가_발생하지_않는다() { + Stance stance = Stance.NEUTRAL; + + assertThatCode( + () -> new TimeBasedTimeBox(stance, "자유발언", "비토", 120, 60)) + .doesNotThrowAnyException(); + } + } + + @Nested + class ValidateTimes { + + @Test + void 팀_당_발언_시간이_양수이어야_한다() { + int timePerTeam = 0; + int timePerSpeaking = 1; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(Stance.NEUTRAL, "자유발언", "비토", timePerTeam, timePerSpeaking)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + + @Test + void 팀_당_발언_시간이_비어있으면_안된다() { + Integer timePerTeam = null; + int timePerSpeaking = 1; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(Stance.NEUTRAL, "자유발언", "비토", timePerTeam, timePerSpeaking)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + + @Test + void 회_당_시간이_양수이어야_한다() { + int timePerSpeaking = 0; + int timePerTeam = 1; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(Stance.NEUTRAL, "자유발언", "비토", timePerTeam, timePerSpeaking)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + + @Test + void 회_당_발언_시간은_비어있을_수_있다() { + Integer timePerSpeaking = null; + int timePerTeam = 1; + + assertThatCode( + () -> new TimeBasedTimeBox(Stance.NEUTRAL, "자유발언", "비토", timePerTeam, timePerSpeaking)) + .doesNotThrowAnyException(); + } + + @Test + void 팀_당_발언시간은_회_당_발언시간보다_많거나_같아야_한다() { + int timePerTeam = 60; + int timePerSpeaking = timePerTeam + 1; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(Stance.NEUTRAL, "자유발언", "비토", timePerTeam, timePerSpeaking)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/poll/ParticipantNameTest.java b/src/test/java/com/debatetimer/domain/poll/ParticipantNameTest.java new file mode 100644 index 00000000..a6f25f32 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/poll/ParticipantNameTest.java @@ -0,0 +1,24 @@ +package com.debatetimer.domain.poll; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.fixture.NullAndEmptyAndBlankSource; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; + +class ParticipantNameTest { + + @Nested + class Validate { + + @ParameterizedTest + @NullAndEmptyAndBlankSource + void 투표참여자_이름은_널이거나_빈_값_일_수_없다(String name) { + assertThatThrownBy(() -> new ParticipantName(name)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_POLL_PARTICIPANT_NAME.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/poll/ParticipateCodeTest.java b/src/test/java/com/debatetimer/domain/poll/ParticipateCodeTest.java new file mode 100644 index 00000000..d8d5245c --- /dev/null +++ b/src/test/java/com/debatetimer/domain/poll/ParticipateCodeTest.java @@ -0,0 +1,24 @@ +package com.debatetimer.domain.poll; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.fixture.NullAndEmptyAndBlankSource; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; + +class ParticipateCodeTest { + + @Nested + class Validate { + + @ParameterizedTest + @NullAndEmptyAndBlankSource + void 투표_참여_코드는_널이거나_빈_값_일_수_없다(String participatecode) { + assertThatThrownBy(() -> new ParticipateCode(participatecode)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_POLL_PARTICIPANT_CODE.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domainrepository/BaseDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/BaseDomainRepositoryTest.java new file mode 100644 index 00000000..485a7f25 --- /dev/null +++ b/src/test/java/com/debatetimer/domainrepository/BaseDomainRepositoryTest.java @@ -0,0 +1,63 @@ +package com.debatetimer.domainrepository; + +import com.debatetimer.DataBaseCleaner; +import com.debatetimer.fixture.domain.BellGenerator; +import com.debatetimer.fixture.domain.CustomizeTableGenerator; +import com.debatetimer.fixture.domain.CustomizeTimeBoxGenerator; +import com.debatetimer.fixture.entity.BellEntityGenerator; +import com.debatetimer.fixture.entity.CustomizeTableEntityGenerator; +import com.debatetimer.fixture.entity.CustomizeTimeBoxEntityGenerator; +import com.debatetimer.fixture.entity.MemberGenerator; +import com.debatetimer.fixture.entity.PollEntityGenerator; +import com.debatetimer.fixture.entity.VoteEntityGenerator; +import com.debatetimer.repository.customize.BellRepository; +import com.debatetimer.repository.customize.CustomizeTableRepository; +import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; +import com.debatetimer.repository.poll.PollRepository; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@ExtendWith(DataBaseCleaner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +public abstract class BaseDomainRepositoryTest { + + @Autowired + protected CustomizeTableGenerator tableGenerator; + + @Autowired + protected CustomizeTimeBoxGenerator timeBoxGenerator; + + @Autowired + protected BellGenerator bellGenerator; + + @Autowired + protected MemberGenerator memberGenerator; + + @Autowired + protected CustomizeTableEntityGenerator tableEntityGenerator; + + @Autowired + protected CustomizeTimeBoxEntityGenerator timeBoxEntityGenerator; + + @Autowired + protected BellEntityGenerator bellEntityGenerator; + + @Autowired + protected PollEntityGenerator pollEntityGenerator; + + @Autowired + protected VoteEntityGenerator voteEntityGenerator; + + @Autowired + protected PollRepository pollRepository; + + @Autowired + protected CustomizeTableRepository tableRepository; + + @Autowired + protected CustomizeTimeBoxRepository timeBoxRepository; + + @Autowired + protected BellRepository bellRepository; +} diff --git a/src/test/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepositoryTest.java new file mode 100644 index 00000000..e1dbbf53 --- /dev/null +++ b/src/test/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepositoryTest.java @@ -0,0 +1,188 @@ +package com.debatetimer.domainrepository.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.BellType; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domainrepository.BaseDomainRepositoryTest; +import com.debatetimer.entity.customize.BellEntity; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CustomizeTableDomainRepositoryTest extends BaseDomainRepositoryTest { + + @Autowired + private CustomizeTableDomainRepository customizeTableDomainRepository; + + @Nested + class Save { + + @Test + void 테이블을_저장한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTable table = tableGenerator.generate(member); + List bells = bellGenerator.generate(3); + CustomizeTimeBox timeBox1 = timeBoxGenerator.generate(bells); + CustomizeTimeBox timeBox2 = timeBoxGenerator.generate(Collections.emptyList()); + + CustomizeTable savedTable = customizeTableDomainRepository.save(table, List.of(timeBox1, timeBox2)); + List timeBoxEntities = timeBoxRepository.findAllByCustomizeTable( + new CustomizeTableEntity(savedTable)); + List bellEntities = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntities); + + assertAll( + () -> assertThat(savedTable.getName()).isEqualTo(table.getName()), + () -> assertThat(timeBoxEntities).hasSize(2), + () -> assertThat(bellEntities).hasSize(3) + ); + } + } + + @Nested + class GetByIdAndMember { + + @Test + void 회원이_소유한_테이블을_가져온다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + + CustomizeTable foundTable = customizeTableDomainRepository.getByIdAndMember(table.getId(), member); + + assertThat(foundTable.getId()).isEqualTo(table.getId()); + } + } + + @Nested + class GetCustomizeTimeBoxes { + + @Test + void 테이블의_시간박스는_순서대로_가져온다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity tableEntity = tableEntityGenerator.generate(member); + timeBoxEntityGenerator.generate(tableEntity, CustomizeBoxType.NORMAL, 1, 60); + timeBoxEntityGenerator.generate(tableEntity, CustomizeBoxType.NORMAL, 2, 180); + timeBoxEntityGenerator.generate(tableEntity, CustomizeBoxType.NORMAL, 3, 120); + + List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes( + tableEntity.getId(), member); + + assertThat(timeBoxes).hasSize(3) + .extracting(CustomizeTimeBox::getTime) + .containsExactly(60, 180, 120); + } + + @Test + void 테이블의_시간박스와_벨을_가져온다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity tableEntity = tableEntityGenerator.generate(member); + CustomizeTimeBoxEntity timeBoxEntity1 = timeBoxEntityGenerator.generate(tableEntity, + CustomizeBoxType.NORMAL, 1); + CustomizeTimeBoxEntity timeBoxEntity2 = timeBoxEntityGenerator.generate(tableEntity, + CustomizeBoxType.NORMAL, 2); + bellEntityGenerator.generate(timeBoxEntity1, BellType.BEFORE_END, 20, 1); + bellEntityGenerator.generate(timeBoxEntity1, BellType.BEFORE_END, 30, 1); + bellEntityGenerator.generate(timeBoxEntity2, BellType.BEFORE_END, 10, 1); + + List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes( + tableEntity.getId(), member); + + assertAll( + () -> assertThat(timeBoxes).hasSize(2), + () -> assertThat(timeBoxes.get(0).getBells()).hasSize(2) + .extracting(Bell::getTime) + .containsExactly(20, 30), + () -> assertThat(timeBoxes.get(1).getBells()).hasSize(1) + .extracting(Bell::getTime) + .containsExactlyInAnyOrder(10) + ); + } + } + + @Nested + class Update { + + @Test + void 테이블을_수정한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity tableEntity = tableEntityGenerator.generate(member); + CustomizeTimeBoxEntity timeBox1 = timeBoxEntityGenerator.generate(tableEntity, CustomizeBoxType.NORMAL, 1); + CustomizeTimeBoxEntity timeBox2 = timeBoxEntityGenerator.generate(tableEntity, CustomizeBoxType.NORMAL, 2); + bellEntityGenerator.generate(timeBox1, BellType.AFTER_START, 10, 1); + bellEntityGenerator.generate(timeBox1, BellType.AFTER_START, 20, 1); + bellEntityGenerator.generate(timeBox2, BellType.AFTER_START, 20, 1); + + CustomizeTable table = tableGenerator.generate(member, "수정된 테이블"); + List bells = bellGenerator.generate(3); + CustomizeTimeBox timeBox = timeBoxGenerator.generate(bells, "수정"); + + CustomizeTable updatedTable = customizeTableDomainRepository.update(table, tableEntity.getId(), member, + List.of(timeBox)); + CustomizeTableEntity foundTable = tableRepository.getByIdAndMember(tableEntity.getId(), member); + List timeBoxEntities = timeBoxRepository.findAllByCustomizeTable( + new CustomizeTableEntity(updatedTable)); + List bellEntities = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntities); + + assertAll( + () -> assertThat(foundTable.getName()).isEqualTo(updatedTable.getName()), + () -> assertThat(timeBoxEntities).hasSize(1), + () -> assertThat(bellEntities).hasSize(3) + ); + } + } + + @Nested + class UpdateUsedAt { + + @Test + void 테이블의_사용_시간을_업데이트한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity tableEntity = tableEntityGenerator.generate(member); + LocalDateTime beforeUsedAt = tableEntity.getUsedAt(); + + CustomizeTable customizeTable = customizeTableDomainRepository.updateUsedAt(tableEntity.getId(), member); + + assertAll( + () -> assertThat(customizeTable.getId()).isEqualTo(tableEntity.getId()), + () -> assertThat(customizeTable.getUsedAt()).isAfter(beforeUsedAt) + ); + } + } + + @Nested + class Delete { + + @Test + void 테이블을_삭제한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + CustomizeTimeBoxEntity timeBox1 = timeBoxEntityGenerator.generate(table, CustomizeBoxType.NORMAL, 1); + CustomizeTimeBoxEntity timeBox2 = timeBoxEntityGenerator.generate(table, CustomizeBoxType.NORMAL, 2); + bellEntityGenerator.generate(timeBox1, BellType.AFTER_START, 10, 1); + bellEntityGenerator.generate(timeBox1, BellType.AFTER_START, 20, 1); + bellEntityGenerator.generate(timeBox2, BellType.AFTER_START, 20, 1); + + customizeTableDomainRepository.delete(table.getId(), member); + Optional foundTable = tableRepository.findById(table.getId()); + List timeBoxEntities = timeBoxRepository.findAllByCustomizeTable(table); + List bellEntities = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntities); + + assertAll( + () -> assertThat(foundTable).isEmpty(), + () -> assertThat(timeBoxEntities).isEmpty(), + () -> assertThat(bellEntities).isEmpty() + ); + } + } +} diff --git a/src/test/java/com/debatetimer/domainrepository/poll/PollDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/poll/PollDomainRepositoryTest.java new file mode 100644 index 00000000..48a65773 --- /dev/null +++ b/src/test/java/com/debatetimer/domainrepository/poll/PollDomainRepositoryTest.java @@ -0,0 +1,74 @@ +package com.debatetimer.domainrepository.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domainrepository.BaseDomainRepositoryTest; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class PollDomainRepositoryTest extends BaseDomainRepositoryTest { + + @Autowired + private PollDomainRepository pollDomainRepository; + + @Nested + class Create { + + @Test + void 선거를_생성한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + Poll poll = new Poll(table.getId(), member.getId(), "찬성", "반대", "주제"); + + Poll createdPoll = pollDomainRepository.create(poll); + + Optional foundPollEntity = pollRepository.findById(createdPoll.getId()); + assertThat(foundPollEntity).isPresent(); + } + } + + @Nested + class GetByIdAndMemberId { + + @Test + void 회원이_개최한_선거를_가져온다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + + Poll foundPoll = pollDomainRepository.getByIdAndMemberId(pollEntity.getId(), member.getId()); + + assertAll( + () -> assertThat(foundPoll.getId()).isEqualTo(pollEntity.getId()), + () -> assertThat(foundPoll.getAgenda().getValue()).isEqualTo(pollEntity.getAgenda()), + () -> assertThat(foundPoll.getStatus()).isEqualTo(pollEntity.getStatus()), + () -> assertThat(foundPoll.getMemberId()).isEqualTo(pollEntity.getMemberId()), + () -> assertThat(foundPoll.getProsTeamName().getValue()).isEqualTo(pollEntity.getProsTeamName()), + () -> assertThat(foundPoll.getConsTeamName().getValue()).isEqualTo(pollEntity.getConsTeamName()) + ); + } + } + + @Nested + class FinishPoll { + + @Test + void 선거를_완료_상태로_변경한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + + Poll updatedPoll = pollDomainRepository.finishPoll(pollEntity.getId(), member.getId()); + + assertThat(updatedPoll.getStatus()).isEqualTo(PollStatus.DONE); + } + } +} diff --git a/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java new file mode 100644 index 00000000..1aea13bb --- /dev/null +++ b/src/test/java/com/debatetimer/domainrepository/poll/VoteDomainRepositoryTest.java @@ -0,0 +1,107 @@ +package com.debatetimer.domainrepository.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.ParticipateCode; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.Vote; +import com.debatetimer.domain.poll.VoteInfo; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.domainrepository.BaseDomainRepositoryTest; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.UUID; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class VoteDomainRepositoryTest extends BaseDomainRepositoryTest { + + @Autowired + private VoteDomainRepository voteDomainRepository; + + @Nested + class GetVoteInfo { + + @Test + void 팀별_투표_현황을_알_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "비토"); + voteEntityGenerator.generate(pollEntity, VoteTeam.CONS, "커찬"); + + VoteInfo voteInfo = voteDomainRepository.findVoteInfoByPollId(pollEntity.getId()); + + assertAll( + () -> assertThat(voteInfo.getPollId()).isEqualTo(pollEntity.getId()), + () -> assertThat(voteInfo.getTotalCount()).isEqualTo(3L), + () -> assertThat(voteInfo.getProsCount()).isEqualTo(2L), + () -> assertThat(voteInfo.getConsCount()).isEqualTo(1L) + ); + } + } + + @Nested + class isExists { + + @Test + void 이미_참여한_투표인지_알_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + PollEntity alreadyParticipatedPoll = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + PollEntity notYetParticipatedPoll = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + ParticipateCode participateCode = new ParticipateCode(UUID.randomUUID().toString()); + voteEntityGenerator.generate(alreadyParticipatedPoll, VoteTeam.PROS, "콜리", participateCode.getValue()); + + boolean participated = voteDomainRepository.isExists(alreadyParticipatedPoll.getId(), participateCode); + boolean notYetParticipated = voteDomainRepository.isExists(notYetParticipatedPoll.getId(), + participateCode); + + assertAll( + () -> assertThat(participated).isTrue(), + () -> assertThat(notYetParticipated).isFalse() + ); + } + } + + @Nested + class Save { + + @Test + void 투표를_저장할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + Vote vote = new Vote(pollEntity.getId(), VoteTeam.PROS, "콜리", UUID.randomUUID().toString()); + + Vote savedVote = voteDomainRepository.save(vote); + + assertAll( + () -> assertThat(savedVote.getName().getValue()).isEqualTo(vote.getName().getValue()), + () -> assertThat(savedVote.getCode().getValue()).isEqualTo(vote.getCode().getValue()), + () -> assertThat(savedVote.getTeam()).isEqualTo(vote.getTeam()) + ); + } + + @Test + void 중복_투표할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = tableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + String participateCode = UUID.randomUUID().toString(); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participateCode); + Vote vote = new Vote(pollEntity.getId(), VoteTeam.PROS, "콜리", participateCode); + + assertThatThrownBy(() -> voteDomainRepository.save(vote)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.ALREADY_VOTED_PARTICIPANT.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/entity/customize/BellEntityTest.java b/src/test/java/com/debatetimer/entity/customize/BellEntityTest.java new file mode 100644 index 00000000..c0483f9f --- /dev/null +++ b/src/test/java/com/debatetimer/entity/customize/BellEntityTest.java @@ -0,0 +1,33 @@ +package com.debatetimer.entity.customize; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.customize.BellType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class BellEntityTest { + + @Nested + class Validate { + + @Test + void 벨_시간은_0이상이어야_한다() { + assertThatThrownBy(() -> new BellEntity(null, BellType.AFTER_END, -1, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_BELL_TIME.getMessage()); + } + + @ValueSource(ints = {0, BellEntity.MAX_BELL_COUNT + 1}) + @ParameterizedTest + void 벨_횟수는_정해진_횟수_이내여야_한다(int count) { + assertThatThrownBy(() -> new BellEntity(null, BellType.AFTER_END, 1, count)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_BELL_COUNT.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/entity/customize/CustomizeTableEntityTest.java b/src/test/java/com/debatetimer/entity/customize/CustomizeTableEntityTest.java new file mode 100644 index 00000000..6dbfa1c5 --- /dev/null +++ b/src/test/java/com/debatetimer/entity/customize/CustomizeTableEntityTest.java @@ -0,0 +1,82 @@ +package com.debatetimer.entity.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.TableType; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CustomizeTableEntityTest { + + @Nested + class GetType { + + @Test + void 사용자_지정_테이블_타입을_반환한다() { + CustomizeTableEntity customizeTableEntity = new CustomizeTableEntity(); + + assertThat(customizeTableEntity.getType()).isEqualTo(TableType.CUSTOMIZE); + } + } + + @Nested + class UpdateUsedAt { + + @Test + void 테이블의_사용_시각을_업데이트한다() { + Member member = new Member("default@gmail.com"); + CustomizeTable table = new CustomizeTable(member, "tableName", "agenda", "찬성", "반대", + true, true, LocalDateTime.now().minusNanos(1L)); + CustomizeTableEntity customizeTableEntity = new CustomizeTableEntity(table); + LocalDateTime beforeUsedAt = customizeTableEntity.getUsedAt(); + + customizeTableEntity.updateUsedAt(); + + assertThat(customizeTableEntity.getUsedAt()).isAfter(beforeUsedAt); + } + } + + @Nested + class Update { + + @Test + void 테이블_정보를_업데이트_할_수_있다() { + Member member = new Member("default@gmail.com"); + CustomizeTable table = new CustomizeTable(member, "tableName", "agenda", "찬성", "반대", + true, true, LocalDateTime.now().minusNanos(1L)); + CustomizeTableEntity customizeTableEntity = new CustomizeTableEntity(table); + CustomizeTable renewTable = new CustomizeTable(member, "newName", "newAgenda", + "newPros", "newCons", false, false, LocalDateTime.now()); + + customizeTableEntity.updateTable(renewTable); + + assertAll( + () -> assertThat(customizeTableEntity.getName()).isEqualTo("newName"), + () -> assertThat(customizeTableEntity.getAgenda()).isEqualTo("newAgenda"), + () -> assertThat(customizeTableEntity.getProsTeamName()).isEqualTo("newPros"), + () -> assertThat(customizeTableEntity.getConsTeamName()).isEqualTo("newCons"), + () -> assertThat(customizeTableEntity.isWarningBell()).isFalse(), + () -> assertThat(customizeTableEntity.isFinishBell()).isFalse() + ); + } + + @Test + void 테이블_업데이트_할_때_사용_시간을_변경한다() { + Member member = new Member("default@gmail.com"); + CustomizeTable table = new CustomizeTable(member, "tableName", "agenda", "찬성", "반대", + true, true, LocalDateTime.now().minusNanos(1L)); + CustomizeTableEntity customizeTableEntity = new CustomizeTableEntity(table); + CustomizeTable renewTable = new CustomizeTable(member, "newName", "newAgenda", + "newPros", "newCons", false, false, LocalDateTime.now()); + LocalDateTime beforeUsedAt = customizeTableEntity.getUsedAt(); + + customizeTableEntity.updateTable(renewTable); + + assertThat(customizeTableEntity.getUsedAt()).isAfter(beforeUsedAt); + } + } +} diff --git a/src/test/java/com/debatetimer/entity/customize/CustomizeTimeBoxEntityTest.java b/src/test/java/com/debatetimer/entity/customize/CustomizeTimeBoxEntityTest.java new file mode 100644 index 00000000..3db1b1d4 --- /dev/null +++ b/src/test/java/com/debatetimer/entity/customize/CustomizeTimeBoxEntityTest.java @@ -0,0 +1,182 @@ +package com.debatetimer.entity.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +class CustomizeTimeBoxEntityTest { + + @Nested + class ValidateSequence { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 순서는_양수만_가능하다(int sequence) { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; + + assertThatThrownBy(() -> new CustomizeTimeBoxEntity(table, sequence, Stance.NEUTRAL, "자유토론", + customizeBoxType, 120, 60, "발언자")) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); + } + } + + @Nested + class ValidateTime { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 시간은_양수만_가능하다(int time) { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; + + assertThatThrownBy( + () -> new CustomizeTimeBoxEntity(table, 1, Stance.CONS, "자유토론", + customizeBoxType, time, 60, "발언자")) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + } + + @Nested + class ValidateSpeaker { + + @Test + void 발언자_이름은_일정길이_이내로_허용된다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; + String speaker = "k".repeat(CustomizeTimeBoxEntity.SPEAKER_MAX_LENGTH + 1); + + assertThatThrownBy(() -> new CustomizeTimeBoxEntity(table, 1, Stance.CONS, "입론", + customizeBoxType, 120, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH.getMessage()); + } + + @NullSource + @ParameterizedTest + void 발언자는_빈_값이_허용된다(String speaker) { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; + + assertThatCode(() -> new CustomizeTimeBoxEntity(table, 1, Stance.CONS, "입론", + customizeBoxType, 120, speaker)) + .doesNotThrowAnyException(); + } + + @ValueSource(strings = {" ", " "}) + @ParameterizedTest + void 발언자는_공백이_입력되면_null로_저장된다(String speaker) { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.NORMAL; + + CustomizeTimeBoxEntity timeBox = new CustomizeTimeBoxEntity(table, 1, Stance.CONS, "입론", + customizeBoxType, 120, speaker); + + assertThat(timeBox.getSpeaker()).isNull(); + } + } + + @Nested + class ValidateCustomizeTime { + + @Test + void 자유토론_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; + + assertThatCode(() -> new CustomizeTimeBoxEntity(table, 1, Stance.NEUTRAL, "자유토론", + customizeBoxType, 120, 60, "발언자") + ).doesNotThrowAnyException(); + } + + @Test + void 자유토론_타입이_개인_발언_시간과_팀_발언_시간을_입력하지_않을_경우_예외가_발생한다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; + + assertThatThrownBy(() -> new CustomizeTimeBoxEntity(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 10, + "발언자")).isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + } + + @Test + void 자유토론_타입이_아닌_타임박스가_개인_발언_시간과_팀_발언_시간을_입력할_경우_예외가_발생한다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeBoxType notTimeBasedBoxType = CustomizeBoxType.NORMAL; + + assertThatThrownBy( + () -> new CustomizeTimeBoxEntity(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 120, 60, + "발언자")).isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + } + + @Test + void 팀_발언_시간은_있으며_개인_발언_시간은_없을_수_있다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + Integer timePerTeam = 60; + Integer timePerSpeaking = null; + + assertThatCode(() -> new CustomizeTimeBoxEntity(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); + } + + @Test + void 개인_발언_시간은_팀_발언_시간보다_적거나_같아야_한다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + int timePerTeam = 60; + int timePerSpeaking = 59; + + assertThatCode(() -> new CustomizeTimeBoxEntity(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); + } + + @Test + void 개인_발언_시간이_팀_발언_시간보다_길면_예외가_발생한다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + int timePerTeam = 60; + int timePerSpeaking = 61; + + assertThatThrownBy(() -> new CustomizeTimeBoxEntity(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, "발언자")).isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); + } + + @Test + void 발언_유형의_길이는_일정_범위_이내여야_한다() { + CustomizeTableEntity table = new CustomizeTableEntity(); + String longSpeechType = "s".repeat(CustomizeTimeBoxEntity.SPEECH_TYPE_MAX_LENGTH + 1); + + assertThatThrownBy( + () -> new CustomizeTimeBoxEntity(table, 1, Stance.NEUTRAL, longSpeechType, CustomizeBoxType.TIME_BASED, + 120, 60, "발언자")).isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH.getMessage()); + } + } + + @Nested + class getTime { + + @Test + void 자유_토론_타임_박스의_시간은_팀_당_발언_시간의_배수이어야_한다() { + int timePerTeam = 300; + int timePerSpeaking = 120; + CustomizeTableEntity table = new CustomizeTableEntity(); + CustomizeTimeBoxEntity timeBasedTimeBox = new CustomizeTimeBoxEntity(table, 1, Stance.CONS, "자유 토론", + CustomizeBoxType.TIME_BASED, timePerTeam, timePerSpeaking, "콜리"); + + assertThat(timeBasedTimeBox.getTime()).isEqualTo(timePerTeam * CustomizeTimeBoxEntity.TIME_MULTIPLIER); + } + } +} diff --git a/src/test/java/com/debatetimer/exception/decoder/H2ErrorDecoderTest.java b/src/test/java/com/debatetimer/exception/decoder/H2ErrorDecoderTest.java new file mode 100644 index 00000000..147ff327 --- /dev/null +++ b/src/test/java/com/debatetimer/exception/decoder/H2ErrorDecoderTest.java @@ -0,0 +1,51 @@ +package com.debatetimer.exception.decoder; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.SQLException; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DataIntegrityViolationException; + +class H2ErrorDecoderTest { + + private H2ErrorDecoder errorDecoder; + + + @BeforeEach + void setUp() { + errorDecoder = new H2ErrorDecoder(); + } + + @Nested + class isUniqueError { + + @Test + void 유니크_제약조건_에러를_판단할_수_있다() { + SQLException uniqueError = new SQLException("유니크 에러", H2ErrorDecoder.UNIQUE_CONSTRAINT_VIOLATION_SQL_STATE); + ConstraintViolationException uniqueViolation = new ConstraintViolationException("유니크 에러", uniqueError, + "vote_poll_id_participate_code"); + DataIntegrityViolationException uniqueException = new DataIntegrityViolationException("유니크 에러", + uniqueViolation); + + boolean isUniqueError = errorDecoder.isUniqueConstraintViolation(uniqueException); + + assertThat(isUniqueError).isTrue(); + } + + @Test + void 유니크_제약조건_에러가_아님을_판단한다() { + SQLException notUniqueError = new SQLException("다른 에러", "23000"); + ConstraintViolationException notUniqueViolation = new ConstraintViolationException("기타 에러", notUniqueError, + "some_constraint"); + DataIntegrityViolationException extraException = new DataIntegrityViolationException("에러", + notUniqueViolation); + + boolean isNotUniqueError = errorDecoder.isUniqueConstraintViolation(extraException); + + assertThat(isNotUniqueError).isFalse(); + } + } +} diff --git a/src/test/java/com/debatetimer/exception/decoder/MySqlErrorDecoderTest.java b/src/test/java/com/debatetimer/exception/decoder/MySqlErrorDecoderTest.java new file mode 100644 index 00000000..f49d5978 --- /dev/null +++ b/src/test/java/com/debatetimer/exception/decoder/MySqlErrorDecoderTest.java @@ -0,0 +1,54 @@ +package com.debatetimer.exception.decoder; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.SQLException; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.dao.DataIntegrityViolationException; + +class MySqlErrorDecoderTest { + + private MySqlErrorDecoder errorDecoder; + + + @BeforeEach + void setUp() { + errorDecoder = new MySqlErrorDecoder(); + } + + @Nested + class isUniqueError { + + @Test + void 유니크_제약조건_에러를_판단할_수_있다() { + SQLException uniqueError = new SQLException("유니크 에러", + MySqlErrorDecoder.MYSQL_UNIQUE_VIOLATION, + MySqlErrorDecoder.MYSQL_DUP_ERROR_CODE + ); + ConstraintViolationException uniqueViolation = new ConstraintViolationException("유니크 에러", uniqueError, + "vote_poll_id_participate_code"); + DataIntegrityViolationException uniqueException = new DataIntegrityViolationException("유니크 에러", + uniqueViolation); + + boolean isUniqueError = errorDecoder.isUniqueConstraintViolation(uniqueException); + + assertThat(isUniqueError).isTrue(); + } + + @Test + void 유니크_제약조건_에러가_아님을_판단한다() { + SQLException notUniqueError = new SQLException("다른 에러", "32050", 1234); + ConstraintViolationException notUniqueViolation = new ConstraintViolationException("기타 에러", notUniqueError, + "some_constraint"); + DataIntegrityViolationException extraException = new DataIntegrityViolationException("에러", + notUniqueViolation); + + boolean isNotUniqueError = errorDecoder.isUniqueConstraintViolation(extraException); + + assertThat(isNotUniqueError).isFalse(); + } + } +} diff --git a/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java deleted file mode 100644 index df77fb3d..00000000 --- a/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.debatetimer.fixture; - -import com.debatetimer.domain.Stance; -import com.debatetimer.domain.customize.CustomizeBoxType; -import com.debatetimer.domain.customize.CustomizeTable; -import com.debatetimer.domain.customize.CustomizeTimeBox; -import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; -import org.springframework.stereotype.Component; - -@Component -public class CustomizeTimeBoxGenerator { - - private final CustomizeTimeBoxRepository customizeTimeBoxRepository; - - public CustomizeTimeBoxGenerator(CustomizeTimeBoxRepository customizeTimeBoxRepository) { - this.customizeTimeBoxRepository = customizeTimeBoxRepository; - } - - public CustomizeTimeBox generate(CustomizeTable testTable, CustomizeBoxType boxType, int sequence) { - CustomizeTimeBox timeBox = new CustomizeTimeBox( - testTable, - sequence, - Stance.PROS, - "입론", - boxType, - 180, - "콜리" - ); - return customizeTimeBoxRepository.save(timeBox); - } - - public CustomizeTimeBox generateNotExistSpeaker(CustomizeTable testTable, CustomizeBoxType boxType, int sequence) { - CustomizeTimeBox timeBox = new CustomizeTimeBox( - testTable, - sequence, - Stance.PROS, - "입론", - boxType, - 180, - null - ); - return customizeTimeBoxRepository.save(timeBox); - } -} diff --git a/src/test/java/com/debatetimer/fixture/domain/BellGenerator.java b/src/test/java/com/debatetimer/fixture/domain/BellGenerator.java new file mode 100644 index 00000000..4db99724 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/domain/BellGenerator.java @@ -0,0 +1,23 @@ +package com.debatetimer.fixture.domain; + +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.BellType; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class BellGenerator { + + public List generate(int size) { + List bells = new ArrayList<>(); + for (int i = 0; i < size; i++) { + bells.add(generate()); + } + return bells; + } + + private Bell generate() { + return new Bell(BellType.AFTER_START, 10, 1); + } +} diff --git a/src/test/java/com/debatetimer/fixture/domain/CustomizeTableGenerator.java b/src/test/java/com/debatetimer/fixture/domain/CustomizeTableGenerator.java new file mode 100644 index 00000000..3efb05cc --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/domain/CustomizeTableGenerator.java @@ -0,0 +1,36 @@ +package com.debatetimer.fixture.domain; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import java.time.LocalDateTime; +import org.springframework.stereotype.Component; + +@Component +public class CustomizeTableGenerator { + + public CustomizeTable generate(Member member) { + return new CustomizeTable( + member, + "토론 테이블", + "주제", + "찬성", + "반대", + false, + false, + LocalDateTime.now() + ); + } + + public CustomizeTable generate(Member member, String name) { + return new CustomizeTable( + member, + name, + "주제", + "찬성", + "반대", + false, + false, + LocalDateTime.now() + ); + } +} diff --git a/src/test/java/com/debatetimer/fixture/domain/CustomizeTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/domain/CustomizeTimeBoxGenerator.java new file mode 100644 index 00000000..35bc429e --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/domain/CustomizeTimeBoxGenerator.java @@ -0,0 +1,32 @@ +package com.debatetimer.fixture.domain; + +import com.debatetimer.domain.customize.Bell; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.customize.NormalTimeBox; +import com.debatetimer.domain.customize.Stance; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class CustomizeTimeBoxGenerator { + + public CustomizeTimeBox generate(List bells) { + return new NormalTimeBox( + Stance.PROS, + "입론", + "콜리", + 10, + bells + ); + } + + public CustomizeTimeBox generate(List bells, String speechType) { + return new NormalTimeBox( + Stance.PROS, + speechType, + "콜리", + 10, + bells + ); + } +} diff --git a/src/test/java/com/debatetimer/fixture/entity/BellEntityGenerator.java b/src/test/java/com/debatetimer/fixture/entity/BellEntityGenerator.java new file mode 100644 index 00000000..53f5d421 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/entity/BellEntityGenerator.java @@ -0,0 +1,22 @@ +package com.debatetimer.fixture.entity; + +import com.debatetimer.domain.customize.BellType; +import com.debatetimer.entity.customize.BellEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; +import com.debatetimer.repository.customize.BellRepository; +import org.springframework.stereotype.Component; + +@Component +public class BellEntityGenerator { + + private final BellRepository bellRepository; + + public BellEntityGenerator(BellRepository bellRepository) { + this.bellRepository = bellRepository; + } + + public BellEntity generate(CustomizeTimeBoxEntity timeBox, BellType type, int time, int count) { + BellEntity bell = new BellEntity(timeBox, type, time, count); + return bellRepository.save(bell); + } +} diff --git a/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java b/src/test/java/com/debatetimer/fixture/entity/CustomizeTableEntityGenerator.java similarity index 53% rename from src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java rename to src/test/java/com/debatetimer/fixture/entity/CustomizeTableEntityGenerator.java index f7420bf5..93ec96aa 100644 --- a/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java +++ b/src/test/java/com/debatetimer/fixture/entity/CustomizeTableEntityGenerator.java @@ -1,29 +1,33 @@ -package com.debatetimer.fixture; +package com.debatetimer.fixture.entity; import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; +import com.debatetimer.entity.customize.CustomizeTableEntity; import com.debatetimer.repository.customize.CustomizeTableRepository; +import java.time.LocalDateTime; import org.springframework.stereotype.Component; @Component -public class CustomizeTableGenerator { +public class CustomizeTableEntityGenerator { private final CustomizeTableRepository customizeTableRepository; - public CustomizeTableGenerator(CustomizeTableRepository customizeTableRepository) { + public CustomizeTableEntityGenerator(CustomizeTableRepository customizeTableRepository) { this.customizeTableRepository = customizeTableRepository; } - public CustomizeTable generate(Member member) { - CustomizeTable table = new CustomizeTable( + public CustomizeTableEntity generate(Member member) { + CustomizeTable customizeTable = new CustomizeTable( member, "토론 테이블", "주제", + "찬성", + "반대", false, false, - "찬성", - "반대" + LocalDateTime.now() ); + CustomizeTableEntity table = new CustomizeTableEntity(customizeTable); return customizeTableRepository.save(table); } } diff --git a/src/test/java/com/debatetimer/fixture/entity/CustomizeTimeBoxEntityGenerator.java b/src/test/java/com/debatetimer/fixture/entity/CustomizeTimeBoxEntityGenerator.java new file mode 100644 index 00000000..fd0d8bf6 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/entity/CustomizeTimeBoxEntityGenerator.java @@ -0,0 +1,61 @@ +package com.debatetimer.fixture.entity; + +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; +import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; +import org.springframework.stereotype.Component; + +@Component +public class CustomizeTimeBoxEntityGenerator { + + private final CustomizeTimeBoxRepository customizeTimeBoxRepository; + + public CustomizeTimeBoxEntityGenerator(CustomizeTimeBoxRepository customizeTimeBoxRepository) { + this.customizeTimeBoxRepository = customizeTimeBoxRepository; + } + + public CustomizeTimeBoxEntity generate(CustomizeTableEntity testTable, CustomizeBoxType boxType, int sequence) { + CustomizeTimeBoxEntity timeBox = new CustomizeTimeBoxEntity( + testTable, + sequence, + Stance.PROS, + "입론", + boxType, + 180, + "콜리" + ); + return customizeTimeBoxRepository.save(timeBox); + } + + public CustomizeTimeBoxEntity generate(CustomizeTableEntity testTable, + CustomizeBoxType boxType, + int sequence, + int time) { + CustomizeTimeBoxEntity timeBox = new CustomizeTimeBoxEntity( + testTable, + sequence, + Stance.PROS, + "입론", + boxType, + time, + "콜리" + ); + return customizeTimeBoxRepository.save(timeBox); + } + + public CustomizeTimeBoxEntity generateNotExistSpeaker(CustomizeTableEntity testTable, CustomizeBoxType boxType, + int sequence) { + CustomizeTimeBoxEntity timeBox = new CustomizeTimeBoxEntity( + testTable, + sequence, + Stance.PROS, + "입론", + boxType, + 180, + null + ); + return customizeTimeBoxRepository.save(timeBox); + } +} diff --git a/src/test/java/com/debatetimer/fixture/MemberGenerator.java b/src/test/java/com/debatetimer/fixture/entity/MemberGenerator.java similarity index 92% rename from src/test/java/com/debatetimer/fixture/MemberGenerator.java rename to src/test/java/com/debatetimer/fixture/entity/MemberGenerator.java index d0aa97bb..69dc56a3 100644 --- a/src/test/java/com/debatetimer/fixture/MemberGenerator.java +++ b/src/test/java/com/debatetimer/fixture/entity/MemberGenerator.java @@ -1,4 +1,4 @@ -package com.debatetimer.fixture; +package com.debatetimer.fixture.entity; import com.debatetimer.domain.member.Member; import com.debatetimer.repository.member.MemberRepository; diff --git a/src/test/java/com/debatetimer/fixture/entity/PollEntityGenerator.java b/src/test/java/com/debatetimer/fixture/entity/PollEntityGenerator.java new file mode 100644 index 00000000..5f01a4b5 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/entity/PollEntityGenerator.java @@ -0,0 +1,31 @@ +package com.debatetimer.fixture.entity; + +import com.debatetimer.domain.poll.Poll; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.repository.poll.PollRepository; +import org.springframework.stereotype.Component; + +@Component +public class PollEntityGenerator { + + private final PollRepository pollRepository; + + public PollEntityGenerator(final PollRepository pollRepository) { + this.pollRepository = pollRepository; + } + + public PollEntity generate(CustomizeTableEntity customizeTableEntity, PollStatus status) { + Poll poll = new Poll( + null, + customizeTableEntity.getId(), + customizeTableEntity.getMember().getId(), + status, + "찬성", + "반대", + "주제" + ); + return pollRepository.save(new PollEntity(poll)); + } +} diff --git a/src/test/java/com/debatetimer/fixture/entity/VoteEntityGenerator.java b/src/test/java/com/debatetimer/fixture/entity/VoteEntityGenerator.java new file mode 100644 index 00000000..b083ce45 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/entity/VoteEntityGenerator.java @@ -0,0 +1,27 @@ +package com.debatetimer.fixture.entity; + +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.entity.poll.VoteEntity; +import com.debatetimer.repository.poll.VoteRepository; +import java.util.UUID; +import org.springframework.stereotype.Component; + +@Component +public class VoteEntityGenerator { + + private final VoteRepository voteRepository; + + public VoteEntityGenerator(VoteRepository voteRepository) { + this.voteRepository = voteRepository; + } + + public VoteEntity generate(PollEntity pollEntity, VoteTeam team, String name) { + return generate(pollEntity, team, name, UUID.randomUUID().toString()); + } + + public VoteEntity generate(PollEntity pollEntity, VoteTeam team, String name, String code) { + VoteEntity vote = new VoteEntity(null, pollEntity, team, name, code); + return voteRepository.save(vote); + } +} diff --git a/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java b/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java index 338f7430..ab973a1e 100644 --- a/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java @@ -1,9 +1,10 @@ package com.debatetimer.repository; import com.debatetimer.config.JpaAuditingConfig; -import com.debatetimer.fixture.CustomizeTableGenerator; -import com.debatetimer.fixture.CustomizeTimeBoxGenerator; -import com.debatetimer.fixture.MemberGenerator; +import com.debatetimer.fixture.entity.BellEntityGenerator; +import com.debatetimer.fixture.entity.CustomizeTableEntityGenerator; +import com.debatetimer.fixture.entity.CustomizeTimeBoxEntityGenerator; +import com.debatetimer.fixture.entity.MemberGenerator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; @@ -11,8 +12,9 @@ @Import({ JpaAuditingConfig.class, MemberGenerator.class, - CustomizeTableGenerator.class, - CustomizeTimeBoxGenerator.class + CustomizeTableEntityGenerator.class, + CustomizeTimeBoxEntityGenerator.class, + BellEntityGenerator.class, }) @DataJpaTest public abstract class BaseRepositoryTest { @@ -21,8 +23,11 @@ public abstract class BaseRepositoryTest { protected MemberGenerator memberGenerator; @Autowired - protected CustomizeTableGenerator customizeTableGenerator; + protected CustomizeTableEntityGenerator customizeTableEntityGenerator; @Autowired - protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; + protected CustomizeTimeBoxEntityGenerator customizeTimeBoxEntityGenerator; + + @Autowired + protected BellEntityGenerator bellEntityGenerator; } diff --git a/src/test/java/com/debatetimer/repository/customize/BellRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/BellRepositoryTest.java new file mode 100644 index 00000000..7747fb03 --- /dev/null +++ b/src/test/java/com/debatetimer/repository/customize/BellRepositoryTest.java @@ -0,0 +1,48 @@ +package com.debatetimer.repository.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.customize.BellType; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.member.Member; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; +import com.debatetimer.repository.BaseRepositoryTest; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class BellRepositoryTest extends BaseRepositoryTest { + + @Autowired + private BellRepository bellRepository; + + @Nested + class DeleteAllByTable { + + @Test + void 특정_테이블에_해당하는_벨을_삭제한다() { + Member member = memberGenerator.generate("chan@gmail.com"); + CustomizeTableEntity deleteBellTable = customizeTableEntityGenerator.generate(member); + CustomizeTableEntity otherTable = customizeTableEntityGenerator.generate(member); + CustomizeTimeBoxEntity deleteBellTimeBox = customizeTimeBoxEntityGenerator.generate(deleteBellTable, + CustomizeBoxType.NORMAL, 1); + CustomizeTimeBoxEntity otherTimeBox = customizeTimeBoxEntityGenerator.generate(otherTable, + CustomizeBoxType.NORMAL, 1); + bellEntityGenerator.generate(deleteBellTimeBox, BellType.AFTER_START, 45, 1); + bellEntityGenerator.generate(deleteBellTimeBox, BellType.AFTER_START, 60, 1); + bellEntityGenerator.generate(otherTimeBox, BellType.AFTER_START, 45, 1); + + bellRepository.deleteAllByTable(deleteBellTable.getId()); + + assertAll( + () -> assertThat(bellRepository.findAllByCustomizeTimeBoxIn(List.of(deleteBellTimeBox))).isEmpty(), + () -> assertThat(bellRepository.findAllByCustomizeTimeBoxIn(List.of(otherTimeBox))).hasSize(1) + ); + + } + } + +} diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTableEntityRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTableEntityRepositoryTest.java new file mode 100644 index 00000000..428ae069 --- /dev/null +++ b/src/test/java/com/debatetimer/repository/customize/CustomizeTableEntityRepositoryTest.java @@ -0,0 +1,62 @@ +package com.debatetimer.repository.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.BaseRepositoryTest; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CustomizeTableRepositoryTest extends BaseRepositoryTest { + + @Autowired + private CustomizeTableRepository tableRepository; + + @Nested + class FindAllByMember { + + @Test + void 특정_회원의_테이블만_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member bito = memberGenerator.generate("default2@gmail.com"); + CustomizeTableEntity chanTable1 = customizeTableEntityGenerator.generate(chan); + CustomizeTableEntity chanTable2 = customizeTableEntityGenerator.generate(chan); + customizeTableEntityGenerator.generate(bito); + + List foundKeoChanTables = tableRepository.findAllByMember(chan); + + assertThat(foundKeoChanTables).containsExactly(chanTable1, chanTable2); + } + } + + @Nested + class GetByIdAndMember { + + @Test + void 특정_회원의_테이블을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(chan); + + CustomizeTableEntity foundTable = tableRepository.getByIdAndMember(table.getId(), chan); + + assertThat(foundTable).isEqualTo(table); + } + + @Test + void 존재하지_않는_테이블을_조회하면_예외를_던진다() { + Member chan = memberGenerator.generate("default@gmail.com"); + customizeTableEntityGenerator.generate(chan); + long nonExistTableId = 99999999L; + + assertThatThrownBy(() -> tableRepository.getByIdAndMember(nonExistTableId, chan)) + .isInstanceOf(DTClientErrorException.class) + .hasMessageContaining(ClientErrorCode.TABLE_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java deleted file mode 100644 index 69482003..00000000 --- a/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.debatetimer.repository.customize; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.debatetimer.domain.customize.CustomizeTable; -import com.debatetimer.domain.member.Member; -import com.debatetimer.repository.BaseRepositoryTest; -import java.util.List; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -class CustomizeTableRepositoryTest extends BaseRepositoryTest { - - @Autowired - private CustomizeTableRepository tableRepository; - - @Nested - class FindAllByMember { - - @Test - void 특정_회원의_테이블만_조회한다() { - Member chan = memberGenerator.generate("default@gmail.com"); - Member bito = memberGenerator.generate("default2@gmail.com"); - CustomizeTable chanTable1 = customizeTableGenerator.generate(chan); - CustomizeTable chanTable2 = customizeTableGenerator.generate(chan); - customizeTableGenerator.generate(bito); - - List foundKeoChanTables = tableRepository.findAllByMember(chan); - - assertThat(foundKeoChanTables).containsExactly(chanTable1, chanTable2); - } - } -} diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java index 151b62ee..5fcf6b39 100644 --- a/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java @@ -1,11 +1,12 @@ package com.debatetimer.repository.customize; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import com.debatetimer.domain.customize.CustomizeBoxType; -import com.debatetimer.domain.customize.CustomizeTable; -import com.debatetimer.domain.customize.CustomizeTimeBox; import com.debatetimer.domain.member.Member; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; import com.debatetimer.repository.BaseRepositoryTest; import java.util.List; import org.junit.jupiter.api.Nested; @@ -18,22 +19,65 @@ class CustomizeTimeBoxRepositoryTest extends BaseRepositoryTest { private CustomizeTimeBoxRepository customizeTimeBoxRepository; @Nested - class FindAllByCustomizeTable { + class FindAllByCustomizeTableEntity { @Test void 특정_테이블의_타임박스를_모두_조회한다() { Member chan = memberGenerator.generate("default@gmail.com"); Member bito = memberGenerator.generate("default2@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); - CustomizeTable bitoTable = customizeTableGenerator.generate(bito); - CustomizeTimeBox chanBox1 = customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); - CustomizeTimeBox chanBox2 = customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); + CustomizeTimeBoxEntity chanBox1 = customizeTimeBoxEntityGenerator.generate(chanTable, + CustomizeBoxType.NORMAL, 1); + CustomizeTimeBoxEntity chanBox2 = customizeTimeBoxEntityGenerator.generate(chanTable, + CustomizeBoxType.NORMAL, 2); + customizeTimeBoxEntityGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + customizeTimeBoxEntityGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); - List foundBoxes = customizeTimeBoxRepository.findAllByCustomizeTable(chanTable); + List foundBoxes = customizeTimeBoxRepository.findAllByCustomizeTable(chanTable); assertThat(foundBoxes).containsExactly(chanBox1, chanBox2); } } + + @Nested + class DeleteAllByTable { + + @Test + void 특정_테이블의_타임박스를_모두_삭제한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + + customizeTimeBoxRepository.deleteAllByTable(chanTable.getId()); + + List timeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable(chanTable); + assertThat(timeBoxes).isEmpty(); + } + + @Test + void 특정_테이블의_타임_박스를_삭제해도_다른_테이블의_타임_박스는_삭제되지_않는다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity filledTable = customizeTableEntityGenerator.generate(chan); + customizeTimeBoxEntityGenerator.generate(filledTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generate(filledTable, CustomizeBoxType.NORMAL, 2); + CustomizeTableEntity deletedTable = customizeTableEntityGenerator.generate(chan); + customizeTimeBoxEntityGenerator.generate(deletedTable, CustomizeBoxType.NORMAL, 1); + + customizeTimeBoxRepository.deleteAllByTable(deletedTable.getId()); + + List timeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable(filledTable); + assertThat(timeBoxes).hasSize(2); + } + + @Test + void 테이블의_타임_박스가_없을_경우_타임_박스_삭제_시_예외가_발생하지_않는다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity emptyTable = customizeTableEntityGenerator.generate(chan); + + assertThatCode(() -> customizeTimeBoxRepository.deleteAllByTable(emptyTable.getId())) + .doesNotThrowAnyException(); + } + } } diff --git a/src/test/java/com/debatetimer/service/BaseServiceTest.java b/src/test/java/com/debatetimer/service/BaseServiceTest.java index f512da4e..3bd0a8c1 100644 --- a/src/test/java/com/debatetimer/service/BaseServiceTest.java +++ b/src/test/java/com/debatetimer/service/BaseServiceTest.java @@ -1,12 +1,17 @@ package com.debatetimer.service; import com.debatetimer.DataBaseCleaner; -import com.debatetimer.fixture.CustomizeTableGenerator; -import com.debatetimer.fixture.CustomizeTimeBoxGenerator; -import com.debatetimer.fixture.MemberGenerator; +import com.debatetimer.fixture.entity.BellEntityGenerator; +import com.debatetimer.fixture.entity.CustomizeTableEntityGenerator; +import com.debatetimer.fixture.entity.CustomizeTimeBoxEntityGenerator; +import com.debatetimer.fixture.entity.MemberGenerator; +import com.debatetimer.fixture.entity.PollEntityGenerator; +import com.debatetimer.fixture.entity.VoteEntityGenerator; +import com.debatetimer.repository.customize.BellRepository; import com.debatetimer.repository.customize.CustomizeTableRepository; import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; import com.debatetimer.repository.member.MemberRepository; +import com.debatetimer.repository.poll.PollRepository; import java.util.List; import java.util.stream.IntStream; import org.junit.jupiter.api.extension.ExtendWith; @@ -26,14 +31,29 @@ public abstract class BaseServiceTest { @Autowired protected CustomizeTimeBoxRepository customizeTimeBoxRepository; + @Autowired + protected BellRepository bellRepository; + + @Autowired + protected PollRepository pollRepository; + @Autowired protected MemberGenerator memberGenerator; @Autowired - protected CustomizeTableGenerator customizeTableGenerator; + protected CustomizeTableEntityGenerator customizeTableEntityGenerator; + + @Autowired + protected CustomizeTimeBoxEntityGenerator customizeTimeBoxEntityGenerator; + + @Autowired + protected BellEntityGenerator bellEntityGenerator; + + @Autowired + protected PollEntityGenerator pollEntityGenerator; @Autowired - protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; + protected VoteEntityGenerator voteEntityGenerator; protected void runAtSameTime(int count, Runnable task) throws InterruptedException { List threads = IntStream.range(0, count) diff --git a/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java index 71a10611..664a50c5 100644 --- a/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java +++ b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java @@ -4,15 +4,18 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.BellType; import com.debatetimer.domain.customize.CustomizeBoxType; -import com.debatetimer.domain.customize.CustomizeTable; -import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.customize.Stance; import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.request.BellRequest; import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTableInfoCreateRequest; import com.debatetimer.dto.customize.request.CustomizeTimeBoxCreateRequest; import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.entity.customize.BellEntity; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.customize.CustomizeTimeBoxEntity; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.service.BaseServiceTest; @@ -39,20 +42,23 @@ class Save { "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자1"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), 60, null, "발언자1"), new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자2") + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), 60, null, "발언자2") ) ); CustomizeTableResponse savedTableResponse = customizeService.save(customizeTableCreateRequest, chan); - Optional foundTable = customizeTableRepository.findById(savedTableResponse.id()); - List foundTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( - foundTable.get()); + CustomizeTableEntity foundTable = customizeTableRepository.getByIdAndMember(savedTableResponse.id(), chan); + List foundTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + foundTable); + List foundBells = bellRepository.findAllByCustomizeTimeBoxIn(foundTimeBoxes); assertAll( - () -> assertThat(foundTable.get().getName()).isEqualTo(customizeTableCreateRequest.info().name()), - () -> assertThat(foundTimeBoxes).hasSize(customizeTableCreateRequest.table().size()) + () -> assertThat(foundTable.getName()).isEqualTo(customizeTableCreateRequest.info().name()), + () -> assertThat(foundTimeBoxes).hasSize(customizeTableCreateRequest.table().size()), + () -> assertThat(foundBells).hasSize(3) ); } } @@ -63,15 +69,20 @@ class FindTable { @Test void 사용자_지정_토론_테이블을_조회한다() { Member chan = memberGenerator.generate("default@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); - customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); + CustomizeTimeBoxEntity customizeTimeBox = customizeTimeBoxEntityGenerator.generate( + chanTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + bellEntityGenerator.generate(customizeTimeBox, BellType.AFTER_START, 1, 1); + bellEntityGenerator.generate(customizeTimeBox, BellType.AFTER_START, 1, 2); CustomizeTableResponse foundResponse = customizeService.findTable(chanTable.getId(), chan); assertAll( () -> assertThat(foundResponse.id()).isEqualTo(chanTable.getId()), - () -> assertThat(foundResponse.table()).hasSize(2) + () -> assertThat(foundResponse.table()).hasSize(2), + () -> assertThat(foundResponse.table().get(0).bell()).hasSize(2), + () -> assertThat(foundResponse.table().get(1).bell()).isNull() ); } @@ -79,7 +90,7 @@ class FindTable { void 회원_소유가_아닌_테이블_조회_시_예외를_발생시킨다() { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); long chanTableId = chanTable.getId(); assertThatThrownBy(() -> customizeService.findTable(chanTableId, coli)) @@ -94,28 +105,31 @@ class UpdateTable { @Test void 사용자_지정_토론_테이블을_수정한다() { Member chan = memberGenerator.generate("default@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); CustomizeTableCreateRequest renewTableRequest = new CustomizeTableCreateRequest( new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자1"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), 60, null, "발언자1"), new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자2") + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), 60, null, "발언자2") ) ); customizeService.updateTable(renewTableRequest, chanTable.getId(), chan); - Optional updatedTable = customizeTableRepository.findById(chanTable.getId()); - List updatedTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( - updatedTable.get()); + CustomizeTableEntity updatedTable = customizeTableRepository.getByIdAndMember(chanTable.getId(), chan); + List updatedTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + updatedTable); + List bells = bellRepository.findAllByCustomizeTimeBoxIn(updatedTimeBoxes); assertAll( - () -> assertThat(updatedTable.get().getId()).isEqualTo(chanTable.getId()), - () -> assertThat(updatedTable.get().getName()).isEqualTo(renewTableRequest.info().name()), - () -> assertThat(updatedTimeBoxes).hasSize(renewTableRequest.table().size()) + () -> assertThat(updatedTable.getId()).isEqualTo(chanTable.getId()), + () -> assertThat(updatedTable.getName()).isEqualTo(renewTableRequest.info().name()), + () -> assertThat(updatedTimeBoxes).hasSize(renewTableRequest.table().size()), + () -> assertThat(bells).hasSize(3) ); } @@ -123,16 +137,17 @@ class UpdateTable { void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); long chanTableId = chanTable.getId(); CustomizeTableCreateRequest renewTableRequest = new CustomizeTableCreateRequest( new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자1"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), 60, null, "발언자1"), new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자2") + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), 60, null, "발언자2") ) ); @@ -144,15 +159,16 @@ class UpdateTable { @Test void 테이블_정보_수정을_동시에_요청할_때_동시에_처리하지_않는다() throws InterruptedException { Member member = memberGenerator.generate("default@gmail.com"); - CustomizeTable table = customizeTableGenerator.generate(member); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", "반대", true, true), List.of( new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자1"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), 60, null, "발언자1"), new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자2") + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), 60, null, "발언자2") ) ); @@ -168,15 +184,15 @@ class UpdateUsedAt { @Test void 사용자_지정_토론_테이블의_사용_시각을_최신화한다() { Member member = memberGenerator.generate("default@gmail.com"); - CustomizeTable table = customizeTableGenerator.generate(member); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); LocalDateTime beforeUsedAt = table.getUsedAt(); customizeService.updateUsedAt(table.getId(), member); - Optional updatedTable = customizeTableRepository.findById(table.getId()); + CustomizeTableEntity updatedTable = customizeTableRepository.getByIdAndMember(table.getId(), member); assertAll( - () -> assertThat(updatedTable.get().getId()).isEqualTo(table.getId()), - () -> assertThat(updatedTable.get().getUsedAt()).isAfter(beforeUsedAt) + () -> assertThat(updatedTable.getId()).isEqualTo(table.getId()), + () -> assertThat(updatedTable.getUsedAt()).isAfter(beforeUsedAt) ); } @@ -184,20 +200,10 @@ class UpdateUsedAt { void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); long chanTableId = chanTable.getId(); - CustomizeTableCreateRequest renewTableRequest = new CustomizeTableCreateRequest( - new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", - "반대", true, true), - List.of( - new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자1"), - new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자2") - ) - ); - assertThatThrownBy(() -> customizeService.updateTable(renewTableRequest, chanTableId, coli)) + assertThatThrownBy(() -> customizeService.updateUsedAt(chanTableId, coli)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.TABLE_NOT_FOUND.getMessage()); } @@ -209,19 +215,21 @@ class DeleteTable { @Test void 사용자_지정_토론_테이블을_삭제한다() { Member chan = memberGenerator.generate("default@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); - customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); customizeService.deleteTable(chanTable.getId(), chan); - Optional foundTable = customizeTableRepository.findById(chanTable.getId()); - List timeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + Optional foundTable = customizeTableRepository.findById(chanTable.getId()); + List timeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( chanTable); + List bells = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxes); assertAll( () -> assertThat(foundTable).isEmpty(), - () -> assertThat(timeBoxes).isEmpty() + () -> assertThat(timeBoxes).isEmpty(), + () -> assertThat(bells).isEmpty() ); } @@ -229,7 +237,7 @@ class DeleteTable { void 회원_소유가_아닌_테이블_삭제_시_예외를_발생시킨다() { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); - CustomizeTable chanTable = customizeTableGenerator.generate(chan); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); long chanTableId = chanTable.getId(); assertThatThrownBy(() -> customizeService.deleteTable(chanTableId, coli)) diff --git a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java index 1777798b..185476e9 100644 --- a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java +++ b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java @@ -3,11 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.MemberCreateResponse; import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; +import com.debatetimer.entity.customize.CustomizeTableEntity; import com.debatetimer.service.BaseServiceTest; import java.util.Optional; import org.junit.jupiter.api.Nested; @@ -52,8 +52,8 @@ class GetTables { @Test void 회원의_전체_토론_시간표를_조회한다() { Member member = memberGenerator.generate("default@gmail.com"); - customizeTableGenerator.generate(member); - customizeTableGenerator.generate(member); + customizeTableEntityGenerator.generate(member); + customizeTableEntityGenerator.generate(member); TableResponses response = memberService.getTables(member.getId()); @@ -63,7 +63,7 @@ class GetTables { @Test void 회원의_전체_토론_시간표는_정해진_순서대로_반환한다() throws InterruptedException { Member member = memberGenerator.generate("default@gmail.com"); - CustomizeTable table = customizeTableGenerator.generate(member); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); Thread.sleep(1); TableResponses response = memberService.getTables(member.getId()); diff --git a/src/test/java/com/debatetimer/service/poll/PollServiceTest.java b/src/test/java/com/debatetimer/service/poll/PollServiceTest.java new file mode 100644 index 00000000..4dd3e913 --- /dev/null +++ b/src/test/java/com/debatetimer/service/poll/PollServiceTest.java @@ -0,0 +1,79 @@ +package com.debatetimer.service.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.response.PollCreateResponse; +import com.debatetimer.dto.poll.response.PollInfoResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.service.BaseServiceTest; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class PollServiceTest extends BaseServiceTest { + + @Autowired + private PollService pollService; + + @Nested + class CreatePoll { + + @Test + void 선거를_생성한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + + PollCreateResponse createdPoll = pollService.create(table.getId(), member); + + Optional foundPoll = pollRepository.findById(createdPoll.id()); + assertThat(foundPoll).isPresent(); + } + } + + @Nested + class GetPollInfo { + + @Test + void 선거_정보를_읽어온다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "비토"); + voteEntityGenerator.generate(pollEntity, VoteTeam.CONS, "커찬"); + + PollInfoResponse pollInfo = pollService.getPollInfo(table.getId(), member); + + assertAll( + () -> assertThat(pollInfo.id()).isEqualTo(pollEntity.getId()), + () -> assertThat(pollInfo.prosTeamName()).isEqualTo(pollEntity.getProsTeamName()), + () -> assertThat(pollInfo.consTeamName()).isEqualTo(pollEntity.getConsTeamName()), + () -> assertThat(pollInfo.status()).isEqualTo(pollEntity.getStatus()), + () -> assertThat(pollInfo.totalCount()).isEqualTo(3L), + () -> assertThat(pollInfo.prosCount()).isEqualTo(2L), + () -> assertThat(pollInfo.consCount()).isEqualTo(1L) + ); + } + } + + @Nested + class FinishPoll { + + @Test + void 선거를_완료상태로_변경한다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + pollEntityGenerator.generate(table, PollStatus.PROGRESS); + + PollInfoResponse pollInfo = pollService.finishPoll(table.getId(), member); + + assertThat(pollInfo.status()).isEqualTo(PollStatus.DONE); + } + } +} diff --git a/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java new file mode 100644 index 00000000..e2f26ecb --- /dev/null +++ b/src/test/java/com/debatetimer/service/poll/VoteServiceTest.java @@ -0,0 +1,121 @@ +package com.debatetimer.service.poll; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.poll.PollStatus; +import com.debatetimer.domain.poll.VoteTeam; +import com.debatetimer.dto.poll.request.VoteRequest; +import com.debatetimer.dto.poll.response.VoteCreateResponse; +import com.debatetimer.dto.poll.response.VoterPollInfoResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import com.debatetimer.entity.poll.PollEntity; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.poll.VoteRepository; +import com.debatetimer.service.BaseServiceTest; +import java.util.UUID; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class VoteServiceTest extends BaseServiceTest { + + @Autowired + private VoteService voteService; + + @Autowired + private VoteRepository voteRepository; + + @Nested + class Vote { + + @Test + void 진행_중인_선거에_최초로_투표_할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + String participateCode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); + + VoteCreateResponse response = voteService.vote(pollEntity.getId(), voteRequest); + + assertAll( + () -> assertThat(response.name()).isEqualTo(voteRequest.name()), + () -> assertThat(response.participateCode()).isEqualTo(voteRequest.participateCode()), + () -> assertThat(response.team()).isEqualTo(voteRequest.team()) + ); + } + + @Test + void 이미_참여한_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + String participateCode = UUID.randomUUID().toString(); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리", participateCode); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); + + assertThatThrownBy(() -> voteService.vote(pollEntity.getId(), voteRequest)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.ALREADY_VOTED_PARTICIPANT.getMessage()); + } + + @Test + void 투표_동시성_이슈에_단일_표만_유효하게_취급한다() throws InterruptedException { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + String participateCode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); + + runAtSameTime(2, () -> voteService.vote(pollEntity.getId(), voteRequest)); + + long voteCount = voteRepository.count(); + assertThat(voteCount).isEqualTo(1); + } + + @Test + void 끝난_선거에_투표_할_수_없다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity alreadyDonePoll = pollEntityGenerator.generate(table, PollStatus.DONE); + String participateCode = UUID.randomUUID().toString(); + VoteRequest voteRequest = new VoteRequest("콜리", participateCode, VoteTeam.PROS); + + assertThatThrownBy(() -> voteService.vote(alreadyDonePoll.getId(), voteRequest)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.ALREADY_DONE_POLL.getMessage()); + } + } + + @Nested + class GetVoterPollInfo { + + @Test + void 투표자가_선거정보를_조회할_수_있다() { + Member member = memberGenerator.generate("email@email.com"); + CustomizeTableEntity table = customizeTableEntityGenerator.generate(member); + PollEntity pollEntity = pollEntityGenerator.generate(table, PollStatus.PROGRESS); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "콜리"); + voteEntityGenerator.generate(pollEntity, VoteTeam.PROS, "비토"); + voteEntityGenerator.generate(pollEntity, VoteTeam.CONS, "커찬"); + + VoterPollInfoResponse response = voteService.getVoterPollInfo(pollEntity.getId()); + + assertAll( + () -> assertThat(response.id()).isEqualTo(pollEntity.getId()), + () -> assertThat(response.prosTeamName()).isEqualTo(pollEntity.getProsTeamName()), + () -> assertThat(response.consTeamName()).isEqualTo(pollEntity.getConsTeamName()), + () -> assertThat(response.status()).isEqualTo(pollEntity.getStatus()), + () -> assertThat(response.totalCount()).isEqualTo(3L), + () -> assertThat(response.participateCode()).isNotBlank(), + () -> assertThat(response.prosCount()).isEqualTo(2L), + () -> assertThat(response.consCount()).isEqualTo(1L) + ); + } + } +}