diff --git a/build.gradle b/build.gradle index 031c7a72..1c12f1fd 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,10 @@ configurations { compileOnly { extendsFrom annotationProcessor } + + configureEach { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + } } repositories { @@ -32,11 +36,25 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-aop' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-gson:0.11.5' + + // Excel Export + implementation 'org.apache.poi:poi-ooxml:5.2.3' + implementation 'org.apache.poi:poi:5.2.3' + + // DB schema manager + implementation 'org.flywaydb:flyway-mysql' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' @@ -47,14 +65,9 @@ dependencies { testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2' - // Excel Export - implementation 'org.apache.poi:poi-ooxml:5.2.3' - implementation 'org.apache.poi:poi:5.2.3' - - // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.11.5' - implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' - implementation 'io.jsonwebtoken:jjwt-gson:0.11.5' + // Logging + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" } bootJar { diff --git a/src/main/java/com/debatetimer/DebateTimerApplication.java b/src/main/java/com/debatetimer/DebateTimerApplication.java index 0d8907b7..acee3c1b 100644 --- a/src/main/java/com/debatetimer/DebateTimerApplication.java +++ b/src/main/java/com/debatetimer/DebateTimerApplication.java @@ -9,5 +9,4 @@ public class DebateTimerApplication { public static void main(String[] args) { SpringApplication.run(DebateTimerApplication.class, args); } - } diff --git a/src/main/java/com/debatetimer/aop/logging/ClientLoggingAspect.java b/src/main/java/com/debatetimer/aop/logging/ClientLoggingAspect.java new file mode 100644 index 00000000..ca5cb7c2 --- /dev/null +++ b/src/main/java/com/debatetimer/aop/logging/ClientLoggingAspect.java @@ -0,0 +1,47 @@ +package com.debatetimer.aop.logging; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; + +@Slf4j +@Aspect +@Component +public class ClientLoggingAspect extends LoggingAspect { + + private static final String CLIENT_REQUEST_TIME_KEY = "clientRequestTime"; + + @Pointcut("@within(com.debatetimer.aop.logging.LoggingClient)") + public void loggingClients() { + } + + @Around("loggingClients()") + public Object loggingControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + setMdc(CLIENT_REQUEST_TIME_KEY, System.currentTimeMillis()); + logClientRequest(proceedingJoinPoint); + + Object responseBody = proceedingJoinPoint.proceed(); + + logClientResponse(proceedingJoinPoint); + removeMdc(CLIENT_REQUEST_TIME_KEY); + return responseBody; + } + + private void logClientRequest(ProceedingJoinPoint joinPoint) { + String clientName = joinPoint.getSignature().getDeclaringType().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + log.info("Client Request Logging - Client Name: {} | MethodName: {}", clientName, methodName); + } + + private void logClientResponse(ProceedingJoinPoint joinPoint) { + String clientName = joinPoint.getSignature().getDeclaringType().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + long latency = getLatency(CLIENT_REQUEST_TIME_KEY); + log.info("Client Response Logging - Client Name: {} | MethodName: {} | Latency: {}ms", + clientName, methodName, latency); + } +} + diff --git a/src/main/java/com/debatetimer/aop/logging/ControllerLoggingAspect.java b/src/main/java/com/debatetimer/aop/logging/ControllerLoggingAspect.java new file mode 100644 index 00000000..c4645ee5 --- /dev/null +++ b/src/main/java/com/debatetimer/aop/logging/ControllerLoggingAspect.java @@ -0,0 +1,75 @@ +package com.debatetimer.aop.logging; + + +import jakarta.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.CodeSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Slf4j +@Aspect +@Component +public class ControllerLoggingAspect extends LoggingAspect { + + private static final String REQUEST_ID_KEY = "requestId"; + private static final String START_TIME_KEY = "startTime"; + + @Pointcut("@within(org.springframework.web.bind.annotation.RestController)") + public void allController() { + } + + @Around("allController()") + public Object loggingControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + setMdc(REQUEST_ID_KEY, UUID.randomUUID().toString()); + setMdc(START_TIME_KEY, System.currentTimeMillis()); + logControllerRequest(proceedingJoinPoint); + + Object responseBody = proceedingJoinPoint.proceed(); + + logControllerResponse(responseBody); + removeMdc(START_TIME_KEY); + return responseBody; + } + + private void logControllerRequest(ProceedingJoinPoint proceedingJoinPoint) { + HttpServletRequest request = getHttpServletRequest(); + String requestParameters = getRequestParameters(proceedingJoinPoint); + String uri = request.getRequestURI(); + String httpMethod = request.getMethod(); + log.info("Request Logging: {} {} parameters - {}", httpMethod, uri, requestParameters); + } + + private void logControllerResponse(Object responseBody) { + HttpServletRequest request = getHttpServletRequest(); + String uri = request.getRequestURI(); + String httpMethod = request.getMethod(); + long latency = getLatency(START_TIME_KEY); + log.info("Response Logging: {} {} Body: {} latency - {}ms", httpMethod, uri, responseBody, latency); + } + + private HttpServletRequest getHttpServletRequest() { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + return requestAttributes.getRequest(); + } + + private String getRequestParameters(JoinPoint joinPoint) { + CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); + String[] parameterNames = codeSignature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + Map params = new HashMap<>(); + for (int i = 0; i < parameterNames.length; i++) { + params.put(parameterNames[i], args[i]); + } + return params.toString(); + } +} diff --git a/src/main/java/com/debatetimer/aop/logging/LoggingAspect.java b/src/main/java/com/debatetimer/aop/logging/LoggingAspect.java new file mode 100644 index 00000000..07c2c7d3 --- /dev/null +++ b/src/main/java/com/debatetimer/aop/logging/LoggingAspect.java @@ -0,0 +1,21 @@ +package com.debatetimer.aop.logging; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; + +@Slf4j +public abstract class LoggingAspect { + + protected final void setMdc(String key, Object value) { + MDC.put(key, String.valueOf(value)); + } + + protected final void removeMdc(String key) { + MDC.remove(key); + } + + protected final long getLatency(String startTimeKey) { + long startTime = Long.parseLong(MDC.get(startTimeKey)); + return System.currentTimeMillis() - startTime; + } +} diff --git a/src/main/java/com/debatetimer/aop/logging/LoggingClient.java b/src/main/java/com/debatetimer/aop/logging/LoggingClient.java new file mode 100644 index 00000000..0fc01034 --- /dev/null +++ b/src/main/java/com/debatetimer/aop/logging/LoggingClient.java @@ -0,0 +1,11 @@ +package com.debatetimer.aop.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface LoggingClient { +} diff --git a/src/main/java/com/debatetimer/client/OAuthClient.java b/src/main/java/com/debatetimer/client/OAuthClient.java index e2fa1607..56523120 100644 --- a/src/main/java/com/debatetimer/client/OAuthClient.java +++ b/src/main/java/com/debatetimer/client/OAuthClient.java @@ -1,5 +1,6 @@ package com.debatetimer.client; +import com.debatetimer.aop.logging.LoggingClient; import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.OAuthToken; @@ -9,6 +10,7 @@ import org.springframework.web.client.RestClient; @Component +@LoggingClient @EnableConfigurationProperties(OAuthProperties.class) public class OAuthClient { diff --git a/src/main/java/com/debatetimer/client/OAuthProperties.java b/src/main/java/com/debatetimer/client/OAuthProperties.java index a747f84a..bb5ac44c 100644 --- a/src/main/java/com/debatetimer/client/OAuthProperties.java +++ b/src/main/java/com/debatetimer/client/OAuthProperties.java @@ -1,6 +1,8 @@ package com.debatetimer.client; import com.debatetimer.dto.member.MemberCreateRequest; +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import lombok.Getter; @@ -20,11 +22,21 @@ public OAuthProperties( String clientId, String clientSecret, String grantType) { + validate(clientId); + validate(clientSecret); + validate(grantType); + this.clientId = clientId; this.clientSecret = clientSecret; this.grantType = grantType; } + private void validate(String element) { + if (element == null || element.isBlank()) { + throw new DTInitializationException(InitializationErrorCode.OAUTH_PROPERTIES_EMPTY); + } + } + public MultiValueMap createTokenRequestBody(MemberCreateRequest request) { String code = request.code(); String decodedVerificationCode = URLDecoder.decode(code, StandardCharsets.UTF_8); diff --git a/src/main/java/com/debatetimer/config/CorsConfig.java b/src/main/java/com/debatetimer/config/CorsConfig.java index a63eb1d1..216c15da 100644 --- a/src/main/java/com/debatetimer/config/CorsConfig.java +++ b/src/main/java/com/debatetimer/config/CorsConfig.java @@ -1,5 +1,7 @@ package com.debatetimer.config; +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; @@ -13,9 +15,21 @@ public class CorsConfig implements WebMvcConfigurer { private final String[] corsOrigin; public CorsConfig(@Value("${cors.origin}") String[] corsOrigin) { + validate(corsOrigin); this.corsOrigin = corsOrigin; } + private void validate(String[] corsOrigin) { + if (corsOrigin == null || corsOrigin.length == 0) { + throw new DTInitializationException(InitializationErrorCode.CORS_ORIGIN_EMPTY); + } + for (String origin : corsOrigin) { + if (origin == null || origin.isBlank()) { + throw new DTInitializationException(InitializationErrorCode.CORS_ORIGIN_STRING_BLANK); + } + } + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -30,7 +44,7 @@ public void addCorsMappings(CorsRegistry registry) { ) .allowCredentials(true) .allowedHeaders("*") - .exposedHeaders(HttpHeaders.AUTHORIZATION); + .exposedHeaders(HttpHeaders.AUTHORIZATION, HttpHeaders.CONTENT_DISPOSITION); } } diff --git a/src/main/java/com/debatetimer/config/JpaAuditingConfig.java b/src/main/java/com/debatetimer/config/JpaAuditingConfig.java new file mode 100644 index 00000000..df13d943 --- /dev/null +++ b/src/main/java/com/debatetimer/config/JpaAuditingConfig.java @@ -0,0 +1,10 @@ +package com.debatetimer.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { + +} diff --git a/src/main/java/com/debatetimer/config/WebConfig.java b/src/main/java/com/debatetimer/config/WebConfig.java index 0ce8ab1e..5f3ed66f 100644 --- a/src/main/java/com/debatetimer/config/WebConfig.java +++ b/src/main/java/com/debatetimer/config/WebConfig.java @@ -1,11 +1,13 @@ package com.debatetimer.config; +import com.debatetimer.controller.tool.export.ExcelExportInterceptor; import com.debatetimer.controller.tool.jwt.AuthManager; import com.debatetimer.service.auth.AuthService; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @@ -19,4 +21,9 @@ public class WebConfig implements WebMvcConfigurer { public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(new AuthMemberArgumentResolver(authManager, authService)); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new ExcelExportInterceptor()); + } } diff --git a/src/main/java/com/debatetimer/controller/member/MemberController.java b/src/main/java/com/debatetimer/controller/member/MemberController.java index 25d12555..a63be8ab 100644 --- a/src/main/java/com/debatetimer/controller/member/MemberController.java +++ b/src/main/java/com/debatetimer/controller/member/MemberController.java @@ -11,22 +11,23 @@ import com.debatetimer.dto.member.TableResponses; import com.debatetimer.service.auth.AuthService; import com.debatetimer.service.member.MemberService; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor public class MemberController { + private static final String REFRESH_TOKEN_COOKIE_KEY = "refreshToken"; + private final MemberService memberService; private final AuthService authService; private final CookieManager cookieManager; @@ -38,36 +39,40 @@ public TableResponses getTables(@AuthMember Member member) { } @PostMapping("/api/member") - @ResponseStatus(HttpStatus.CREATED) - public MemberCreateResponse createMember(@RequestBody MemberCreateRequest request, HttpServletResponse response) { + public ResponseEntity createMember(@RequestBody MemberCreateRequest request) { MemberInfo memberInfo = authService.getMemberInfo(request); MemberCreateResponse memberCreateResponse = memberService.createMember(memberInfo); - JwtTokenResponse jwtTokenResponse = authManager.issueToken(memberInfo); - ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); + JwtTokenResponse jwtToken = authManager.issueToken(memberInfo); + ResponseCookie refreshTokenCookie = cookieManager.createCookie(REFRESH_TOKEN_COOKIE_KEY, + jwtToken.refreshToken(), jwtToken.refreshExpiration()); - response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken()); - response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); - return memberCreateResponse; + return ResponseEntity.status(HttpStatus.CREATED) + .header(HttpHeaders.AUTHORIZATION, jwtToken.accessToken()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) + .body(memberCreateResponse); } @PostMapping("/api/member/reissue") - public void reissueAccessToken(HttpServletRequest request, HttpServletResponse response) { - String refreshToken = cookieManager.extractRefreshToken(request.getCookies()); - JwtTokenResponse jwtTokenResponse = authManager.reissueToken(refreshToken); - ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); + public ResponseEntity reissueAccessToken(@CookieValue(REFRESH_TOKEN_COOKIE_KEY) String refreshToken) { + JwtTokenResponse jwtToken = authManager.reissueToken(refreshToken); + ResponseCookie refreshTokenCookie = cookieManager.createCookie(REFRESH_TOKEN_COOKIE_KEY, + jwtToken.refreshToken(), jwtToken.refreshExpiration()); - response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken()); - response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); + return ResponseEntity.ok() + .header(HttpHeaders.AUTHORIZATION, jwtToken.accessToken()) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()) + .build(); } @PostMapping("/api/member/logout") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void logout(@AuthMember Member member, HttpServletRequest request, HttpServletResponse response) { - String refreshToken = cookieManager.extractRefreshToken(request.getCookies()); + public ResponseEntity logout(@AuthMember Member member, + @CookieValue(REFRESH_TOKEN_COOKIE_KEY) String refreshToken) { String email = authManager.resolveRefreshToken(refreshToken); authService.logout(member, email); - ResponseCookie deletedRefreshTokenCookie = cookieManager.deleteRefreshTokenCookie(); + ResponseCookie expiredRefreshTokenCookie = cookieManager.createExpiredCookie(REFRESH_TOKEN_COOKIE_KEY); - response.addHeader(HttpHeaders.SET_COOKIE, deletedRefreshTokenCookie.toString()); + return ResponseEntity.noContent() + .header(HttpHeaders.SET_COOKIE, expiredRefreshTokenCookie.toString()) + .build(); } } diff --git a/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java index 1589899e..9a2176f2 100644 --- a/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java +++ b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java @@ -1,15 +1,20 @@ package com.debatetimer.controller.parliamentary; import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.controller.tool.export.ExcelExport; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.parliamentary.request.ParliamentaryTableCreateRequest; import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; import com.debatetimer.service.parliamentary.ParliamentaryService; +import com.debatetimer.view.exporter.ParliamentaryTableExcelExporter; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; 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; @@ -22,6 +27,7 @@ public class ParliamentaryController { private final ParliamentaryService parliamentaryService; + private final ParliamentaryTableExcelExporter parliamentaryTableExcelExporter; @PostMapping("/api/table/parliamentary") @ResponseStatus(HttpStatus.CREATED) @@ -51,6 +57,15 @@ public ParliamentaryTableResponse updateTable( return parliamentaryService.updateTable(tableCreateRequest, tableId, member); } + @PatchMapping("/api/table/parliamentary/{tableId}/debate") + @ResponseStatus(HttpStatus.OK) + public ParliamentaryTableResponse debate( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return parliamentaryService.updateUsedAt(tableId, member); + } + @DeleteMapping("/api/table/parliamentary/{tableId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteTable( @@ -59,4 +74,15 @@ public void deleteTable( ) { parliamentaryService.deleteTable(tableId, member); } + + @GetMapping("/api/table/parliamentary/export/{tableId}") + @ExcelExport + public ResponseEntity export( + @AuthMember Member member, + @PathVariable Long tableId + ) { + ParliamentaryTableResponse foundTable = parliamentaryService.findTableById(tableId, member.getId()); + InputStreamResource excelStream = parliamentaryTableExcelExporter.export(foundTable); + return ResponseEntity.ok(excelStream); + } } diff --git a/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java new file mode 100644 index 00000000..2e67ca6f --- /dev/null +++ b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java @@ -0,0 +1,72 @@ +package com.debatetimer.controller.timebased; + +import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.timebased.request.TimeBasedTableCreateRequest; +import com.debatetimer.dto.timebased.response.TimeBasedTableResponse; +import com.debatetimer.service.timebased.TimeBasedService; +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 TimeBasedController { + + private final TimeBasedService timeBasedService; + + @PostMapping("/api/table/time-based") + @ResponseStatus(HttpStatus.CREATED) + public TimeBasedTableResponse save( + @Valid @RequestBody TimeBasedTableCreateRequest tableCreateRequest, + @AuthMember Member member + ) { + return timeBasedService.save(tableCreateRequest, member); + } + + @GetMapping("/api/table/time-based/{tableId}") + @ResponseStatus(HttpStatus.OK) + public TimeBasedTableResponse getTable( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return timeBasedService.findTable(tableId, member); + } + + @PutMapping("/api/table/time-based/{tableId}") + @ResponseStatus(HttpStatus.OK) + public TimeBasedTableResponse updateTable( + @Valid @RequestBody TimeBasedTableCreateRequest tableCreateRequest, + @PathVariable Long tableId, + @AuthMember Member member + ) { + return timeBasedService.updateTable(tableCreateRequest, tableId, member); + } + + @PatchMapping("/api/table/time-based/{tableId}/debate") + @ResponseStatus(HttpStatus.OK) + public TimeBasedTableResponse debate( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return timeBasedService.updateUsedAt(tableId, member); + } + + @DeleteMapping("/api/table/time-based/{tableId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteTable( + @PathVariable Long tableId, + @AuthMember Member member + ) { + timeBasedService.deleteTable(tableId, member); + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java deleted file mode 100644 index 936ea6d8..00000000 --- a/src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.debatetimer.controller.tool.cookie; - -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; -import jakarta.servlet.http.Cookie; -import java.util.Arrays; -import org.springframework.stereotype.Component; - -@Component -public class CookieExtractor { - - public String extractCookie(String cookieName, Cookie[] cookies) { - return Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals(cookieName)) - .findAny() - .map(Cookie::getValue) - .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.EMPTY_COOKIE)); - } -} diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java index c54ee6f8..64f7a36b 100644 --- a/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java +++ b/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java @@ -1,7 +1,6 @@ package com.debatetimer.controller.tool.cookie; -import com.debatetimer.controller.tool.jwt.JwtTokenProperties; -import jakarta.servlet.http.Cookie; +import java.time.Duration; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; @@ -10,22 +9,16 @@ @RequiredArgsConstructor public class CookieManager { - private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + private static final String EMPTY_TOKEN = ""; + private static final Duration EXPIRED_DURATION = Duration.ZERO; private final CookieProvider cookieProvider; - private final CookieExtractor cookieExtractor; - private final JwtTokenProperties jwtTokenProperties; - public ResponseCookie createRefreshTokenCookie(String token) { - return cookieProvider.createCookie(REFRESH_TOKEN_COOKIE_NAME, token, - jwtTokenProperties.getRefreshTokenExpirationMillis()); + public ResponseCookie createCookie(String key, String value, Duration expiration) { + return cookieProvider.createCookie(key, value, expiration); } - public String extractRefreshToken(Cookie[] cookies) { - return cookieExtractor.extractCookie(REFRESH_TOKEN_COOKIE_NAME, cookies); - } - - public ResponseCookie deleteRefreshTokenCookie() { - return cookieProvider.deleteCookie(REFRESH_TOKEN_COOKIE_NAME); + public ResponseCookie createExpiredCookie(String key) { + return cookieProvider.createCookie(key, EMPTY_TOKEN, EXPIRED_DURATION); } } diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java index 6fd4456d..1a6827a2 100644 --- a/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java +++ b/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java @@ -1,6 +1,7 @@ package com.debatetimer.controller.tool.cookie; import java.time.Duration; +import org.springframework.boot.web.server.Cookie.SameSite; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @@ -8,21 +9,13 @@ public class CookieProvider { private static final String PATH = "/"; + private static final String SAME_SITE = SameSite.NONE.attributeValue(); - public ResponseCookie createCookie(String cookieName, String token, long expirationMillis) { - return ResponseCookie.from(cookieName, token) - .maxAge(Duration.ofMillis(expirationMillis)) + public ResponseCookie createCookie(String key, String value, Duration expiration) { + return ResponseCookie.from(key, value) + .maxAge(expiration) .path(PATH) - .sameSite("None") - .secure(true) - .build(); - } - - public ResponseCookie deleteCookie(String cookieName) { - return ResponseCookie.from(cookieName, "") - .maxAge(0) - .path(PATH) - .sameSite("None") + .sameSite(SAME_SITE) .secure(true) .build(); } diff --git a/src/main/java/com/debatetimer/controller/tool/export/ExcelExport.java b/src/main/java/com/debatetimer/controller/tool/export/ExcelExport.java new file mode 100644 index 00000000..bad11bb5 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/export/ExcelExport.java @@ -0,0 +1,12 @@ +package com.debatetimer.controller.tool.export; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExcelExport { + +} diff --git a/src/main/java/com/debatetimer/controller/tool/export/ExcelExportInterceptor.java b/src/main/java/com/debatetimer/controller/tool/export/ExcelExportInterceptor.java new file mode 100644 index 00000000..aab2163d --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/export/ExcelExportInterceptor.java @@ -0,0 +1,55 @@ +package com.debatetimer.controller.tool.export; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class ExcelExportInterceptor implements HandlerInterceptor { + + private static final String SPREAD_SHEET_MEDIA_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + private static final String EXCEL_FILE_NAME = "my_debate_template.xlsx"; + + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) { + if (isPreflight(request)) { + return true; + } + if (isExcelExportRequest(handler)) { + setExcelHeader(response); + } + return true; + } + + private boolean isExcelExportRequest(Object handler) { + if (!(handler instanceof HandlerMethod)) { + return false; + } + + HandlerMethod handlerMethod = (HandlerMethod) handler; + return handlerMethod.hasMethodAnnotation(ExcelExport.class) + && handlerMethod.getBeanType().isAnnotationPresent(RestController.class); + } + + private boolean isPreflight(HttpServletRequest request) { + return HttpMethod.OPTIONS.toString() + .equals(request.getMethod()); + } + + private void setExcelHeader(HttpServletResponse response) { + ContentDisposition contentDisposition = ContentDisposition.attachment() + .filename(EXCEL_FILE_NAME) + .build(); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()); + response.setContentType(SPREAD_SHEET_MEDIA_TYPE); + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java b/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java index 2cc86a3d..a614224f 100644 --- a/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java +++ b/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java @@ -2,6 +2,7 @@ import com.debatetimer.dto.member.JwtTokenResponse; import com.debatetimer.dto.member.MemberInfo; +import java.time.Duration; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -15,15 +16,18 @@ public class AuthManager { public JwtTokenResponse issueToken(MemberInfo memberInfo) { String accessToken = jwtTokenProvider.createAccessToken(memberInfo); String refreshToken = jwtTokenProvider.createRefreshToken(memberInfo); - return new JwtTokenResponse(accessToken, refreshToken); + Duration refreshTokenExpiration = jwtTokenProvider.getRefreshTokenExpiration(); + return new JwtTokenResponse(accessToken, refreshToken, refreshTokenExpiration); } public JwtTokenResponse reissueToken(String refreshToken) { String email = jwtTokenResolver.resolveRefreshToken(refreshToken); MemberInfo memberInfo = new MemberInfo(email); + String accessToken = jwtTokenProvider.createAccessToken(memberInfo); String newRefreshToken = jwtTokenProvider.createRefreshToken(memberInfo); - return new JwtTokenResponse(accessToken, newRefreshToken); + Duration refreshTokenExpiration = jwtTokenProvider.getRefreshTokenExpiration(); + return new JwtTokenResponse(accessToken, newRefreshToken, refreshTokenExpiration); } public String resolveAccessToken(String accessToken) { diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java index 95653df5..323f7613 100644 --- a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java +++ b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java @@ -1,6 +1,9 @@ package com.debatetimer.controller.tool.jwt; +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; import io.jsonwebtoken.security.Keys; +import java.time.Duration; import javax.crypto.SecretKey; import lombok.Getter; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -10,13 +13,32 @@ public class JwtTokenProperties { private final String secretKey; - private final long accessTokenExpirationMillis; - private final long refreshTokenExpirationMillis; + private final Duration accessTokenExpiration; + private final Duration refreshTokenExpiration; + + public JwtTokenProperties(String secretKey, Duration accessTokenExpiration, Duration refreshTokenExpiration) { + validate(secretKey); + validate(accessTokenExpiration); + validate(refreshTokenExpiration); - public JwtTokenProperties(String secretKey, long accessTokenExpirationMillis, long refreshTokenExpirationMillis) { this.secretKey = secretKey; - this.accessTokenExpirationMillis = accessTokenExpirationMillis; - this.refreshTokenExpirationMillis = refreshTokenExpirationMillis; + this.accessTokenExpiration = accessTokenExpiration; + this.refreshTokenExpiration = refreshTokenExpiration; + } + + private void validate(String secretKey) { + if (secretKey == null || secretKey.isBlank()) { + throw new DTInitializationException(InitializationErrorCode.JWT_SECRET_KEY_EMPTY); + } + } + + private void validate(Duration expiration) { + if (expiration == null) { + throw new DTInitializationException(InitializationErrorCode.JWT_TOKEN_DURATION_EMPTY); + } + if (expiration.isZero() || expiration.isNegative()) { + throw new DTInitializationException(InitializationErrorCode.JWT_TOKEN_DURATION_INVALID); + } } public SecretKey getSecretKey() { diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java index 5b903884..ffd28bca 100644 --- a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java +++ b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java @@ -2,6 +2,7 @@ import com.debatetimer.dto.member.MemberInfo; import io.jsonwebtoken.Jwts; +import java.time.Duration; import java.util.Date; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -13,18 +14,18 @@ public class JwtTokenProvider { private final JwtTokenProperties jwtTokenProperties; public String createAccessToken(MemberInfo memberInfo) { - long accessTokenExpirationMillis = jwtTokenProperties.getAccessTokenExpirationMillis(); - return createToken(memberInfo, accessTokenExpirationMillis, TokenType.ACCESS_TOKEN); + Duration accessTokenExpiration = jwtTokenProperties.getAccessTokenExpiration(); + return createToken(memberInfo, accessTokenExpiration, TokenType.ACCESS_TOKEN); } public String createRefreshToken(MemberInfo memberInfo) { - long refreshTokenExpirationMillis = jwtTokenProperties.getRefreshTokenExpirationMillis(); - return createToken(memberInfo, refreshTokenExpirationMillis, TokenType.REFRESH_TOKEN); + Duration refreshTokenExpiration = jwtTokenProperties.getRefreshTokenExpiration(); + return createToken(memberInfo, refreshTokenExpiration, TokenType.REFRESH_TOKEN); } - private String createToken(MemberInfo memberInfo, long expirationMillis, TokenType tokenType) { + private String createToken(MemberInfo memberInfo, Duration expiration, TokenType tokenType) { Date now = new Date(); - Date expiredDate = new Date(now.getTime() + expirationMillis); + Date expiredDate = new Date(now.getTime() + expiration.toMillis()); return Jwts.builder() .setSubject(memberInfo.email()) .setIssuedAt(now) @@ -33,4 +34,8 @@ private String createToken(MemberInfo memberInfo, long expirationMillis, TokenTy .signWith(jwtTokenProperties.getSecretKey()) .compact(); } + + public Duration getRefreshTokenExpiration() { + return jwtTokenProperties.getRefreshTokenExpiration(); + } } diff --git a/src/main/java/com/debatetimer/domain/BaseTimeEntity.java b/src/main/java/com/debatetimer/domain/BaseTimeEntity.java new file mode 100644 index 00000000..ea8677cc --- /dev/null +++ b/src/main/java/com/debatetimer/domain/BaseTimeEntity.java @@ -0,0 +1,24 @@ +package com.debatetimer.domain; + +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public abstract class BaseTimeEntity { + + @NotNull + @CreatedDate + private LocalDateTime createdAt; + + @NotNull + @LastModifiedDate + private LocalDateTime modifiedAt; +} diff --git a/src/main/java/com/debatetimer/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java new file mode 100644 index 00000000..dbbb83f9 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -0,0 +1,82 @@ +package com.debatetimer.domain; + +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.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@MappedSuperclass +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class DebateTable extends BaseTimeEntity { + + private static final String NAME_REGEX = "^[a-zA-Z가-힣0-9 ]+$"; + public static final int NAME_MAX_LENGTH = 20; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @NotNull + private String name; + + private String agenda; + private boolean warningBell; + private boolean finishBell; + + @NotNull + private LocalDateTime usedAt; + + protected DebateTable(Member member, String name, String agenda, boolean warningBell, boolean finishBell) { + validate(name); + + this.member = member; + this.name = name; + this.agenda = agenda; + this.warningBell = warningBell; + this.finishBell = finishBell; + this.usedAt = LocalDateTime.now(); + } + + public final boolean isOwner(long memberId) { + return Objects.equals(this.member.getId(), memberId); + } + + public final void updateUsedAt() { + this.usedAt = LocalDateTime.now(); + } + + protected final void updateTable(DebateTable renewTable) { + validate(renewTable.getName()); + + this.name = renewTable.getName(); + this.agenda = renewTable.getAgenda(); + this.warningBell = renewTable.isWarningBell(); + this.finishBell = renewTable.isFinishBell(); + updateUsedAt(); + } + + private void validate(String name) { + if (name.isBlank() || name.length() > NAME_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_LENGTH); + } + if (!name.matches(NAME_REGEX)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_FORM); + } + } + + public abstract long getId(); + + public abstract TableType getType(); +} diff --git a/src/main/java/com/debatetimer/domain/DebateTimeBox.java b/src/main/java/com/debatetimer/domain/DebateTimeBox.java new file mode 100644 index 00000000..0538104a --- /dev/null +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -0,0 +1,55 @@ +package com.debatetimer.domain; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.MappedSuperclass; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@MappedSuperclass +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class DebateTimeBox { + + private int sequence; + + @NotNull + @Enumerated(EnumType.STRING) + private Stance stance; + + private int time; + private Integer speaker; + + protected DebateTimeBox(int sequence, Stance stance, int time, Integer speaker) { + validateSequence(sequence); + validateTime(time); + validateSpeakerNumber(speaker); + + this.sequence = sequence; + this.stance = stance; + this.time = time; + this.speaker = speaker; + } + + private void validateSequence(int sequence) { + if (sequence <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE); + } + } + + private void validateTime(int time) { + if (time <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + private void 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/TimeBoxes.java b/src/main/java/com/debatetimer/domain/TimeBoxes.java new file mode 100644 index 00000000..62159ec0 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/TimeBoxes.java @@ -0,0 +1,20 @@ +package com.debatetimer.domain; + +import java.util.Comparator; +import java.util.List; +import lombok.Getter; + +@Getter +public class TimeBoxes { + + private static final Comparator TIME_BOX_COMPARATOR = Comparator + .comparing(DebateTimeBox::getSequence); + + private final List timeBoxes; + + public TimeBoxes(List timeBoxes) { + this.timeBoxes = timeBoxes.stream() + .sorted(TIME_BOX_COMPARATOR) + .toList(); + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeBoxType.java b/src/main/java/com/debatetimer/domain/customize/CustomizeBoxType.java new file mode 100644 index 00000000..29f7dd66 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeBoxType.java @@ -0,0 +1,15 @@ +package com.debatetimer.domain.customize; + +public enum CustomizeBoxType { + + NORMAL, + TIME_BASED; + + public boolean isTimeBased() { + return this == TIME_BASED; + } + + public boolean isNotTimeBased() { + return !isTimeBased(); + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java new file mode 100644 index 00000000..4b6e43eb --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java @@ -0,0 +1,53 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.TableType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CustomizeTable extends DebateTable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank + private String prosTeamName; + + @NotBlank + private String consTeamName; + + public CustomizeTable( + Member member, + String name, + String agenda, + boolean warningBell, + boolean finishBell, + String prosTeamName, + String consTeamName + ) { + super(member, name, agenda, warningBell, finishBell); + this.prosTeamName = prosTeamName; + this.consTeamName = consTeamName; + } + + @Override + public long getId() { + return id; + } + + @Override + public TableType getType() { + return TableType.CUSTOMIZE; + } +} diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java new file mode 100644 index 00000000..5c3028c4 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java @@ -0,0 +1,117 @@ +package com.debatetimer.domain.customize; + +import com.debatetimer.domain.DebateTimeBox; +import com.debatetimer.domain.Stance; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CustomizeTimeBox extends DebateTimeBox { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "table_id") + private CustomizeTable customizeTable; + + @NotBlank + private String speechType; + + @NotNull + @Enumerated(value = EnumType.STRING) + private CustomizeBoxType boxType; + + private Integer timePerTeam; + private Integer timePerSpeaking; + + public CustomizeTimeBox( + CustomizeTable customizeTable, + int sequence, + Stance stance, + String speechType, + CustomizeBoxType boxType, + int time, + Integer speaker + ) { + super(sequence, stance, time, speaker); + validateNotTimeBasedType(boxType); + + this.customizeTable = customizeTable; + this.speechType = speechType; + this.boxType = boxType; + } + + public CustomizeTimeBox( + CustomizeTable customizeTable, + int sequence, + Stance stance, + String speechType, + CustomizeBoxType boxType, + int time, + int timePerTeam, + Integer timePerSpeaking, + Integer speaker + ) { + super(sequence, stance, time, speaker); + validateTime(timePerTeam, timePerSpeaking); + validateTimeBasedTime(time, timePerTeam); + validateTimeBasedType(boxType); + + this.customizeTable = customizeTable; + this.speechType = speechType; + this.boxType = boxType; + this.timePerTeam = timePerTeam; + this.timePerSpeaking = timePerSpeaking; + } + + private void validateTime(int time) { + if (time <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + private void validateTime(int timePerTeam, int timePerSpeaking) { + validateTime(timePerTeam); + 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); + } + } + + private void validateNotTimeBasedType(CustomizeBoxType boxType) { + if (boxType.isTimeBased()) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/member/Member.java b/src/main/java/com/debatetimer/domain/member/Member.java index 1366b91e..2299c385 100644 --- a/src/main/java/com/debatetimer/domain/member/Member.java +++ b/src/main/java/com/debatetimer/domain/member/Member.java @@ -1,5 +1,6 @@ package com.debatetimer.domain.member; +import com.debatetimer.domain.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -14,7 +15,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/debatetimer/domain/BoxType.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryBoxType.java similarity index 80% rename from src/main/java/com/debatetimer/domain/BoxType.java rename to src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryBoxType.java index 88771570..c0131db7 100644 --- a/src/main/java/com/debatetimer/domain/BoxType.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryBoxType.java @@ -1,10 +1,11 @@ -package com.debatetimer.domain; +package com.debatetimer.domain.parliamentary; +import com.debatetimer.domain.Stance; import java.util.Set; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor -public enum BoxType { +public enum ParliamentaryBoxType { OPENING(Set.of(Stance.PROS, Stance.CONS)), REBUTTAL(Set.of(Stance.PROS, Stance.CONS)), diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java index e97dbd0e..08041d58 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java @@ -1,17 +1,12 @@ package com.debatetimer.domain.parliamentary; +import com.debatetimer.domain.DebateTable; import com.debatetimer.domain.member.Member; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.dto.member.TableType; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.validation.constraints.NotNull; -import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -19,70 +14,33 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ParliamentaryTable { - - private static final String NAME_REGEX = "^[a-zA-Z가-힣0-9 ]+$"; - public static final int NAME_MAX_LENGTH = 20; +public class ParliamentaryTable extends DebateTable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @NotNull - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") - private Member member; - - @NotNull - private String name; - - @NotNull - private String agenda; - - private int duration; - - private boolean warningBell; - - private boolean finishBell; - public ParliamentaryTable( Member member, String name, String agenda, - int duration, boolean warningBell, boolean finishBell ) { - validate(name, duration); - this.member = member; - this.name = name; - this.agenda = agenda; - this.duration = duration; - this.warningBell = warningBell; - this.finishBell = finishBell; + super(member, name, agenda, warningBell, finishBell); } - private void validate(String name, int duration) { - if (name.isBlank() || name.length() > NAME_MAX_LENGTH) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_LENGTH); - } - if (!name.matches(NAME_REGEX)) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_FORM); - } - if (duration <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_TIME); - } + @Override + public long getId() { + return id; } - public void update(ParliamentaryTable renewTable) { - this.name = renewTable.getName(); - this.agenda = renewTable.getAgenda(); - this.duration = renewTable.getDuration(); - this.warningBell = renewTable.isWarningBell(); - this.finishBell = renewTable.isFinishBell(); + @Override + public TableType getType() { + return TableType.PARLIAMENTARY; } - public boolean isOwner(long memberId) { - return Objects.equals(this.member.getId(), memberId); + public void update(ParliamentaryTable renewTable) { + updateTable(renewTable); } } diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java index b2f8236d..fa66cff2 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java @@ -1,6 +1,6 @@ package com.debatetimer.domain.parliamentary; -import com.debatetimer.domain.BoxType; +import com.debatetimer.domain.DebateTimeBox; import com.debatetimer.domain.Stance; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; @@ -21,7 +21,7 @@ @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ParliamentaryTimeBox { +public class ParliamentaryTimeBox extends DebateTimeBox { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -32,41 +32,26 @@ public class ParliamentaryTimeBox { @JoinColumn(name = "table_id") private ParliamentaryTable parliamentaryTable; - @NotNull - private int sequence; - - @NotNull - @Enumerated(EnumType.STRING) - private Stance stance; - @NotNull @Enumerated(EnumType.STRING) - private BoxType type; + private ParliamentaryBoxType type; + + public ParliamentaryTimeBox( + ParliamentaryTable parliamentaryTable, + int sequence, + Stance stance, + ParliamentaryBoxType type, + int time, + Integer speaker + ) { + super(sequence, stance, time, speaker); + validate(stance, type); - @NotNull - private int time; - - private Integer speaker; - - public ParliamentaryTimeBox(ParliamentaryTable parliamentaryTable, int sequence, Stance stance, BoxType type, - int time, Integer speaker) { - validate(sequence, time, stance, type); this.parliamentaryTable = parliamentaryTable; - this.sequence = sequence; - this.stance = stance; this.type = type; - this.time = time; - this.speaker = speaker; } - private void validate(int sequence, int time, Stance stance, BoxType boxType) { - if (sequence <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE); - } - if (time <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); - } - + private void validate(Stance stance, ParliamentaryBoxType boxType) { if (!boxType.isAvailable(stance)) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); } diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxes.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxes.java deleted file mode 100644 index e0431e3b..00000000 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxes.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.debatetimer.domain.parliamentary; - -import java.util.Comparator; -import java.util.List; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -@Getter -public class ParliamentaryTimeBoxes { - - private static final Comparator TIME_BOX_COMPARATOR = Comparator - .comparing(ParliamentaryTimeBox::getSequence); - - private final List timeBoxes; - - public ParliamentaryTimeBoxes(List timeBoxes) { - this.timeBoxes = timeBoxes.stream() - .sorted(TIME_BOX_COMPARATOR) - .toList(); - } -} diff --git a/src/main/java/com/debatetimer/domain/timebased/TimeBasedBoxType.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedBoxType.java new file mode 100644 index 00000000..8dfa4ce6 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedBoxType.java @@ -0,0 +1,32 @@ +package com.debatetimer.domain.timebased; + +import com.debatetimer.domain.Stance; +import java.util.Set; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum TimeBasedBoxType { + + OPENING(Set.of(Stance.PROS, Stance.CONS)), + REBUTTAL(Set.of(Stance.PROS, Stance.CONS)), + CROSS(Set.of(Stance.PROS, Stance.CONS)), + CLOSING(Set.of(Stance.PROS, Stance.CONS)), + TIME_BASED(Set.of(Stance.NEUTRAL)), + LEADING(Set.of(Stance.PROS, Stance.CONS)), + TIME_OUT(Set.of(Stance.NEUTRAL)), + ; + + private final Set availableStances; + + public boolean isAvailable(Stance stance) { + return availableStances.contains(stance); + } + + public boolean isTimeBased() { + return this == TIME_BASED; + } + + public boolean isNotTimeBased() { + return !isTimeBased(); + } +} diff --git a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java new file mode 100644 index 00000000..d3a17a0d --- /dev/null +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java @@ -0,0 +1,46 @@ +package com.debatetimer.domain.timebased; + +import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.TableType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TimeBasedTable extends DebateTable { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + public TimeBasedTable( + Member member, + String name, + String agenda, + boolean warningBell, + boolean finishBell + ) { + super(member, name, agenda, warningBell, finishBell); + } + + @Override + public long getId() { + return id; + } + + @Override + public TableType getType() { + return TableType.TIME_BASED; + } + + public void update(TimeBasedTable renewTable) { + updateTable(renewTable); + } +} diff --git a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java new file mode 100644 index 00000000..27184f61 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java @@ -0,0 +1,117 @@ +package com.debatetimer.domain.timebased; + +import com.debatetimer.domain.DebateTimeBox; +import com.debatetimer.domain.Stance; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TimeBasedTimeBox extends DebateTimeBox { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "table_id") + private TimeBasedTable timeBasedTable; + + @NotNull + @Enumerated(EnumType.STRING) + private TimeBasedBoxType type; + + private Integer timePerTeam; + private Integer timePerSpeaking; + + public TimeBasedTimeBox( + TimeBasedTable timeBasedTable, + int sequence, + Stance stance, + TimeBasedBoxType type, + int time, + Integer speaker + ) { + super(sequence, stance, time, speaker); + validateStance(stance, type); + validateNotTimeBasedType(type); + + this.timeBasedTable = timeBasedTable; + this.type = type; + } + + public TimeBasedTimeBox( + TimeBasedTable timeBasedTable, + int sequence, + Stance stance, + TimeBasedBoxType type, + int time, + int timePerTeam, + int timePerSpeaking, + Integer speaker + ) { + super(sequence, stance, time, speaker); + validateTime(timePerTeam, timePerSpeaking); + validateTimeBasedTime(time, timePerTeam); + validateStance(stance, type); + validateTimeBasedType(type); + + this.timeBasedTable = timeBasedTable; + this.type = type; + this.timePerTeam = timePerTeam; + this.timePerSpeaking = timePerSpeaking; + } + + private void validateTime(int time) { + if (time <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); + } + } + + private void validateTime(int timePerTeam, int timePerSpeaking) { + validateTime(timePerTeam); + 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 validateStance(Stance stance, TimeBasedBoxType boxType) { + if (!boxType.isAvailable(stance)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); + } + } + + private void validateTimeBasedType(TimeBasedBoxType boxType) { + if (boxType.isNotTimeBased()) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + } + + private void validateNotTimeBasedType(TimeBasedBoxType boxType) { + if (boxType.isTimeBased()) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + } +} diff --git a/src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java b/src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java index dd109b43..b3ea0bb4 100644 --- a/src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java +++ b/src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java @@ -1,5 +1,7 @@ package com.debatetimer.dto.member; -public record JwtTokenResponse(String accessToken, String refreshToken) { +import java.time.Duration; + +public record JwtTokenResponse(String accessToken, String refreshToken, Duration refreshExpiration) { } diff --git a/src/main/java/com/debatetimer/dto/member/TableResponse.java b/src/main/java/com/debatetimer/dto/member/TableResponse.java index 769d0ace..7c6baa62 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponse.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponse.java @@ -1,15 +1,15 @@ package com.debatetimer.dto.member; -import com.debatetimer.domain.parliamentary.ParliamentaryTable; +import com.debatetimer.domain.DebateTable; -public record TableResponse(long id, String name, TableType type, int duration) { +public record TableResponse(long id, String name, TableType type, String agenda) { - public TableResponse(ParliamentaryTable parliamentaryTable) { + public TableResponse(DebateTable debateTable) { this( - parliamentaryTable.getId(), - parliamentaryTable.getName(), - TableType.PARLIAMENTARY, - parliamentaryTable.getDuration() + debateTable.getId(), + debateTable.getName(), + debateTable.getType(), + debateTable.getAgenda() ); } } diff --git a/src/main/java/com/debatetimer/dto/member/TableResponses.java b/src/main/java/com/debatetimer/dto/member/TableResponses.java index 398ad724..651acfbc 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponses.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponses.java @@ -1,16 +1,27 @@ package com.debatetimer.dto.member; +import com.debatetimer.domain.DebateTable; 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; public record TableResponses(List tables) { - public static TableResponses from(List parliamentaryTables) { - return new TableResponses(toTableResponses(parliamentaryTables)); + private static final Comparator DEBATE_TABLE_COMPARATOR = Comparator + .comparing(DebateTable::getUsedAt) + .reversed(); + + public TableResponses(List parliamentaryTables, + List timeBasedTables) { + this(toTableResponses(parliamentaryTables, timeBasedTables)); } - private static List toTableResponses(List parliamentaryTables) { - return parliamentaryTables.stream() + private static List toTableResponses(List parliamentaryTables, + List timeBasedTables) { + return Stream.concat(parliamentaryTables.stream(), timeBasedTables.stream()) + .sorted(DEBATE_TABLE_COMPARATOR) .map(TableResponse::new) .toList(); } diff --git a/src/main/java/com/debatetimer/dto/member/TableType.java b/src/main/java/com/debatetimer/dto/member/TableType.java index 4d634420..8b7efa1d 100644 --- a/src/main/java/com/debatetimer/dto/member/TableType.java +++ b/src/main/java/com/debatetimer/dto/member/TableType.java @@ -3,5 +3,6 @@ public enum TableType { PARLIAMENTARY, - ; + TIME_BASED, + CUSTOMIZE; } 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 5d6b9f9e..5c193733 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -1,27 +1,23 @@ package com.debatetimer.dto.parliamentary.request; +import com.debatetimer.domain.TimeBoxes; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.parliamentary.ParliamentaryTimeBoxes; +import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; -public record ParliamentaryTableCreateRequest(TableInfoCreateRequest info, List table) { +public record ParliamentaryTableCreateRequest(ParliamentaryTableInfoCreateRequest info, + List table) { public ParliamentaryTable toTable(Member member) { - return info.toTable(member, sumOfTime(), info.warningBell(), info().finishBell()); + return info.toTable(member); } - private int sumOfTime() { - return table.stream() - .mapToInt(TimeBoxCreateRequest::time) - .sum(); - } - - public ParliamentaryTimeBoxes toTimeBoxes(ParliamentaryTable parliamentaryTable) { + public TimeBoxes toTimeBoxes(ParliamentaryTable parliamentaryTable) { return IntStream.range(0, table.size()) .mapToObj(i -> table.get(i).toTimeBox(parliamentaryTable, i + 1)) - .collect(Collectors.collectingAndThen(Collectors.toList(), ParliamentaryTimeBoxes::new)); + .collect(Collectors.collectingAndThen(Collectors.toList(), TimeBoxes::new)); } } diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/TableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java similarity index 62% rename from src/main/java/com/debatetimer/dto/parliamentary/request/TableInfoCreateRequest.java rename to src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java index 390af80d..764fd62b 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/TableInfoCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -public record TableInfoCreateRequest( +public record ParliamentaryTableInfoCreateRequest( @NotBlank String name, @@ -16,7 +16,7 @@ public record TableInfoCreateRequest( boolean finishBell ) { - public ParliamentaryTable toTable(Member member, int duration, boolean warningBell, boolean finishBell) { - return new ParliamentaryTable(member, name, agenda, duration, warningBell, finishBell); + public ParliamentaryTable toTable(Member member) { + return new ParliamentaryTable(member, name, agenda, warningBell, finishBell); } } diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/TimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java similarity index 81% rename from src/main/java/com/debatetimer/dto/parliamentary/request/TimeBoxCreateRequest.java rename to src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java index 9204113c..9313ea90 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/TimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java @@ -1,18 +1,18 @@ package com.debatetimer.dto.parliamentary.request; -import com.debatetimer.domain.BoxType; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.Stance; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; -public record TimeBoxCreateRequest( +public record ParliamentaryTimeBoxCreateRequest( @NotBlank Stance stance, @NotBlank - BoxType type, + ParliamentaryBoxType type, @Positive int time, diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableInfoResponse.java similarity index 50% rename from src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java rename to src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableInfoResponse.java index 615cf99b..7894432f 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableInfoResponse.java @@ -1,12 +1,15 @@ package com.debatetimer.dto.parliamentary.response; import com.debatetimer.domain.parliamentary.ParliamentaryTable; +import com.debatetimer.dto.member.TableType; -public record TableInfoResponse(String name, String agenda, boolean warningBell, boolean finishBell) { +public record ParliamentaryTableInfoResponse(String name, TableType type, String agenda, boolean warningBell, + boolean finishBell) { - public TableInfoResponse(ParliamentaryTable parliamentaryTable) { + public ParliamentaryTableInfoResponse(ParliamentaryTable parliamentaryTable) { this( parliamentaryTable.getName(), + TableType.PARLIAMENTARY, parliamentaryTable.getAgenda(), parliamentaryTable.isWarningBell(), parliamentaryTable.isFinishBell() 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 c75a0a92..6701c51e 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java @@ -1,26 +1,29 @@ package com.debatetimer.dto.parliamentary.response; +import com.debatetimer.domain.TimeBoxes; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.parliamentary.ParliamentaryTimeBoxes; +import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; import java.util.List; -public record ParliamentaryTableResponse(long id, TableInfoResponse info, List table) { +public record ParliamentaryTableResponse(long id, ParliamentaryTableInfoResponse info, + List table) { public ParliamentaryTableResponse( ParliamentaryTable parliamentaryTable, - ParliamentaryTimeBoxes parliamentaryTimeBoxes + TimeBoxes parliamentaryTimeBoxes ) { this( parliamentaryTable.getId(), - new TableInfoResponse(parliamentaryTable), + new ParliamentaryTableInfoResponse(parliamentaryTable), toTimeBoxResponses(parliamentaryTimeBoxes) ); } - private static List toTimeBoxResponses(ParliamentaryTimeBoxes parliamentaryTimeBoxes) { - return parliamentaryTimeBoxes.getTimeBoxes() + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List parliamentaryTimeBoxes = (List) timeBoxes.getTimeBoxes(); + return parliamentaryTimeBoxes .stream() - .map(TimeBoxResponse::new) + .map(ParliamentaryTimeBoxResponse::new) .toList(); } } diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java similarity index 57% rename from src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java rename to src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java index 2eb8dd29..70ae83c6 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java @@ -1,12 +1,12 @@ package com.debatetimer.dto.parliamentary.response; -import com.debatetimer.domain.BoxType; import com.debatetimer.domain.Stance; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; -public record TimeBoxResponse(Stance stance, BoxType type, int time, Integer speakerNumber) { +public record ParliamentaryTimeBoxResponse(Stance stance, ParliamentaryBoxType type, int time, Integer speakerNumber) { - public TimeBoxResponse(ParliamentaryTimeBox parliamentaryTimeBox) { + public ParliamentaryTimeBoxResponse(ParliamentaryTimeBox parliamentaryTimeBox) { this(parliamentaryTimeBox.getStance(), parliamentaryTimeBox.getType(), parliamentaryTimeBox.getTime(), diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java new file mode 100644 index 00000000..3012958a --- /dev/null +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java @@ -0,0 +1,23 @@ +package com.debatetimer.dto.timebased.request; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public record TimeBasedTableCreateRequest(TimeBasedTableInfoCreateRequest info, + List table) { + + public TimeBasedTable toTable(Member member) { + return info.toTable(member); + } + + public TimeBoxes toTimeBoxes(TimeBasedTable timeBasedTable) { + return IntStream.range(0, table.size()) + .mapToObj(i -> table.get(i).toTimeBox(timeBasedTable, i + 1)) + .collect(Collectors.collectingAndThen(Collectors.toList(), TimeBoxes::new)); + } +} diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java new file mode 100644 index 00000000..51db8b89 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java @@ -0,0 +1,22 @@ +package com.debatetimer.dto.timebased.request; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record TimeBasedTableInfoCreateRequest( + @NotBlank + String name, + + @NotNull + String agenda, + + boolean warningBell, + boolean finishBell +) { + + public TimeBasedTable toTable(Member member) { + return new TimeBasedTable(member, name, agenda, warningBell, finishBell); + } +} diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java new file mode 100644 index 00000000..d9419190 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java @@ -0,0 +1,29 @@ +package com.debatetimer.dto.timebased.request; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.timebased.TimeBasedBoxType; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import jakarta.validation.constraints.NotBlank; + +public record TimeBasedTimeBoxCreateRequest( + @NotBlank + Stance stance, + + @NotBlank + TimeBasedBoxType type, + + int time, + Integer timePerTeam, + Integer timePerSpeaking, + Integer speakerNumber +) { + + public TimeBasedTimeBox toTimeBox(TimeBasedTable timeBasedTable, int sequence) { + if (type.isTimeBased()) { + return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, time, timePerTeam, timePerSpeaking, + speakerNumber); + } + return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, time, speakerNumber); + } +} diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableInfoResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableInfoResponse.java new file mode 100644 index 00000000..a5e4b935 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableInfoResponse.java @@ -0,0 +1,23 @@ +package com.debatetimer.dto.timebased.response; + +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.dto.member.TableType; + +public record TimeBasedTableInfoResponse( + String name, + TableType type, + String agenda, + boolean warningBell, + boolean finishBell +) { + + public TimeBasedTableInfoResponse(TimeBasedTable timeBasedTable) { + this( + timeBasedTable.getName(), + TableType.TIME_BASED, + timeBasedTable.getAgenda(), + timeBasedTable.isWarningBell(), + timeBasedTable.isFinishBell() + ); + } +} diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java new file mode 100644 index 00000000..a1f3d608 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.timebased.response; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import java.util.List; + +public record TimeBasedTableResponse(long id, TimeBasedTableInfoResponse info, List table) { + + public TimeBasedTableResponse( + TimeBasedTable timeBasedTable, + TimeBoxes timeBasedTimeBoxes + ) { + this( + timeBasedTable.getId(), + new TimeBasedTableInfoResponse(timeBasedTable), + toTimeBoxResponses(timeBasedTimeBoxes) + ); + } + + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List timeBasedTimeBoxes = (List) timeBoxes.getTimeBoxes(); + return timeBasedTimeBoxes + .stream() + .map(TimeBasedTimeBoxResponse::new) + .toList(); + } +} diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java new file mode 100644 index 00000000..1a094aa2 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java @@ -0,0 +1,25 @@ +package com.debatetimer.dto.timebased.response; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.timebased.TimeBasedBoxType; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; + +public record TimeBasedTimeBoxResponse( + Stance stance, + TimeBasedBoxType type, + Integer time, + Integer timePerTeam, + Integer timePerSpeaking, + Integer speakerNumber +) { + + public TimeBasedTimeBoxResponse(TimeBasedTimeBox timeBasedTimeBox) { + this(timeBasedTimeBox.getStance(), + timeBasedTimeBox.getType(), + timeBasedTimeBox.getTime(), + timeBasedTimeBox.getTimePerTeam(), + timeBasedTimeBox.getTimePerSpeaking(), + timeBasedTimeBox.getSpeaker() + ); + } +} diff --git a/src/main/java/com/debatetimer/exception/custom/DTClientErrorException.java b/src/main/java/com/debatetimer/exception/custom/DTClientErrorException.java index bd2b0e75..15f063fd 100644 --- a/src/main/java/com/debatetimer/exception/custom/DTClientErrorException.java +++ b/src/main/java/com/debatetimer/exception/custom/DTClientErrorException.java @@ -2,7 +2,7 @@ import com.debatetimer.exception.errorcode.ClientErrorCode; -public class DTClientErrorException extends DTException { +public class DTClientErrorException extends DTErrorResponseException { public DTClientErrorException(ClientErrorCode clientErrorCode) { super(clientErrorCode.getMessage(), clientErrorCode.getStatus()); diff --git a/src/main/java/com/debatetimer/exception/custom/DTException.java b/src/main/java/com/debatetimer/exception/custom/DTErrorResponseException.java similarity index 60% rename from src/main/java/com/debatetimer/exception/custom/DTException.java rename to src/main/java/com/debatetimer/exception/custom/DTErrorResponseException.java index 51157630..a144817f 100644 --- a/src/main/java/com/debatetimer/exception/custom/DTException.java +++ b/src/main/java/com/debatetimer/exception/custom/DTErrorResponseException.java @@ -4,11 +4,11 @@ import org.springframework.http.HttpStatus; @Getter -public abstract class DTException extends RuntimeException { +public abstract class DTErrorResponseException extends RuntimeException { private final HttpStatus httpStatus; - protected DTException(String message, HttpStatus httpStatus) { + protected DTErrorResponseException(String message, HttpStatus httpStatus) { super(message); this.httpStatus = httpStatus; } diff --git a/src/main/java/com/debatetimer/exception/custom/DTInitializationException.java b/src/main/java/com/debatetimer/exception/custom/DTInitializationException.java new file mode 100644 index 00000000..0aa40688 --- /dev/null +++ b/src/main/java/com/debatetimer/exception/custom/DTInitializationException.java @@ -0,0 +1,10 @@ +package com.debatetimer.exception.custom; + +import com.debatetimer.exception.errorcode.InitializationErrorCode; + +public class DTInitializationException extends RuntimeException { + + public DTInitializationException(InitializationErrorCode errorCode) { + super(errorCode.getMessage()); + } +} diff --git a/src/main/java/com/debatetimer/exception/custom/DTServerErrorException.java b/src/main/java/com/debatetimer/exception/custom/DTServerErrorException.java index 1e26c354..12198f41 100644 --- a/src/main/java/com/debatetimer/exception/custom/DTServerErrorException.java +++ b/src/main/java/com/debatetimer/exception/custom/DTServerErrorException.java @@ -2,7 +2,7 @@ import com.debatetimer.exception.errorcode.ServerErrorCode; -public class DTServerErrorException extends DTException { +public class DTServerErrorException extends DTErrorResponseException { public DTServerErrorException(ServerErrorCode serverErrorCode) { super(serverErrorCode.getMessage(), serverErrorCode.getStatus()); diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index db0ce1bf..7134e1cd 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -5,7 +5,7 @@ import org.springframework.http.HttpStatus; @Getter -public enum ClientErrorCode implements ErrorCode { +public enum ClientErrorCode implements ResponseErrorCode { INVALID_TABLE_NAME_LENGTH( HttpStatus.BAD_REQUEST, @@ -18,8 +18,12 @@ public enum ClientErrorCode implements ErrorCode { INVALID_TABLE_TIME(HttpStatus.BAD_REQUEST, "시간은 양수만 가능합니다"), INVALID_TIME_BOX_SEQUENCE(HttpStatus.BAD_REQUEST, "순서는 양수만 가능합니다"), + INVALID_TIME_BOX_SPEAKER(HttpStatus.BAD_REQUEST, "발표자 번호는 양수만 가능합니다"), INVALID_TIME_BOX_TIME(HttpStatus.BAD_REQUEST, "시간은 양수만 가능합니다"), 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, "입력이 잘못되었습니다."), diff --git a/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java new file mode 100644 index 00000000..a037a69f --- /dev/null +++ b/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java @@ -0,0 +1,21 @@ +package com.debatetimer.exception.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum InitializationErrorCode { + + OAUTH_PROPERTIES_EMPTY("OAuth 구성 요소들이 입력되지 않았습니다"), + + CORS_ORIGIN_EMPTY("CORS Origin 은 적어도 한 개 있어야 합니다"), + CORS_ORIGIN_STRING_BLANK("CORS Origin 에 빈 값이 들어올 수 없습니다"), + + JWT_SECRET_KEY_EMPTY("JWT secretKey 가 입력되지 않았습니다"), + JWT_TOKEN_DURATION_EMPTY("토큰 만료 기간이 입력되지 않았습니다"), + JWT_TOKEN_DURATION_INVALID("토큰 만료 기간은 양수이어야 합니다"), + ; + + private final String message; +} diff --git a/src/main/java/com/debatetimer/exception/errorcode/ErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ResponseErrorCode.java similarity index 80% rename from src/main/java/com/debatetimer/exception/errorcode/ErrorCode.java rename to src/main/java/com/debatetimer/exception/errorcode/ResponseErrorCode.java index fc9fe3f4..2f6ae03f 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ResponseErrorCode.java @@ -2,7 +2,7 @@ import org.springframework.http.HttpStatus; -public interface ErrorCode { +public interface ResponseErrorCode { HttpStatus getStatus(); diff --git a/src/main/java/com/debatetimer/exception/errorcode/ServerErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ServerErrorCode.java index c3ebba80..007b0ec2 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ServerErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ServerErrorCode.java @@ -4,7 +4,7 @@ import org.springframework.http.HttpStatus; @Getter -public enum ServerErrorCode implements ErrorCode { +public enum ServerErrorCode implements ResponseErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다. 관리자에게 문의하세요."), EXCEL_EXPORT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "엑셀 변환 과정에서 오류가 발생하였습니다"); diff --git a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java index a0d381c3..efeaab4c 100644 --- a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java @@ -4,7 +4,7 @@ import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.custom.DTServerErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; -import com.debatetimer.exception.errorcode.ErrorCode; +import com.debatetimer.exception.errorcode.ResponseErrorCode; import com.debatetimer.exception.errorcode.ServerErrorCode; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; @@ -77,17 +77,17 @@ public ResponseEntity handleClientException(DTClientErrorExceptio @ExceptionHandler(DTServerErrorException.class) public ResponseEntity handleServerException(DTServerErrorException exception) { - log.warn("message: {}", exception.getMessage()); + log.error("message: {}", exception.getMessage()); return toResponse(exception.getHttpStatus(), exception.getMessage()); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception exception) { - log.error("exception: {}", exception); + log.error("exception: {}", exception.getMessage()); return toResponse(ServerErrorCode.INTERNAL_SERVER_ERROR); } - private ResponseEntity toResponse(ErrorCode errorCode) { + private ResponseEntity toResponse(ResponseErrorCode errorCode) { return toResponse(errorCode.getStatus(), errorCode.getMessage()); } diff --git a/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java index a2ebb2d9..63120aa6 100644 --- a/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java @@ -1,8 +1,8 @@ package com.debatetimer.repository.parliamentary; +import com.debatetimer.domain.TimeBoxes; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; -import com.debatetimer.domain.parliamentary.ParliamentaryTimeBoxes; import java.util.List; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -22,9 +22,9 @@ default List saveAll(List timeBoxes) List findAllByParliamentaryTable(ParliamentaryTable table); - default ParliamentaryTimeBoxes findTableTimeBoxes(ParliamentaryTable table) { + default TimeBoxes findTableTimeBoxes(ParliamentaryTable table) { List timeBoxes = findAllByParliamentaryTable(table); - return new ParliamentaryTimeBoxes(timeBoxes); + return new TimeBoxes<>(timeBoxes); } @Query("DELETE FROM ParliamentaryTimeBox ptb WHERE ptb IN :timeBoxes") diff --git a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTableRepository.java b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTableRepository.java new file mode 100644 index 00000000..c8a9287e --- /dev/null +++ b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTableRepository.java @@ -0,0 +1,25 @@ +package com.debatetimer.repository.timebased; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +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 TimeBasedTableRepository extends Repository { + + TimeBasedTable save(TimeBasedTable timeBasedTable); + + Optional findById(long id); + + default TimeBasedTable getById(long tableId) { + return findById(tableId) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); + } + + List findAllByMember(Member member); + + void delete(TimeBasedTable table); +} diff --git a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java new file mode 100644 index 00000000..a34e4c07 --- /dev/null +++ b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java @@ -0,0 +1,34 @@ +package com.debatetimer.repository.timebased; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +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 TimeBasedTimeBoxRepository extends Repository { + + TimeBasedTimeBox save(TimeBasedTimeBox timeBox); + + @Transactional + default List saveAll(List timeBoxes) { + return timeBoxes.stream() + .map(this::save) + .toList(); + } + + List findAllByTimeBasedTable(TimeBasedTable table); + + default TimeBoxes findTableTimeBoxes(TimeBasedTable table) { + List timeBoxes = findAllByTimeBasedTable(table); + return new TimeBoxes<>(timeBoxes); + } + + @Query("DELETE FROM TimeBasedTimeBox ptb WHERE ptb IN :timeBoxes") + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Transactional + void deleteAll(List timeBoxes); +} diff --git a/src/main/java/com/debatetimer/service/member/MemberService.java b/src/main/java/com/debatetimer/service/member/MemberService.java index 06796d00..67bb3b7d 100644 --- a/src/main/java/com/debatetimer/service/member/MemberService.java +++ b/src/main/java/com/debatetimer/service/member/MemberService.java @@ -2,11 +2,13 @@ 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.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; @@ -18,12 +20,14 @@ public class MemberService { private final MemberRepository memberRepository; private final ParliamentaryTableRepository parliamentaryTableRepository; + private final TimeBasedTableRepository timeBasedTableRepository; @Transactional(readOnly = true) - public TableResponses getTables(Long memberId) { + public TableResponses getTables(long memberId) { Member member = memberRepository.getById(memberId); - List parliamentaryTable = parliamentaryTableRepository.findAllByMember(member); - return TableResponses.from(parliamentaryTable); + List parliamentaryTables = parliamentaryTableRepository.findAllByMember(member); + List timeBasedTables = timeBasedTableRepository.findAllByMember(member); + return new TableResponses(parliamentaryTables, timeBasedTables); } @Transactional diff --git a/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java index 1aad472a..5b76f221 100644 --- a/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java +++ b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java @@ -1,9 +1,9 @@ package com.debatetimer.service.parliamentary; +import com.debatetimer.domain.TimeBoxes; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; -import com.debatetimer.domain.parliamentary.ParliamentaryTimeBoxes; import com.debatetimer.dto.parliamentary.request.ParliamentaryTableCreateRequest; import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; import com.debatetimer.exception.custom.DTClientErrorException; @@ -27,14 +27,21 @@ public ParliamentaryTableResponse save(ParliamentaryTableCreateRequest tableCrea ParliamentaryTable table = tableCreateRequest.toTable(member); ParliamentaryTable savedTable = tableRepository.save(table); - ParliamentaryTimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, savedTable); + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, savedTable); return new ParliamentaryTableResponse(savedTable, savedTimeBoxes); } @Transactional(readOnly = true) public ParliamentaryTableResponse findTable(long tableId, Member member) { ParliamentaryTable table = getOwnerTable(tableId, member.getId()); - ParliamentaryTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + return new ParliamentaryTableResponse(table, timeBoxes); + } + + @Transactional(readOnly = true) + public ParliamentaryTableResponse findTableById(long tableId, long id) { + ParliamentaryTable table = getOwnerTable(tableId, id); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); return new ParliamentaryTableResponse(table, timeBoxes); } @@ -48,27 +55,35 @@ public ParliamentaryTableResponse updateTable( ParliamentaryTable renewedTable = tableCreateRequest.toTable(member); existingTable.update(renewedTable); - ParliamentaryTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); - ParliamentaryTimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); return new ParliamentaryTableResponse(existingTable, savedTimeBoxes); } @Transactional - public void deleteTable(Long tableId, Member member) { + public ParliamentaryTableResponse updateUsedAt(long tableId, Member member) { + ParliamentaryTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + table.updateUsedAt(); + return new ParliamentaryTableResponse(table, timeBoxes); + } + + @Transactional + public void deleteTable(long tableId, Member member) { ParliamentaryTable table = getOwnerTable(tableId, member.getId()); - ParliamentaryTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); tableRepository.delete(table); } - private ParliamentaryTimeBoxes saveTimeBoxes( + private TimeBoxes saveTimeBoxes( ParliamentaryTableCreateRequest tableCreateRequest, ParliamentaryTable table ) { - ParliamentaryTimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); + TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); List savedTimeBoxes = timeBoxRepository.saveAll(timeBoxes.getTimeBoxes()); - return new ParliamentaryTimeBoxes(savedTimeBoxes); + return new TimeBoxes<>(savedTimeBoxes); } private ParliamentaryTable getOwnerTable(long tableId, long memberId) { diff --git a/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java b/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java new file mode 100644 index 00000000..f984b7c8 --- /dev/null +++ b/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java @@ -0,0 +1,94 @@ +package com.debatetimer.service.timebased; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import com.debatetimer.dto.timebased.request.TimeBasedTableCreateRequest; +import com.debatetimer.dto.timebased.response.TimeBasedTableResponse; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.timebased.TimeBasedTableRepository; +import com.debatetimer.repository.timebased.TimeBasedTimeBoxRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class TimeBasedService { + + private final TimeBasedTableRepository tableRepository; + private final TimeBasedTimeBoxRepository timeBoxRepository; + + @Transactional + public TimeBasedTableResponse save(TimeBasedTableCreateRequest tableCreateRequest, Member member) { + TimeBasedTable table = tableCreateRequest.toTable(member); + TimeBasedTable savedTable = tableRepository.save(table); + + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, savedTable); + return new TimeBasedTableResponse(savedTable, savedTimeBoxes); + } + + @Transactional(readOnly = true) + public TimeBasedTableResponse findTable(long tableId, Member member) { + TimeBasedTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + return new TimeBasedTableResponse(table, timeBoxes); + } + + @Transactional + public TimeBasedTableResponse updateTable( + TimeBasedTableCreateRequest tableCreateRequest, + long tableId, + Member member + ) { + TimeBasedTable existingTable = getOwnerTable(tableId, member.getId()); + TimeBasedTable renewedTable = tableCreateRequest.toTable(member); + existingTable.update(renewedTable); + + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); + timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); + return new TimeBasedTableResponse(existingTable, savedTimeBoxes); + } + + @Transactional + public TimeBasedTableResponse updateUsedAt(long tableId, Member member) { + TimeBasedTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + table.updateUsedAt(); + + return new TimeBasedTableResponse(table, timeBoxes); + } + + @Transactional + public void deleteTable(long tableId, Member member) { + TimeBasedTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); + tableRepository.delete(table); + } + + private TimeBoxes saveTimeBoxes( + TimeBasedTableCreateRequest tableCreateRequest, + TimeBasedTable table + ) { + TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); + List savedTimeBoxes = timeBoxRepository.saveAll(timeBoxes.getTimeBoxes()); + return new TimeBoxes<>(savedTimeBoxes); + } + + private TimeBasedTable getOwnerTable(long tableId, long memberId) { + TimeBasedTable foundTable = tableRepository.getById(tableId); + validateOwn(foundTable, memberId); + return foundTable; + } + + private void validateOwn(TimeBasedTable table, long memberId) { + if (!table.isOwner(memberId)) { + throw new DTClientErrorException(ClientErrorCode.NOT_TABLE_OWNER); + } + } +} diff --git a/src/main/java/com/debatetimer/view/exporter/BoxTypeView.java b/src/main/java/com/debatetimer/view/exporter/ParliamentaryBoxTypeView.java similarity index 53% rename from src/main/java/com/debatetimer/view/exporter/BoxTypeView.java rename to src/main/java/com/debatetimer/view/exporter/ParliamentaryBoxTypeView.java index e42bcea8..0cb7c60f 100644 --- a/src/main/java/com/debatetimer/view/exporter/BoxTypeView.java +++ b/src/main/java/com/debatetimer/view/exporter/ParliamentaryBoxTypeView.java @@ -1,6 +1,6 @@ package com.debatetimer.view.exporter; -import com.debatetimer.domain.BoxType; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.exception.custom.DTServerErrorException; import com.debatetimer.exception.errorcode.ServerErrorCode; import java.util.stream.Stream; @@ -9,18 +9,18 @@ @Getter @RequiredArgsConstructor -public enum BoxTypeView { - OPENING_VIEW(BoxType.OPENING, "입론"), - REBUTTAL_VIEW(BoxType.REBUTTAL, "반론"), - CROSS(BoxType.CROSS, "교차 질의"), - CLOSING(BoxType.CLOSING, "최종 발언"), - TIME_OUT(BoxType.TIME_OUT, "작전 시간"), +public enum ParliamentaryBoxTypeView { + OPENING_VIEW(ParliamentaryBoxType.OPENING, "입론"), + REBUTTAL_VIEW(ParliamentaryBoxType.REBUTTAL, "반론"), + CROSS(ParliamentaryBoxType.CROSS, "교차 질의"), + CLOSING(ParliamentaryBoxType.CLOSING, "최종 발언"), + TIME_OUT(ParliamentaryBoxType.TIME_OUT, "작전 시간"), ; - private final BoxType boxType; + private final ParliamentaryBoxType boxType; private final String viewMessage; - public static String mapView(BoxType target) { + public static String mapView(ParliamentaryBoxType target) { return Stream.of(values()) .filter(value -> value.boxType == target) .findAny() diff --git a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java index 20a27ccc..d3ea6be9 100644 --- a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java +++ b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java @@ -1,9 +1,14 @@ package com.debatetimer.view.exporter; import com.debatetimer.domain.Stance; +import com.debatetimer.dto.parliamentary.response.ParliamentaryTableInfoResponse; import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; -import com.debatetimer.dto.parliamentary.response.TableInfoResponse; -import com.debatetimer.dto.parliamentary.response.TimeBoxResponse; +import com.debatetimer.dto.parliamentary.response.ParliamentaryTimeBoxResponse; +import com.debatetimer.exception.custom.DTServerErrorException; +import com.debatetimer.exception.errorcode.ServerErrorCode; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.List; import lombok.RequiredArgsConstructor; import org.apache.poi.ss.usermodel.Cell; @@ -18,6 +23,7 @@ import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.core.io.InputStreamResource; import org.springframework.stereotype.Component; @Component @@ -96,9 +102,11 @@ private static void initializeStyle(Workbook workbook) { HorizontalAlignment.CENTER); } - public Workbook export(ParliamentaryTableResponse parliamentaryTableResponse) { - TableInfoResponse tableInfo = parliamentaryTableResponse.info(); - List timeBoxes = parliamentaryTableResponse.table(); + public InputStreamResource export( + ParliamentaryTableResponse parliamentaryTableResponse + ) { + ParliamentaryTableInfoResponse tableInfo = parliamentaryTableResponse.info(); + List timeBoxes = parliamentaryTableResponse.table(); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet(tableInfo.name()); @@ -112,7 +120,18 @@ public Workbook export(ParliamentaryTableResponse parliamentaryTableResponse) { createTableHeader(sheet); createTimeBoxRows(timeBoxes, sheet); setColumnWidth(sheet); - return workbook; + return writeToInputStream(workbook); + } + + private InputStreamResource writeToInputStream(Workbook workbook) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + workbook.write(outputStream); + workbook.close(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + return new InputStreamResource(inputStream); + } catch (IOException e) { + throw new DTServerErrorException(ServerErrorCode.EXCEL_EXPORT_ERROR); + } } private void createHeader( @@ -144,13 +163,13 @@ private void setColumnWidth(Sheet sheet) { } } - private void createTimeBoxRows(List timeBoxes, Sheet sheet) { + private void createTimeBoxRows(List timeBoxes, Sheet sheet) { for (int i = 0; i < timeBoxes.size(); i++) { createTimeBoxRow(sheet, i + TIME_BOX_FIRST_ROW_NUMBER, timeBoxes.get(i)); } } - private void createTimeBoxRow(Sheet sheet, int rowNumber, TimeBoxResponse timeBox) { + private void createTimeBoxRow(Sheet sheet, int rowNumber, ParliamentaryTimeBoxResponse timeBox) { Row row = sheet.createRow(rowNumber); String timeBoxMessage = messageResolver.resolveBoxMessage(timeBox); Stance stance = timeBox.stance(); diff --git a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java index b4a069d5..08971443 100644 --- a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java +++ b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java @@ -1,7 +1,7 @@ package com.debatetimer.view.exporter; -import com.debatetimer.domain.BoxType; -import com.debatetimer.dto.parliamentary.response.TimeBoxResponse; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; +import com.debatetimer.dto.parliamentary.response.ParliamentaryTimeBoxResponse; import org.springframework.stereotype.Component; @Component @@ -15,10 +15,10 @@ public class ParliamentaryTableExportMessageResolver { private static final String MESSAGE_DELIMITER = "/"; private static final String SPACE = " "; - public String resolveBoxMessage(TimeBoxResponse timeBox) { + public String resolveBoxMessage(ParliamentaryTimeBoxResponse timeBox) { String defaultMessage = resolveDefaultMessage(timeBox); - BoxType type = timeBox.type(); - if (type == BoxType.TIME_OUT) { + ParliamentaryBoxType type = timeBox.type(); + if (type == ParliamentaryBoxType.TIME_OUT) { return defaultMessage; } return defaultMessage @@ -26,22 +26,25 @@ public String resolveBoxMessage(TimeBoxResponse timeBox) { + resolveSpeakerMessage(timeBox.speakerNumber()); } - private String resolveDefaultMessage(TimeBoxResponse timeBox) { - BoxType boxType = timeBox.type(); - return BoxTypeView.mapView(boxType) + private String resolveDefaultMessage(ParliamentaryTimeBoxResponse timeBox) { + ParliamentaryBoxType boxType = timeBox.type(); + return ParliamentaryBoxTypeView.mapView(boxType) + resolveTimeMessage(timeBox.time()); } private String resolveTimeMessage(int totalSecond) { + StringBuilder messageBuilder = new StringBuilder(); int minutes = totalSecond / 60; int second = totalSecond % 60; - String message = minutes + MINUTES_MESSAGE; + if (minutes != 0) { + messageBuilder.append(minutes + MINUTES_MESSAGE + SPACE); + } if (second != 0) { - message += SPACE + second + SECOND_MESSAGE; + messageBuilder.append(second + SECOND_MESSAGE); } return TIME_MESSAGE_PREFIX - + message + + messageBuilder + TIME_MESSAGE_SUFFIX; } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5aa0987d..51b83166 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -13,8 +13,12 @@ spring: format_sql: true dialect: org.hibernate.dialect.MySQLDialect hibernate: - ddl-auto: update - defer-datasource-initialization: true + ddl-auto: validate + defer-datasource-initialization: false + flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 1 cors: origin: ${secret.cors.origin} @@ -22,10 +26,12 @@ cors: oauth: client_id: ${secret.oauth.client_id} client_secret: ${secret.oauth.client_secret} - redirect_uri: ${secret.oauth.redirect_uri} grant_type: ${secret.oauth.grant_type} jwt: secret_key: ${secret.jwt.secret_key} - access_token_expiration_millis: ${secret.jwt.access_token_expiration_millis} - refresh_token_expiration_millis: ${secret.jwt.refresh_token_expiration_millis} + access_token_expiration: ${secret.jwt.access_token_expiration} + refresh_token_expiration: ${secret.jwt.refresh_token_expiration} + +#logging: +# config: classpath:logging/log4j2-dev.yml diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml deleted file mode 100644 index 6c0b99ea..00000000 --- a/src/main/resources/application-local.yml +++ /dev/null @@ -1,21 +0,0 @@ -spring: - datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:database - username: sa - password: - h2: - console: - enabled: true - path: /h2-console - jpa: - show-sql: true - properties: - hibernate: - format_sql: true - hibernate: - ddl-auto: create-drop - defer-datasource-initialization: true - -cors: - origin: '*' diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index c23b5bde..2af3608c 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -13,7 +13,11 @@ spring: format_sql: true hibernate: ddl-auto: validate - defer-datasource-initialization: true + defer-datasource-initialization: false + flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 1 cors: origin: ${secret.cors.origin} @@ -21,11 +25,12 @@ cors: oauth: client_id: ${secret.oauth.client_id} client_secret: ${secret.oauth.client_secret} - redirect_uri: ${secret.oauth.redirect_uri} grant_type: ${secret.oauth.grant_type} jwt: secret_key: ${secret.jwt.secret_key} - access_token_expiration_millis: ${secret.jwt.access_token_expiration_millis} - refresh_token_expiration_millis: ${secret.jwt.refresh_token_expiration_millis} + access_token_expiration: ${secret.jwt.access_token_expiration} + refresh_token_expiration: ${secret.jwt.refresh_token_expiration} +#logging: +# config: classpath:logging/log4j2-prod.yml diff --git a/src/main/resources/db/migration/V1__initialize_table.sql b/src/main/resources/db/migration/V1__initialize_table.sql new file mode 100644 index 00000000..d4a4c0b0 --- /dev/null +++ b/src/main/resources/db/migration/V1__initialize_table.sql @@ -0,0 +1,40 @@ +create table member +( + id bigint auto_increment, + email varchar(255) not null unique, + primary key (id) +); + +create table parliamentary_table +( + duration integer not null, + finish_bell boolean not null, + warning_bell boolean not null, + id bigint auto_increment, + member_id bigint not null, + agenda varchar(255) not null, + name varchar(255) not null, + primary key (id) +); + +create table parliamentary_time_box +( + sequence integer not null, + speaker integer, + time integer not null, + id bigint auto_increment, + table_id bigint not null, + stance enum ('CONS','NEUTRAL','PROS') not null, + type enum ('CLOSING','CROSS','OPENING','REBUTTAL','TIME_OUT') not null, + primary key (id) +); + +alter table parliamentary_table + add constraint parliamentary_table_to_member + foreign key (member_id) + references member(id); + +alter table parliamentary_time_box + add constraint parliamentary_time_box_to_parliamentary_table + foreign key (table_id) + references parliamentary_table(id); diff --git a/src/main/resources/db/migration/V2__add_time_based_table.sql b/src/main/resources/db/migration/V2__add_time_based_table.sql new file mode 100644 index 00000000..158ed685 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_time_based_table.sql @@ -0,0 +1,35 @@ +create table time_based_table +( + duration integer not null, + finish_bell boolean not null, + warning_bell boolean not null, + id bigint auto_increment, + member_id bigint not null, + agenda varchar(255) not null, + name varchar(255) not null, + primary key (id) +); + +create table time_based_time_box +( + sequence integer not null, + speaker integer, + time integer, + time_per_speaking integer, + time_per_team integer, + id bigint auto_increment, + table_id bigint not null, + stance enum ('CONS','NEUTRAL','PROS') not null, + type enum ('CLOSING','CROSS','LEADING','OPENING','REBUTTAL','TIME_BASED','TIME_OUT') not null, + primary key (id) +); + +alter table time_based_table + add constraint time_based_table_to_member + foreign key (member_id) + references member(id); + +alter table time_based_time_box + add constraint time_based_time_box_to_time_based_table + foreign key (table_id) + references time_based_table(id); diff --git a/src/main/resources/db/migration/V3__add_auditing_column.sql b/src/main/resources/db/migration/V3__add_auditing_column.sql new file mode 100644 index 00000000..5897e3a9 --- /dev/null +++ b/src/main/resources/db/migration/V3__add_auditing_column.sql @@ -0,0 +1,10 @@ +alter table member add column created_at timestamp default current_timestamp not null; +alter table member add column modified_at timestamp default current_timestamp not null; + +alter table parliamentary_table add column created_at timestamp default current_timestamp not null; +alter table parliamentary_table add column modified_at timestamp default current_timestamp not null; +alter table parliamentary_table add column used_at timestamp default current_timestamp not null; + +alter table time_based_table add column created_at timestamp default current_timestamp not null; +alter table time_based_table add column modified_at timestamp default current_timestamp not null; +alter table time_based_table add column used_at timestamp default current_timestamp not null; diff --git a/src/main/resources/db/migration/V4__drop_duration_column.sql b/src/main/resources/db/migration/V4__drop_duration_column.sql new file mode 100644 index 00000000..b01a6d85 --- /dev/null +++ b/src/main/resources/db/migration/V4__drop_duration_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE parliamentary_table DROP COLUMN duration; +ALTER TABLE time_based_table DROP COLUMN duration; diff --git a/src/main/resources/db/migration/V5__add_customize_table.sql b/src/main/resources/db/migration/V5__add_customize_table.sql new file mode 100644 index 00000000..f7e4934c --- /dev/null +++ b/src/main/resources/db/migration/V5__add_customize_table.sql @@ -0,0 +1,40 @@ +create table customize_table +( + finish_bell boolean not null, + warning_bell boolean not null, + id bigint auto_increment, + member_id bigint not null, + agenda varchar(255) not null, + name varchar(255) not null, + pros_team_name varchar(255) not null, + cons_team_name varchar(255) not null, + created_at timestamp not null, + modified_at timestamp not null, + used_at timestamp not null, + primary key (id) +); + +create table customize_time_box +( + sequence integer not null, + speaker integer, + time integer not null, + time_per_speaking integer, + time_per_team integer, + id bigint auto_increment, + table_id bigint not null, + stance enum ('CONS','NEUTRAL','PROS') not null, + speech_type varchar(255) not null, + box_type enum ('NORMAL','TIME_BASED') not null, + primary key (id) +); + +alter table customize_table + add constraint customize_table_to_member + foreign key (member_id) + references member (id); + +alter table customize_time_box + add constraint customize_time_box_to_time_based_table + foreign key (table_id) + references customize_table (id); diff --git a/src/main/resources/db/migration/V6__agenda_modify_nullable.sql b/src/main/resources/db/migration/V6__agenda_modify_nullable.sql new file mode 100644 index 00000000..5d5d8124 --- /dev/null +++ b/src/main/resources/db/migration/V6__agenda_modify_nullable.sql @@ -0,0 +1,3 @@ +alter table parliamentary_table modify agenda varchar(255) null; +alter table time_based_table modify agenda varchar(255) null; +alter table customize_table modify agenda varchar(255) null; diff --git a/src/main/resources/logging/log4j2-dev.yml b/src/main/resources/logging/log4j2-dev.yml new file mode 100644 index 00000000..25ca8591 --- /dev/null +++ b/src/main/resources/logging/log4j2-dev.yml @@ -0,0 +1,44 @@ +Configuration: + name: Dev-Logger + status: debug + + Properties: + Property: + name: log-dir + value: "logs" + + Appenders: + RollingFile: + name: RollingFile_Appender + fileName: ${log-dir}/logfile.log + filePattern: "${log-dir}logfile-%d{yyyy-MM-dd}.%i.txt" + PatternLayout: + pattern: "%d{yyyy-MM-dd HH:mm:ss} [%X{requestId}] [%thread] [%-5level] %logger{35} - %msg%n" + immediateFlush: false #false로 설정되어야 Async로 buffer에 저장됨 + + Policies: + SizeBasedTriggeringPolicy: + size: "10 MB" + TimeBasedTriggeringPolicy: + Interval: 1 + modulate: true #다음 롤오버 시간을 정각 바운더리에 맞추는 설정 + DefaultRollOverStrategy: + max: 10 + Delete: + basePath: "${log-dir}" + maxDepth: "1" + IfLastModified: + age: "P7D" + + Loggers: + Root: + level: info + AppenderRef: + ref: RollingFile_Appender + Logger: + name: debate-timer-dev + additivity: false + level: debug + includeLocation: false + AppenderRef: + ref: RollingFile_Appender diff --git a/src/main/resources/logging/log4j2-local.yml b/src/main/resources/logging/log4j2-local.yml new file mode 100644 index 00000000..dd6e7059 --- /dev/null +++ b/src/main/resources/logging/log4j2-local.yml @@ -0,0 +1,22 @@ +Configuration: + name: Local-Logger + status: debug + + appenders: + Console: + name: Console_Appender + target: SYSTEM_OUT + PatternLayout: + pattern: "%d{yyyy-MM-dd HH:mm:ss} [%X{requestId}] [%thread] [%highlight{%-5level}] %logger{35} - %msg%n" + + Loggers: + Root: + level: info + AppenderRef: + ref: Console_Appender + Logger: + name: debate-timer-local + additivity: false + level: debug + AppenderRef: + ref: Console_Appender diff --git a/src/main/resources/logging/log4j2-prod.yml b/src/main/resources/logging/log4j2-prod.yml new file mode 100644 index 00000000..bb3a2793 --- /dev/null +++ b/src/main/resources/logging/log4j2-prod.yml @@ -0,0 +1,44 @@ +Configuration: + name: Dev-Logger + status: debug + + Properties: + Property: + name: log-dir + value: "logs" + + Appenders: + RollingFile: + name: RollingFile_Appender + fileName: ${log-dir}/logfile.log + filePattern: "${log-dir}logfile-%d{yyyy-MM-dd}.%i.txt" + PatternLayout: + pattern: "%d{yyyy-MM-dd HH:mm:ss} [%X{requestId}] [%thread] [%-5level] %logger{35} - %msg%n" + immediateFlush: false #false로 설정되어야 Async로 buffer에 저장됨 + + Policies: + SizeBasedTriggeringPolicy: + size: "10 MB" + TimeBasedTriggeringPolicy: + Interval: 1 + modulate: true #다음 롤오버 시간을 정각 바운더리에 맞추는 설정 + DefaultRollOverStrategy: + max: 30 + Delete: + basePath: "${log-dir}" + maxDepth: "1" + IfLastModified: + age: "P30D" #30일간 데이터는 저장됨 + + Loggers: + Root: + level: info + AppenderRef: + ref: RollingFile_Appender + Logger: + name: debate-timer-prod + additivity: false + level: debug + includeLocation: false + AppenderRef: + ref: RollingFile_Appender diff --git a/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java new file mode 100644 index 00000000..e89fce15 --- /dev/null +++ b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java @@ -0,0 +1,22 @@ +package com.debatetimer; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import org.flywaydb.core.Flyway; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("flyway") +class DatabaseSchemaManagerTest { + + @Autowired + private Flyway flyway; + + @Test + void contextLoads() { + assertThatCode(() -> flyway.validate()).doesNotThrowAnyException(); + } +} diff --git a/src/test/java/com/debatetimer/DebateTimerApplicationTest.java b/src/test/java/com/debatetimer/DebateTimerApplicationTest.java index aad9bc29..fcf32f13 100644 --- a/src/test/java/com/debatetimer/DebateTimerApplicationTest.java +++ b/src/test/java/com/debatetimer/DebateTimerApplicationTest.java @@ -9,5 +9,4 @@ class DebateTimerApplicationTest { @Test void contextLoads() { } - } diff --git a/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java b/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java new file mode 100644 index 00000000..1b0250f9 --- /dev/null +++ b/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java @@ -0,0 +1,44 @@ +package com.debatetimer.client; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +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 OAuthPropertiesTest { + + @Nested + class Validate { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"\n", "\t "}) + void 클라이언트_아이디가_비어있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new OAuthProperties(empty, "client_secret", "grant_type")) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.OAUTH_PROPERTIES_EMPTY.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"\n", "\t "}) + void 클라이언트_비밀키가_비어있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new OAuthProperties("client_id", empty, "grant_type")) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.OAUTH_PROPERTIES_EMPTY.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"\n", "\t "}) + void 타입이_비어있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new OAuthProperties("client_id", "client_secret", empty)) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.OAUTH_PROPERTIES_EMPTY.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/config/CorsConfigTest.java b/src/test/java/com/debatetimer/config/CorsConfigTest.java new file mode 100644 index 00000000..0011729a --- /dev/null +++ b/src/test/java/com/debatetimer/config/CorsConfigTest.java @@ -0,0 +1,40 @@ +package com.debatetimer.config; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class CorsConfigTest { + + @Nested + class Validate { + + @Test + void 허용된_도메인이_null_일_경우_예외를_발생시칸다() { + assertThatThrownBy(() -> new CorsConfig(null)) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.CORS_ORIGIN_EMPTY.getMessage()); + } + + @Test + void 허용된_도메인이_빈_배열일_경우_예외를_발생시칸다() { + assertThatThrownBy(() -> new CorsConfig(new String[0])) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.CORS_ORIGIN_EMPTY.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void 허용된_도메인_중에_빈_값이_있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new CorsConfig(new String[]{empty})) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.CORS_ORIGIN_STRING_BLANK.getMessage()); + + } + } +} diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 17084b51..0bc58e12 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -2,14 +2,15 @@ import com.debatetimer.DataBaseCleaner; import com.debatetimer.client.OAuthClient; -import com.debatetimer.fixture.CookieGenerator; import com.debatetimer.fixture.HeaderGenerator; 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.fixture.TokenGenerator; -import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; +import com.debatetimer.repository.timebased.TimeBasedTableRepository; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; @@ -27,25 +28,28 @@ public abstract class BaseControllerTest { @Autowired - protected MemberRepository memberRepository; + protected ParliamentaryTableRepository parliamentaryTableRepository; @Autowired - protected ParliamentaryTableRepository parliamentaryTableRepository; + protected TimeBasedTableRepository timeBasedTableRepository; @Autowired protected MemberGenerator memberGenerator; @Autowired - protected ParliamentaryTableGenerator tableGenerator; + protected ParliamentaryTableGenerator parliamentaryTableGenerator; @Autowired - protected ParliamentaryTimeBoxGenerator timeBoxGenerator; + protected ParliamentaryTimeBoxGenerator parliamentaryTimeBoxGenerator; @Autowired - protected HeaderGenerator headerGenerator; + protected TimeBasedTableGenerator timeBasedTableGenerator; @Autowired - protected CookieGenerator cookieGenerator; + protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; + + @Autowired + protected HeaderGenerator headerGenerator; @Autowired protected TokenGenerator tokenGenerator; diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index f7d69304..bb92a2ed 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -12,6 +12,7 @@ import com.debatetimer.service.auth.AuthService; import com.debatetimer.service.member.MemberService; import com.debatetimer.service.parliamentary.ParliamentaryService; +import com.debatetimer.service.timebased.TimeBasedService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; @@ -19,7 +20,7 @@ import io.restassured.http.Header; import io.restassured.http.Headers; import io.restassured.specification.RequestSpecification; -import jakarta.servlet.http.Cookie; +import java.time.Duration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -44,11 +45,9 @@ public abstract class BaseDocumentTest { protected static String EXIST_MEMBER_ACCESS_TOKEN = "dflskgnkds"; protected static String EXIST_MEMBER_REFRESH_TOKEN = "dfsfsdgrs"; protected static JwtTokenResponse EXIST_MEMBER_TOKEN_RESPONSE = new JwtTokenResponse(EXIST_MEMBER_ACCESS_TOKEN, - EXIST_MEMBER_REFRESH_TOKEN); + EXIST_MEMBER_REFRESH_TOKEN, Duration.ofHours(1)); protected static Headers EXIST_MEMBER_HEADER = new Headers( new Header(HttpHeaders.AUTHORIZATION, EXIST_MEMBER_ACCESS_TOKEN)); - protected static Cookie EXIST_MEMBER_COOKIE = new Cookie("refreshToken", EXIST_MEMBER_REFRESH_TOKEN); - protected static Cookie DELETE_MEMBER_COOKIE = new Cookie("refreshToken", ""); protected static RestDocumentationResponse ERROR_RESPONSE = new RestDocumentationResponse() .responseBodyField( @@ -61,6 +60,9 @@ public abstract class BaseDocumentTest { @MockitoBean protected ParliamentaryService parliamentaryService; + @MockitoBean + protected TimeBasedService timeBasedService; + @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 4b244dea..d65c7cb2 100644 --- a/src/test/java/com/debatetimer/controller/Tag.java +++ b/src/test/java/com/debatetimer/controller/Tag.java @@ -4,7 +4,7 @@ public enum Tag { MEMBER_API("Member API"), PARLIAMENTARY_API("Parliamentary Table API"), - ; + TIME_BASED_API("Time Based Table API"); private final String displayName; diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index 8f463980..1594468f 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -23,8 +23,8 @@ class GetTables { @Test void 회원의_전체_토론_시간표를_조회한다() { Member member = memberGenerator.generate("default@gmail.com"); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800, false, false)); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900, false, false)); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", false, false)); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", false, false)); Headers headers = headerGenerator.generateAccessTokenHeader(member); diff --git a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java index c4504b6f..e1e50bf8 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java @@ -57,7 +57,8 @@ class CreateMember { doReturn(memberInfo).when(authService).getMemberInfo(request); doReturn(response).when(memberService).createMember(memberInfo); doReturn(EXIST_MEMBER_TOKEN_RESPONSE).when(authManager).issueToken(memberInfo); - doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 500)).when(cookieManager).createRefreshTokenCookie(EXIST_MEMBER_REFRESH_TOKEN); + doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 500)).when(cookieManager) + .createCookie(any(), any(), any()); var document = document("member/create", 201).request(requestDocument).response(responseDocument).build(); @@ -84,13 +85,13 @@ class GetTables { fieldWithPath("tables[].id").type(NUMBER).description("토론 테이블 ID (토론 타입 별로 ID를 가짐)"), fieldWithPath("tables[].name").type(STRING).description("토론 테이블 이름"), fieldWithPath("tables[].type").type(STRING).description("토론 타입"), - fieldWithPath("tables[].duration").type(NUMBER).description("소요 시간 (초)")); + fieldWithPath("tables[].agenda").type(STRING).description("토론 주제")); @Test void 테이블_조회_성공() { TableResponses response = new TableResponses( - List.of(new TableResponse(1L, "토론 테이블 1", TableType.PARLIAMENTARY, 1800), - new TableResponse(2L, "토론 테이블 2", TableType.PARLIAMENTARY, 2000)) + List.of(new TableResponse(1L, "토론 테이블 1", TableType.PARLIAMENTARY, "주제1"), + new TableResponse(2L, "토론 테이블 2", TableType.PARLIAMENTARY, "주제2")) ); doReturn(response).when(memberService).getTables(EXIST_MEMBER_ID); @@ -143,7 +144,8 @@ class ReissueAccessToken { @Test void 토큰_갱신_성공() { doReturn(EXIST_MEMBER_TOKEN_RESPONSE).when(authManager).reissueToken(any()); - doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 500)).when(cookieManager).createRefreshTokenCookie(any()); + doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 500)).when(cookieManager) + .createCookie(any(), any(), any()); var document = document("member/logout", 204) .request(requestDocument) @@ -157,22 +159,6 @@ class ReissueAccessToken { .then().statusCode(200); } - @EnumSource(value = ClientErrorCode.class, names = {"EMPTY_COOKIE"}) - @ParameterizedTest - void 토큰_갱신_실패_쿠키_추출(ClientErrorCode errorCode) { - doThrow(new DTClientErrorException(errorCode)).when(cookieManager).extractRefreshToken(any()); - - var document = document("member/reissue", errorCode) - .request(requestDocument) - .response(ERROR_RESPONSE) - .build(); - - given(document) - .cookie("refreshToken") - .when().post("/api/member/reissue") - .then().statusCode(errorCode.getStatus().value()); - } - @EnumSource(value = ClientErrorCode.class, names = {"EXPIRED_TOKEN", "UNAUTHORIZED_MEMBER"}) @ParameterizedTest void 토큰_갱신_실패_토큰_갱신(ClientErrorCode errorCode) { @@ -205,7 +191,7 @@ class Logout { @Test void 로그아웃_성공() { - doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 0)).when(cookieManager).deleteRefreshTokenCookie(); + doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 0)).when(cookieManager).createExpiredCookie(any()); var document = document("member/logout", 204) .request(requestDocument) @@ -218,23 +204,6 @@ class Logout { .then().statusCode(204); } - @EnumSource(value = ClientErrorCode.class, names = {"EMPTY_COOKIE"}) - @ParameterizedTest - void 로그아웃_실패_쿠키_추출(ClientErrorCode errorCode) { - doThrow(new DTClientErrorException(errorCode)).when(cookieManager).extractRefreshToken(any()); - - var document = document("member/logout", errorCode) - .request(requestDocument) - .response(ERROR_RESPONSE) - .build(); - - given(document) - .headers(EXIST_MEMBER_HEADER) - .cookie("refreshToken") - .when().post("/api/member/logout") - .then().statusCode(errorCode.getStatus().value()); - } - @EnumSource(value = ClientErrorCode.class, names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN"}) @ParameterizedTest void 로그아웃_실패(ClientErrorCode errorCode) { diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index fdf092b3..559f4456 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -4,13 +4,13 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.debatetimer.controller.BaseControllerTest; -import com.debatetimer.domain.BoxType; import com.debatetimer.domain.Stance; import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.dto.parliamentary.request.ParliamentaryTableCreateRequest; -import com.debatetimer.dto.parliamentary.request.TableInfoCreateRequest; -import com.debatetimer.dto.parliamentary.request.TimeBoxCreateRequest; +import com.debatetimer.dto.parliamentary.request.ParliamentaryTableInfoCreateRequest; +import com.debatetimer.dto.parliamentary.request.ParliamentaryTimeBoxCreateRequest; import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; import io.restassured.http.ContentType; import io.restassured.http.Headers; @@ -27,10 +27,10 @@ class Save { void 의회식_테이블을_생성한다() { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블", "주제", true, true), + new ParliamentaryTableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -56,9 +56,9 @@ class GetTable { @Test void 의회식_테이블을_조회한다() { Member bito = memberGenerator.generate("default@gmail.com"); - ParliamentaryTable bitoTable = tableGenerator.generate(bito); - timeBoxGenerator.generate(bitoTable, 1); - timeBoxGenerator.generate(bitoTable, 2); + ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); + parliamentaryTimeBoxGenerator.generate(bitoTable, 1); + parliamentaryTimeBoxGenerator.generate(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() @@ -82,12 +82,12 @@ class UpdateTable { @Test void 의회식_토론_테이블을_업데이트한다() { Member bito = memberGenerator.generate("default@gmail.com"); - ParliamentaryTable bitoTable = tableGenerator.generate(bito); + ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블", "주제", true, true), + new ParliamentaryTableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -109,15 +109,42 @@ class UpdateTable { } } + @Nested + class Debate { + + @Test + void 의회식_토론을_진행한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); + parliamentaryTimeBoxGenerator.generate(bitoTable, 1); + parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + ParliamentaryTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().patch("/api/table/parliamentary/{tableId}/debate") + .then().statusCode(200) + .extract().as(ParliamentaryTableResponse.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"); - ParliamentaryTable bitoTable = tableGenerator.generate(bito); - timeBoxGenerator.generate(bitoTable, 1); - timeBoxGenerator.generate(bitoTable, 2); + ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); + parliamentaryTimeBoxGenerator.generate(bitoTable, 1); + parliamentaryTimeBoxGenerator.generate(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index be8ea8dd..0e2ef73b 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -18,14 +18,15 @@ import com.debatetimer.controller.RestDocumentationRequest; import com.debatetimer.controller.RestDocumentationResponse; import com.debatetimer.controller.Tag; -import com.debatetimer.domain.BoxType; import com.debatetimer.domain.Stance; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; +import com.debatetimer.dto.member.TableType; import com.debatetimer.dto.parliamentary.request.ParliamentaryTableCreateRequest; -import com.debatetimer.dto.parliamentary.request.TableInfoCreateRequest; -import com.debatetimer.dto.parliamentary.request.TimeBoxCreateRequest; +import com.debatetimer.dto.parliamentary.request.ParliamentaryTableInfoCreateRequest; +import com.debatetimer.dto.parliamentary.request.ParliamentaryTimeBoxCreateRequest; +import com.debatetimer.dto.parliamentary.response.ParliamentaryTableInfoResponse; import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; -import com.debatetimer.dto.parliamentary.response.TableInfoResponse; -import com.debatetimer.dto.parliamentary.response.TimeBoxResponse; +import com.debatetimer.dto.parliamentary.response.ParliamentaryTimeBoxResponse; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; import io.restassured.http.ContentType; @@ -65,6 +66,7 @@ class Save { fieldWithPath("id").type(NUMBER).description("테이블 ID"), fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.type").type(STRING).description("토론 형식"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), @@ -78,18 +80,18 @@ class Save { @Test void 의회식_테이블_생성_성공() { ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), + new ParliamentaryTableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 1", "토론 주제", true, true), + new ParliamentaryTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( - new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxResponse(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); doReturn(response).when(parliamentaryService).save(eq(request), any()); @@ -113,17 +115,20 @@ class Save { "INVALID_TABLE_NAME_LENGTH", "INVALID_TABLE_NAME_FORM", "INVALID_TABLE_TIME", + "INVALID_TIME_BOX_SEQUENCE", + "INVALID_TIME_BOX_SPEAKER", "INVALID_TIME_BOX_TIME", - "INVALID_TIME_BOX_STANCE" + "INVALID_TIME_BOX_STANCE", + "INVALID_TIME_BOX_FORMAT" } ) @ParameterizedTest void 의회식_테이블_생성_실패(ClientErrorCode errorCode) { ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), + new ParliamentaryTableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService).save(eq(request), any()); @@ -160,6 +165,7 @@ class GetTable { fieldWithPath("id").type(NUMBER).description("테이블 ID"), fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.type").type(STRING).description("토론 형식"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), @@ -172,14 +178,13 @@ class GetTable { @Test void 의회식_테이블_조회_성공() { - long memberId = 4L; long tableId = 5L; ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 1", "토론 주제", true, true), + new ParliamentaryTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( - new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxResponse(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); doReturn(response).when(parliamentaryService).findTable(eq(tableId), any()); @@ -200,7 +205,6 @@ class GetTable { @ParameterizedTest @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) void 의회식_테이블_조회_실패(ClientErrorCode errorCode) { - long memberId = 4L; long tableId = 5L; doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService).findTable(eq(tableId), any()); @@ -248,6 +252,7 @@ class UpdateTable { fieldWithPath("id").type(NUMBER).description("테이블 ID"), fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.type").type(STRING).description("토론 형식"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), @@ -260,21 +265,20 @@ class UpdateTable { @Test void 의회식_토론_테이블_수정() { - long memberId = 4L; long tableId = 5L; ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), + new ParliamentaryTableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 300, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 300, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 300, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 300, 1) ) ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 2", "토론 주제 2", true, true), + new ParliamentaryTableInfoResponse("비토 테이블 2", TableType.PARLIAMENTARY, "토론 주제 2", true, true), List.of( - new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 300, 1), - new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 300, 1) + new ParliamentaryTimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 300, 1), + new ParliamentaryTimeBoxResponse(Stance.CONS, ParliamentaryBoxType.OPENING, 300, 1) ) ); doReturn(response).when(parliamentaryService).updateTable(eq(request), eq(tableId), any()); @@ -299,20 +303,21 @@ class UpdateTable { "INVALID_TABLE_NAME_LENGTH", "INVALID_TABLE_NAME_FORM", "INVALID_TABLE_TIME", + "INVALID_TIME_BOX_SEQUENCE", + "INVALID_TIME_BOX_SPEAKER", "INVALID_TIME_BOX_TIME", "INVALID_TIME_BOX_STANCE", - "NOT_TABLE_OWNER" + "INVALID_TIME_BOX_FORMAT" } ) @ParameterizedTest - void 의회식_테이블_생성_실패(ClientErrorCode errorCode) { - long memberId = 4L; + void 의회식_테이블_수정_실패(ClientErrorCode errorCode) { long tableId = 5L; ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), + new ParliamentaryTableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 300, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 300, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 300, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 300, 1) ) ); doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService) @@ -333,6 +338,81 @@ class UpdateTable { } } + @Nested + class Debate { + + private final RestDocumentationRequest requestDocument = request() + .summary("의회식 토론 진행") + .tag(Tag.PARLIAMENTARY_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.type").type(STRING).description("토론 형식"), + fieldWithPath("info.agenda").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[].type").type(STRING).description("발언 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"), + fieldWithPath("table[].speakerNumber").type(NUMBER).description("발언자 번호").optional() + ); + + @Test + void 의회식_토론_진행_성공() { + long tableId = 5L; + ParliamentaryTableResponse response = new ParliamentaryTableResponse( + 5L, + new ParliamentaryTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), + List.of( + new ParliamentaryTimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxResponse(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) + ) + ); + doReturn(response).when(parliamentaryService).updateUsedAt(eq(tableId), any()); + + var document = document("parliamentary/patch_debate", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/parliamentary/{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(parliamentaryService).updateUsedAt(eq(tableId), any()); + + var document = document("parliamentary/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/parliamentary/{tableId}/debate") + .then().statusCode(errorCode.getStatus().value()); + } + } + @Nested class DeleteTable { @@ -348,7 +428,6 @@ class DeleteTable { @Test void 의회식_테이블_삭제_성공() { - long memberId = 4L; long tableId = 5L; doNothing().when(parliamentaryService).deleteTable(eq(tableId), any()); @@ -366,7 +445,6 @@ class DeleteTable { @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) @ParameterizedTest void 의회식_테이블_삭제_실패(ClientErrorCode errorCode) { - long memberId = 4L; long tableId = 5L; doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService).deleteTable(eq(tableId), any()); diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java new file mode 100644 index 00000000..034e1952 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java @@ -0,0 +1,158 @@ +package com.debatetimer.controller.timebased; + +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.member.Member; +import com.debatetimer.domain.timebased.TimeBasedBoxType; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.dto.timebased.request.TimeBasedTableCreateRequest; +import com.debatetimer.dto.timebased.request.TimeBasedTableInfoCreateRequest; +import com.debatetimer.dto.timebased.request.TimeBasedTimeBoxCreateRequest; +import com.debatetimer.dto.timebased.response.TimeBasedTableResponse; +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 TimeBasedControllerTest extends BaseControllerTest { + + @Nested + class Save { + + @Test + void 시간총량제_테이블을_생성한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + TimeBasedTableCreateRequest request = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("비토 테이블", "주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + TimeBasedTableResponse response = given() + .contentType(ContentType.JSON) + .headers(headers) + .body(request) + .when().post("/api/table/time-based") + .then().statusCode(201) + .extract().as(TimeBasedTableResponse.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"); + TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); + timeBasedTimeBoxGenerator.generate(bitoTable, 1); + timeBasedTimeBoxGenerator.generate(bitoTable, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + TimeBasedTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().get("/api/table/time-based/{tableId}") + .then().statusCode(200) + .extract().as(TimeBasedTableResponse.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"); + TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); + TimeBasedTableCreateRequest renewTableRequest = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("비토 테이블", "주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + TimeBasedTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .body(renewTableRequest) + .when().put("/api/table/time-based/{tableId}") + .then().statusCode(200) + .extract().as(TimeBasedTableResponse.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"); + TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); + timeBasedTimeBoxGenerator.generate(bitoTable, 1); + timeBasedTimeBoxGenerator.generate(bitoTable, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + TimeBasedTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().patch("/api/table/time-based/{tableId}/debate") + .then().statusCode(200) + .extract().as(TimeBasedTableResponse.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"); + TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); + timeBasedTimeBoxGenerator.generate(bitoTable, 1); + timeBasedTimeBoxGenerator.generate(bitoTable, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().delete("/api/table/time-based/{tableId}") + .then().statusCode(204); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java new file mode 100644 index 00000000..2c1eb608 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java @@ -0,0 +1,475 @@ +package com.debatetimer.controller.timebased; + +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.timebased.TimeBasedBoxType; +import com.debatetimer.dto.member.TableType; +import com.debatetimer.dto.timebased.request.TimeBasedTableCreateRequest; +import com.debatetimer.dto.timebased.request.TimeBasedTableInfoCreateRequest; +import com.debatetimer.dto.timebased.request.TimeBasedTimeBoxCreateRequest; +import com.debatetimer.dto.timebased.response.TimeBasedTableInfoResponse; +import com.debatetimer.dto.timebased.response.TimeBasedTableResponse; +import com.debatetimer.dto.timebased.response.TimeBasedTimeBoxResponse; +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 TimeBasedDocumentTest extends BaseDocumentTest { + + @Nested + class Save { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.TIME_BASED_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.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].type").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[].speakerNumber").type(NUMBER).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.type").type(STRING).description("토론 형식"), + fieldWithPath("info.agenda").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[].type").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[].speakerNumber").type(NUMBER).description("발언자 번호").optional() + ); + + @Test + void 시간총량제_테이블_생성_성공() { + TimeBasedTableCreateRequest request = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + TimeBasedTableResponse response = new TimeBasedTableResponse( + 5L, + new TimeBasedTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), + List.of( + new TimeBasedTimeBoxResponse(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, 1), + new TimeBasedTimeBoxResponse(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, 1) + ) + ); + doReturn(response).when(timeBasedService).save(eq(request), any()); + + var document = document("time_based/post", 201) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .body(request) + .when().post("/api/table/time-based") + .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_SPEAKER", + "INVALID_TIME_BOX_TIME", + "INVALID_TIME_BOX_STANCE", + "INVALID_TIME_BOX_FORMAT" + } + ) + @ParameterizedTest + void 시간총량제_테이블_생성_실패(ClientErrorCode errorCode) { + TimeBasedTableCreateRequest request = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + doThrow(new DTClientErrorException(errorCode)).when(timeBasedService).save(eq(request), any()); + + var document = document("time_based/post", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .body(request) + .when().post("/api/table/time-based") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class GetTable { + + private final RestDocumentationRequest requestDocument = request() + .summary("시간총량제 토론 시간표 조회") + .tag(Tag.TIME_BASED_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.type").type(STRING).description("토론 형식"), + fieldWithPath("info.agenda").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[].type").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[].speakerNumber").type(NUMBER).description("발언자 번호").optional() + ); + + @Test + void 시간총량제_테이블_조회_성공() { + long tableId = 5L; + TimeBasedTableResponse response = new TimeBasedTableResponse( + 5L, + new TimeBasedTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), + List.of( + new TimeBasedTimeBoxResponse(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, 1), + new TimeBasedTimeBoxResponse(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, 1) + ) + ); + doReturn(response).when(timeBasedService).findTable(eq(tableId), any()); + + var document = document("time_based/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().get("/api/table/time-based/{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(timeBasedService).findTable(eq(tableId), any()); + + var document = document("time_based/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().get("/api/table/time-based/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class UpdateTable { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.TIME_BASED_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.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].type").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[].speakerNumber").type(NUMBER).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.type").type(STRING).description("토론 형식"), + fieldWithPath("info.agenda").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[].type").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[].speakerNumber").type(NUMBER).description("발언자 번호").optional() + ); + + @Test + void 시간총량제_토론_테이블_수정() { + long tableId = 5L; + TimeBasedTableCreateRequest request = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + TimeBasedTableResponse response = new TimeBasedTableResponse( + 5L, + new TimeBasedTableInfoResponse("비토 테이블 2", TableType.PARLIAMENTARY, "토론 주제 2", true, true), + List.of( + new TimeBasedTimeBoxResponse(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, 1), + new TimeBasedTimeBoxResponse(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, 1) + ) + ); + doReturn(response).when(timeBasedService).updateTable(eq(request), eq(tableId), any()); + + var document = document("time_based/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/time-based/{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_SPEAKER", + "INVALID_TIME_BOX_TIME", + "INVALID_TIME_BOX_STANCE", + "INVALID_TIME_BOX_FORMAT" + } + ) + @ParameterizedTest + void 시간총량제_테이블_생성_실패(ClientErrorCode errorCode) { + long tableId = 5L; + TimeBasedTableCreateRequest request = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + doThrow(new DTClientErrorException(errorCode)).when(timeBasedService) + .updateTable(eq(request), eq(tableId), any()); + + var document = document("time_based/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/time-based/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class Debate { + + private final RestDocumentationRequest requestDocument = request() + .summary("시간총량제 토론 시작") + .tag(Tag.TIME_BASED_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.type").type(STRING).description("토론 형식"), + fieldWithPath("info.agenda").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[].type").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[].speakerNumber").type(NUMBER).description("발언자 번호").optional() + ); + + @Test + void 시간총량제_토론_진행_성공() { + long tableId = 5L; + TimeBasedTableResponse response = new TimeBasedTableResponse( + 5L, + new TimeBasedTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), + List.of( + new TimeBasedTimeBoxResponse(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, 1), + new TimeBasedTimeBoxResponse(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, 1) + ) + ); + doReturn(response).when(timeBasedService).updateUsedAt(eq(tableId), any()); + + var document = document("time_based/patch_debate", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/time-based/{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(timeBasedService).updateUsedAt(eq(tableId), any()); + + var document = document("time_based/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/time-based/{tableId}/debate") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class DeleteTable { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.TIME_BASED_API) + .summary("시간총량제 토론 시간표 삭제") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + @Test + void 시간총량제_테이블_삭제_성공() { + long tableId = 5L; + doNothing().when(timeBasedService).deleteTable(eq(tableId), any()); + + var document = document("time_based/delete", 204) + .request(requestDocument) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().delete("/api/table/time-based/{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(timeBasedService).deleteTable(eq(tableId), any()); + + var document = document("time_based/delete", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().delete("/api/table/time-based/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java b/src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java deleted file mode 100644 index 84eb34ff..00000000 --- a/src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.debatetimer.controller.tool.cookie; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; -import com.debatetimer.fixture.CookieGenerator; -import jakarta.servlet.http.Cookie; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -class CookieExtractorTest { - - private CookieGenerator cookieGenerator; - private CookieExtractor cookieExtractor; - - @BeforeEach - void setUp() { - this.cookieGenerator = new CookieGenerator(); - this.cookieExtractor = new CookieExtractor(); - } - - @Nested - class ExtractCookie { - - @Test - void 쿠키에서_해당하는_키의_값을_추출한다() { - String key = "key"; - String value = "value"; - Cookie[] cookies = cookieGenerator.generateCookie(key, value, 100000); - - assertThat(cookieExtractor.extractCookie(key, cookies)).isEqualTo(value); - } - - @Test - void 쿠키에서_해당하는_값이_없으면_예외를_발생시킨다() { - String key = "key"; - String value = "value"; - Cookie[] cookies = cookieGenerator.generateCookie("token", value, 100000); - - assertThatThrownBy(() -> cookieExtractor.extractCookie(key, cookies)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.EMPTY_COOKIE.getMessage()); - } - } -} diff --git a/src/test/java/com/debatetimer/controller/tool/cookie/CookieProviderTest.java b/src/test/java/com/debatetimer/controller/tool/cookie/CookieProviderTest.java new file mode 100644 index 00000000..ecf78372 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/tool/cookie/CookieProviderTest.java @@ -0,0 +1,38 @@ +package com.debatetimer.controller.tool.cookie; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseCookie; + +class CookieProviderTest { + + @Nested + class CreateCookie { + + @Test + void 쿠키_이름과_값을_설정한다() { + CookieProvider cookieProvider = new CookieProvider(); + String key = "cookieKey"; + String value = "tokenValue"; + + ResponseCookie cookie = cookieProvider.createCookie(key, value, Duration.ofHours(1)); + + assertThat(cookie.toString()) + .contains("%s=%s;".formatted(key, value)); + } + + @Test + void 클라이언트와_서버가_분리된_환경에서_쿠키가_정상작동하도록_설정한다() { + CookieProvider cookieProvider = new CookieProvider(); + + ResponseCookie cookie = cookieProvider.createCookie("key", "value", Duration.ofHours(1)); + + assertThat(cookie.toString()) + .contains("SameSite=None") + .contains("Secure"); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java b/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java new file mode 100644 index 00000000..7232ac9a --- /dev/null +++ b/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java @@ -0,0 +1,67 @@ +package com.debatetimer.controller.tool.jwt; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import java.time.Duration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class JwtTokenPropertiesTest { + + @Nested + class ValidateSecretKey { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", "\n\t"}) + void 시크릿_키가_비어있을_경우_예외를_발생시킨다(String emptyKey) { + Duration accessTokenExpiration = Duration.ofMinutes(1); + Duration refreshTokenExpiration = Duration.ofMinutes(5); + + assertThatThrownBy(() -> new JwtTokenProperties(emptyKey, accessTokenExpiration, refreshTokenExpiration)) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_SECRET_KEY_EMPTY.getMessage()); + } + } + + @Nested + class ValidateToken { + + @Test + void 유효_기간이_비어있을_경우_예외를_발생시킨다() { + String secretKey = "testtesttest"; + + assertAll( + () -> assertThatThrownBy(() -> new JwtTokenProperties(secretKey, null, Duration.ofMinutes(5))) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_EMPTY.getMessage()), + () -> assertThatThrownBy(() -> new JwtTokenProperties(secretKey, Duration.ofMinutes(1), null)) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_EMPTY.getMessage()) + ); + } + + @Test + void 유효_기간이_음수일_경우_예외를_발생시킨다() { + String secretKey = "testtesttest"; + Duration negativeExpiration = Duration.ofMinutes(-1); + + assertAll( + () -> assertThatThrownBy( + () -> new JwtTokenProperties(secretKey, negativeExpiration, Duration.ofMinutes(5))) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_INVALID.getMessage()), + () -> assertThatThrownBy( + () -> new JwtTokenProperties(secretKey, Duration.ofMinutes(1), negativeExpiration)) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_INVALID.getMessage()) + ); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java new file mode 100644 index 00000000..b8ad484c --- /dev/null +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -0,0 +1,134 @@ +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; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.TableType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DebateTableTest { + + @Nested + class Validate { + + @ValueSource(strings = {"a bc가다9", "가0나 다ab"}) + @ParameterizedTest + 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}) + @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) { + 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()); + } + + @ValueSource(strings = {"abc@", "가나다*", "abc\tde"}) + @ParameterizedTest + 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_FORM.getMessage()); + } + } + + @Nested + class UpdateUsedAt { + + @Test + void 테이블의_사용_시각을_업데이트한다() throws InterruptedException { + Member member = new Member("default@gmail.com"); + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", true, true); + LocalDateTime beforeUsedAt = table.getUsedAt(); + Thread.sleep(1); + + table.updateUsedAt(); + + assertThat(table.getUsedAt()).isAfter(beforeUsedAt); + } + } + + @Nested + class Update { + + @Test + void 테이블_정보를_업데이트_할_수_있다() { + Member member = new Member("default@gmail.com"); + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", true, true); + DebateTableTestObject renewTable = new DebateTableTestObject(member, "newName", "newAgenda", false, + false); + + table.updateTable(renewTable); + + assertAll( + () -> assertThat(table.getName()).isEqualTo("newName"), + () -> assertThat(table.getAgenda()).isEqualTo("newAgenda"), + () -> assertThat(table.isWarningBell()).isEqualTo(false), + () -> assertThat(table.isFinishBell()).isEqualTo(false) + ); + } + + @Test + void 테이블_업데이트_할_때_사용_시간을_변경한다() throws InterruptedException { + Member member = new Member("default@gmail.com"); + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", true, true); + DebateTableTestObject renewTable = new DebateTableTestObject(member, "newName", "newAgenda", false, + false); + LocalDateTime beforeUsedAt = table.getUsedAt(); + Thread.sleep(1); + + table.updateTable(renewTable); + + assertThat(table.getUsedAt()).isAfter(beforeUsedAt); + } + } + + private static class DebateTableTestObject extends DebateTable { + + public DebateTableTestObject(Member member, + String name, + String agenda, + boolean warningBell, + boolean finishBell) { + super(member, name, agenda, warningBell, finishBell); + } + + @Override + public long getId() { + return 0; + } + + @Override + public TableType getType() { + return null; + } + + public void updateTable(DebateTableTestObject renewTable) { + super.updateTable(renewTable); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java new file mode 100644 index 00000000..52b8dba0 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java @@ -0,0 +1,58 @@ +package com.debatetimer.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class DebateTimeBoxTest { + + @Nested + class Validate { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 순서는_양수만_가능하다(int sequence) { + assertThatThrownBy(() -> new DebateTimeBoxTestObject(sequence, Stance.CONS, 60, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); + } + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 시간은_양수만_가능하다(int time) { + assertThatThrownBy( + () -> new DebateTimeBoxTestObject(1, Stance.CONS, time, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + + @Test + void 발표자_번호는_빈_값이_허용된다() { + Integer speaker = null; + + assertThatCode(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) + .doesNotThrowAnyException(); + } + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 발표자_번호는_양수만_가능하다(int speaker) { + assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); + } + } + + private static class DebateTimeBoxTestObject extends DebateTimeBox { + + public DebateTimeBoxTestObject(int sequence, Stance stance, int time, Integer speaker) { + super(sequence, stance, time, speaker); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java similarity index 60% rename from src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java rename to src/test/java/com/debatetimer/domain/TimeBoxesTest.java index c64fa43b..c4534a71 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java @@ -1,17 +1,18 @@ -package com.debatetimer.domain.parliamentary; +package com.debatetimer.domain; import static org.assertj.core.api.Assertions.assertThat; -import com.debatetimer.domain.BoxType; -import com.debatetimer.domain.Stance; import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; +import com.debatetimer.domain.parliamentary.ParliamentaryTable; +import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -class ParliamentaryTimeBoxesTest { +class TimeBoxesTest { @Nested class SortedBySequence { @@ -19,14 +20,14 @@ class SortedBySequence { @Test void 타임박스의_순서에_따라_정렬된다() { Member member = new Member("default@gmail.com"); - ParliamentaryTable testTable = new ParliamentaryTable(member, "토론 테이블", "주제", 1800, true, true); - ParliamentaryTimeBox firstBox = new ParliamentaryTimeBox(testTable, 1, Stance.PROS, BoxType.OPENING, 300, - 1); - ParliamentaryTimeBox secondBox = new ParliamentaryTimeBox(testTable, 2, Stance.PROS, BoxType.OPENING, 300, - 1); + ParliamentaryTable testTable = new ParliamentaryTable(member, "토론 테이블", "주제", true, true); + ParliamentaryTimeBox firstBox = new ParliamentaryTimeBox(testTable, 1, Stance.PROS, + ParliamentaryBoxType.OPENING, 300, 1); + ParliamentaryTimeBox secondBox = new ParliamentaryTimeBox(testTable, 2, Stance.PROS, + ParliamentaryBoxType.OPENING, 300, 1); List timeBoxes = new ArrayList<>(Arrays.asList(secondBox, firstBox)); - ParliamentaryTimeBoxes actual = new ParliamentaryTimeBoxes(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 new file mode 100644 index 00000000..10fb988f --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java @@ -0,0 +1,21 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.debatetimer.dto.member.TableType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CustomizeTableTest { + + @Nested + class GetType { + + @Test + void 사용자_지정_테이블_타입을_반환한다() { + CustomizeTable customizeTable = new CustomizeTable(); + + assertThat(customizeTable.getType()).isEqualTo(TableType.CUSTOMIZE); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java new file mode 100644 index 00000000..c9a65495 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java @@ -0,0 +1,86 @@ +package com.debatetimer.domain.customize; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.Stance; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +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()); + } + + @Test + void 자유토론_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { + CustomizeTable table = new CustomizeTable(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; + + assertThatCode( + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 240, 120, 60, 1)) + .doesNotThrowAnyException(); + } + + @Test + void 자유토론_타입이_개인_발언_시간과_팀_발언_시간을_입력하지_않을_경우_예외가_발생한다() { + CustomizeTable table = new CustomizeTable(); + CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; + + assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 10, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + } + + @Test + void 자유토론_타입이_아닌_타임박스가_개인_발언_시간과_팀_발언_시간을_입력할_경우_예외가_발생한다() { + CustomizeTable table = new CustomizeTable(); + CustomizeBoxType notTimeBasedBoxType = CustomizeBoxType.NORMAL; + + assertThatThrownBy( + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 240, 120, 60, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + } + + @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(); + } + + @Test + void 개인_발언_시간이_팀_발언_시간보다_길면_예외가_발생한다() { + CustomizeTable table = new CustomizeTable(); + int timePerTeam = 60; + int timePerSpeaking = 61; + + assertThatThrownBy( + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam * 2, + timePerTeam, timePerSpeaking, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java index 84bc003d..1a16fc1d 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java @@ -1,62 +1,21 @@ package com.debatetimer.domain.parliamentary; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThat; -import com.debatetimer.domain.member.Member; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.dto.member.TableType; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.api.Test; class ParliamentaryTableTest { @Nested - class Validate { + class GetType { - @ParameterizedTest - @ValueSource(strings = {"a bc가다9", "가0나 다ab"}) - void 테이블_이름은_영문과_한글_숫자_띄어쓰기만_가능하다(String name) { - Member member = new Member("default@gmail.com"); - assertThatCode(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) - .doesNotThrowAnyException(); - } - - @ParameterizedTest - @ValueSource(ints = {0, ParliamentaryTable.NAME_MAX_LENGTH + 1}) - void 테이블_이름은_정해진_길이_이내여야_한다(int length) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new ParliamentaryTable(member, "f".repeat(length), "agenda", 10, true, true)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"", "\t", "\n"}) - void 테이블_이름은_적어도_한_자_있어야_한다(String name) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"abc@", "가나다*", "abc\tde"}) - void 허용된_글자_이외의_문자는_불가능하다(String name) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); - } + @Test + void 의회식_테이블_타입을_반환한다() { + ParliamentaryTable parliamentaryTable = new ParliamentaryTable(); - @ParameterizedTest - @ValueSource(ints = {0, -1, -60}) - void 테이블_시간은_양수만_가능하다(int duration) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new ParliamentaryTable(member, "nickname", "agenda", duration, true, true)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_TIME.getMessage()); + assertThat(parliamentaryTable.getType()).isEqualTo(TableType.PARLIAMENTARY); } } } diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java index d698b8e2..59859496 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.debatetimer.domain.BoxType; import com.debatetimer.domain.Stance; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.errorcode.ClientErrorCode; @@ -13,35 +12,20 @@ class ParliamentaryTimeBoxTest { @Nested - class Validate { - - @Test - void 순서는_양수만_가능하다() { - ParliamentaryTable table = new ParliamentaryTable(); - assertThatThrownBy(() -> new ParliamentaryTimeBox(table, 0, Stance.CONS, BoxType.OPENING, 10, 1)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); - } - - @Test - void 시간은_양수만_가능하다() { - ParliamentaryTable table = new ParliamentaryTable(); - assertThatThrownBy(() -> new ParliamentaryTimeBox(table, 1, Stance.CONS, BoxType.OPENING, 0, 1)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); - } + class ValidateStance { @Test void 박스타입에_가능한_입장을_검증한다() { ParliamentaryTable table = new ParliamentaryTable(); - assertThatCode(() -> new ParliamentaryTimeBox(table, 1, Stance.CONS, BoxType.OPENING, 10, 1)) + assertThatCode(() -> new ParliamentaryTimeBox(table, 1, Stance.CONS, ParliamentaryBoxType.OPENING, 10, 1)) .doesNotThrowAnyException(); } @Test void 박스타입에_불가한_입장으로_생성을_시도하면_예외를_발생시킨다() { ParliamentaryTable table = new ParliamentaryTable(); - assertThatThrownBy(() -> new ParliamentaryTimeBox(table, 1, Stance.NEUTRAL, BoxType.OPENING, 10, 1)) + assertThatThrownBy( + () -> new ParliamentaryTimeBox(table, 1, Stance.NEUTRAL, ParliamentaryBoxType.OPENING, 10, 1)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_STANCE.getMessage()); } diff --git a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTableTest.java b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTableTest.java new file mode 100644 index 00000000..73a7cbcf --- /dev/null +++ b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTableTest.java @@ -0,0 +1,21 @@ +package com.debatetimer.domain.timebased; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.debatetimer.dto.member.TableType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class TimeBasedTableTest { + + @Nested + class GetType { + + @Test + void 시간총량제_테이블_타입을_반환한다() { + TimeBasedTable timeBasedTable = new TimeBasedTable(); + + assertThat(timeBasedTable.getType()).isEqualTo(TableType.TIME_BASED); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java new file mode 100644 index 00000000..48f1336d --- /dev/null +++ b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java @@ -0,0 +1,105 @@ +package com.debatetimer.domain.timebased; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.Stance; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class TimeBasedTimeBoxTest { + + @Nested + class ValidateStance { + + @Test + void 박스타입에_가능한_입장을_검증한다() { + TimeBasedTable table = new TimeBasedTable(); + + assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.CONS, TimeBasedBoxType.OPENING, 10, 1)) + .doesNotThrowAnyException(); + } + + @Test + void 박스타입에_불가한_입장으로_생성을_시도하면_예외를_발생시킨다() { + TimeBasedTable table = new TimeBasedTable(); + + assertThatThrownBy( + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.OPENING, 10, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_STANCE.getMessage()); + } + } + + @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)) + .doesNotThrowAnyException(); + } + + @Test + void 시간총량제_타입이_개인_발언_시간과_팀_발언_시간을_입력하지_않을_경우_예외가_발생한다() { + TimeBasedTable table = new TimeBasedTable(); + TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; + + assertThatThrownBy(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 10, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + } + + @Test + void 시간총량제가_아닌_타입이__개인_발언_시간과_팀_발언_시간을_입력할_경우_예외가_발생한다() { + TimeBasedTable table = new TimeBasedTable(); + TimeBasedBoxType notTimeBasedBoxType = TimeBasedBoxType.TIME_OUT; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, notTimeBasedBoxType, 240, 120, 60, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); + } + + @Test + void 개인_발언_시간은_팀_발언_시간보다_적거나_같아야_한다() { + TimeBasedTable table = new TimeBasedTable(); + int timePerTeam = 60; + int timePerSpeaking = 59; + + assertThatCode( + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam * 2, + timePerTeam, timePerSpeaking, 1)) + .doesNotThrowAnyException(); + } + + @Test + void 개인_발언_시간이_팀_발언_시간보다_길면_예외가_발생한다() { + TimeBasedTable table = new TimeBasedTable(); + int timePerTeam = 60; + int timePerSpeaking = 61; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam * 2, + timePerTeam, timePerSpeaking, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/fixture/CookieGenerator.java b/src/test/java/com/debatetimer/fixture/CookieGenerator.java deleted file mode 100644 index b8999b4e..00000000 --- a/src/test/java/com/debatetimer/fixture/CookieGenerator.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.debatetimer.fixture; - -import com.debatetimer.controller.tool.cookie.CookieProvider; -import com.debatetimer.controller.tool.jwt.JwtTokenProvider; -import com.debatetimer.dto.member.MemberInfo; -import jakarta.servlet.http.Cookie; -import org.springframework.http.ResponseCookie; -import org.springframework.stereotype.Component; - -@Component -public class CookieGenerator { - - private final JwtTokenProvider jwtTokenProvider; - private final CookieProvider cookieProvider; - - public CookieGenerator() { - this.jwtTokenProvider = new JwtTokenProvider(JwtTokenFixture.TEST_TOKEN_PROPERTIES); - this.cookieProvider = new CookieProvider(); - } - - public Cookie[] generateRefreshCookie(String email) { - String refreshToken = jwtTokenProvider.createRefreshToken(new MemberInfo(email)); - return generateCookie("refreshToken", refreshToken, 100000); - } - - public Cookie[] generateCookie(String cookieName, String value, long expirationMills) { - ResponseCookie responseCookie = cookieProvider.createCookie(cookieName, value, expirationMills); - - Cookie servletCookie = new Cookie(responseCookie.getName(), responseCookie.getValue()); - servletCookie.setMaxAge((int) (expirationMills / 1000)); - servletCookie.setPath(responseCookie.getPath()); - servletCookie.setSecure(responseCookie.isSecure()); - servletCookie.setHttpOnly(responseCookie.isHttpOnly()); - return new Cookie[]{servletCookie}; - } -} diff --git a/src/test/java/com/debatetimer/fixture/JwtTokenFixture.java b/src/test/java/com/debatetimer/fixture/JwtTokenFixture.java index 0c9eedd0..4fe2f6fe 100644 --- a/src/test/java/com/debatetimer/fixture/JwtTokenFixture.java +++ b/src/test/java/com/debatetimer/fixture/JwtTokenFixture.java @@ -1,12 +1,13 @@ package com.debatetimer.fixture; import com.debatetimer.controller.tool.jwt.JwtTokenProperties; +import java.time.Duration; public class JwtTokenFixture { public static final JwtTokenProperties TEST_TOKEN_PROPERTIES = new JwtTokenProperties( "test".repeat(8), - 5000, - 10000 + Duration.ofMinutes(5L), + Duration.ofMinutes(30L) ); } diff --git a/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java b/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java index 0f0aba43..698b370b 100644 --- a/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java +++ b/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java @@ -19,7 +19,6 @@ public ParliamentaryTable generate(Member member) { member, "토론 테이블", "주제", - 1800, false, false ); diff --git a/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java index d4fe23d7..376c9136 100644 --- a/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java +++ b/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java @@ -1,7 +1,7 @@ package com.debatetimer.fixture; -import com.debatetimer.domain.BoxType; import com.debatetimer.domain.Stance; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; import com.debatetimer.repository.parliamentary.ParliamentaryTimeBoxRepository; @@ -17,8 +17,8 @@ public ParliamentaryTimeBoxGenerator(ParliamentaryTimeBoxRepository parliamentar } public ParliamentaryTimeBox generate(ParliamentaryTable testTable, int sequence) { - ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(testTable, sequence, Stance.PROS, BoxType.OPENING, 180, - 1); + ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(testTable, sequence, Stance.PROS, + ParliamentaryBoxType.OPENING, 180, 1); return parliamentaryTimeBoxRepository.save(timeBox); } } diff --git a/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java b/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java new file mode 100644 index 00000000..51fd2c2a --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java @@ -0,0 +1,27 @@ +package com.debatetimer.fixture; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.repository.timebased.TimeBasedTableRepository; +import org.springframework.stereotype.Component; + +@Component +public class TimeBasedTableGenerator { + + private final TimeBasedTableRepository timeBasedTableRepository; + + public TimeBasedTableGenerator(TimeBasedTableRepository timeBasedTableRepository) { + this.timeBasedTableRepository = timeBasedTableRepository; + } + + public TimeBasedTable generate(Member member) { + TimeBasedTable table = new TimeBasedTable( + member, + "토론 테이블", + "주제", + false, + false + ); + return timeBasedTableRepository.save(table); + } +} diff --git a/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java new file mode 100644 index 00000000..319dfb6e --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java @@ -0,0 +1,24 @@ +package com.debatetimer.fixture; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.timebased.TimeBasedBoxType; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import com.debatetimer.repository.timebased.TimeBasedTimeBoxRepository; +import org.springframework.stereotype.Component; + +@Component +public class TimeBasedTimeBoxGenerator { + + private final TimeBasedTimeBoxRepository timeBasedTimeBoxRepository; + + public TimeBasedTimeBoxGenerator(TimeBasedTimeBoxRepository timeBasedTimeBoxRepository) { + this.timeBasedTimeBoxRepository = timeBasedTimeBoxRepository; + } + + public TimeBasedTimeBox generate(TimeBasedTable testTable, int sequence) { + TimeBasedTimeBox timeBox = new TimeBasedTimeBox(testTable, sequence, Stance.PROS, + TimeBasedBoxType.OPENING, 180, 1); + 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 baf01463..91c0e36c 100644 --- a/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java @@ -3,11 +3,14 @@ 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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -@Import({MemberGenerator.class, ParliamentaryTableGenerator.class, ParliamentaryTimeBoxGenerator.class}) +@Import({MemberGenerator.class, ParliamentaryTableGenerator.class, ParliamentaryTimeBoxGenerator.class, + TimeBasedTableGenerator.class, TimeBasedTimeBoxGenerator.class}) @DataJpaTest public abstract class BaseRepositoryTest { @@ -15,8 +18,14 @@ public abstract class BaseRepositoryTest { protected MemberGenerator memberGenerator; @Autowired - protected ParliamentaryTableGenerator tableGenerator; + protected ParliamentaryTableGenerator parliamentaryTableGenerator; @Autowired - protected ParliamentaryTimeBoxGenerator timeBoxGenerator; + protected ParliamentaryTimeBoxGenerator parliamentaryTimeBoxGenerator; + + @Autowired + protected TimeBasedTableGenerator timeBasedTableGenerator; + + @Autowired + protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; } diff --git a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java index 246a1212..efce7327 100644 --- a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java @@ -25,9 +25,9 @@ class FindAllByMember { void 특정_회원의_테이블만_조회한다() { Member chan = memberGenerator.generate("default@gmail.com"); Member bito = memberGenerator.generate("default2@gmail.com"); - ParliamentaryTable chanTable1 = tableGenerator.generate(chan); - ParliamentaryTable chanTable2 = tableGenerator.generate(chan); - ParliamentaryTable bitoTable = tableGenerator.generate(bito); + ParliamentaryTable chanTable1 = parliamentaryTableGenerator.generate(chan); + ParliamentaryTable chanTable2 = parliamentaryTableGenerator.generate(chan); + ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); List foundKeoChanTables = tableRepository.findAllByMember(chan); @@ -41,7 +41,7 @@ class GetById { @Test void 특정_아이디의_테이블을_조회한다() { Member chan = memberGenerator.generate("default@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); ParliamentaryTable foundChanTable = tableRepository.getById(chanTable.getId()); diff --git a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java index 17c09566..c412db5c 100644 --- a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java @@ -23,12 +23,12 @@ class FindAllByParliamentaryTable { void 특정_테이블의_타임박스를_모두_조회한다() { Member chan = memberGenerator.generate("default@gmail.com"); Member bito = memberGenerator.generate("default2@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); - ParliamentaryTable bitoTable = tableGenerator.generate(bito); - ParliamentaryTimeBox chanBox1 = timeBoxGenerator.generate(chanTable, 1); - ParliamentaryTimeBox chanBox2 = timeBoxGenerator.generate(chanTable, 2); - ParliamentaryTimeBox bitoBox1 = timeBoxGenerator.generate(bitoTable, 2); - ParliamentaryTimeBox bitoBox2 = timeBoxGenerator.generate(bitoTable, 2); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); + ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); + ParliamentaryTimeBox chanBox1 = parliamentaryTimeBoxGenerator.generate(chanTable, 1); + ParliamentaryTimeBox chanBox2 = parliamentaryTimeBoxGenerator.generate(chanTable, 2); + ParliamentaryTimeBox bitoBox1 = parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + ParliamentaryTimeBox bitoBox2 = parliamentaryTimeBoxGenerator.generate(bitoTable, 2); List foundBoxes = parliamentaryTimeBoxRepository.findAllByParliamentaryTable( chanTable); diff --git a/src/test/java/com/debatetimer/repository/timebased/TimeBasedTableRepositoryTest.java b/src/test/java/com/debatetimer/repository/timebased/TimeBasedTableRepositoryTest.java new file mode 100644 index 00000000..16739d4e --- /dev/null +++ b/src/test/java/com/debatetimer/repository/timebased/TimeBasedTableRepositoryTest.java @@ -0,0 +1,58 @@ +package com.debatetimer.repository.timebased; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +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 TimeBasedTableRepositoryTest extends BaseRepositoryTest { + + @Autowired + private TimeBasedTableRepository tableRepository; + + @Nested + class FindAllByMember { + + @Test + void 특정_회원의_테이블만_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member bito = memberGenerator.generate("default2@gmail.com"); + TimeBasedTable chanTable1 = timeBasedTableGenerator.generate(chan); + TimeBasedTable chanTable2 = timeBasedTableGenerator.generate(chan); + TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); + + List foundKeoChanTables = tableRepository.findAllByMember(chan); + + assertThat(foundKeoChanTables).containsExactly(chanTable1, chanTable2); + } + } + + @Nested + class GetById { + + @Test + void 특정_아이디의_테이블을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + + TimeBasedTable 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/timebased/TimeBasedTimeBoxRepositoryTest.java b/src/test/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepositoryTest.java new file mode 100644 index 00000000..5b5af88e --- /dev/null +++ b/src/test/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepositoryTest.java @@ -0,0 +1,39 @@ +package com.debatetimer.repository.timebased; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +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 TimeBasedTimeBoxRepositoryTest extends BaseRepositoryTest { + + @Autowired + private TimeBasedTimeBoxRepository timeBasedTimeBoxRepository; + + @Nested + class FindAllByParliamentaryTable { + + @Test + void 특정_테이블의_타임박스를_모두_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member bito = memberGenerator.generate("default2@gmail.com"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); + TimeBasedTimeBox chanBox1 = timeBasedTimeBoxGenerator.generate(chanTable, 1); + TimeBasedTimeBox chanBox2 = timeBasedTimeBoxGenerator.generate(chanTable, 2); + TimeBasedTimeBox bitoBox1 = timeBasedTimeBoxGenerator.generate(bitoTable, 2); + TimeBasedTimeBox bitoBox2 = timeBasedTimeBoxGenerator.generate(bitoTable, 2); + + List foundBoxes = timeBasedTimeBoxRepository.findAllByTimeBasedTable( + 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 4e2a2d65..4323ae6f 100644 --- a/src/test/java/com/debatetimer/service/BaseServiceTest.java +++ b/src/test/java/com/debatetimer/service/BaseServiceTest.java @@ -1,14 +1,16 @@ package com.debatetimer.service; import com.debatetimer.DataBaseCleaner; -import com.debatetimer.fixture.CookieGenerator; import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.ParliamentaryTableGenerator; import com.debatetimer.fixture.ParliamentaryTimeBoxGenerator; -import com.debatetimer.fixture.TokenGenerator; +import com.debatetimer.fixture.TimeBasedTableGenerator; +import com.debatetimer.fixture.TimeBasedTimeBoxGenerator; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTimeBoxRepository; +import com.debatetimer.repository.timebased.TimeBasedTableRepository; +import com.debatetimer.repository.timebased.TimeBasedTimeBoxRepository; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -24,20 +26,26 @@ public abstract class BaseServiceTest { protected ParliamentaryTableRepository parliamentaryTableRepository; @Autowired - protected ParliamentaryTimeBoxRepository timeBoxRepository; + protected ParliamentaryTimeBoxRepository parliamentaryTimeBoxRepository; + + @Autowired + protected TimeBasedTableRepository timeBasedTableRepository; + + @Autowired + protected TimeBasedTimeBoxRepository timeBasedTimeBoxRepository; @Autowired protected MemberGenerator memberGenerator; @Autowired - protected ParliamentaryTableGenerator tableGenerator; + protected ParliamentaryTableGenerator parliamentaryTableGenerator; @Autowired - protected ParliamentaryTimeBoxGenerator timeBoxGenerator; + protected ParliamentaryTimeBoxGenerator parliamentaryTimeBoxGenerator; @Autowired - protected TokenGenerator tokenGenerator; + protected TimeBasedTableGenerator timeBasedTableGenerator; @Autowired - protected CookieGenerator cookieGenerator; + protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; } diff --git a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java index 43ea0767..08e3b496 100644 --- a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java +++ b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java @@ -5,6 +5,7 @@ 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; @@ -51,13 +52,29 @@ class GetTables { @Test void 회원의_전체_토론_시간표를_조회한다() { - Member member = memberRepository.save(new Member("default@gmail.com")); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800, true, true)); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900, true, true)); + Member member = memberGenerator.generate("default@gmail.com"); + parliamentaryTableGenerator.generate(member); + timeBasedTableGenerator.generate(member); TableResponses response = memberService.getTables(member.getId()); assertThat(response.tables()).hasSize(2); } + + @Test + void 회원의_전체_토론_시간표는_정해진_순서대로_반환한다() throws InterruptedException { + Member member = memberGenerator.generate("default@gmail.com"); + ParliamentaryTable table1 = parliamentaryTableGenerator.generate(member); + TimeBasedTable table2 = timeBasedTableGenerator.generate(member); + Thread.sleep(1); + table1.updateUsedAt(); + + TableResponses response = memberService.getTables(member.getId()); + + assertAll( + () -> assertThat(response.tables().get(0).id()).isEqualTo(table1.getId()), + () -> assertThat(response.tables().get(1).id()).isEqualTo(table2.getId()) + ); + } } } diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index 1539ed88..fe0df654 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -4,18 +4,19 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import com.debatetimer.domain.BoxType; import com.debatetimer.domain.Stance; import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; import com.debatetimer.dto.parliamentary.request.ParliamentaryTableCreateRequest; -import com.debatetimer.dto.parliamentary.request.TableInfoCreateRequest; -import com.debatetimer.dto.parliamentary.request.TimeBoxCreateRequest; +import com.debatetimer.dto.parliamentary.request.ParliamentaryTableInfoCreateRequest; +import com.debatetimer.dto.parliamentary.request.ParliamentaryTimeBoxCreateRequest; import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; 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; @@ -33,19 +34,15 @@ class Save { @Test void 의회식_토론_테이블을_생성한다() { Member chan = memberGenerator.generate("default@gmail.com"); - TableInfoCreateRequest requestTableInfo = new TableInfoCreateRequest("커찬의 테이블", "주제", true, true); - List requestTimeBoxes = List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ); ParliamentaryTableCreateRequest chanTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("커찬의 테이블", "주제", true, true), - List.of(new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1))); + new ParliamentaryTableInfoCreateRequest("커찬의 테이블", "주제", true, true), + List.of(new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1))); ParliamentaryTableResponse savedTableResponse = parliamentaryService.save(chanTableRequest, chan); Optional foundTable = parliamentaryTableRepository.findById(savedTableResponse.id()); - List foundTimeBoxes = timeBoxRepository.findAllByParliamentaryTable(foundTable.get()); + List foundTimeBoxes = parliamentaryTimeBoxRepository.findAllByParliamentaryTable( + foundTable.get()); assertAll( () -> assertThat(foundTable.get().getName()).isEqualTo(chanTableRequest.info().name()), @@ -60,9 +57,9 @@ class FindTable { @Test void 의회식_토론_테이블을_조회한다() { Member chan = memberGenerator.generate("default@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); - timeBoxGenerator.generate(chanTable, 1); - timeBoxGenerator.generate(chanTable, 2); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); + parliamentaryTimeBoxGenerator.generate(chanTable, 1); + parliamentaryTimeBoxGenerator.generate(chanTable, 2); ParliamentaryTableResponse foundResponse = parliamentaryService.findTable(chanTable.getId(), chan); @@ -76,7 +73,7 @@ class FindTable { void 회원_소유가_아닌_테이블_조회_시_예외를_발생시킨다() { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); long chanTableId = chanTable.getId(); assertThatThrownBy(() -> parliamentaryService.findTable(chanTableId, coli)) @@ -91,16 +88,16 @@ class UpdateTable { @Test void 의회식_토론_테이블을_수정한다() { Member chan = memberGenerator.generate("default@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("커찬의 테이블", "주제", true, true), - List.of(new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1))); + new ParliamentaryTableInfoCreateRequest("커찬의 테이블", "주제", true, true), + List.of(new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1))); parliamentaryService.updateTable(renewTableRequest, chanTable.getId(), chan); Optional updatedTable = parliamentaryTableRepository.findById(chanTable.getId()); - List updatedTimeBoxes = timeBoxRepository.findAllByParliamentaryTable( + List updatedTimeBoxes = parliamentaryTimeBoxRepository.findAllByParliamentaryTable( updatedTable.get()); assertAll( @@ -114,12 +111,12 @@ class UpdateTable { void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); long chanTableId = chanTable.getId(); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("새로운 테이블", "주제", true, true), - List.of(new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1))); + new ParliamentaryTableInfoCreateRequest("새로운 테이블", "주제", true, true), + List.of(new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1))); assertThatThrownBy(() -> parliamentaryService.updateTable(renewTableRequest, chanTableId, coli)) .isInstanceOf(DTClientErrorException.class) @@ -127,20 +124,53 @@ class UpdateTable { } } + @Nested + class UpdateUsedAt { + + @Test + void 의회식_토론_테이블의_사용_시각을_최신화한다() { + Member member = memberGenerator.generate("default@gmail.com"); + ParliamentaryTable table = parliamentaryTableGenerator.generate(member); + LocalDateTime beforeUsedAt = table.getUsedAt(); + + parliamentaryService.updateUsedAt(table.getId(), member); + + Optional updatedTable = parliamentaryTableRepository.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"); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> parliamentaryService.updateUsedAt(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + @Nested class DeleteTable { @Test void 의회식_토론_테이블을_삭제한다() { Member chan = memberGenerator.generate("default@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); - timeBoxGenerator.generate(chanTable, 1); - timeBoxGenerator.generate(chanTable, 2); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); + parliamentaryTimeBoxGenerator.generate(chanTable, 1); + parliamentaryTimeBoxGenerator.generate(chanTable, 2); parliamentaryService.deleteTable(chanTable.getId(), chan); Optional foundTable = parliamentaryTableRepository.findById(chanTable.getId()); - List timeBoxes = timeBoxRepository.findAllByParliamentaryTable(chanTable); + List timeBoxes = parliamentaryTimeBoxRepository.findAllByParliamentaryTable( + chanTable); assertAll( () -> assertThat(foundTable).isEmpty(), @@ -152,8 +182,8 @@ class DeleteTable { void 회원_소유가_아닌_테이블_삭제_시_예외를_발생시킨다() { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); - ParliamentaryTable chanTable = tableGenerator.generate(chan); - Long chanTableId = chanTable.getId(); + ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); assertThatThrownBy(() -> parliamentaryService.deleteTable(chanTableId, coli)) .isInstanceOf(DTClientErrorException.class) diff --git a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java new file mode 100644 index 00000000..be5410fa --- /dev/null +++ b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java @@ -0,0 +1,208 @@ +package com.debatetimer.service.timebased; + +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.member.Member; +import com.debatetimer.domain.timebased.TimeBasedBoxType; +import com.debatetimer.domain.timebased.TimeBasedTable; +import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import com.debatetimer.dto.timebased.request.TimeBasedTableCreateRequest; +import com.debatetimer.dto.timebased.request.TimeBasedTableInfoCreateRequest; +import com.debatetimer.dto.timebased.request.TimeBasedTimeBoxCreateRequest; +import com.debatetimer.dto.timebased.response.TimeBasedTableResponse; +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 TimeBasedServiceTest extends BaseServiceTest { + + @Autowired + private TimeBasedService timeBasedService; + + @Nested + class Save { + + @Test + void 시간총량제_토론_테이블을_생성한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + TimeBasedTableCreateRequest chanTableRequest = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("커찬의 테이블", "주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + + TimeBasedTableResponse savedTableResponse = timeBasedService.save(chanTableRequest, chan); + Optional foundTable = timeBasedTableRepository.findById(savedTableResponse.id()); + List foundTimeBoxes = timeBasedTimeBoxRepository.findAllByTimeBasedTable( + foundTable.get()); + + assertAll( + () -> assertThat(foundTable.get().getName()).isEqualTo(chanTableRequest.info().name()), + () -> assertThat(foundTimeBoxes).hasSize(chanTableRequest.table().size()) + ); + } + } + + @Nested + class FindTable { + + @Test + void 시간총량제_토론_테이블을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + timeBasedTimeBoxGenerator.generate(chanTable, 1); + timeBasedTimeBoxGenerator.generate(chanTable, 2); + + TimeBasedTableResponse foundResponse = timeBasedService.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"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> timeBasedService.findTable(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + + @Nested + class UpdateTable { + + @Test + void 시간총량제_토론_테이블을_수정한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + TimeBasedTableCreateRequest renewTableRequest = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("커찬의 테이블", "주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + + timeBasedService.updateTable(renewTableRequest, chanTable.getId(), chan); + + Optional updatedTable = timeBasedTableRepository.findById(chanTable.getId()); + List updatedTimeBoxes = timeBasedTimeBoxRepository.findAllByTimeBasedTable( + 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"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + TimeBasedTableCreateRequest renewTableRequest = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("새로운 테이블", "주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + + assertThatThrownBy(() -> timeBasedService.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"); + TimeBasedTable table = timeBasedTableGenerator.generate(member); + LocalDateTime beforeUsedAt = table.getUsedAt(); + + timeBasedService.updateUsedAt(table.getId(), member); + + Optional updatedTable = timeBasedTableRepository.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"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + TimeBasedTableCreateRequest renewTableRequest = new TimeBasedTableCreateRequest( + new TimeBasedTableInfoCreateRequest("새로운 테이블", "주제", true, true), + List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1))); + + assertThatThrownBy(() -> timeBasedService.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"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + timeBasedTimeBoxGenerator.generate(chanTable, 1); + timeBasedTimeBoxGenerator.generate(chanTable, 2); + + timeBasedService.deleteTable(chanTable.getId(), chan); + + Optional foundTable = timeBasedTableRepository.findById(chanTable.getId()); + List timeBoxes = timeBasedTimeBoxRepository.findAllByTimeBasedTable( + chanTable); + + assertAll( + () -> assertThat(foundTable).isEmpty(), + () -> assertThat(timeBoxes).isEmpty() + ); + } + + @Test + void 회원_소유가_아닌_테이블_삭제_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> timeBasedService.deleteTable(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/view/exporter/BoxTypeViewTest.java b/src/test/java/com/debatetimer/view/exporter/ParliamentaryBoxTypeViewTest.java similarity index 61% rename from src/test/java/com/debatetimer/view/exporter/BoxTypeViewTest.java rename to src/test/java/com/debatetimer/view/exporter/ParliamentaryBoxTypeViewTest.java index 55c552de..b8035a86 100644 --- a/src/test/java/com/debatetimer/view/exporter/BoxTypeViewTest.java +++ b/src/test/java/com/debatetimer/view/exporter/ParliamentaryBoxTypeViewTest.java @@ -2,20 +2,20 @@ import static org.assertj.core.api.Assertions.assertThatCode; -import com.debatetimer.domain.BoxType; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import org.junit.jupiter.api.Nested; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -class BoxTypeViewTest { +class ParliamentaryBoxTypeViewTest { @Nested class MapView { @ParameterizedTest - @EnumSource(value = BoxType.class) - void 타임박스_타입과_일치하는_메시지를_반환한다(BoxType boxType) { - assertThatCode(() -> BoxTypeView.mapView(boxType)) + @EnumSource(value = ParliamentaryBoxType.class) + void 타임박스_타입과_일치하는_메시지를_반환한다(ParliamentaryBoxType boxType) { + assertThatCode(() -> ParliamentaryBoxTypeView.mapView(boxType)) .doesNotThrowAnyException(); } } diff --git a/src/test/resources/application-flyway.yml b/src/test/resources/application-flyway.yml new file mode 100644 index 00000000..bdad3efa --- /dev/null +++ b/src/test/resources/application-flyway.yml @@ -0,0 +1,23 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:flyway;MODE=MySQL + username: sa + password: + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: validate + defer-datasource-initialization: false + flyway: + enabled: on + baseline-on-migrate: false + output-query-results: true + +logging: + level: + org.flywaydb: DEBUG + org.springframework.jdbc.core: DEBUG diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 1b0ceaf0..b844ae6c 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -2,6 +2,19 @@ spring: profiles: active: test +cors: + origin: http://test.debate-timer.com + +oauth: + client_id: oauth_client_id + client_secret: oauth_client_secret + grant_type: oauth_grant_type + +jwt: + secret_key: testtesttesttesttesttesttesttest + access_token_expiration: 1h + refresh_token_expiration: 1d + --- spring: @@ -21,11 +34,5 @@ spring: hibernate: ddl-auto: create-drop defer-datasource-initialization: true - -cors: - origin: http://test.debate-timer.com - -jwt: - secret_key: testtesttesttesttesttesttesttest - access_token_expiration_millis: 5000 - refresh_token_expiration_millis: 10000 + flyway: + enabled: false