diff --git a/build.gradle b/build.gradle index 1c12f1fd..c35390a3 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,17 @@ dependencies { implementation 'org.apache.poi:poi-ooxml:5.2.3' implementation 'org.apache.poi:poi:5.2.3' + // Logging + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" + + // Discord + implementation 'net.dv8tion:JDA:5.0.0-beta.24' + + // Monitoring + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' + // DB schema manager implementation 'org.flywaydb:flyway-mysql' @@ -64,10 +75,6 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2' - - // Logging - implementation 'org.springframework.boot:spring-boot-starter-log4j2' - implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" } bootJar { diff --git a/scripts/dev/replace-new-version.sh b/scripts/dev/replace-new-version.sh index e86b27e8..3fd434fb 100644 --- a/scripts/dev/replace-new-version.sh +++ b/scripts/dev/replace-new-version.sh @@ -19,4 +19,4 @@ fi JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1) -sudo nohup java -Dspring.profiles.active=dev -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & +sudo nohup java -Dspring.profiles.active=dev,monitor -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & diff --git a/scripts/prod/replace-new-version.sh b/scripts/prod/replace-new-version.sh index d78b646f..ea5d2914 100644 --- a/scripts/prod/replace-new-version.sh +++ b/scripts/prod/replace-new-version.sh @@ -19,4 +19,4 @@ fi JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1) -sudo nohup java -Dspring.profiles.active=prod -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & +sudo nohup java -Dspring.profiles.active=prod,monitor -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & diff --git a/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java b/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java new file mode 100644 index 00000000..59ff577b --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java @@ -0,0 +1,23 @@ +package com.debatetimer.client.notifier; + +import java.util.Arrays; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ConsoleNotifier implements ErrorNotifier { + + private static final String ERROR_SEND_MESSAGE = "에러 정보가 채널로 발송되었습니다"; + + @Override + public void sendErrorMessage(Throwable throwable) { + log.error("{} : {}", ERROR_SEND_MESSAGE, throwable); + log.error(getStackTraceAsString(throwable)); + } + + private String getStackTraceAsString(Throwable throwable) { + return Arrays.stream(throwable.getStackTrace()) + .map(StackTraceElement::toString) + .collect(Collectors.joining(System.lineSeparator())); + } +} diff --git a/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java new file mode 100644 index 00000000..c4bb1529 --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java @@ -0,0 +1,56 @@ +package com.debatetimer.client.notifier; + +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import java.util.Arrays; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; + +@Slf4j +public class DiscordNotifier implements ErrorNotifier { + + private static final String NOTIFICATION_PREFIX = ":rotating_light: [**Error 발생!**]\n"; + private static final String STACK_TRACE_AFFIX = "\n```\n"; + private static final String DISCORD_LINE_SEPARATOR = "\n"; + private static final int STACK_TRACE_LENGTH = 10; + + private final DiscordProperties properties; + private final JDA jda; + + public DiscordNotifier(DiscordProperties discordProperties) { + this.properties = discordProperties; + this.jda = initializeJda(properties.getToken()); + } + + private JDA initializeJda(String token) { + try { + return JDABuilder.createDefault(token).build().awaitReady(); + } catch (InterruptedException e) { + throw new DTInitializationException(InitializationErrorCode.JDA_INITIALIZATION_FAIL); + } + } + + public void sendErrorMessage(Throwable throwable) { + TextChannel channel = jda.getTextChannelById(properties.getChannelId()); + String errorMessage = throwable.toString(); + String stackTrace = getStackTraceAsString(throwable); + + String errorNotification = NOTIFICATION_PREFIX + + errorMessage + + STACK_TRACE_AFFIX + + stackTrace + + STACK_TRACE_AFFIX; + channel.sendMessage(errorNotification).queue(); + } + + private String getStackTraceAsString(Throwable throwable) { + return Arrays.stream(throwable.getStackTrace()) + .map(StackTraceElement::toString) + .limit(STACK_TRACE_LENGTH) + .collect(Collectors.joining(DISCORD_LINE_SEPARATOR)); + } +} + diff --git a/src/main/java/com/debatetimer/client/notifier/DiscordProperties.java b/src/main/java/com/debatetimer/client/notifier/DiscordProperties.java new file mode 100644 index 00000000..0012e83e --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/DiscordProperties.java @@ -0,0 +1,27 @@ +package com.debatetimer.client.notifier; + +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@ConfigurationProperties(prefix = "discord") +public class DiscordProperties { + + private final String token; + private final String channelId; + + public DiscordProperties(String token, String channelId) { + validate(token); + validate(channelId); + this.token = token; + this.channelId = channelId; + } + + private void validate(String element) { + if (element == null || element.isBlank()) { + throw new DTInitializationException(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY); + } + } +} diff --git a/src/main/java/com/debatetimer/client/notifier/ErrorNotifier.java b/src/main/java/com/debatetimer/client/notifier/ErrorNotifier.java new file mode 100644 index 00000000..4385976c --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/ErrorNotifier.java @@ -0,0 +1,6 @@ +package com.debatetimer.client.notifier; + +public interface ErrorNotifier { + + void sendErrorMessage(Throwable throwable); +} diff --git a/src/main/java/com/debatetimer/client/OAuthClient.java b/src/main/java/com/debatetimer/client/oauth/OAuthClient.java similarity index 97% rename from src/main/java/com/debatetimer/client/OAuthClient.java rename to src/main/java/com/debatetimer/client/oauth/OAuthClient.java index 56523120..3e041f3e 100644 --- a/src/main/java/com/debatetimer/client/OAuthClient.java +++ b/src/main/java/com/debatetimer/client/oauth/OAuthClient.java @@ -1,4 +1,4 @@ -package com.debatetimer.client; +package com.debatetimer.client.oauth; import com.debatetimer.aop.logging.LoggingClient; import com.debatetimer.dto.member.MemberCreateRequest; diff --git a/src/main/java/com/debatetimer/client/OAuthProperties.java b/src/main/java/com/debatetimer/client/oauth/OAuthProperties.java similarity index 97% rename from src/main/java/com/debatetimer/client/OAuthProperties.java rename to src/main/java/com/debatetimer/client/oauth/OAuthProperties.java index bb5ac44c..56598a13 100644 --- a/src/main/java/com/debatetimer/client/OAuthProperties.java +++ b/src/main/java/com/debatetimer/client/oauth/OAuthProperties.java @@ -1,4 +1,4 @@ -package com.debatetimer.client; +package com.debatetimer.client.oauth; import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.exception.custom.DTInitializationException; diff --git a/src/main/java/com/debatetimer/config/AuthenticationConfig.java b/src/main/java/com/debatetimer/config/AuthenticationConfig.java index 3b1073fa..4afdc6cf 100644 --- a/src/main/java/com/debatetimer/config/AuthenticationConfig.java +++ b/src/main/java/com/debatetimer/config/AuthenticationConfig.java @@ -1,6 +1,6 @@ package com.debatetimer.config; -import com.debatetimer.client.OAuthProperties; +import com.debatetimer.client.oauth.OAuthProperties; import com.debatetimer.controller.tool.jwt.JwtTokenProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/debatetimer/config/NotifierConfig.java b/src/main/java/com/debatetimer/config/NotifierConfig.java new file mode 100644 index 00000000..0235a042 --- /dev/null +++ b/src/main/java/com/debatetimer/config/NotifierConfig.java @@ -0,0 +1,39 @@ +package com.debatetimer.config; + + +import com.debatetimer.client.notifier.ConsoleNotifier; +import com.debatetimer.client.notifier.DiscordNotifier; +import com.debatetimer.client.notifier.DiscordProperties; +import com.debatetimer.client.notifier.ErrorNotifier; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +public class NotifierConfig { + + @Profile({"dev", "prod"}) + @Configuration + @RequiredArgsConstructor + @EnableConfigurationProperties(DiscordProperties.class) + public static class DiscordNotifierConfig { + + private final DiscordProperties discordProperties; + + @Bean + public ErrorNotifier discordNotifier() { + return new DiscordNotifier(discordProperties); + } + } + + @Profile({"test", "local"}) + @Configuration + public static class ConsoleNotifierConfig { + + @Bean + public ErrorNotifier consoleNotifier() { + return new ConsoleNotifier(); + } + } +} diff --git a/src/main/java/com/debatetimer/controller/customize/CustomizeController.java b/src/main/java/com/debatetimer/controller/customize/CustomizeController.java new file mode 100644 index 00000000..6716d9f1 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/customize/CustomizeController.java @@ -0,0 +1,72 @@ +package com.debatetimer.controller.customize; + +import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.service.customize.CustomizeService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +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.PutMapping; +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 CustomizeController { + + private final CustomizeService customizeService; + + @PostMapping("/api/table/customize") + @ResponseStatus(HttpStatus.CREATED) + public CustomizeTableResponse save( + @Valid @RequestBody CustomizeTableCreateRequest tableCreateRequest, + @AuthMember Member member + ) { + return customizeService.save(tableCreateRequest, member); + } + + @GetMapping("/api/table/customize/{tableId}") + @ResponseStatus(HttpStatus.OK) + public CustomizeTableResponse getTable( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return customizeService.findTable(tableId, member); + } + + @PutMapping("/api/table/customize/{tableId}") + @ResponseStatus(HttpStatus.OK) + public CustomizeTableResponse updateTable( + @Valid @RequestBody CustomizeTableCreateRequest tableCreateRequest, + @PathVariable Long tableId, + @AuthMember Member member + ) { + return customizeService.updateTable(tableCreateRequest, tableId, member); + } + + @PatchMapping("/api/table/customize/{tableId}/debate") + @ResponseStatus(HttpStatus.OK) + public CustomizeTableResponse debate( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return customizeService.updateUsedAt(tableId, member); + } + + @DeleteMapping("/api/table/customize/{tableId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteTable( + @PathVariable Long tableId, + @AuthMember Member member + ) { + customizeService.deleteTable(tableId, member); + } +} diff --git a/src/main/java/com/debatetimer/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java index dbbb83f9..41161031 100644 --- a/src/main/java/com/debatetimer/domain/DebateTable.java +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -20,7 +20,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class DebateTable extends BaseTimeEntity { - private static final String NAME_REGEX = "^[a-zA-Z가-힣0-9 ]+$"; + 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; @NotNull @@ -68,7 +68,7 @@ protected final void updateTable(DebateTable renewTable) { } private void validate(String name) { - if (name.isBlank() || name.length() > NAME_MAX_LENGTH) { + if (name.length() > NAME_MAX_LENGTH) { throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_LENGTH); } if (!name.matches(NAME_REGEX)) { diff --git a/src/main/java/com/debatetimer/domain/DebateTimeBox.java b/src/main/java/com/debatetimer/domain/DebateTimeBox.java index 0538104a..c0409032 100644 --- a/src/main/java/com/debatetimer/domain/DebateTimeBox.java +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -15,6 +15,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class DebateTimeBox { + public static final int SPEAKER_MAX_LENGTH = 5; + private int sequence; @NotNull @@ -22,17 +24,30 @@ public abstract class DebateTimeBox { private Stance stance; private int time; - private Integer speaker; + private String speaker; - protected DebateTimeBox(int sequence, Stance stance, int time, Integer speaker) { + protected DebateTimeBox(int sequence, Stance stance, int time, String speaker) { + validateSpeaker(speaker); validateSequence(sequence); validateTime(time); - validateSpeakerNumber(speaker); this.sequence = sequence; this.stance = stance; this.time = time; - this.speaker = speaker; + this.speaker = initializeSpeaker(speaker); + } + + 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) { @@ -46,10 +61,4 @@ private void validateTime(int time) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); } } - - private void validateSpeakerNumber(Integer speaker) { - if (speaker != null && speaker <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER); - } - } } diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java index 4b6e43eb..0bc38a83 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java @@ -3,6 +3,8 @@ import com.debatetimer.domain.DebateTable; 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.GeneratedValue; import jakarta.persistence.GenerationType; @@ -17,6 +19,9 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CustomizeTable extends DebateTable { + private static final String TEAM_NAME_REGEX = "^[\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\s]+$"; + public static final int TEAM_NAME_MAX_LENGTH = 8; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -37,10 +42,27 @@ public CustomizeTable( String consTeamName ) { super(member, name, agenda, warningBell, finishBell); + validateTeamName(prosTeamName); + validateTeamName(consTeamName); this.prosTeamName = prosTeamName; this.consTeamName = consTeamName; } + public void update(CustomizeTable renewTable) { + this.prosTeamName = renewTable.getProsTeamName(); + this.consTeamName = renewTable.getConsTeamName(); + updateTable(renewTable); + } + + 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); + } + } + @Override public long getId() { return id; diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java index 5c3028c4..cc98e171 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java @@ -24,6 +24,9 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CustomizeTimeBox extends DebateTimeBox { + public static final int SPEECH_TYPE_MAX_LENGTH = 10; + public static final int TIME_MULTIPLIER = 2; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -49,11 +52,12 @@ public CustomizeTimeBox( Stance stance, String speechType, CustomizeBoxType boxType, - int time, - Integer speaker + Integer time, + String speaker ) { super(sequence, stance, time, speaker); validateNotTimeBasedType(boxType); + validateSpeechType(speechType); this.customizeTable = customizeTable; this.speechType = speechType; @@ -66,15 +70,14 @@ public CustomizeTimeBox( Stance stance, String speechType, CustomizeBoxType boxType, - int time, - int timePerTeam, + Integer timePerTeam, Integer timePerSpeaking, - Integer speaker + String speaker ) { - super(sequence, stance, time, speaker); - validateTime(timePerTeam, timePerSpeaking); - validateTimeBasedTime(time, timePerTeam); + super(sequence, stance, convertToTime(timePerTeam), speaker); + validateTimeBasedTimes(timePerTeam, timePerSpeaking); validateTimeBasedType(boxType); + validateSpeechType(speechType); this.customizeTable = customizeTable; this.speechType = speechType; @@ -83,26 +86,31 @@ public CustomizeTimeBox( this.timePerSpeaking = timePerSpeaking; } - private void validateTime(int time) { - if (time <= 0) { + private static int convertToTime(Integer timePerTeam) { + if (timePerTeam == null) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + return timePerTeam * TIME_MULTIPLIER; + } + + private void validateTime(Integer time) { + if (time == null || time <= 0) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); } } - private void validateTime(int timePerTeam, int timePerSpeaking) { + 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 validateTimeBasedTime(int time, int timePerTeam) { - if (time != timePerTeam * 2) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE); - } - } - private void validateTimeBasedType(CustomizeBoxType boxType) { if (boxType.isNotTimeBased()) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); @@ -114,4 +122,10 @@ private void validateNotTimeBasedType(CustomizeBoxType boxType) { 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); + } + } } diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java index fa66cff2..a987552d 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java @@ -44,16 +44,38 @@ public ParliamentaryTimeBox( int time, Integer speaker ) { - super(sequence, stance, time, speaker); + super(sequence, stance, time, convertToSpeaker(speaker)); validate(stance, type); + validateSpeakerNumber(speaker); this.parliamentaryTable = parliamentaryTable; this.type = type; } + private static String convertToSpeaker(Integer speakerNumber) { + if (speakerNumber == null) { + return null; + } + return String.valueOf(speakerNumber); + } + private void validate(Stance stance, ParliamentaryBoxType boxType) { if (!boxType.isAvailable(stance)) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); } } + + private void validateSpeakerNumber(Integer speaker) { + if (speaker != null && speaker <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER); + } + } + + public Integer getSpeakerNumber() { + String speaker = getSpeaker(); + if (speaker == null || speaker.isBlank() || speaker.equals("null")) { + return null; + } + return Integer.parseInt(speaker); + } } diff --git a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java index 27184f61..0995acf0 100644 --- a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java @@ -23,6 +23,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TimeBasedTimeBox extends DebateTimeBox { + public static final int TIME_MULTIPLIER = 2; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -47,7 +49,7 @@ public TimeBasedTimeBox( int time, Integer speaker ) { - super(sequence, stance, time, speaker); + super(sequence, stance, time, String.valueOf(speaker)); validateStance(stance, type); validateNotTimeBasedType(type); @@ -60,16 +62,15 @@ public TimeBasedTimeBox( int sequence, Stance stance, TimeBasedBoxType type, - int time, int timePerTeam, int timePerSpeaking, Integer speaker ) { - super(sequence, stance, time, speaker); + super(sequence, stance, timePerTeam * TIME_MULTIPLIER, String.valueOf(speaker)); validateTime(timePerTeam, timePerSpeaking); - validateTimeBasedTime(time, timePerTeam); validateStance(stance, type); validateTimeBasedType(type); + validateSpeakerNumber(speaker); this.timeBasedTable = timeBasedTable; this.type = type; @@ -91,12 +92,6 @@ private void validateTime(int timePerTeam, int timePerSpeaking) { } } - private void validateTimeBasedTime(int time, int timePerTeam) { - if (time != timePerTeam * 2) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE); - } - } - private void validateStance(Stance stance, TimeBasedBoxType boxType) { if (!boxType.isAvailable(stance)) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); @@ -114,4 +109,10 @@ private void validateNotTimeBasedType(TimeBasedBoxType boxType) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); } } + + private void validateSpeakerNumber(Integer speaker) { + if (speaker != null && speaker <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER); + } + } } diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java new file mode 100644 index 00000000..da62fb26 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java @@ -0,0 +1,26 @@ +package com.debatetimer.dto.customize.request; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +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 TimeBoxes toTimeBoxes(CustomizeTable customizeTable) { + return IntStream.range(0, table.size()) + .mapToObj(i -> table.get(i).toTimeBox(customizeTable, i + 1)) + .collect(Collectors.collectingAndThen(Collectors.toList(), TimeBoxes::new)); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java new file mode 100644 index 00000000..c83a7d5d --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.customize.request; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record CustomizeTableInfoCreateRequest( + @NotBlank + String name, + + @NotNull + String agenda, + + @NotBlank + String prosTeamName, + + @NotBlank + String consTeamName, + + boolean warningBell, + boolean finishBell +) { + + public CustomizeTable toTable(Member member) { + return new CustomizeTable(member, name, agenda, warningBell, finishBell, prosTeamName, consTeamName); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java new file mode 100644 index 00000000..d5c6d320 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java @@ -0,0 +1,41 @@ +package com.debatetimer.dto.customize.request; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.springframework.lang.Nullable; + +public record CustomizeTimeBoxCreateRequest( + @NotNull + Stance stance, + + @NotBlank + String speechType, + + @NotNull + CustomizeBoxType boxType, + + @Nullable + Integer time, + + @Nullable + Integer timePerTeam, + + @Nullable + Integer timePerSpeaking, + + @Nullable + String speaker +) { + + public CustomizeTimeBox toTimeBox(CustomizeTable customizeTable, int sequence) { + if (boxType.isTimeBased()) { + return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, timePerTeam, + timePerSpeaking, speaker); + } + return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, time, speaker); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableInfoResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableInfoResponse.java new file mode 100644 index 00000000..380cd7de --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableInfoResponse.java @@ -0,0 +1,27 @@ +package com.debatetimer.dto.customize.response; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.dto.member.TableType; + +public record CustomizeTableInfoResponse( + String name, + TableType type, + String agenda, + String prosTeamName, + String consTeamName, + boolean warningBell, + boolean finishBell +) { + + public CustomizeTableInfoResponse(CustomizeTable customizeTable) { + this( + customizeTable.getName(), + TableType.CUSTOMIZE, + customizeTable.getAgenda(), + customizeTable.getProsTeamName(), + customizeTable.getConsTeamName(), + customizeTable.isWarningBell(), + customizeTable.isFinishBell() + ); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java new file mode 100644 index 00000000..bbd1bf95 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.customize.response; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import java.util.List; + +public record CustomizeTableResponse(long id, CustomizeTableInfoResponse info, List table) { + + public CustomizeTableResponse( + CustomizeTable customizeTable, + TimeBoxes timeBasedTimeBoxes + ) { + this( + customizeTable.getId(), + new CustomizeTableInfoResponse(customizeTable), + toTimeBoxResponses(timeBasedTimeBoxes) + ); + } + + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List customizeTimeBoxes = timeBoxes.getTimeBoxes(); + return customizeTimeBoxes + .stream() + .map(CustomizeTimeBoxResponse::new) + .toList(); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java new file mode 100644 index 00000000..113bd23e --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java @@ -0,0 +1,35 @@ +package com.debatetimer.dto.customize.response; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTimeBox; + +public record CustomizeTimeBoxResponse( + Stance stance, + String speechType, + CustomizeBoxType boxType, + Integer time, + Integer timePerTeam, + Integer timePerSpeaking, + String speaker +) { + + public CustomizeTimeBoxResponse(CustomizeTimeBox customizeTimeBox) { + this( + customizeTimeBox.getStance(), + customizeTimeBox.getSpeechType(), + customizeTimeBox.getBoxType(), + convertTime(customizeTimeBox), + customizeTimeBox.getTimePerTeam(), + customizeTimeBox.getTimePerSpeaking(), + customizeTimeBox.getSpeaker() + ); + } + + private static Integer convertTime(CustomizeTimeBox customizeTimeBox) { + if (customizeTimeBox.getBoxType() == CustomizeBoxType.TIME_BASED) { + return null; + } + return customizeTimeBox.getTime(); + } +} diff --git a/src/main/java/com/debatetimer/dto/member/TableResponses.java b/src/main/java/com/debatetimer/dto/member/TableResponses.java index 651acfbc..51cbde50 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponses.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponses.java @@ -1,8 +1,8 @@ package com.debatetimer.dto.member; import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.timebased.TimeBasedTable; import java.util.Comparator; import java.util.List; import java.util.stream.Stream; @@ -13,14 +13,19 @@ public record TableResponses(List tables) { .comparing(DebateTable::getUsedAt) .reversed(); - public TableResponses(List parliamentaryTables, - List timeBasedTables) { - this(toTableResponses(parliamentaryTables, timeBasedTables)); + public TableResponses( + List parliamentaryTables, + List customizeTables + ) { + this(toTableResponses(parliamentaryTables, customizeTables)); } - private static List toTableResponses(List parliamentaryTables, - List timeBasedTables) { - return Stream.concat(parliamentaryTables.stream(), timeBasedTables.stream()) + private static List toTableResponses( + List parliamentaryTables, + List customizeTables + ) { + return Stream.of(parliamentaryTables, customizeTables) + .flatMap(List::stream) .sorted(DEBATE_TABLE_COMPARATOR) .map(TableResponse::new) .toList(); diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java index 5c193733..9e86c58a 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -4,12 +4,15 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; +import jakarta.validation.Valid; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; -public record ParliamentaryTableCreateRequest(ParliamentaryTableInfoCreateRequest info, - List table) { +public record ParliamentaryTableCreateRequest( + @Valid ParliamentaryTableInfoCreateRequest info, + @Valid List table +) { public ParliamentaryTable toTable(Member member) { return info.toTable(member); diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java index 9313ea90..7cf15f01 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java @@ -1,17 +1,17 @@ package com.debatetimer.dto.parliamentary.request; -import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.Stance; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; public record ParliamentaryTimeBoxCreateRequest( - @NotBlank + @NotNull Stance stance, - @NotBlank + @NotNull ParliamentaryBoxType type, @Positive diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java index 6701c51e..0d4dbb63 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java @@ -10,7 +10,7 @@ public record ParliamentaryTableResponse(long id, ParliamentaryTableInfoResponse public ParliamentaryTableResponse( ParliamentaryTable parliamentaryTable, - TimeBoxes parliamentaryTimeBoxes + TimeBoxes parliamentaryTimeBoxes ) { this( parliamentaryTable.getId(), @@ -19,8 +19,8 @@ public ParliamentaryTableResponse( ); } - private static List toTimeBoxResponses(TimeBoxes timeBoxes) { - List parliamentaryTimeBoxes = (List) timeBoxes.getTimeBoxes(); + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List parliamentaryTimeBoxes = timeBoxes.getTimeBoxes(); return parliamentaryTimeBoxes .stream() .map(ParliamentaryTimeBoxResponse::new) diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java index 70ae83c6..d30cfade 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java @@ -10,7 +10,7 @@ public ParliamentaryTimeBoxResponse(ParliamentaryTimeBox parliamentaryTimeBox) { this(parliamentaryTimeBox.getStance(), parliamentaryTimeBox.getType(), parliamentaryTimeBox.getTime(), - parliamentaryTimeBox.getSpeaker() + parliamentaryTimeBox.getSpeakerNumber() ); } } diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java index 3012958a..c051f94c 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java @@ -4,12 +4,15 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import jakarta.validation.Valid; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; -public record TimeBasedTableCreateRequest(TimeBasedTableInfoCreateRequest info, - List table) { +public record TimeBasedTableCreateRequest( + @Valid TimeBasedTableInfoCreateRequest info, + @Valid List table +) { public TimeBasedTable toTable(Member member) { return info.toTable(member); diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java index d9419190..d3da4518 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java @@ -4,13 +4,13 @@ import com.debatetimer.domain.timebased.TimeBasedBoxType; import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.domain.timebased.TimeBasedTimeBox; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record TimeBasedTimeBoxCreateRequest( - @NotBlank + @NotNull Stance stance, - @NotBlank + @NotNull TimeBasedBoxType type, int time, @@ -21,7 +21,7 @@ public record TimeBasedTimeBoxCreateRequest( public TimeBasedTimeBox toTimeBox(TimeBasedTable timeBasedTable, int sequence) { if (type.isTimeBased()) { - return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, time, timePerTeam, timePerSpeaking, + return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, timePerTeam, timePerSpeaking, speakerNumber); } return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, time, speakerNumber); diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java index a1f3d608..1c19a87e 100644 --- a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java @@ -9,7 +9,7 @@ public record TimeBasedTableResponse(long id, TimeBasedTableInfoResponse info, L public TimeBasedTableResponse( TimeBasedTable timeBasedTable, - TimeBoxes timeBasedTimeBoxes + TimeBoxes timeBasedTimeBoxes ) { this( timeBasedTable.getId(), @@ -18,8 +18,8 @@ public TimeBasedTableResponse( ); } - private static List toTimeBoxResponses(TimeBoxes timeBoxes) { - List timeBasedTimeBoxes = (List) timeBoxes.getTimeBoxes(); + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List timeBasedTimeBoxes = timeBoxes.getTimeBoxes(); return timeBasedTimeBoxes .stream() .map(TimeBasedTimeBoxResponse::new) diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java index 1a094aa2..587d95ac 100644 --- a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java @@ -19,7 +19,14 @@ public TimeBasedTimeBoxResponse(TimeBasedTimeBox timeBasedTimeBox) { timeBasedTimeBox.getTime(), timeBasedTimeBox.getTimePerTeam(), timeBasedTimeBox.getTimePerSpeaking(), - timeBasedTimeBox.getSpeaker() + getSpeakerNumber(timeBasedTimeBox) ); } + + private static Integer getSpeakerNumber(TimeBasedTimeBox timeBasedTimeBox) { + if (timeBasedTimeBox.getSpeaker() == null || timeBasedTimeBox.getSpeaker().equals("null")) { + return null; + } + return Integer.parseInt(timeBasedTimeBox.getSpeaker()); + } } diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 7134e1cd..72e772c7 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -1,6 +1,9 @@ package com.debatetimer.exception.errorcode; -import com.debatetimer.domain.parliamentary.ParliamentaryTable; +import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.DebateTimeBox; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -9,11 +12,11 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_TABLE_NAME_LENGTH( HttpStatus.BAD_REQUEST, - "테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(ParliamentaryTable.NAME_MAX_LENGTH) + "테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(DebateTable.NAME_MAX_LENGTH) ), INVALID_TABLE_NAME_FORM( HttpStatus.BAD_REQUEST, - "테이블 이름은 영문/한글/숫자/띄어쓰기만 가능합니다" + "테이블 이름에 이모지를 넣을 수 없습니다" ), INVALID_TABLE_TIME(HttpStatus.BAD_REQUEST, "시간은 양수만 가능합니다"), @@ -23,15 +26,22 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_TIME_BOX_STANCE(HttpStatus.BAD_REQUEST, "타임박스 유형과 일치하지 않는 입장입니다."), INVALID_TIME_BOX_FORMAT(HttpStatus.BAD_REQUEST, "타임박스 유형과 일치하지 않는 형식입니다"), INVALID_TIME_BASED_TIME(HttpStatus.BAD_REQUEST, "팀 발언 시간은 개인 발언 시간보다 길어야합니다"), - INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE(HttpStatus.BAD_REQUEST, "총 시간은 팀 발언 시간의 2배여야 합니다"), - - FIELD_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), - URL_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), - METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "입력한 값의 타입이 잘못되었습니다."), - NO_RESOURCE_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."), - METHOD_NOT_SUPPORTED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 메서드입니다."), - MEDIA_TYPE_NOT_SUPPORTED(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "허용되지 않은 미디어 타입입니다."), - ALREADY_DISCONNECTED(HttpStatus.BAD_REQUEST, "이미 클라이언트에서 요청이 종료되었습니다."), + INVALID_TIME_BOX_SPEECH_TYPE_LENGTH( + HttpStatus.BAD_REQUEST, + "발언 유형 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH) + ), + INVALID_TIME_BOX_SPEAKER_LENGTH( + HttpStatus.BAD_REQUEST, + "발언자 이름은 1자 이상 %d자 이하여야 합니다.".formatted(DebateTimeBox.SPEAKER_MAX_LENGTH) + ), + INVALID_TEAM_NAME_LENGTH( + HttpStatus.BAD_REQUEST, + "팀 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTable.TEAM_NAME_MAX_LENGTH) + ), + INVALID_TEAM_NAME_FORM( + HttpStatus.BAD_REQUEST, + "팀 이름에 이모지를 넣을 수 없습니다" + ), TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "토론 테이블을 찾을 수 없습니다."), NOT_TABLE_OWNER(HttpStatus.UNAUTHORIZED, "테이블을 소유한 회원이 아닙니다."), @@ -40,7 +50,15 @@ public enum ClientErrorCode implements ResponseErrorCode { EMPTY_COOKIE(HttpStatus.UNAUTHORIZED, "쿠키에 값이 없습니다"), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원이 존재하지 않습니다"), - ; + + FIELD_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), + URL_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), + METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "입력한 값의 타입이 잘못되었습니다."), + NO_RESOURCE_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."), + METHOD_NOT_SUPPORTED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 메서드입니다."), + MEDIA_TYPE_NOT_SUPPORTED(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "허용되지 않은 미디어 타입입니다."), + ALREADY_DISCONNECTED(HttpStatus.BAD_REQUEST, "이미 클라이언트에서 요청이 종료되었습니다."), + NO_COOKIE_FOUND(HttpStatus.BAD_REQUEST, "필수 쿠키 값이 존재하지 않습니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java index a037a69f..153067b0 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java @@ -8,6 +8,8 @@ public enum InitializationErrorCode { OAUTH_PROPERTIES_EMPTY("OAuth 구성 요소들이 입력되지 않았습니다"), + DISCORD_PROPERTIES_EMPTY("디스코드 봇 구성 요소들이 입력되지 않았습니다"), + JDA_INITIALIZATION_FAIL("디스코드 client 구성에 실패하였습니다"), CORS_ORIGIN_EMPTY("CORS Origin 은 적어도 한 개 있어야 합니다"), CORS_ORIGIN_STRING_BLANK("CORS Origin 에 빈 값이 들어올 수 없습니다"), diff --git a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java index efeaab4c..ee411661 100644 --- a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.debatetimer.exception.handler; +import com.debatetimer.client.notifier.ErrorNotifier; import com.debatetimer.exception.ErrorResponse; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.custom.DTServerErrorException; @@ -7,6 +8,7 @@ import com.debatetimer.exception.errorcode.ResponseErrorCode; import com.debatetimer.exception.errorcode.ServerErrorCode; import jakarta.validation.ConstraintViolationException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; import org.springframework.http.HttpStatus; @@ -14,6 +16,7 @@ import org.springframework.validation.BindException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingRequestCookieException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @@ -21,8 +24,11 @@ @Slf4j @RestControllerAdvice +@RequiredArgsConstructor public class GlobalExceptionHandler { + private final ErrorNotifier errorNotifier; + @ExceptionHandler(BindException.class) public ResponseEntity handleBindingException(BindException exception) { log.warn("message: {}", exception.getMessage()); @@ -69,6 +75,11 @@ public ResponseEntity handleNoResourceFoundException(NoResourceFo return toResponse(ClientErrorCode.NO_RESOURCE_FOUND); } + @ExceptionHandler(MissingRequestCookieException.class) + public ResponseEntity handleMissingRequestCookieException(MissingRequestCookieException exception) { + return toResponse(ClientErrorCode.NO_COOKIE_FOUND); + } + @ExceptionHandler(DTClientErrorException.class) public ResponseEntity handleClientException(DTClientErrorException exception) { log.warn("message: {}", exception.getMessage()); @@ -78,12 +89,14 @@ public ResponseEntity handleClientException(DTClientErrorExceptio @ExceptionHandler(DTServerErrorException.class) public ResponseEntity handleServerException(DTServerErrorException exception) { log.error("message: {}", exception.getMessage()); + errorNotifier.sendErrorMessage(exception); return toResponse(exception.getHttpStatus(), exception.getMessage()); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception exception) { log.error("exception: {}", exception.getMessage()); + errorNotifier.sendErrorMessage(exception); return toResponse(ServerErrorCode.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java new file mode 100644 index 00000000..e461e58c --- /dev/null +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java @@ -0,0 +1,25 @@ +package com.debatetimer.repository.customize; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.List; +import java.util.Optional; +import org.springframework.data.repository.Repository; + +public interface CustomizeTableRepository extends Repository { + + CustomizeTable save(CustomizeTable customizeTable); + + Optional findById(long id); + + default CustomizeTable getById(long tableId) { + return findById(tableId) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); + } + + List findAllByMember(Member member); + + void delete(CustomizeTable table); +} diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java new file mode 100644 index 00000000..85657e38 --- /dev/null +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java @@ -0,0 +1,34 @@ +package com.debatetimer.repository.customize; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +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 { + + CustomizeTimeBox save(CustomizeTimeBox timeBox); + + @Transactional + default List saveAll(List timeBoxes) { + return timeBoxes.stream() + .map(this::save) + .toList(); + } + + List findAllByCustomizeTable(CustomizeTable table); + + default TimeBoxes findTableTimeBoxes(CustomizeTable table) { + List timeBoxes = findAllByCustomizeTable(table); + return new TimeBoxes<>(timeBoxes); + } + + @Query("DELETE FROM CustomizeTimeBox ctb WHERE ctb IN :timeBoxes") + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Transactional + void deleteAll(List timeBoxes); +} diff --git a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java index a34e4c07..df4bdbf6 100644 --- a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java @@ -27,7 +27,7 @@ default TimeBoxes findTableTimeBoxes(TimeBasedTable table) { return new TimeBoxes<>(timeBoxes); } - @Query("DELETE FROM TimeBasedTimeBox ptb WHERE ptb IN :timeBoxes") + @Query("DELETE FROM TimeBasedTimeBox tbtb WHERE tbtb IN :timeBoxes") @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional void deleteAll(List timeBoxes); diff --git a/src/main/java/com/debatetimer/service/auth/AuthService.java b/src/main/java/com/debatetimer/service/auth/AuthService.java index aec9e8e1..2e5f37ff 100644 --- a/src/main/java/com/debatetimer/service/auth/AuthService.java +++ b/src/main/java/com/debatetimer/service/auth/AuthService.java @@ -1,6 +1,6 @@ package com.debatetimer.service.auth; -import com.debatetimer.client.OAuthClient; +import com.debatetimer.client.oauth.OAuthClient; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.dto.member.MemberInfo; diff --git a/src/main/java/com/debatetimer/service/customize/CustomizeService.java b/src/main/java/com/debatetimer/service/customize/CustomizeService.java new file mode 100644 index 00000000..6bd3ec6b --- /dev/null +++ b/src/main/java/com/debatetimer/service/customize/CustomizeService.java @@ -0,0 +1,94 @@ +package com.debatetimer.service.customize; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.member.Member; +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; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CustomizeService { + + private final CustomizeTableRepository tableRepository; + private final CustomizeTimeBoxRepository timeBoxRepository; + + @Transactional + public CustomizeTableResponse save(CustomizeTableCreateRequest tableCreateRequest, Member member) { + CustomizeTable table = tableCreateRequest.toTable(member); + CustomizeTable savedTable = tableRepository.save(table); + + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, savedTable); + return new CustomizeTableResponse(savedTable, savedTimeBoxes); + } + + @Transactional(readOnly = true) + public CustomizeTableResponse findTable(long tableId, Member member) { + CustomizeTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + return new CustomizeTableResponse(table, timeBoxes); + } + + @Transactional + public CustomizeTableResponse updateTable( + CustomizeTableCreateRequest tableCreateRequest, + long tableId, + Member member + ) { + CustomizeTable existingTable = getOwnerTable(tableId, member.getId()); + CustomizeTable renewedTable = tableCreateRequest.toTable(member); + existingTable.update(renewedTable); + + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); + timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); + return new CustomizeTableResponse(existingTable, savedTimeBoxes); + } + + @Transactional + public CustomizeTableResponse updateUsedAt(long tableId, Member member) { + CustomizeTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + table.updateUsedAt(); + + return new CustomizeTableResponse(table, timeBoxes); + } + + @Transactional + public void deleteTable(long tableId, Member member) { + CustomizeTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); + tableRepository.delete(table); + } + + private TimeBoxes saveTimeBoxes( + CustomizeTableCreateRequest tableCreateRequest, + CustomizeTable table + ) { + TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); + List savedTimeBoxes = timeBoxRepository.saveAll(timeBoxes.getTimeBoxes()); + return new TimeBoxes<>(savedTimeBoxes); + } + + private CustomizeTable getOwnerTable(long tableId, long memberId) { + CustomizeTable foundTable = tableRepository.getById(tableId); + validateOwn(foundTable, memberId); + return foundTable; + } + + private void validateOwn(CustomizeTable table, long memberId) { + if (!table.isOwner(memberId)) { + throw new DTClientErrorException(ClientErrorCode.NOT_TABLE_OWNER); + } + } +} diff --git a/src/main/java/com/debatetimer/service/member/MemberService.java b/src/main/java/com/debatetimer/service/member/MemberService.java index 67bb3b7d..210b2067 100644 --- a/src/main/java/com/debatetimer/service/member/MemberService.java +++ b/src/main/java/com/debatetimer/service/member/MemberService.java @@ -1,14 +1,14 @@ package com.debatetimer.service.member; +import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.dto.member.MemberCreateResponse; import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; +import com.debatetimer.repository.customize.CustomizeTableRepository; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; -import com.debatetimer.repository.timebased.TimeBasedTableRepository; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,14 +20,14 @@ public class MemberService { private final MemberRepository memberRepository; private final ParliamentaryTableRepository parliamentaryTableRepository; - private final TimeBasedTableRepository timeBasedTableRepository; + private final CustomizeTableRepository customizeTableRepository; @Transactional(readOnly = true) public TableResponses getTables(long memberId) { Member member = memberRepository.getById(memberId); List parliamentaryTables = parliamentaryTableRepository.findAllByMember(member); - List timeBasedTables = timeBasedTableRepository.findAllByMember(member); - return new TableResponses(parliamentaryTables, timeBasedTables); + List customizeTables = customizeTableRepository.findAllByMember(member); + return new TableResponses(parliamentaryTables, customizeTables); } @Transactional diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 51b83166..af61d46d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -33,5 +33,9 @@ jwt: access_token_expiration: ${secret.jwt.access_token_expiration} refresh_token_expiration: ${secret.jwt.refresh_token_expiration} +discord: + token: ${secret.discord.token} + channelId: ${secret.discord.channelId} + #logging: # config: classpath:logging/log4j2-dev.yml diff --git a/src/main/resources/application-monitor.yml b/src/main/resources/application-monitor.yml new file mode 100644 index 00000000..d9809cb1 --- /dev/null +++ b/src/main/resources/application-monitor.yml @@ -0,0 +1,24 @@ +management: + server: + port: 8083 + + endpoints: + web: + exposure: + include: '*' + base-path: /monitoring + + endpoint: + health: + show-components: always + + info: + java: + enabled: true + os: + enabled: true + +server: + tomcat: + mbeanregistry: + enabled: true diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 2af3608c..79040396 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -32,5 +32,9 @@ jwt: access_token_expiration: ${secret.jwt.access_token_expiration} refresh_token_expiration: ${secret.jwt.refresh_token_expiration} +discord: + token: ${secret.discord.token} + channelId: ${secret.discord.channelId} + #logging: # config: classpath:logging/log4j2-prod.yml diff --git a/src/main/resources/db/migration/V7__speaker_modify_varchar.sql b/src/main/resources/db/migration/V7__speaker_modify_varchar.sql new file mode 100644 index 00000000..54a7d7a2 --- /dev/null +++ b/src/main/resources/db/migration/V7__speaker_modify_varchar.sql @@ -0,0 +1,3 @@ +alter table parliamentary_time_box modify speaker varchar (255) null; +alter table time_based_time_box modify speaker varchar (255) null; +alter table customize_time_box modify speaker varchar (255) null; diff --git a/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java index e89fce15..fb3953ea 100644 --- a/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java +++ b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java @@ -6,10 +6,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.context.ActiveProfiles; -@SpringBootTest -@ActiveProfiles("flyway") +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@ActiveProfiles({"test", "flyway"}) class DatabaseSchemaManagerTest { @Autowired diff --git a/src/test/java/com/debatetimer/DebateTimerApplicationTest.java b/src/test/java/com/debatetimer/DebateTimerApplicationTest.java index fcf32f13..453ef46a 100644 --- a/src/test/java/com/debatetimer/DebateTimerApplicationTest.java +++ b/src/test/java/com/debatetimer/DebateTimerApplicationTest.java @@ -1,11 +1,16 @@ package com.debatetimer; +import com.debatetimer.client.notifier.ErrorNotifier; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; @SpringBootTest class DebateTimerApplicationTest { + @MockitoBean + private ErrorNotifier errorNotifier; + @Test void contextLoads() { } diff --git a/src/test/java/com/debatetimer/client/DiscordPropertiesTest.java b/src/test/java/com/debatetimer/client/DiscordPropertiesTest.java new file mode 100644 index 00000000..85fc72a4 --- /dev/null +++ b/src/test/java/com/debatetimer/client/DiscordPropertiesTest.java @@ -0,0 +1,36 @@ +package com.debatetimer.client; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.client.notifier.DiscordProperties; +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class DiscordPropertiesTest { + + @Nested + class Validate { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"\n", "\t "}) + void 디스코드봇_토큰이_비어있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new DiscordProperties(empty, "channelId")) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"\n", "\t "}) + void 디스코드_채널_아이디가_비어있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new DiscordProperties("token", empty)) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java b/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java index 1b0250f9..05fbc406 100644 --- a/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java +++ b/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.debatetimer.client.oauth.OAuthProperties; import com.debatetimer.exception.custom.DTInitializationException; import com.debatetimer.exception.errorcode.InitializationErrorCode; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 0bc58e12..c764dd15 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -1,7 +1,9 @@ package com.debatetimer.controller; import com.debatetimer.DataBaseCleaner; -import com.debatetimer.client.OAuthClient; +import com.debatetimer.client.oauth.OAuthClient; +import com.debatetimer.fixture.CustomizeTableGenerator; +import com.debatetimer.fixture.CustomizeTimeBoxGenerator; import com.debatetimer.fixture.HeaderGenerator; import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.ParliamentaryTableGenerator; @@ -9,6 +11,7 @@ import com.debatetimer.fixture.TimeBasedTableGenerator; import com.debatetimer.fixture.TimeBasedTimeBoxGenerator; import com.debatetimer.fixture.TokenGenerator; +import com.debatetimer.repository.customize.CustomizeTableRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; import com.debatetimer.repository.timebased.TimeBasedTableRepository; import io.restassured.RestAssured; @@ -33,6 +36,9 @@ public abstract class BaseControllerTest { @Autowired protected TimeBasedTableRepository timeBasedTableRepository; + @Autowired + protected CustomizeTableRepository customizeTableRepository; + @Autowired protected MemberGenerator memberGenerator; @@ -48,6 +54,12 @@ public abstract class BaseControllerTest { @Autowired protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; + @Autowired + protected CustomizeTableGenerator customizeTableGenerator; + + @Autowired + protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; + @Autowired protected HeaderGenerator headerGenerator; diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index bb92a2ed..4e70556f 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -10,6 +10,7 @@ import com.debatetimer.dto.member.JwtTokenResponse; import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.service.auth.AuthService; +import com.debatetimer.service.customize.CustomizeService; import com.debatetimer.service.member.MemberService; import com.debatetimer.service.parliamentary.ParliamentaryService; import com.debatetimer.service.timebased.TimeBasedService; @@ -63,6 +64,9 @@ public abstract class BaseDocumentTest { @MockitoBean protected TimeBasedService timeBasedService; + @MockitoBean + protected CustomizeService customizeService; + @MockitoBean protected AuthService authService; diff --git a/src/test/java/com/debatetimer/controller/Tag.java b/src/test/java/com/debatetimer/controller/Tag.java index d65c7cb2..c7dfa897 100644 --- a/src/test/java/com/debatetimer/controller/Tag.java +++ b/src/test/java/com/debatetimer/controller/Tag.java @@ -4,7 +4,9 @@ public enum Tag { MEMBER_API("Member API"), PARLIAMENTARY_API("Parliamentary Table API"), - TIME_BASED_API("Time Based Table API"); + TIME_BASED_API("Time Based Table API"), + CUSTOMIZE_API("Customize Table 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 new file mode 100644 index 00000000..bbaf87b7 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java @@ -0,0 +1,164 @@ +package com.debatetimer.controller.customize; + +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.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +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 io.restassured.http.ContentType; +import io.restassured.http.Headers; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CustomizeControllerTest extends BaseControllerTest { + + @Nested + class Save { + + @Test + void 사용자_지정_테이블을_생성한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTableCreateRequest request = 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, null) + ) + ); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .headers(headers) + .body(request) + .when().post("/api/table/customize") + .then().statusCode(201) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.info().name()).isEqualTo(request.info().name()), + () -> assertThat(response.table()).hasSize(request.table().size()) + ); + } + } + + @Nested + 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); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().get("/api/table/customize/{tableId}") + .then().statusCode(200) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), + () -> assertThat(response.table()).hasSize(2) + ); + } + } + + @Nested + class UpdateTable { + + @Test + void 사용자_지정_토론_테이블을_업데이트한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + 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, null) + ) + ); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .body(renewTableRequest) + .when().put("/api/table/customize/{tableId}") + .then().statusCode(200) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), + () -> assertThat(response.info().name()).isEqualTo(renewTableRequest.info().name()), + () -> assertThat(response.table()).hasSize(renewTableRequest.table().size()) + ); + } + } + + @Nested + 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); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().patch("/api/table/customize/{tableId}/debate") + .then().statusCode(200) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), + () -> assertThat(response.info().name()).isEqualTo(bitoTable.getName()), + () -> assertThat(response.table()).hasSize(2) + ); + } + } + + @Nested + 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); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().delete("/api/table/customize/{tableId}") + .then().statusCode(204); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java new file mode 100644 index 00000000..1f3ca5b9 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java @@ -0,0 +1,516 @@ +package com.debatetimer.controller.customize; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.OBJECT; +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.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +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.CustomizeTableInfoResponse; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.dto.customize.response.CustomizeTimeBoxResponse; +import com.debatetimer.dto.member.TableType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import io.restassured.http.ContentType; +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.EnumSource; +import org.springframework.http.HttpHeaders; + +public class CustomizeDocumentTest extends BaseDocumentTest { + + @Nested + class Save { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.CUSTOMIZE_API) + .summary("새로운 사용자 지정 토론 시간표 생성") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .requestBodyField( + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").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() + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").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() + ); + + @Test + void 사용자_지정_테이블_생성_성공() { + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).save(eq(request), any()); + + var document = document("customize/post", 201) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .body(request) + .when().post("/api/table/customize") + .then().statusCode(201); + } + + @EnumSource( + value = ClientErrorCode.class, + names = { + "INVALID_TABLE_NAME_LENGTH", + "INVALID_TABLE_NAME_FORM", + "INVALID_TABLE_TIME", + "INVALID_TIME_BOX_SEQUENCE", + "INVALID_TIME_BOX_TIME", + "INVALID_TIME_BOX_STANCE", + "INVALID_TIME_BOX_FORMAT" + } + ) + @ParameterizedTest + void 사용자_지정_테이블_생성_실패(ClientErrorCode errorCode) { + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doThrow(new DTClientErrorException(errorCode)).when(customizeService).save(eq(request), any()); + + var document = document("customize/post", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .body(request) + .when().post("/api/table/customize") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class GetTable { + + private final RestDocumentationRequest requestDocument = request() + .summary("사용자_지정 토론 시간표 조회") + .tag(Tag.CUSTOMIZE_API) + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").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() + ); + + @Test + void 사용자_지정_테이블_조회_성공() { + long tableId = 5L; + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).findTable(eq(tableId), any()); + + var document = document("customize/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().get("/api/table/customize/{tableId}") + .then().statusCode(200); + } + + @ParameterizedTest + @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) + void 사용자_지정_테이블_조회_실패(ClientErrorCode errorCode) { + long tableId = 5L; + doThrow(new DTClientErrorException(errorCode)).when(customizeService).findTable(eq(tableId), any()); + + var document = document("customize/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().get("/api/table/customize/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class UpdateTable { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.CUSTOMIZE_API) + .summary("사용자 지정 토론 시간표 수정") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ) + .requestBodyField( + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").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() + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").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() + ); + + @Test + void 사용자_지정_토론_테이블_수정() { + long tableId = 5L; + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).updateTable(eq(request), eq(tableId), any()); + + var document = document("customize/patch", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .body(request) + .when().put("/api/table/customize/{tableId}") + .then().statusCode(200); + } + + @EnumSource( + value = ClientErrorCode.class, + names = { + "INVALID_TABLE_NAME_LENGTH", + "INVALID_TABLE_NAME_FORM", + "INVALID_TABLE_TIME", + "INVALID_TIME_BOX_SEQUENCE", + "INVALID_TIME_BOX_TIME", + "INVALID_TIME_BOX_STANCE", + "INVALID_TIME_BOX_FORMAT" + } + ) + @ParameterizedTest + void 사용자_지정_테이블_생성_실패(ClientErrorCode errorCode) { + long tableId = 5L; + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doThrow(new DTClientErrorException(errorCode)).when(customizeService) + .updateTable(eq(request), eq(tableId), any()); + + var document = document("customize/patch", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .body(request) + .when().put("/api/table/customize/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class Debate { + + private final RestDocumentationRequest requestDocument = request() + .summary("사용자 지정 토론 시작") + .tag(Tag.CUSTOMIZE_API) + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").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() + ); + + + @Test + void 사용자_지정_토론_진행_성공() { + long tableId = 5L; + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).updateUsedAt(eq(tableId), any()); + + var document = document("customize/patch_debate", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/customize/{tableId}/debate") + .then().statusCode(200); + } + + @ParameterizedTest + @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) + void 사용자_지정_토론_진행_실패(ClientErrorCode errorCode) { + long tableId = 5L; + doThrow(new DTClientErrorException(errorCode)).when(customizeService).updateUsedAt(eq(tableId), any()); + + var document = document("customize/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/customize/{tableId}/debate") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class DeleteTable { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.CUSTOMIZE_API) + .summary("사용자 지정 토론 시간표 삭제") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + @Test + void 사용자_지정_테이블_삭제_성공() { + long tableId = 5L; + doNothing().when(customizeService).deleteTable(eq(tableId), any()); + + var document = document("customize/delete", 204) + .request(requestDocument) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().delete("/api/table/customize/{tableId}") + .then().statusCode(204); + } + + @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) + @ParameterizedTest + void 사용자_지정_테이블_삭제_실패(ClientErrorCode errorCode) { + long tableId = 5L; + doThrow(new DTClientErrorException(errorCode)).when(customizeService).deleteTable(eq(tableId), any()); + + var document = document("customize/delete", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().delete("/api/table/customize/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index 1594468f..0028d5aa 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.doReturn; import com.debatetimer.controller.BaseControllerTest; +import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.dto.member.MemberCreateRequest; @@ -24,7 +25,8 @@ class GetTables { void 회원의_전체_토론_시간표를_조회한다() { Member member = memberGenerator.generate("default@gmail.com"); parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", false, false)); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", false, false)); + customizeTableRepository.save(new CustomizeTable(member, "커스텀 테이블", "주제", false, false, + "찬성", "반대")); Headers headers = headerGenerator.generateAccessTokenHeader(member); @@ -71,6 +73,13 @@ class ReissueAccessToken { .when().post("/api/member/reissue") .then().statusCode(200); } + + @Test + void 토큰이_없을_경우_400_에러를_반환한다() { + given() + .when().post("/api/member/reissue") + .then().statusCode(400); + } } @Nested @@ -88,5 +97,16 @@ class Logout { .when().post("/api/member/logout") .then().statusCode(204); } + + @Test + void 토큰이_없을_경우_400_에러를_반환한다() { + Member bito = memberGenerator.generate("bito@gmail.com"); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + given() + .headers(headers) + .when().post("/api/member/logout") + .then().statusCode(400); + } } } diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index 559f4456..6e38e278 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -30,7 +30,7 @@ class Save { new ParliamentaryTableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, null) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -58,7 +58,7 @@ class GetTable { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); parliamentaryTimeBoxGenerator.generate(bitoTable, 1); - parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + parliamentaryTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() @@ -87,7 +87,7 @@ class UpdateTable { new ParliamentaryTableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, null) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -117,7 +117,7 @@ class Debate { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); parliamentaryTimeBoxGenerator.generate(bitoTable, 1); - parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + parliamentaryTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() @@ -144,7 +144,7 @@ class DeleteTable { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); parliamentaryTimeBoxGenerator.generate(bitoTable, 1); - parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + parliamentaryTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java index 034e1952..e5d41d4a 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java @@ -32,7 +32,7 @@ class Save { 1), new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, - 1))); + null))); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -58,7 +58,7 @@ class GetTable { Member bito = memberGenerator.generate("default@gmail.com"); TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); timeBasedTimeBoxGenerator.generate(bitoTable, 1); - timeBasedTimeBoxGenerator.generate(bitoTable, 2); + timeBasedTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -89,7 +89,7 @@ class UpdateTable { 1), new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, - 1))); + null))); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -117,7 +117,7 @@ class Debate { Member bito = memberGenerator.generate("default@gmail.com"); TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); timeBasedTimeBoxGenerator.generate(bitoTable, 1); - timeBasedTimeBoxGenerator.generate(bitoTable, 2); + timeBasedTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -144,7 +144,7 @@ class DeleteTable { Member bito = memberGenerator.generate("default@gmail.com"); TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); timeBasedTimeBoxGenerator.generate(bitoTable, 1); - timeBasedTimeBoxGenerator.generate(bitoTable, 2); + timeBasedTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() diff --git a/src/test/java/com/debatetimer/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java index b8ad484c..3035f0c8 100644 --- a/src/test/java/com/debatetimer/domain/DebateTableTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -20,39 +20,31 @@ class DebateTableTest { @Nested class Validate { - @ValueSource(strings = {"a bc가다9", "가0나 다ab"}) + @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfältig"}) @ParameterizedTest - void 테이블_이름은_영문과_한글_숫자_띄어쓰기만_가능하다(String name) { + void 테이블_이름은_이모지를_제외한_글자만_가능하다(String name) { Member member = new Member("default@gmail.com"); assertThatCode(() -> new DebateTableTestObject(member, name, "agenda", true, true)) .doesNotThrowAnyException(); } - @ValueSource(ints = {0, DebateTable.NAME_MAX_LENGTH + 1}) + @ValueSource(strings = {"a😀bc가다9", "🐥", "🥦"}) @ParameterizedTest - void 테이블_이름은_정해진_길이_이내여야_한다(int length) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new DebateTableTestObject(member, "f".repeat(length), "agenda", true, true)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); - } - - @ValueSource(strings = {"", "\t", "\n"}) - @ParameterizedTest - void 테이블_이름은_적어도_한_자_있어야_한다(String name) { + void 테이블_이름에_이모지를_넣을_수_없다(String name) { Member member = new Member("default@gmail.com"); assertThatThrownBy(() -> new DebateTableTestObject(member, name, "agenda", true, true)) .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); + .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); } - @ValueSource(strings = {"abc@", "가나다*", "abc\tde"}) - @ParameterizedTest - void 허용된_글자_이외의_문자는_불가능하다(String name) { + @Test + void 테이블_이름은_정해진_길이_이내여야_한다() { Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new DebateTableTestObject(member, name, "agenda", true, true)) + String longTableName = "f".repeat(DebateTable.NAME_MAX_LENGTH + 1); + + assertThatThrownBy(() -> new DebateTableTestObject(member, longTableName, "agenda", true, true)) .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); + .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); } } diff --git a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java index 52b8dba0..d6ea6af1 100644 --- a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java @@ -1,5 +1,6 @@ package com.debatetimer.domain; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -8,50 +9,69 @@ 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 DebateTimeBoxTest { @Nested - class Validate { + class ValidateSequence { @ValueSource(ints = {0, -1}) @ParameterizedTest void 순서는_양수만_가능하다(int sequence) { - assertThatThrownBy(() -> new DebateTimeBoxTestObject(sequence, Stance.CONS, 60, 1)) + assertThatThrownBy(() -> new DebateTimeBoxTestObject(sequence, Stance.CONS, 60, "발언자")) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); } + + } + + @Nested + class ValidateTime { + @ValueSource(ints = {0, -1}) @ParameterizedTest void 시간은_양수만_가능하다(int time) { assertThatThrownBy( - () -> new DebateTimeBoxTestObject(1, Stance.CONS, time, 1)) + () -> new DebateTimeBoxTestObject(1, Stance.CONS, time, "발언자")) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); } + } + + @Nested + class ValidateSpeaker { @Test - void 발표자_번호는_빈_값이_허용된다() { - Integer speaker = null; + void 발언자_이름은_일정길이_이내로_허용된다() { + String speaker = "k".repeat(DebateTimeBox.SPEAKER_MAX_LENGTH + 1); + assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH.getMessage()); + } + + @NullSource + @ParameterizedTest + void 발언자는_빈_값이_허용된다(String speaker) { assertThatCode(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) .doesNotThrowAnyException(); } - @ValueSource(ints = {0, -1}) + @ValueSource(strings = {" ", " "}) @ParameterizedTest - void 발표자_번호는_양수만_가능하다(int speaker) { - assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); + void 발언자는_공백이_입력되면_null로_저장된다(String speaker) { + DebateTimeBoxTestObject timeBox = new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker); + + assertThat(timeBox.getSpeaker()).isNull(); } } private static class DebateTimeBoxTestObject extends DebateTimeBox { - public DebateTimeBoxTestObject(int sequence, Stance stance, int time, Integer speaker) { + public DebateTimeBoxTestObject(int sequence, Stance stance, int time, String speaker) { super(sequence, stance, time, speaker); } } diff --git a/src/test/java/com/debatetimer/domain/TimeBoxesTest.java b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java index c4534a71..c3a80805 100644 --- a/src/test/java/com/debatetimer/domain/TimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java @@ -27,7 +27,7 @@ class SortedBySequence { ParliamentaryBoxType.OPENING, 300, 1); List timeBoxes = new ArrayList<>(Arrays.asList(secondBox, firstBox)); - TimeBoxes actual = new TimeBoxes(timeBoxes); + TimeBoxes actual = new TimeBoxes<>(timeBoxes); assertThat(actual.getTimeBoxes()).containsExactly(firstBox, secondBox); } diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java index 10fb988f..e2f5d389 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java @@ -1,10 +1,17 @@ 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.member.Member; import com.debatetimer.dto.member.TableType; +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 CustomizeTableTest { @@ -18,4 +25,64 @@ class GetType { assertThat(customizeTable.getType()).isEqualTo(TableType.CUSTOMIZE); } } + + @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()); + } + } } diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java index c9a65495..4d482d1d 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java @@ -1,5 +1,6 @@ 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; @@ -12,27 +13,16 @@ class CustomizeTimeBoxTest { @Nested - class ValidateCustomize { - - @Test - void 자유토론_타입은_총_시간이_팀_발언_시간의_2배여야_한다() { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - - assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 150, 120, 60, 1)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE.getMessage()); - } + class ValidateCustomizeTime { @Test void 자유토론_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { CustomizeTable table = new CustomizeTable(); CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - assertThatCode( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 240, 120, 60, 1)) - .doesNotThrowAnyException(); + assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", + customizeBoxType, 120, 60, "발언자") + ).doesNotThrowAnyException(); } @Test @@ -40,8 +30,8 @@ class ValidateCustomize { CustomizeTable table = new CustomizeTable(); CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 10, 1)) - .isInstanceOf(DTClientErrorException.class) + assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 10, + "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } @@ -51,22 +41,29 @@ class ValidateCustomize { CustomizeBoxType notTimeBasedBoxType = CustomizeBoxType.NORMAL; assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 240, 120, 60, 1)) - .isInstanceOf(DTClientErrorException.class) + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 120, 60, + "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } + @Test + void 팀_발언_시간은_있으며_개인_발언_시간은_없을_수_있다() { + CustomizeTable table = new CustomizeTable(); + Integer timePerTeam = 60; + Integer timePerSpeaking = null; + + assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); + } + @Test void 개인_발언_시간은_팀_발언_시간보다_적거나_같아야_한다() { CustomizeTable table = new CustomizeTable(); int timePerTeam = 60; int timePerSpeaking = 59; - assertThatCode( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam * 2, - timePerTeam, timePerSpeaking, 1)) - .doesNotThrowAnyException(); + assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); } @Test @@ -75,12 +72,35 @@ class ValidateCustomize { int timePerTeam = 60; int timePerSpeaking = 61; - assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam * 2, - timePerTeam, timePerSpeaking, 1)) - .isInstanceOf(DTClientErrorException.class) + assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); } + + @Test + void 발언_유형의_길이는_일정_범위_이내여야_한다() { + CustomizeTable table = new CustomizeTable(); + String longSpeechType = "s".repeat(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH + 1); + + 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()); + } + } + + @Nested + class getTime { + + @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); + } } } diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java index 59859496..dcd6cbb8 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java @@ -1,5 +1,6 @@ package com.debatetimer.domain.parliamentary; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -8,6 +9,8 @@ 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 ParliamentaryTimeBoxTest { @@ -30,4 +33,45 @@ class ValidateStance { .hasMessage(ClientErrorCode.INVALID_TIME_BOX_STANCE.getMessage()); } } + + @Nested + class ValidateSpeakerNumber { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 의회식_타임박스의_발표자_번호_음수는_불가능하다(int speakerNumber) { + ParliamentaryTable table = new ParliamentaryTable(); + + assertThatThrownBy( + () -> new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speakerNumber)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); + } + } + + @Nested + class getSpeakerNumber { + + @ValueSource(ints = {1, 5}) + @ParameterizedTest + void 의회식_타임박스의_발표자_번호는_양수만_가능하다(int speakerNumber) { + ParliamentaryTable table = new ParliamentaryTable(); + ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speakerNumber); + + Integer actual = timeBox.getSpeakerNumber(); + + assertThat(actual).isEqualTo(speakerNumber); + } + + @Test + void 의회식_타임박스의_발표자는_비어있을_수_있다() { + ParliamentaryTable table = new ParliamentaryTable(); + Integer speakerNumber = null; + ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speakerNumber); + + Integer actual = timeBox.getSpeakerNumber(); + + assertThat(actual).isEqualTo(speakerNumber); + } + } } diff --git a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java index 48f1336d..5ea3b7ae 100644 --- a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java @@ -1,5 +1,6 @@ package com.debatetimer.domain.timebased; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -8,6 +9,8 @@ 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 TimeBasedTimeBoxTest { @@ -36,23 +39,12 @@ class ValidateStance { @Nested class ValidateTimeBased { - @Test - void 시간총량제_타입은_총_시간이_팀_발언_시간의_2배여야_한다() { - TimeBasedTable table = new TimeBasedTable(); - TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; - - assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 150, 120, 60, 1)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE.getMessage()); - } - @Test void 시간총량제_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { TimeBasedTable table = new TimeBasedTable(); TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; - assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 240, 120, 60, 1)) + assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 120, 60, 1)) .doesNotThrowAnyException(); } @@ -72,7 +64,7 @@ class ValidateTimeBased { TimeBasedBoxType notTimeBasedBoxType = TimeBasedBoxType.TIME_OUT; assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, notTimeBasedBoxType, 240, 120, 60, 1)) + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, notTimeBasedBoxType, 120, 60, 1)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } @@ -84,7 +76,7 @@ class ValidateTimeBased { int timePerSpeaking = 59; assertThatCode( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam * 2, + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam, timePerSpeaking, 1)) .doesNotThrowAnyException(); } @@ -96,10 +88,40 @@ class ValidateTimeBased { int timePerSpeaking = 61; assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam * 2, + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam, timePerSpeaking, 1)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); } } + + @Nested + class ValidateSpeakerNumber { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 시간총량제_타임박스의_발표자_번호는_양수만_가능하다(int speaker) { + TimeBasedTable table = new TimeBasedTable(); + + assertThatThrownBy(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, + 120, 60, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); + } + } + + @Nested + class getTime { + + @Test + void 자유_토론_타임_박스의_시간은_팀_당_발언_시간의_배수이어야_한다() { + int timePerTeam = 300; + int timePerSpeaking = 120; + TimeBasedTable table = new TimeBasedTable(); + TimeBasedTimeBox timeBasedTimeBox = new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, 1); + + assertThat(timeBasedTimeBox.getTime()).isEqualTo(timePerTeam * TimeBasedTimeBox.TIME_MULTIPLIER); + } + } } diff --git a/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java b/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java new file mode 100644 index 00000000..f7420bf5 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java @@ -0,0 +1,29 @@ +package com.debatetimer.fixture; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.repository.customize.CustomizeTableRepository; +import org.springframework.stereotype.Component; + +@Component +public class CustomizeTableGenerator { + + private final CustomizeTableRepository customizeTableRepository; + + public CustomizeTableGenerator(CustomizeTableRepository customizeTableRepository) { + this.customizeTableRepository = customizeTableRepository; + } + + public CustomizeTable generate(Member member) { + CustomizeTable table = new CustomizeTable( + member, + "토론 테이블", + "주제", + false, + false, + "찬성", + "반대" + ); + return customizeTableRepository.save(table); + } +} diff --git a/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java new file mode 100644 index 00000000..df77fb3d --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java @@ -0,0 +1,44 @@ +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/ParliamentaryTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java index 376c9136..05107cd3 100644 --- a/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java +++ b/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java @@ -21,4 +21,10 @@ public ParliamentaryTimeBox generate(ParliamentaryTable testTable, int sequence) ParliamentaryBoxType.OPENING, 180, 1); return parliamentaryTimeBoxRepository.save(timeBox); } + + public ParliamentaryTimeBox generateNotExistSpeaker(ParliamentaryTable testTable, int sequence) { + ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(testTable, sequence, Stance.PROS, + ParliamentaryBoxType.OPENING, 180, null); + return parliamentaryTimeBoxRepository.save(timeBox); + } } diff --git a/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java index 319dfb6e..f1df0beb 100644 --- a/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java +++ b/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java @@ -21,4 +21,10 @@ public TimeBasedTimeBox generate(TimeBasedTable testTable, int sequence) { TimeBasedBoxType.OPENING, 180, 1); return timeBasedTimeBoxRepository.save(timeBox); } + + public TimeBasedTimeBox generateNotExistSpeaker(TimeBasedTable testTable, int sequence) { + TimeBasedTimeBox timeBox = new TimeBasedTimeBox(testTable, sequence, Stance.PROS, + TimeBasedBoxType.OPENING, 180, null); + return timeBasedTimeBoxRepository.save(timeBox); + } } diff --git a/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java b/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java index 91c0e36c..ea98a19a 100644 --- a/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java @@ -1,5 +1,8 @@ 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.ParliamentaryTableGenerator; import com.debatetimer.fixture.ParliamentaryTimeBoxGenerator; @@ -9,8 +12,16 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -@Import({MemberGenerator.class, ParliamentaryTableGenerator.class, ParliamentaryTimeBoxGenerator.class, - TimeBasedTableGenerator.class, TimeBasedTimeBoxGenerator.class}) +@Import({ + JpaAuditingConfig.class, + MemberGenerator.class, + ParliamentaryTableGenerator.class, + ParliamentaryTimeBoxGenerator.class, + TimeBasedTableGenerator.class, + TimeBasedTimeBoxGenerator.class, + CustomizeTableGenerator.class, + CustomizeTimeBoxGenerator.class +}) @DataJpaTest public abstract class BaseRepositoryTest { @@ -28,4 +39,10 @@ public abstract class BaseRepositoryTest { @Autowired protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; + + @Autowired + protected CustomizeTableGenerator customizeTableGenerator; + + @Autowired + protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; } diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java new file mode 100644 index 00000000..049387ff --- /dev/null +++ b/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java @@ -0,0 +1,58 @@ +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.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +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"); + CustomizeTable chanTable1 = customizeTableGenerator.generate(chan); + CustomizeTable chanTable2 = customizeTableGenerator.generate(chan); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + + List foundKeoChanTables = tableRepository.findAllByMember(chan); + + assertThat(foundKeoChanTables).containsExactly(chanTable1, chanTable2); + } + } + + @Nested + class GetById { + + @Test + void 특정_아이디의_테이블을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + + CustomizeTable foundChanTable = tableRepository.getById(chanTable.getId()); + + assertThat(foundChanTable).usingRecursiveComparison().isEqualTo(chanTable); + } + + @Test + void 특정_아이디의_테이블이_없으면_에러를_발생시킨다() { + assertThatThrownBy(() -> tableRepository.getById(1L)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.TABLE_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java new file mode 100644 index 00000000..7afe406d --- /dev/null +++ b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java @@ -0,0 +1,39 @@ +package com.debatetimer.repository.customize; + +import static org.assertj.core.api.Assertions.assertThat; + +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.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 CustomizeTimeBoxRepositoryTest extends BaseRepositoryTest { + + @Autowired + private CustomizeTimeBoxRepository customizeTimeBoxRepository; + + @Nested + class FindAllByCustomizeTable { + + @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); + CustomizeTimeBox bitoBox1 = customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + CustomizeTimeBox bitoBox2 = customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + + List foundBoxes = customizeTimeBoxRepository.findAllByCustomizeTable(chanTable); + + assertThat(foundBoxes).containsExactly(chanBox1, chanBox2); + } + } +} diff --git a/src/test/java/com/debatetimer/service/BaseServiceTest.java b/src/test/java/com/debatetimer/service/BaseServiceTest.java index 4323ae6f..69cb79bc 100644 --- a/src/test/java/com/debatetimer/service/BaseServiceTest.java +++ b/src/test/java/com/debatetimer/service/BaseServiceTest.java @@ -1,11 +1,15 @@ 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.ParliamentaryTableGenerator; import com.debatetimer.fixture.ParliamentaryTimeBoxGenerator; import com.debatetimer.fixture.TimeBasedTableGenerator; import com.debatetimer.fixture.TimeBasedTimeBoxGenerator; +import com.debatetimer.repository.customize.CustomizeTableRepository; +import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTimeBoxRepository; @@ -34,6 +38,12 @@ public abstract class BaseServiceTest { @Autowired protected TimeBasedTimeBoxRepository timeBasedTimeBoxRepository; + @Autowired + protected CustomizeTableRepository customizeTableRepository; + + @Autowired + protected CustomizeTimeBoxRepository customizeTimeBoxRepository; + @Autowired protected MemberGenerator memberGenerator; @@ -48,4 +58,10 @@ public abstract class BaseServiceTest { @Autowired protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; + + @Autowired + protected CustomizeTableGenerator customizeTableGenerator; + + @Autowired + protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; } diff --git a/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java new file mode 100644 index 00000000..3e38b993 --- /dev/null +++ b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java @@ -0,0 +1,220 @@ +package com.debatetimer.service.customize; + +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.Stance; +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.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.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.service.BaseServiceTest; +import java.time.LocalDateTime; +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 CustomizeServiceTest extends BaseServiceTest { + + @Autowired + private CustomizeService customizeService; + + @Nested + class Save { + + @Test + void 사용자_지정_토론_테이블을_생성한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableCreateRequest customizeTableCreateRequest = 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") + ) + ); + + CustomizeTableResponse savedTableResponse = customizeService.save(customizeTableCreateRequest, chan); + Optional foundTable = customizeTableRepository.findById(savedTableResponse.id()); + List foundTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + foundTable.get()); + + assertAll( + () -> assertThat(foundTable.get().getName()).isEqualTo(customizeTableCreateRequest.info().name()), + () -> assertThat(foundTimeBoxes).hasSize(customizeTableCreateRequest.table().size()) + ); + } + } + + @Nested + 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); + + CustomizeTableResponse foundResponse = customizeService.findTable(chanTable.getId(), chan); + + assertAll( + () -> assertThat(foundResponse.id()).isEqualTo(chanTable.getId()), + () -> assertThat(foundResponse.table()).hasSize(2) + ); + } + + @Test + void 회원_소유가_아닌_테이블_조회_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> customizeService.findTable(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + + @Nested + class UpdateTable { + + @Test + void 사용자_지정_토론_테이블을_수정한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + 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") + ) + ); + + customizeService.updateTable(renewTableRequest, chanTable.getId(), chan); + + Optional updatedTable = customizeTableRepository.findById(chanTable.getId()); + List updatedTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + updatedTable.get()); + + assertAll( + () -> assertThat(updatedTable.get().getId()).isEqualTo(chanTable.getId()), + () -> assertThat(updatedTable.get().getName()).isEqualTo(renewTableRequest.info().name()), + () -> assertThat(updatedTimeBoxes).hasSize(renewTableRequest.table().size()) + ); + } + + @Test + void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.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)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + + @Nested + class UpdateUsedAt { + + @Test + void 사용자_지정_토론_테이블의_사용_시각을_최신화한다() { + Member member = memberGenerator.generate("default@gmail.com"); + CustomizeTable table = customizeTableGenerator.generate(member); + LocalDateTime beforeUsedAt = table.getUsedAt(); + + customizeService.updateUsedAt(table.getId(), member); + + Optional updatedTable = customizeTableRepository.findById(table.getId()); + assertAll( + () -> assertThat(updatedTable.get().getId()).isEqualTo(table.getId()), + () -> assertThat(updatedTable.get().getUsedAt()).isAfter(beforeUsedAt) + ); + } + + @Test + void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.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)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + + @Nested + 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); + + customizeService.deleteTable(chanTable.getId(), chan); + + Optional foundTable = customizeTableRepository.findById(chanTable.getId()); + List timeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + chanTable); + + assertAll( + () -> assertThat(foundTable).isEmpty(), + () -> assertThat(timeBoxes).isEmpty() + ); + } + + @Test + void 회원_소유가_아닌_테이블_삭제_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> customizeService.deleteTable(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java index 08e3b496..17c6000c 100644 --- a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java +++ b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java @@ -3,9 +3,9 @@ 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.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.dto.member.MemberCreateResponse; import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; @@ -54,7 +54,7 @@ class GetTables { void 회원의_전체_토론_시간표를_조회한다() { Member member = memberGenerator.generate("default@gmail.com"); parliamentaryTableGenerator.generate(member); - timeBasedTableGenerator.generate(member); + customizeTableGenerator.generate(member); TableResponses response = memberService.getTables(member.getId()); @@ -65,7 +65,7 @@ class GetTables { void 회원의_전체_토론_시간표는_정해진_순서대로_반환한다() throws InterruptedException { Member member = memberGenerator.generate("default@gmail.com"); ParliamentaryTable table1 = parliamentaryTableGenerator.generate(member); - TimeBasedTable table2 = timeBasedTableGenerator.generate(member); + CustomizeTable table2 = customizeTableGenerator.generate(member); Thread.sleep(1); table1.updateUsedAt();