From dc15b75bd5e6b9dfa2024e4370ffa14555030bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A4=EC=B0=AC?= <44027393+leegwichan@users.noreply.github.com> Date: Tue, 11 Feb 2025 23:43:42 +0900 Subject: [PATCH 01/33] =?UTF-8?q?[REFACTOR]=20OAuth=20=EB=B0=8F=20Cookie?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20refactor=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/member/MemberController.java | 47 ++++++++------- .../tool/cookie/CookieExtractor.java | 19 ------ .../controller/tool/cookie/CookieManager.java | 21 +++---- .../tool/cookie/CookieProvider.java | 19 ++---- .../controller/tool/jwt/AuthManager.java | 8 ++- .../tool/jwt/JwtTokenProperties.java | 30 ++++++++-- .../controller/tool/jwt/JwtTokenProvider.java | 17 ++++-- .../dto/member/JwtTokenResponse.java | 4 +- src/main/resources/application-dev.yml | 5 +- src/main/resources/application-local.yml | 21 ------- src/main/resources/application-prod.yml | 5 +- .../controller/BaseControllerTest.java | 4 -- .../controller/BaseDocumentTest.java | 6 +- .../controller/member/MemberDocumentTest.java | 41 ++----------- .../tool/cookie/CookieExtractorTest.java | 48 --------------- .../tool/cookie/CookieProviderTest.java | 38 ++++++++++++ .../tool/jwt/JwtTokenPropertiesTest.java | 60 +++++++++++++++++++ .../debatetimer/fixture/CookieGenerator.java | 36 ----------- .../debatetimer/fixture/JwtTokenFixture.java | 5 +- .../debatetimer/service/BaseServiceTest.java | 4 -- src/test/resources/application.yml | 4 +- 21 files changed, 198 insertions(+), 244 deletions(-) delete mode 100644 src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java delete mode 100644 src/main/resources/application-local.yml delete mode 100644 src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java create mode 100644 src/test/java/com/debatetimer/controller/tool/cookie/CookieProviderTest.java create mode 100644 src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java delete mode 100644 src/test/java/com/debatetimer/fixture/CookieGenerator.java 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/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/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..d5f77ed9 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,7 @@ package com.debatetimer.controller.tool.jwt; 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 +11,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 IllegalArgumentException("secretKey가 입력되지 않았습니다"); + } + } + + private void validate(Duration expiration) { + if (expiration == null) { + throw new IllegalArgumentException("토큰 만료 기간이 입력되지 않았습니다"); + } + if (expiration.isZero() || expiration.isNegative()) { + throw new IllegalArgumentException("토큰 만료 기간은 양수이어야 합니다"); + } } 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/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/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5aa0987d..8895ed5f 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -22,10 +22,9 @@ 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} 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..5d0feff0 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -21,11 +21,10 @@ 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} diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 17084b51..52af94cd 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -2,7 +2,6 @@ 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; @@ -44,9 +43,6 @@ public abstract class BaseControllerTest { @Autowired protected HeaderGenerator headerGenerator; - @Autowired - protected CookieGenerator cookieGenerator; - @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..9798c189 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -19,7 +19,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 +44,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( diff --git a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java index c4504b6f..4dc7ae58 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(); @@ -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/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..0541123f --- /dev/null +++ b/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java @@ -0,0 +1,60 @@ +package com.debatetimer.controller.tool.jwt; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +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(IllegalArgumentException.class); + } + } + + @Nested + class ValidateToken { + + @Test + void 유효_기간이_비어있을_경우_예외를_발생시킨다() { + String secretKey = "testtesttest"; + + assertAll( + () -> assertThatThrownBy(() -> new JwtTokenProperties(secretKey, null, Duration.ofMinutes(5))) + .isInstanceOf(IllegalArgumentException.class), + () -> assertThatThrownBy(() -> new JwtTokenProperties(secretKey, Duration.ofMinutes(1), null)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Test + void 유효_기간이_음수일_경우_예외를_발생시킨다() { + String secretKey = "testtesttest"; + Duration negativeExpiration = Duration.ofMinutes(-1); + + assertAll( + () -> assertThatThrownBy( + () -> new JwtTokenProperties(secretKey, negativeExpiration, Duration.ofMinutes(5))) + .isInstanceOf(IllegalArgumentException.class), + () -> assertThatThrownBy( + () -> new JwtTokenProperties(secretKey, Duration.ofMinutes(1), negativeExpiration)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + } +} 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/service/BaseServiceTest.java b/src/test/java/com/debatetimer/service/BaseServiceTest.java index 4e2a2d65..c6b9bf9c 100644 --- a/src/test/java/com/debatetimer/service/BaseServiceTest.java +++ b/src/test/java/com/debatetimer/service/BaseServiceTest.java @@ -1,7 +1,6 @@ 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; @@ -37,7 +36,4 @@ public abstract class BaseServiceTest { @Autowired protected TokenGenerator tokenGenerator; - - @Autowired - protected CookieGenerator cookieGenerator; } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 1b0ceaf0..50198fa7 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -27,5 +27,5 @@ cors: jwt: secret_key: testtesttesttesttesttesttesttest - access_token_expiration_millis: 5000 - refresh_token_expiration_millis: 10000 + access_token_expiration: 1h + refresh_token_expiration: 1d From 279bf953e3e7c693b40786359268820a3484489e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Thu, 13 Feb 2025 18:25:19 +0900 Subject: [PATCH 02/33] =?UTF-8?q?[FEAT]=20=EB=8B=A8=EA=B1=B4=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=97=90=20=ED=86=A0=EB=A1=A0=20=ED=98=95=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EA=B0=99=EC=9D=B4=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/parliamentary/response/TableInfoResponse.java | 4 +++- .../parliamentary/ParliamentaryDocumentTest.java | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java index 615cf99b..ff78a1ed 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java @@ -1,12 +1,14 @@ 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 TableInfoResponse(String name, TableType type, String agenda, boolean warningBell, boolean finishBell) { public TableInfoResponse(ParliamentaryTable parliamentaryTable) { this( parliamentaryTable.getName(), + TableType.PARLIAMENTARY, parliamentaryTable.getAgenda(), parliamentaryTable.isWarningBell(), parliamentaryTable.isFinishBell() diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index be8ea8dd..f70096a0 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -20,6 +20,7 @@ import com.debatetimer.controller.Tag; import com.debatetimer.domain.BoxType; import com.debatetimer.domain.Stance; +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; @@ -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("발언 종료 종소리 유무"), @@ -86,7 +88,7 @@ class Save { ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 1", "토론 주제", true, true), + new TableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) @@ -160,6 +162,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("발언 종료 종소리 유무"), @@ -176,7 +179,7 @@ class GetTable { long tableId = 5L; ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 1", "토론 주제", true, true), + new TableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) @@ -248,6 +251,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("발언 종료 종소리 유무"), @@ -271,7 +275,7 @@ class UpdateTable { ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 2", "토론 주제 2", true, true), + new TableInfoResponse("비토 테이블 2", TableType.PARLIAMENTARY, "토론 주제 2", true, true), List.of( new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 300, 1), new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 300, 1) From e13cd490ebe100a39e1d6085d2278887cb99baa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Tue, 18 Feb 2025 16:35:21 +0900 Subject: [PATCH 03/33] =?UTF-8?q?[FEAT]=20=EC=9D=98=ED=9A=8C=EC=8B=9D=20?= =?UTF-8?q?=ED=86=A0=EB=A1=A0=20=20EXPORT=20Api=20=EA=B5=AC=ED=98=84=20(#9?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/config/CorsConfig.java | 2 +- .../com/debatetimer/config/WebConfig.java | 7 +++ .../ParliamentaryController.java | 16 ++++++ .../controller/tool/export/ExcelExport.java | 12 ++++ .../tool/export/ExcelExportInterceptor.java | 55 +++++++++++++++++++ .../parliamentary/ParliamentaryService.java | 9 +++ .../ParliamentaryTableExcelExporter.java | 23 +++++++- ...rliamentaryTableExportMessageResolver.java | 9 ++- 8 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/debatetimer/controller/tool/export/ExcelExport.java create mode 100644 src/main/java/com/debatetimer/controller/tool/export/ExcelExportInterceptor.java diff --git a/src/main/java/com/debatetimer/config/CorsConfig.java b/src/main/java/com/debatetimer/config/CorsConfig.java index a63eb1d1..f75953d5 100644 --- a/src/main/java/com/debatetimer/config/CorsConfig.java +++ b/src/main/java/com/debatetimer/config/CorsConfig.java @@ -30,7 +30,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/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/parliamentary/ParliamentaryController.java b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java index 1589899e..583a0796 100644 --- a/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java +++ b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java @@ -1,13 +1,17 @@ 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.PathVariable; @@ -22,6 +26,7 @@ public class ParliamentaryController { private final ParliamentaryService parliamentaryService; + private final ParliamentaryTableExcelExporter parliamentaryTableExcelExporter; @PostMapping("/api/table/parliamentary") @ResponseStatus(HttpStatus.CREATED) @@ -59,4 +64,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/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/service/parliamentary/ParliamentaryService.java b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java index 1aad472a..b7344bd1 100644 --- a/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java +++ b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java @@ -38,6 +38,13 @@ public ParliamentaryTableResponse findTable(long tableId, Member member) { return new ParliamentaryTableResponse(table, timeBoxes); } + @Transactional(readOnly = true) + public ParliamentaryTableResponse findTableById(long tableId, long id) { + ParliamentaryTable table = getOwnerTable(tableId, id); + ParliamentaryTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + return new ParliamentaryTableResponse(table, timeBoxes); + } + @Transactional public ParliamentaryTableResponse updateTable( ParliamentaryTableCreateRequest tableCreateRequest, @@ -82,4 +89,6 @@ private void validateOwn(ParliamentaryTable table, long memberId) { throw new DTClientErrorException(ClientErrorCode.NOT_TABLE_OWNER); } } + + } diff --git a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java index 20a27ccc..8bc578db 100644 --- a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java +++ b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java @@ -4,6 +4,11 @@ import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; import com.debatetimer.dto.parliamentary.response.TableInfoResponse; import com.debatetimer.dto.parliamentary.response.TimeBoxResponse; +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,7 +102,9 @@ private static void initializeStyle(Workbook workbook) { HorizontalAlignment.CENTER); } - public Workbook export(ParliamentaryTableResponse parliamentaryTableResponse) { + public InputStreamResource export( + ParliamentaryTableResponse parliamentaryTableResponse + ) { TableInfoResponse tableInfo = parliamentaryTableResponse.info(); List timeBoxes = parliamentaryTableResponse.table(); @@ -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( diff --git a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java index b4a069d5..c1ad12f8 100644 --- a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java +++ b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java @@ -33,15 +33,18 @@ private String resolveDefaultMessage(TimeBoxResponse timeBox) { } 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; } From 067b71f70bc4bc0692ec685d0ae4a70f58a056a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A4=EC=B0=AC?= <44027393+leegwichan@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:54:38 +0900 Subject: [PATCH 04/33] =?UTF-8?q?[FEAT]=20=EC=8B=9C=EA=B0=84=EC=B4=9D?= =?UTF-8?q?=EB=9F=89=EC=A0=9C=20=ED=85=8C=EC=9D=B4=EB=B8=94=20Entity=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/DebateTable.java | 78 ++++++++++++ .../com/debatetimer/domain/DebateTimeBox.java | 46 +++++++ .../ParliamentaryBoxType.java} | 5 +- .../parliamentary/ParliamentaryTable.java | 60 +-------- .../parliamentary/ParliamentaryTimeBox.java | 36 +++--- .../domain/timebased/TimeBasedBoxType.java | 32 +++++ .../domain/timebased/TimeBasedTable.java | 36 ++++++ .../domain/timebased/TimeBasedTimeBox.java | 114 ++++++++++++++++++ .../request/TimeBoxCreateRequest.java | 4 +- .../response/TimeBoxResponse.java | 4 +- .../exception/errorcode/ClientErrorCode.java | 3 + ...iew.java => ParliamentaryBoxTypeView.java} | 18 +-- ...rliamentaryTableExportMessageResolver.java | 10 +- .../ParliamentaryControllerTest.java | 10 +- .../ParliamentaryDocumentTest.java | 45 ++++--- .../debatetimer/domain/DebateTableTest.java | 103 ++++++++++++++++ .../debatetimer/domain/DebateTimeBoxTest.java | 49 ++++++++ .../parliamentary/ParliamentaryTableTest.java | 62 ---------- .../ParliamentaryTimeBoxTest.java | 28 ++--- .../ParliamentaryTimeBoxesTest.java | 9 +- .../timebased/TimeBasedTimeBoxTest.java | 110 +++++++++++++++++ .../ParliamentaryTimeBoxGenerator.java | 6 +- .../ParliamentaryServiceTest.java | 18 +-- ...java => ParliamentaryBoxTypeViewTest.java} | 10 +- 24 files changed, 672 insertions(+), 224 deletions(-) create mode 100644 src/main/java/com/debatetimer/domain/DebateTable.java create mode 100644 src/main/java/com/debatetimer/domain/DebateTimeBox.java rename src/main/java/com/debatetimer/domain/{BoxType.java => parliamentary/ParliamentaryBoxType.java} (80%) create mode 100644 src/main/java/com/debatetimer/domain/timebased/TimeBasedBoxType.java create mode 100644 src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java create mode 100644 src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java rename src/main/java/com/debatetimer/view/exporter/{BoxTypeView.java => ParliamentaryBoxTypeView.java} (53%) create mode 100644 src/test/java/com/debatetimer/domain/DebateTableTest.java create mode 100644 src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java delete mode 100644 src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java create mode 100644 src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java rename src/test/java/com/debatetimer/view/exporter/{BoxTypeViewTest.java => ParliamentaryBoxTypeViewTest.java} (61%) 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..63c2cab7 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -0,0 +1,78 @@ +package com.debatetimer.domain; + +import com.debatetimer.domain.member.Member; +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.util.Objects; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@MappedSuperclass +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class DebateTable { + + 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; + + @NotNull + private String agenda; + + private int duration; + + private boolean warningBell; + + private boolean finishBell; + + protected DebateTable(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; + } + + public final boolean isOwner(long memberId) { + return Objects.equals(this.member.getId(), memberId); + } + + protected final void updateTable(DebateTable renewTable) { + validate(renewTable.getName(), renewTable.getDuration()); + + this.name = renewTable.getName(); + this.agenda = renewTable.getAgenda(); + this.duration = renewTable.getDuration(); + this.warningBell = renewTable.isWarningBell(); + this.finishBell = renewTable.isFinishBell(); + } + + 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); + } + } +} 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..b281c11b --- /dev/null +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -0,0 +1,46 @@ +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 Integer speaker; + + public DebateTimeBox(int sequence, Stance stance, Integer speaker) { + validateSequence(sequence); + validateSpeakerNumber(speaker); + + this.sequence = sequence; + this.stance = stance; + this.speaker = speaker; + } + + private void validateSequence(int sequence) { + if (sequence <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE); + } + } + + 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/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..96810a3d 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java @@ -1,17 +1,11 @@ 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 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,32 +13,12 @@ @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, @@ -53,36 +27,10 @@ public ParliamentaryTable( 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; - } - - 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); - } + super(member, name, agenda, duration, warningBell, finishBell); } 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(); - } - - public boolean isOwner(long memberId) { - return Objects.equals(this.member.getId(), memberId); + 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..d2ee31f9 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,33 @@ 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; @NotNull private int time; - private Integer speaker; + public ParliamentaryTimeBox( + ParliamentaryTable parliamentaryTable, + int sequence, + Stance stance, + ParliamentaryBoxType type, + int time, + Integer speaker + ) { + super(sequence, stance, speaker); + validate(time, stance, type); - 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); - } + private void validate(int time, Stance stance, ParliamentaryBoxType boxType) { if (time <= 0) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); } - if (!boxType.isAvailable(stance)) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); } 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..f5afde74 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java @@ -0,0 +1,36 @@ +package com.debatetimer.domain.timebased; + +import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.member.Member; +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, + int duration, + boolean warningBell, + boolean finishBell + ) { + super(member, name, agenda, duration, warningBell, finishBell); + } + + 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..77cbac8b --- /dev/null +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java @@ -0,0 +1,114 @@ +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 time; + + private Integer timePerTeam; + + private Integer timePerSpeaking; + + public TimeBasedTimeBox( + TimeBasedTable timeBasedTable, + int sequence, + Stance stance, + TimeBasedBoxType type, + int time, + Integer speaker + ) { + super(sequence, stance, speaker); + validateTime(time); + validateStance(stance, type); + validateNotTimeBasedType(type); + + this.timeBasedTable = timeBasedTable; + this.type = type; + this.time = time; + } + + public TimeBasedTimeBox( + TimeBasedTable timeBasedTable, + int sequence, + Stance stance, + TimeBasedBoxType type, + int timePerTeam, + int timePerSpeaking, + Integer speaker + ) { + super(sequence, stance, speaker); + validateTime(timePerTeam, timePerSpeaking); + 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 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/parliamentary/request/TimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/TimeBoxCreateRequest.java index 9204113c..fe0fd4be 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/TimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/TimeBoxCreateRequest.java @@ -1,6 +1,6 @@ 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; @@ -12,7 +12,7 @@ public record TimeBoxCreateRequest( Stance stance, @NotBlank - BoxType type, + ParliamentaryBoxType type, @Positive int time, diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java index 2eb8dd29..31cf8e25 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java @@ -1,10 +1,10 @@ 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 TimeBoxResponse(Stance stance, ParliamentaryBoxType type, int time, Integer speakerNumber) { public TimeBoxResponse(ParliamentaryTimeBox parliamentaryTimeBox) { this(parliamentaryTimeBox.getStance(), diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index db0ce1bf..63828b38 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -18,8 +18,11 @@ 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, "팀 발언 시간은 개인 발언 시간보다 길어야합니다"), FIELD_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), URL_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), 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/ParliamentaryTableExportMessageResolver.java b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java index c1ad12f8..1e6eabea 100644 --- a/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java +++ b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExportMessageResolver.java @@ -1,6 +1,6 @@ package com.debatetimer.view.exporter; -import com.debatetimer.domain.BoxType; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.dto.parliamentary.response.TimeBoxResponse; import org.springframework.stereotype.Component; @@ -17,8 +17,8 @@ public class ParliamentaryTableExportMessageResolver { public String resolveBoxMessage(TimeBoxResponse 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 @@ -27,8 +27,8 @@ public String resolveBoxMessage(TimeBoxResponse timeBox) { } private String resolveDefaultMessage(TimeBoxResponse timeBox) { - BoxType boxType = timeBox.type(); - return BoxTypeView.mapView(boxType) + ParliamentaryBoxType boxType = timeBox.type(); + return ParliamentaryBoxTypeView.mapView(boxType) + resolveTimeMessage(timeBox.time()); } diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index fdf092b3..a45dccf3 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -4,7 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.debatetimer.controller.BaseControllerTest; -import com.debatetimer.domain.BoxType; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.Stance; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; @@ -29,8 +29,8 @@ class Save { ParliamentaryTableCreateRequest request = 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 TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -86,8 +86,8 @@ class UpdateTable { 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 TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index f70096a0..d2412510 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -18,8 +18,8 @@ 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; @@ -82,16 +82,16 @@ class Save { ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, new TableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( - new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) + new TimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxResponse(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); doReturn(response).when(parliamentaryService).save(eq(request), any()); @@ -115,8 +115,11 @@ 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 @@ -124,8 +127,8 @@ class Save { ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService).save(eq(request), any()); @@ -175,14 +178,13 @@ class GetTable { @Test void 의회식_테이블_조회_성공() { - long memberId = 4L; long tableId = 5L; ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, new TableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( - new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) + new TimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxResponse(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); doReturn(response).when(parliamentaryService).findTable(eq(tableId), any()); @@ -203,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()); @@ -264,21 +265,20 @@ class UpdateTable { @Test void 의회식_토론_테이블_수정() { - long memberId = 4L; long tableId = 5L; ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 300, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 300, 1) + new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 300, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 300, 1) ) ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, new TableInfoResponse("비토 테이블 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 TimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 300, 1), + new TimeBoxResponse(Stance.CONS, ParliamentaryBoxType.OPENING, 300, 1) ) ); doReturn(response).when(parliamentaryService).updateTable(eq(request), eq(tableId), any()); @@ -303,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; long tableId = 5L; ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 300, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 300, 1) + new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 300, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 300, 1) ) ); doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService) @@ -352,7 +353,6 @@ class DeleteTable { @Test void 의회식_테이블_삭제_성공() { - long memberId = 4L; long tableId = 5L; doNothing().when(parliamentaryService).deleteTable(eq(tableId), any()); @@ -370,7 +370,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/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java new file mode 100644 index 00000000..bf0cf46c --- /dev/null +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -0,0 +1,103 @@ +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.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 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", 10, 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", 10, 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", 10, 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", 10, true, true)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); + } + + @ValueSource(ints = {0, -1, -60}) + @ParameterizedTest + void 테이블_시간은_양수만_가능하다(int duration) { + Member member = new Member("default@gmail.com"); + assertThatThrownBy(() -> new DebateTableTestObject(member, "nickname", "agenda", duration, true, true)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TABLE_TIME.getMessage()); + } + } + + @Nested + class Update { + + @Test + void 테이블_정보를_업데이트_할_수_있다() { + Member member = new Member("default@gmail.com"); + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", 10, true, true); + DebateTableTestObject renewTable = new DebateTableTestObject(member, "newName", "newAgenda", 100, false, + false); + + table.updateTable(renewTable); + + assertAll( + () -> assertThat(table.getName()).isEqualTo("newName"), + () -> assertThat(table.getAgenda()).isEqualTo("newAgenda"), + () -> assertThat(table.getDuration()).isEqualTo(100), + () -> assertThat(table.isWarningBell()).isEqualTo(false), + () -> assertThat(table.isFinishBell()).isEqualTo(false) + ); + } + } + + private static class DebateTableTestObject extends DebateTable { + + public DebateTableTestObject(Member member, + String name, + String agenda, + int duration, + boolean warningBell, + boolean finishBell) { + super(member, name, agenda, duration, warningBell, finishBell); + } + + 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..685a7605 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java @@ -0,0 +1,49 @@ +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, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); + } + + @Test + void 발표자_번호는_빈_값이_허용된다() { + Integer speaker = null; + + assertThatCode(() -> new DebateTimeBoxTestObject(1, Stance.CONS, speaker)) + .doesNotThrowAnyException(); + } + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 발표자_번호는_양수만_가능하다(int speaker) { + assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); + } + } + + private static class DebateTimeBoxTestObject extends DebateTimeBox { + + public DebateTimeBoxTestObject(int sequence, Stance stance, Integer speaker) { + super(sequence, stance, speaker); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java deleted file mode 100644 index 84bc003d..00000000 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.debatetimer.domain.parliamentary; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.debatetimer.domain.member.Member; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -class ParliamentaryTableTest { - - @Nested - class Validate { - - @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()); - } - - @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()); - } - } -} diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java index d698b8e2..693b05e3 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java @@ -3,45 +3,45 @@ 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; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class ParliamentaryTimeBoxTest { @Nested class Validate { - @Test - void 순서는_양수만_가능하다() { + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 시간은_양수만_가능하다(int time) { 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)) + assertThatThrownBy( + () -> new ParliamentaryTimeBox(table, 1, Stance.CONS, ParliamentaryBoxType.OPENING, time, 1)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); } + } + + @Nested + 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/parliamentary/ParliamentaryTimeBoxesTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java index c64fa43b..ee3e52be 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java @@ -2,7 +2,6 @@ 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 java.util.ArrayList; @@ -20,10 +19,10 @@ class SortedBySequence { 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); + 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); 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..9291a377 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java @@ -0,0 +1,110 @@ +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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class TimeBasedTimeBoxTest { + + @Nested + class Validate { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 시간은_양수만_가능하다(int time) { + TimeBasedTable table = new TimeBasedTable(); + + assertThatThrownBy( + () -> new TimeBasedTimeBox(table, 1, Stance.CONS, TimeBasedBoxType.OPENING, time, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); + } + } + + @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 시간총량제_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { + TimeBasedTable table = new TimeBasedTable(); + TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; + + assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 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, 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, + 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, + timePerSpeaking, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); + } + } +} 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/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index 1539ed88..fb382477 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -4,7 +4,7 @@ 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.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.Stance; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; @@ -35,13 +35,13 @@ class Save { 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) + new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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))); + List.of(new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1))); ParliamentaryTableResponse savedTableResponse = parliamentaryService.save(chanTableRequest, chan); Optional foundTable = parliamentaryTableRepository.findById(savedTableResponse.id()); @@ -94,8 +94,8 @@ class UpdateTable { ParliamentaryTable chanTable = tableGenerator.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))); + List.of(new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1))); parliamentaryService.updateTable(renewTableRequest, chanTable.getId(), chan); @@ -118,8 +118,8 @@ class UpdateTable { 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))); + List.of(new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1))); assertThatThrownBy(() -> parliamentaryService.updateTable(renewTableRequest, chanTableId, coli)) .isInstanceOf(DTClientErrorException.class) 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(); } } From 84a0d8cc81384336096e6bc82b3148a49b739db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A4=EC=B0=AC?= <44027393+leegwichan@users.noreply.github.com> Date: Sat, 22 Feb 2025 02:57:08 +0900 Subject: [PATCH 05/33] =?UTF-8?q?[REFACTOR]=20=EC=8A=A4=ED=94=84=EB=A7=81?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91=20=EC=8B=9C=20=EB=B0=9C=EC=83=9D=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=88=EC=99=B8=20=EC=84=A4=EC=A0=95=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../debatetimer/client/OAuthProperties.java | 12 +++++ .../com/debatetimer/config/CorsConfig.java | 14 ++++++ .../tool/jwt/JwtTokenProperties.java | 8 ++-- .../custom/DTClientErrorException.java | 2 +- ...ion.java => DTErrorResponseException.java} | 4 +- .../custom/DTInitializationException.java | 10 +++++ .../custom/DTServerErrorException.java | 2 +- .../exception/errorcode/ClientErrorCode.java | 2 +- .../errorcode/InitializationErrorCode.java | 21 +++++++++ ...{ErrorCode.java => ResponseErrorCode.java} | 2 +- .../exception/errorcode/ServerErrorCode.java | 2 +- .../handler/GlobalExceptionHandler.java | 8 ++-- .../client/OAuthPropertiesTest.java | 44 +++++++++++++++++++ .../debatetimer/config/CorsConfigTest.java | 40 +++++++++++++++++ .../tool/jwt/JwtTokenPropertiesTest.java | 17 ++++--- src/test/resources/application.yml | 5 +++ 16 files changed, 174 insertions(+), 19 deletions(-) rename src/main/java/com/debatetimer/exception/custom/{DTException.java => DTErrorResponseException.java} (60%) create mode 100644 src/main/java/com/debatetimer/exception/custom/DTInitializationException.java create mode 100644 src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java rename src/main/java/com/debatetimer/exception/errorcode/{ErrorCode.java => ResponseErrorCode.java} (80%) create mode 100644 src/test/java/com/debatetimer/client/OAuthPropertiesTest.java create mode 100644 src/test/java/com/debatetimer/config/CorsConfigTest.java 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 f75953d5..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("/**") 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 d5f77ed9..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,5 +1,7 @@ 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; @@ -26,16 +28,16 @@ public JwtTokenProperties(String secretKey, Duration accessTokenExpiration, Dura private void validate(String secretKey) { if (secretKey == null || secretKey.isBlank()) { - throw new IllegalArgumentException("secretKey가 입력되지 않았습니다"); + throw new DTInitializationException(InitializationErrorCode.JWT_SECRET_KEY_EMPTY); } } private void validate(Duration expiration) { if (expiration == null) { - throw new IllegalArgumentException("토큰 만료 기간이 입력되지 않았습니다"); + throw new DTInitializationException(InitializationErrorCode.JWT_TOKEN_DURATION_EMPTY); } if (expiration.isZero() || expiration.isNegative()) { - throw new IllegalArgumentException("토큰 만료 기간은 양수이어야 합니다"); + throw new DTInitializationException(InitializationErrorCode.JWT_TOKEN_DURATION_INVALID); } } 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 63828b38..2693ea84 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, 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/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/tool/jwt/JwtTokenPropertiesTest.java b/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java index 0541123f..7232ac9a 100644 --- a/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java +++ b/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenPropertiesTest.java @@ -3,6 +3,8 @@ 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; @@ -23,7 +25,8 @@ class ValidateSecretKey { Duration refreshTokenExpiration = Duration.ofMinutes(5); assertThatThrownBy(() -> new JwtTokenProperties(emptyKey, accessTokenExpiration, refreshTokenExpiration)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_SECRET_KEY_EMPTY.getMessage()); } } @@ -36,9 +39,11 @@ class ValidateToken { assertAll( () -> assertThatThrownBy(() -> new JwtTokenProperties(secretKey, null, Duration.ofMinutes(5))) - .isInstanceOf(IllegalArgumentException.class), + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_EMPTY.getMessage()), () -> assertThatThrownBy(() -> new JwtTokenProperties(secretKey, Duration.ofMinutes(1), null)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_EMPTY.getMessage()) ); } @@ -50,10 +55,12 @@ class ValidateToken { assertAll( () -> assertThatThrownBy( () -> new JwtTokenProperties(secretKey, negativeExpiration, Duration.ofMinutes(5))) - .isInstanceOf(IllegalArgumentException.class), + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_INVALID.getMessage()), () -> assertThatThrownBy( () -> new JwtTokenProperties(secretKey, Duration.ofMinutes(1), negativeExpiration)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.JWT_TOKEN_DURATION_INVALID.getMessage()) ); } } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 50198fa7..938d1955 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -25,6 +25,11 @@ spring: 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 From ca217850c078f6959e85d4577e4a759c736fd7ab Mon Sep 17 00:00:00 2001 From: SANGHUN OH <121424793+unifolio0@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:52:28 +0900 Subject: [PATCH 06/33] =?UTF-8?q?[FEAT]=20=EC=8B=9C=EA=B0=84=EC=B4=9D?= =?UTF-8?q?=EB=9F=89=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84=20(#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timebased/TimeBasedController.java | 62 +++ .../com/debatetimer/domain/DebateTable.java | 5 + .../com/debatetimer/domain/DebateTimeBox.java | 11 +- .../com/debatetimer/domain/TimeBoxes.java | 20 + .../parliamentary/ParliamentaryTable.java | 11 + .../parliamentary/ParliamentaryTimeBox.java | 13 +- .../parliamentary/ParliamentaryTimeBoxes.java | 22 - .../domain/timebased/TimeBasedTable.java | 11 + .../domain/timebased/TimeBasedTimeBox.java | 17 +- .../debatetimer/dto/member/TableResponse.java | 12 +- .../dto/member/TableResponses.java | 17 +- .../com/debatetimer/dto/member/TableType.java | 2 +- .../ParliamentaryTableCreateRequest.java | 11 +- ... ParliamentaryTableInfoCreateRequest.java} | 2 +- ...=> ParliamentaryTimeBoxCreateRequest.java} | 2 +- ...va => ParliamentaryTableInfoResponse.java} | 5 +- .../response/ParliamentaryTableResponse.java | 17 +- ...java => ParliamentaryTimeBoxResponse.java} | 4 +- .../request/TimeBasedTableCreateRequest.java | 28 ++ .../TimeBasedTableInfoCreateRequest.java | 22 + .../TimeBasedTimeBoxCreateRequest.java | 29 ++ .../response/TimeBasedTableInfoResponse.java | 23 + .../response/TimeBasedTableResponse.java | 28 ++ .../response/TimeBasedTimeBoxResponse.java | 25 ++ .../exception/errorcode/ClientErrorCode.java | 1 + .../ParliamentaryTimeBoxRepository.java | 6 +- .../timebased/TimeBasedTableRepository.java | 25 ++ .../timebased/TimeBasedTimeBoxRepository.java | 34 ++ .../service/member/MemberService.java | 10 +- .../parliamentary/ParliamentaryService.java | 24 +- .../service/timebased/TimeBasedService.java | 85 ++++ .../ParliamentaryTableExcelExporter.java | 12 +- ...rliamentaryTableExportMessageResolver.java | 6 +- .../controller/BaseControllerTest.java | 18 +- .../controller/BaseDocumentTest.java | 4 + .../java/com/debatetimer/controller/Tag.java | 2 +- .../ParliamentaryControllerTest.java | 32 +- .../ParliamentaryDocumentTest.java | 50 +-- .../timebased/TimeBasedControllerTest.java | 131 ++++++ .../timebased/TimeBasedDocumentTest.java | 398 ++++++++++++++++++ .../debatetimer/domain/DebateTableTest.java | 11 + .../debatetimer/domain/DebateTimeBoxTest.java | 19 +- ...yTimeBoxesTest.java => TimeBoxesTest.java} | 10 +- .../parliamentary/ParliamentaryTableTest.java | 21 + .../ParliamentaryTimeBoxTest.java | 16 - .../domain/timebased/TimeBasedTableTest.java | 21 + .../timebased/TimeBasedTimeBoxTest.java | 41 +- .../fixture/TimeBasedTableGenerator.java | 28 ++ .../fixture/TimeBasedTimeBoxGenerator.java | 24 ++ .../repository/BaseRepositoryTest.java | 15 +- .../ParliamentaryTableRepositoryTest.java | 8 +- .../ParliamentaryTimeBoxRepositoryTest.java | 12 +- .../TimeBasedTableRepositoryTest.java | 58 +++ .../TimeBasedTimeBoxRepositoryTest.java | 39 ++ .../debatetimer/service/BaseServiceTest.java | 22 +- .../ParliamentaryServiceTest.java | 61 +-- .../timebased/TimeBasedServiceTest.java | 177 ++++++++ 57 files changed, 1580 insertions(+), 240 deletions(-) create mode 100644 src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java create mode 100644 src/main/java/com/debatetimer/domain/TimeBoxes.java delete mode 100644 src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxes.java rename src/main/java/com/debatetimer/dto/parliamentary/request/{TableInfoCreateRequest.java => ParliamentaryTableInfoCreateRequest.java} (92%) rename src/main/java/com/debatetimer/dto/parliamentary/request/{TimeBoxCreateRequest.java => ParliamentaryTimeBoxCreateRequest.java} (93%) rename src/main/java/com/debatetimer/dto/parliamentary/response/{TableInfoResponse.java => ParliamentaryTableInfoResponse.java} (62%) rename src/main/java/com/debatetimer/dto/parliamentary/response/{TimeBoxResponse.java => ParliamentaryTimeBoxResponse.java} (68%) create mode 100644 src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java create mode 100644 src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java create mode 100644 src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java create mode 100644 src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableInfoResponse.java create mode 100644 src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java create mode 100644 src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java create mode 100644 src/main/java/com/debatetimer/repository/timebased/TimeBasedTableRepository.java create mode 100644 src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java create mode 100644 src/main/java/com/debatetimer/service/timebased/TimeBasedService.java create mode 100644 src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java create mode 100644 src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java rename src/test/java/com/debatetimer/domain/{parliamentary/ParliamentaryTimeBoxesTest.java => TimeBoxesTest.java} (77%) create mode 100644 src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java create mode 100644 src/test/java/com/debatetimer/domain/timebased/TimeBasedTableTest.java create mode 100644 src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java create mode 100644 src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java create mode 100644 src/test/java/com/debatetimer/repository/timebased/TimeBasedTableRepositoryTest.java create mode 100644 src/test/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepositoryTest.java create mode 100644 src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java 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..dbb25c79 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java @@ -0,0 +1,62 @@ +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.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); + } + + @PatchMapping("/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); + } + + @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/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java index 63c2cab7..7f516156 100644 --- a/src/main/java/com/debatetimer/domain/DebateTable.java +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -1,6 +1,7 @@ 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; @@ -75,4 +76,8 @@ private void validate(String name, int duration) { throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_TIME); } } + + abstract public long getId(); + + abstract public TableType getType(); } diff --git a/src/main/java/com/debatetimer/domain/DebateTimeBox.java b/src/main/java/com/debatetimer/domain/DebateTimeBox.java index b281c11b..9f5217b8 100644 --- a/src/main/java/com/debatetimer/domain/DebateTimeBox.java +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -21,14 +21,17 @@ public abstract class DebateTimeBox { @Enumerated(EnumType.STRING) private Stance stance; + private int time; private Integer speaker; - public DebateTimeBox(int sequence, Stance stance, Integer speaker) { + public 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; } @@ -38,6 +41,12 @@ private void validateSequence(int 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/parliamentary/ParliamentaryTable.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java index 96810a3d..43b33b8d 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java @@ -2,6 +2,7 @@ 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; @@ -30,6 +31,16 @@ public ParliamentaryTable( super(member, name, agenda, duration, warningBell, finishBell); } + @Override + public long getId() { + return id; + } + + @Override + public TableType getType() { + return TableType.PARLIAMENTARY; + } + 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 d2ee31f9..fa66cff2 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java @@ -36,9 +36,6 @@ public class ParliamentaryTimeBox extends DebateTimeBox { @Enumerated(EnumType.STRING) private ParliamentaryBoxType type; - @NotNull - private int time; - public ParliamentaryTimeBox( ParliamentaryTable parliamentaryTable, int sequence, @@ -47,18 +44,14 @@ public ParliamentaryTimeBox( int time, Integer speaker ) { - super(sequence, stance, speaker); - validate(time, stance, type); + super(sequence, stance, time, speaker); + validate(stance, type); this.parliamentaryTable = parliamentaryTable; this.type = type; - this.time = time; } - private void validate(int time, Stance stance, ParliamentaryBoxType boxType) { - 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/TimeBasedTable.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java index f5afde74..7ec28f4a 100644 --- a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java @@ -2,6 +2,7 @@ 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; @@ -30,6 +31,16 @@ public TimeBasedTable( super(member, name, agenda, duration, 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 index 77cbac8b..27184f61 100644 --- a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java @@ -36,10 +36,7 @@ public class TimeBasedTimeBox extends DebateTimeBox { @Enumerated(EnumType.STRING) private TimeBasedBoxType type; - private Integer time; - private Integer timePerTeam; - private Integer timePerSpeaking; public TimeBasedTimeBox( @@ -50,14 +47,12 @@ public TimeBasedTimeBox( int time, Integer speaker ) { - super(sequence, stance, speaker); - validateTime(time); + super(sequence, stance, time, speaker); validateStance(stance, type); validateNotTimeBasedType(type); this.timeBasedTable = timeBasedTable; this.type = type; - this.time = time; } public TimeBasedTimeBox( @@ -65,12 +60,14 @@ public TimeBasedTimeBox( int sequence, Stance stance, TimeBasedBoxType type, + int time, int timePerTeam, int timePerSpeaking, Integer speaker ) { - super(sequence, stance, speaker); + super(sequence, stance, time, speaker); validateTime(timePerTeam, timePerSpeaking); + validateTimeBasedTime(time, timePerTeam); validateStance(stance, type); validateTimeBasedType(type); @@ -94,6 +91,12 @@ private void validateTime(int timePerTeam, int timePerSpeaking) { } } + private void validateTimeBasedTime(int time, int timePerTeam) { + if (time != timePerTeam * 2) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE); + } + } + private void validateStance(Stance stance, TimeBasedBoxType boxType) { if (!boxType.isAvailable(stance)) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); diff --git a/src/main/java/com/debatetimer/dto/member/TableResponse.java b/src/main/java/com/debatetimer/dto/member/TableResponse.java index 769d0ace..624653e3 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 TableResponse(ParliamentaryTable parliamentaryTable) { + public TableResponse(DebateTable debateTable) { this( - parliamentaryTable.getId(), - parliamentaryTable.getName(), - TableType.PARLIAMENTARY, - parliamentaryTable.getDuration() + debateTable.getId(), + debateTable.getName(), + debateTable.getType(), + debateTable.getDuration() ); } } diff --git a/src/main/java/com/debatetimer/dto/member/TableResponses.java b/src/main/java/com/debatetimer/dto/member/TableResponses.java index 398ad724..d4d23a4c 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponses.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponses.java @@ -1,17 +1,24 @@ package com.debatetimer.dto.member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; +import com.debatetimer.domain.timebased.TimeBasedTable; 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)); + public TableResponses(List parliamentaryTables, + List timeBasedTables) { + this(toTableResponses(parliamentaryTables, timeBasedTables)); } - private static List toTableResponses(List parliamentaryTables) { - return parliamentaryTables.stream() - .map(TableResponse::new) + private static List toTableResponses(List parliamentaryTables, + List timeBasedTables) { + Stream parliamentaryTableResponseStream = parliamentaryTables.stream() + .map(TableResponse::new); + Stream timeBasedTableResponseStream = timeBasedTables.stream() + .map(TableResponse::new); + return Stream.concat(parliamentaryTableResponseStream, timeBasedTableResponseStream) .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..3320e0d1 100644 --- a/src/main/java/com/debatetimer/dto/member/TableType.java +++ b/src/main/java/com/debatetimer/dto/member/TableType.java @@ -3,5 +3,5 @@ public enum TableType { PARLIAMENTARY, - ; + TIME_BASED; } 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..ea1a238a 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -1,13 +1,14 @@ 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 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()); @@ -15,13 +16,13 @@ public ParliamentaryTable toTable(Member member) { private int sumOfTime() { return table.stream() - .mapToInt(TimeBoxCreateRequest::time) + .mapToInt(ParliamentaryTimeBoxCreateRequest::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 92% 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..621f95d7 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, 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 93% 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 fe0fd4be..9313ea90 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/TimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java @@ -7,7 +7,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; -public record TimeBoxCreateRequest( +public record ParliamentaryTimeBoxCreateRequest( @NotBlank Stance stance, 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 62% 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 ff78a1ed..7894432f 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableInfoResponse.java @@ -3,9 +3,10 @@ import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.dto.member.TableType; -public record TableInfoResponse(String name, TableType type, 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, 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 68% 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 31cf8e25..70ae83c6 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/TimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java @@ -4,9 +4,9 @@ import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; -public record TimeBoxResponse(Stance stance, ParliamentaryBoxType 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..ad73f58b --- /dev/null +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.timebased.request; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.timebased.TimeBasedTable; +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, sumOfTime()); + } + + private int sumOfTime() { + return table.stream() + .mapToInt(TimeBasedTimeBoxCreateRequest::time) + .sum(); + } + + 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..cff9f3d1 --- /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, int duration) { + return new TimeBasedTable(member, name, agenda, duration, 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/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 2693ea84..7134e1cd 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -23,6 +23,7 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_TIME_BOX_STANCE(HttpStatus.BAD_REQUEST, "타임박스 유형과 일치하지 않는 입장입니다."), INVALID_TIME_BOX_FORMAT(HttpStatus.BAD_REQUEST, "타임박스 유형과 일치하지 않는 형식입니다"), INVALID_TIME_BASED_TIME(HttpStatus.BAD_REQUEST, "팀 발언 시간은 개인 발언 시간보다 길어야합니다"), + INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE(HttpStatus.BAD_REQUEST, "총 시간은 팀 발언 시간의 2배여야 합니다"), FIELD_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), URL_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), diff --git a/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java index a2ebb2d9..3915aecf 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..1657a0ab --- /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 b7344bd1..8b4904e2 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,21 +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); - ParliamentaryTimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); return new ParliamentaryTableResponse(table, timeBoxes); } @@ -55,27 +55,27 @@ 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 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) { @@ -89,6 +89,4 @@ private void validateOwn(ParliamentaryTable table, long memberId) { throw new DTClientErrorException(ClientErrorCode.NOT_TABLE_OWNER); } } - - } 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..d6a1011d --- /dev/null +++ b/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java @@ -0,0 +1,85 @@ +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 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/ParliamentaryTableExcelExporter.java b/src/main/java/com/debatetimer/view/exporter/ParliamentaryTableExcelExporter.java index 8bc578db..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,9 @@ 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; @@ -105,8 +105,8 @@ private static void initializeStyle(Workbook workbook) { public InputStreamResource export( ParliamentaryTableResponse parliamentaryTableResponse ) { - TableInfoResponse tableInfo = parliamentaryTableResponse.info(); - List timeBoxes = parliamentaryTableResponse.table(); + ParliamentaryTableInfoResponse tableInfo = parliamentaryTableResponse.info(); + List timeBoxes = parliamentaryTableResponse.table(); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet(tableInfo.name()); @@ -163,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 1e6eabea..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.parliamentary.ParliamentaryBoxType; -import com.debatetimer.dto.parliamentary.response.TimeBoxResponse; +import com.debatetimer.dto.parliamentary.response.ParliamentaryTimeBoxResponse; import org.springframework.stereotype.Component; @Component @@ -15,7 +15,7 @@ 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); ParliamentaryBoxType type = timeBox.type(); if (type == ParliamentaryBoxType.TIME_OUT) { @@ -26,7 +26,7 @@ public String resolveBoxMessage(TimeBoxResponse timeBox) { + resolveSpeakerMessage(timeBox.speakerNumber()); } - private String resolveDefaultMessage(TimeBoxResponse timeBox) { + private String resolveDefaultMessage(ParliamentaryTimeBoxResponse timeBox) { ParliamentaryBoxType boxType = timeBox.type(); return ParliamentaryBoxTypeView.mapView(boxType) + resolveTimeMessage(timeBox.time()); diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 52af94cd..0bc58e12 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -6,9 +6,11 @@ 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; @@ -26,19 +28,25 @@ 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 parliamentaryTimeBoxGenerator; + + @Autowired + protected TimeBasedTableGenerator timeBasedTableGenerator; @Autowired - protected ParliamentaryTimeBoxGenerator timeBoxGenerator; + protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; @Autowired protected HeaderGenerator headerGenerator; diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index 9798c189..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; @@ -59,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/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index a45dccf3..b5ce0012 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.parliamentary.ParliamentaryBoxType; 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, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -115,9 +115,9 @@ 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 d2412510..78b9bc16 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -22,11 +22,11 @@ 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; @@ -80,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, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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", TableType.PARLIAMENTARY, "토론 주제", true, true), + new ParliamentaryTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( - new TimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxResponse(Stance.CONS, ParliamentaryBoxType.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()); @@ -125,10 +125,10 @@ class Save { @ParameterizedTest void 의회식_테이블_생성_실패(ClientErrorCode errorCode) { ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), + new ParliamentaryTableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), List.of( - new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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()); @@ -181,10 +181,10 @@ class GetTable { long tableId = 5L; ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), + new ParliamentaryTableInfoResponse("비토 테이블 1", TableType.PARLIAMENTARY, "토론 주제", true, true), List.of( - new TimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxResponse(Stance.CONS, ParliamentaryBoxType.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()); @@ -267,18 +267,18 @@ class UpdateTable { void 의회식_토론_테이블_수정() { 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, ParliamentaryBoxType.OPENING, 300, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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", TableType.PARLIAMENTARY, "토론 주제 2", true, true), + new ParliamentaryTableInfoResponse("비토 테이블 2", TableType.PARLIAMENTARY, "토론 주제 2", true, true), List.of( - new TimeBoxResponse(Stance.PROS, ParliamentaryBoxType.OPENING, 300, 1), - new TimeBoxResponse(Stance.CONS, ParliamentaryBoxType.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()); @@ -314,10 +314,10 @@ class UpdateTable { 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, ParliamentaryBoxType.OPENING, 300, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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) 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..2a01686e --- /dev/null +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java @@ -0,0 +1,131 @@ +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().patch("/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 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..073372ac --- /dev/null +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java @@ -0,0 +1,398 @@ +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().patch("/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().patch("/api/table/time-based/{tableId}") + .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/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java index bf0cf46c..62c6b68d 100644 --- a/src/test/java/com/debatetimer/domain/DebateTableTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -6,6 +6,7 @@ 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 org.junit.jupiter.api.Nested; @@ -96,6 +97,16 @@ public DebateTableTestObject(Member member, super(member, name, agenda, duration, 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 index 685a7605..52b8dba0 100644 --- a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java @@ -18,23 +18,32 @@ class Validate { @ValueSource(ints = {0, -1}) @ParameterizedTest void 순서는_양수만_가능하다(int sequence) { - assertThatThrownBy(() -> new DebateTimeBoxTestObject(sequence, Stance.CONS, 1)) + 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, speaker)) + assertThatCode(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) .doesNotThrowAnyException(); } @ValueSource(ints = {0, -1}) @ParameterizedTest void 발표자_번호는_양수만_가능하다(int speaker) { - assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, speaker)) + assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); } @@ -42,8 +51,8 @@ class Validate { private static class DebateTimeBoxTestObject extends DebateTimeBox { - public DebateTimeBoxTestObject(int sequence, Stance stance, Integer speaker) { - super(sequence, stance, speaker); + 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 77% rename from src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java rename to src/test/java/com/debatetimer/domain/TimeBoxesTest.java index ee3e52be..0f203808 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java @@ -1,16 +1,18 @@ -package com.debatetimer.domain.parliamentary; +package com.debatetimer.domain; import static org.assertj.core.api.Assertions.assertThat; -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 { @@ -25,7 +27,7 @@ class SortedBySequence { 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/parliamentary/ParliamentaryTableTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java new file mode 100644 index 00000000..1a16fc1d --- /dev/null +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java @@ -0,0 +1,21 @@ +package com.debatetimer.domain.parliamentary; + +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 ParliamentaryTableTest { + + @Nested + class GetType { + + @Test + void 의회식_테이블_타입을_반환한다() { + ParliamentaryTable parliamentaryTable = new ParliamentaryTable(); + + 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 693b05e3..59859496 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java @@ -8,25 +8,9 @@ import com.debatetimer.exception.errorcode.ClientErrorCode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; class ParliamentaryTimeBoxTest { - @Nested - class Validate { - - @ValueSource(ints = {0, -1}) - @ParameterizedTest - void 시간은_양수만_가능하다(int time) { - ParliamentaryTable table = new ParliamentaryTable(); - assertThatThrownBy( - () -> new ParliamentaryTimeBox(table, 1, Stance.CONS, ParliamentaryBoxType.OPENING, time, 1)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); - } - } - @Nested class ValidateStance { 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 index 9291a377..48f1336d 100644 --- a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java @@ -8,26 +8,9 @@ import com.debatetimer.exception.errorcode.ClientErrorCode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; class TimeBasedTimeBoxTest { - @Nested - class Validate { - - @ValueSource(ints = {0, -1}) - @ParameterizedTest - void 시간은_양수만_가능하다(int time) { - TimeBasedTable table = new TimeBasedTable(); - - assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.CONS, TimeBasedBoxType.OPENING, time, 1)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); - } - } - @Nested class ValidateStance { @@ -53,12 +36,23 @@ class ValidateStance { @Nested class ValidateTimeBased { + @Test + void 시간총량제_타입은_총_시간이_팀_발언_시간의_2배여야_한다() { + TimeBasedTable table = new TimeBasedTable(); + TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; + + assertThatThrownBy( + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 150, 120, 60, 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE.getMessage()); + } + @Test void 시간총량제_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { TimeBasedTable table = new TimeBasedTable(); TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; - assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 120, 60, 1)) + assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 240, 120, 60, 1)) .doesNotThrowAnyException(); } @@ -77,7 +71,8 @@ class ValidateTimeBased { TimeBasedTable table = new TimeBasedTable(); TimeBasedBoxType notTimeBasedBoxType = TimeBasedBoxType.TIME_OUT; - assertThatThrownBy(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, notTimeBasedBoxType, 120, 60, 1)) + assertThatThrownBy( + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, notTimeBasedBoxType, 240, 120, 60, 1)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } @@ -89,8 +84,8 @@ class ValidateTimeBased { int timePerSpeaking = 59; assertThatCode( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam, - timePerSpeaking, 1)) + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam * 2, + timePerTeam, timePerSpeaking, 1)) .doesNotThrowAnyException(); } @@ -101,8 +96,8 @@ class ValidateTimeBased { int timePerSpeaking = 61; assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam, - timePerSpeaking, 1)) + () -> 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/TimeBasedTableGenerator.java b/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java new file mode 100644 index 00000000..5d130f3b --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java @@ -0,0 +1,28 @@ +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, + "토론 테이블", + "주제", + 1800, + 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 c6b9bf9c..4323ae6f 100644 --- a/src/test/java/com/debatetimer/service/BaseServiceTest.java +++ b/src/test/java/com/debatetimer/service/BaseServiceTest.java @@ -4,10 +4,13 @@ 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; @@ -23,17 +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 parliamentaryTimeBoxGenerator; @Autowired - protected ParliamentaryTimeBoxGenerator timeBoxGenerator; + protected TimeBasedTableGenerator timeBasedTableGenerator; @Autowired - protected TokenGenerator tokenGenerator; + protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; } diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index fb382477..b2288903 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -4,14 +4,14 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; 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; @@ -33,19 +33,21 @@ 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, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) + ParliamentaryTableInfoCreateRequest requestTableInfo = new ParliamentaryTableInfoCreateRequest("커찬의 테이블", + "주제", true, true); + List requestTimeBoxes = List.of( + new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) ); ParliamentaryTableCreateRequest chanTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("커찬의 테이블", "주제", true, true), - List.of(new TimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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 +62,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 +78,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 +93,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, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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 +116,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, ParliamentaryBoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.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) @@ -133,14 +135,15 @@ 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,7 +155,7 @@ class DeleteTable { 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.deleteTable(chanTableId, coli)) 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..ffe39fc8 --- /dev/null +++ b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java @@ -0,0 +1,177 @@ +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.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"); + TimeBasedTableInfoCreateRequest requestTableInfo = new TimeBasedTableInfoCreateRequest("커찬의 테이블", + "주제", true, true); + List requestTimeBoxes = List.of( + new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, + 1), + new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, + 60, + 1)); + 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 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()); + } + } +} From c360901e5d4ccaa429b76007e16134c79fc40e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A4=EC=B0=AC?= <44027393+leegwichan@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:21:46 +0900 Subject: [PATCH 07/33] =?UTF-8?q?[CHORE]=20DB=20=ED=98=95=EC=83=81=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=8F=84=EA=B5=AC=20=EB=8F=84=EC=9E=85=20?= =?UTF-8?q?(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 22 +++++----- .../debatetimer/DebateTimerApplication.java | 1 - src/main/resources/application-dev.yml | 8 +++- src/main/resources/application-prod.yml | 6 ++- .../db/migration/V1__initialize_table.sql | 40 +++++++++++++++++++ .../db/migration/V2__add_time_based_table.sql | 35 ++++++++++++++++ .../DatabaseSchemaManagerTest.java | 22 ++++++++++ .../DebateTimerApplicationTest.java | 1 - src/test/resources/application-flyway.yml | 18 +++++++++ src/test/resources/application.yml | 28 +++++++------ 10 files changed, 154 insertions(+), 27 deletions(-) create mode 100644 src/main/resources/db/migration/V1__initialize_table.sql create mode 100644 src/main/resources/db/migration/V2__add_time_based_table.sql create mode 100644 src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java create mode 100644 src/test/resources/application-flyway.yml diff --git a/build.gradle b/build.gradle index 031c7a72..758faa31 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,19 @@ dependencies { 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' @@ -46,15 +59,6 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2' - - // 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' } 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/resources/application-dev.yml b/src/main/resources/application-dev.yml index 8895ed5f..5d4f6fff 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} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 5d0feff0..e97cc90b 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} 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/test/java/com/debatetimer/DatabaseSchemaManagerTest.java b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java new file mode 100644 index 00000000..bac3c13b --- /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") +public 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/resources/application-flyway.yml b/src/test/resources/application-flyway.yml new file mode 100644 index 00000000..e8fb7f10 --- /dev/null +++ b/src/test/resources/application-flyway.yml @@ -0,0 +1,18 @@ +spring: + 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 938d1955..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,16 +34,5 @@ spring: hibernate: ddl-auto: create-drop defer-datasource-initialization: true - -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 + flyway: + enabled: false From 856264201803099a9679175b41bad33f4538aea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Thu, 6 Mar 2025 02:19:37 +0900 Subject: [PATCH 08/33] =?UTF-8?q?[CHORE]=20=EC=95=A0=ED=94=8C=EB=A6=AC?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=97=90=EB=9F=AC=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EB=8F=84=EC=9E=85=20(#109)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +++ .../aop/logging/ClientLoggingAspect.java | 47 ++++++++++++ .../aop/logging/ControllerLoggingAspect.java | 75 +++++++++++++++++++ .../aop/logging/LoggingAspect.java | 21 ++++++ .../aop/logging/LoggingClient.java | 11 +++ .../com/debatetimer/client/OAuthClient.java | 2 + src/main/resources/application-dev.yml | 3 + src/main/resources/application-prod.yml | 2 + src/main/resources/logging/log4j2-dev.yml | 44 +++++++++++ src/main/resources/logging/log4j2-local.yml | 22 ++++++ src/main/resources/logging/log4j2-prod.yml | 44 +++++++++++ 11 files changed, 280 insertions(+) create mode 100644 src/main/java/com/debatetimer/aop/logging/ClientLoggingAspect.java create mode 100644 src/main/java/com/debatetimer/aop/logging/ControllerLoggingAspect.java create mode 100644 src/main/java/com/debatetimer/aop/logging/LoggingAspect.java create mode 100644 src/main/java/com/debatetimer/aop/logging/LoggingClient.java create mode 100644 src/main/resources/logging/log4j2-dev.yml create mode 100644 src/main/resources/logging/log4j2-local.yml create mode 100644 src/main/resources/logging/log4j2-prod.yml diff --git a/build.gradle b/build.gradle index 758faa31..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,6 +36,7 @@ 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' @@ -59,6 +64,10 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2' + + // Logging + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" } bootJar { diff --git a/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/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5d4f6fff..51b83166 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -32,3 +32,6 @@ jwt: secret_key: ${secret.jwt.secret_key} 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-prod.yml b/src/main/resources/application-prod.yml index e97cc90b..2af3608c 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -32,3 +32,5 @@ jwt: 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/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 From 8f0b6f50a229c2350479a79532704593843e00e9 Mon Sep 17 00:00:00 2001 From: Chung-an Lee <44027393+leegwichan@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:21:23 +0900 Subject: [PATCH 09/33] =?UTF-8?q?[FEAT]=20=EB=A9=A4=EB=B2=84=EC=9D=98=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C,?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=88=9C=EC=84=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../debatetimer/config/JpaAuditingConfig.java | 10 ++++++ .../debatetimer/domain/BaseTimeEntity.java | 18 +++++++++++ .../com/debatetimer/domain/DebateTable.java | 16 ++++++++-- .../com/debatetimer/domain/member/Member.java | 3 +- .../dto/member/TableResponses.java | 14 ++++++--- .../db/migration/V3__add_auditing_column.sql | 10 ++++++ .../DatabaseSchemaManagerTest.java | 2 +- .../debatetimer/domain/DebateTableTest.java | 31 +++++++++++++++++++ .../service/member/MemberServiceTest.java | 23 ++++++++++++-- 9 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/debatetimer/config/JpaAuditingConfig.java create mode 100644 src/main/java/com/debatetimer/domain/BaseTimeEntity.java create mode 100644 src/main/resources/db/migration/V3__add_auditing_column.sql 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/domain/BaseTimeEntity.java b/src/main/java/com/debatetimer/domain/BaseTimeEntity.java new file mode 100644 index 00000000..6fe5a75a --- /dev/null +++ b/src/main/java/com/debatetimer/domain/BaseTimeEntity.java @@ -0,0 +1,18 @@ +package com.debatetimer.domain; + +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; + +@Getter +@MappedSuperclass +public abstract class BaseTimeEntity { + + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime modifiedAt; +} diff --git a/src/main/java/com/debatetimer/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java index 7f516156..0dbc9002 100644 --- a/src/main/java/com/debatetimer/domain/DebateTable.java +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -9,6 +9,7 @@ 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; @@ -17,7 +18,7 @@ @Getter @MappedSuperclass @NoArgsConstructor(access = AccessLevel.PROTECTED) -public abstract class DebateTable { +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; @@ -39,6 +40,9 @@ public abstract class DebateTable { private boolean finishBell; + @NotNull + private LocalDateTime usedAt; + protected DebateTable(Member member, String name, String agenda, int duration, boolean warningBell, boolean finishBell) { validate(name, duration); @@ -49,12 +53,17 @@ protected DebateTable(Member member, String name, String agenda, int duration, b this.duration = duration; 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(), renewTable.getDuration()); @@ -63,6 +72,7 @@ protected final void updateTable(DebateTable renewTable) { this.duration = renewTable.getDuration(); this.warningBell = renewTable.isWarningBell(); this.finishBell = renewTable.isFinishBell(); + updateUsedAt(); } private void validate(String name, int duration) { @@ -77,7 +87,7 @@ private void validate(String name, int duration) { } } - abstract public long getId(); + public abstract long getId(); - abstract public TableType getType(); + public abstract TableType getType(); } 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/dto/member/TableResponses.java b/src/main/java/com/debatetimer/dto/member/TableResponses.java index d4d23a4c..651acfbc 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponses.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponses.java @@ -1,12 +1,18 @@ 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) { + private static final Comparator DEBATE_TABLE_COMPARATOR = Comparator + .comparing(DebateTable::getUsedAt) + .reversed(); + public TableResponses(List parliamentaryTables, List timeBasedTables) { this(toTableResponses(parliamentaryTables, timeBasedTables)); @@ -14,11 +20,9 @@ public TableResponses(List parliamentaryTables, private static List toTableResponses(List parliamentaryTables, List timeBasedTables) { - Stream parliamentaryTableResponseStream = parliamentaryTables.stream() - .map(TableResponse::new); - Stream timeBasedTableResponseStream = timeBasedTables.stream() - .map(TableResponse::new); - return Stream.concat(parliamentaryTableResponseStream, timeBasedTableResponseStream) + return Stream.concat(parliamentaryTables.stream(), timeBasedTables.stream()) + .sorted(DEBATE_TABLE_COMPARATOR) + .map(TableResponse::new) .toList(); } } 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/test/java/com/debatetimer/DatabaseSchemaManagerTest.java b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java index bac3c13b..e89fce15 100644 --- a/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java +++ b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java @@ -10,7 +10,7 @@ @SpringBootTest @ActiveProfiles("flyway") -public class DatabaseSchemaManagerTest { +class DatabaseSchemaManagerTest { @Autowired private Flyway flyway; diff --git a/src/test/java/com/debatetimer/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java index 62c6b68d..3b94010a 100644 --- a/src/test/java/com/debatetimer/domain/DebateTableTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -9,6 +9,7 @@ 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; @@ -64,6 +65,22 @@ class Validate { } } + @Nested + class UpdateUsedAt { + + @Test + void 테이블의_사용_시각을_업데이트한다() throws InterruptedException { + Member member = new Member("default@gmail.com"); + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", 10, true, true); + LocalDateTime beforeUsedAt = table.getUsedAt(); + Thread.sleep(1); + + table.updateUsedAt(); + + assertThat(table.getUsedAt()).isAfter(beforeUsedAt); + } + } + @Nested class Update { @@ -84,6 +101,20 @@ class Update { () -> assertThat(table.isFinishBell()).isEqualTo(false) ); } + + @Test + void 테이블_업데이트_할_때_사용_시간을_변경한다() throws InterruptedException { + Member member = new Member("default@gmail.com"); + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", 10, true, true); + DebateTableTestObject renewTable = new DebateTableTestObject(member, "newName", "newAgenda", 100, false, + false); + LocalDateTime beforeUsedAt = table.getUsedAt(); + Thread.sleep(1); + + table.updateTable(renewTable); + + assertThat(table.getUsedAt()).isAfter(beforeUsedAt); + } } private static class DebateTableTestObject extends DebateTable { 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()) + ); + } } } From 785224ea03bc1224ea1163182b8b9f7c5b851842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Sun, 9 Mar 2025 23:03:44 +0900 Subject: [PATCH 10/33] =?UTF-8?q?[FIX]=20time=20auditing=EC=9D=B4=20?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/debatetimer/domain/BaseTimeEntity.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/debatetimer/domain/BaseTimeEntity.java b/src/main/java/com/debatetimer/domain/BaseTimeEntity.java index 6fe5a75a..ea8677cc 100644 --- a/src/main/java/com/debatetimer/domain/BaseTimeEntity.java +++ b/src/main/java/com/debatetimer/domain/BaseTimeEntity.java @@ -1,18 +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; } From f5ba8a7a23145f4eabb8b37b8f184bfeaeca2f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Mon, 10 Mar 2025 13:13:31 +0900 Subject: [PATCH 11/33] =?UTF-8?q?[REFACTOR]=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A1=B0=ED=9A=8C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=86=8C=EC=9A=94=EC=8B=9C=EA=B0=84=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20=EC=A3=BC=EC=A0=9C=20=EB=B0=98=ED=99=98=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/DebateTable.java | 16 +++------- .../parliamentary/ParliamentaryTable.java | 3 +- .../domain/timebased/TimeBasedTable.java | 3 +- .../debatetimer/dto/member/TableResponse.java | 4 +-- .../ParliamentaryTableCreateRequest.java | 8 +---- .../ParliamentaryTableInfoCreateRequest.java | 4 +-- .../request/TimeBasedTableCreateRequest.java | 8 +---- .../TimeBasedTableInfoCreateRequest.java | 4 +-- .../db/migration/V4__drop_duration_column.sql | 2 ++ .../member/MemberControllerTest.java | 4 +-- .../controller/member/MemberDocumentTest.java | 6 ++-- .../debatetimer/domain/DebateTableTest.java | 31 ++++++------------- .../com/debatetimer/domain/TimeBoxesTest.java | 2 +- .../fixture/ParliamentaryTableGenerator.java | 1 - .../fixture/TimeBasedTableGenerator.java | 1 - 15 files changed, 32 insertions(+), 65 deletions(-) create mode 100644 src/main/resources/db/migration/V4__drop_duration_column.sql diff --git a/src/main/java/com/debatetimer/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java index 0dbc9002..fd7f1f36 100644 --- a/src/main/java/com/debatetimer/domain/DebateTable.java +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -34,8 +34,6 @@ public abstract class DebateTable extends BaseTimeEntity { @NotNull private String agenda; - private int duration; - private boolean warningBell; private boolean finishBell; @@ -43,14 +41,12 @@ public abstract class DebateTable extends BaseTimeEntity { @NotNull private LocalDateTime usedAt; - protected DebateTable(Member member, String name, String agenda, int duration, boolean warningBell, - boolean finishBell) { - validate(name, duration); + protected DebateTable(Member member, String name, String agenda, boolean warningBell, boolean finishBell) { + validate(name); this.member = member; this.name = name; this.agenda = agenda; - this.duration = duration; this.warningBell = warningBell; this.finishBell = finishBell; this.usedAt = LocalDateTime.now(); @@ -65,26 +61,22 @@ public final void updateUsedAt() { } protected final void updateTable(DebateTable renewTable) { - validate(renewTable.getName(), renewTable.getDuration()); + validate(renewTable.getName()); this.name = renewTable.getName(); this.agenda = renewTable.getAgenda(); - this.duration = renewTable.getDuration(); this.warningBell = renewTable.isWarningBell(); this.finishBell = renewTable.isFinishBell(); updateUsedAt(); } - private void validate(String name, int duration) { + 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); } - if (duration <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_TIME); - } } public abstract long getId(); diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java index 43b33b8d..08041d58 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java @@ -24,11 +24,10 @@ public ParliamentaryTable( Member member, String name, String agenda, - int duration, boolean warningBell, boolean finishBell ) { - super(member, name, agenda, duration, warningBell, finishBell); + super(member, name, agenda, warningBell, finishBell); } @Override diff --git a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java index 7ec28f4a..d3a17a0d 100644 --- a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTable.java @@ -24,11 +24,10 @@ public TimeBasedTable( Member member, String name, String agenda, - int duration, boolean warningBell, boolean finishBell ) { - super(member, name, agenda, duration, warningBell, finishBell); + super(member, name, agenda, warningBell, finishBell); } @Override diff --git a/src/main/java/com/debatetimer/dto/member/TableResponse.java b/src/main/java/com/debatetimer/dto/member/TableResponse.java index 624653e3..7c6baa62 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponse.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponse.java @@ -2,14 +2,14 @@ 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(DebateTable debateTable) { this( debateTable.getId(), debateTable.getName(), debateTable.getType(), - debateTable.getDuration() + debateTable.getAgenda() ); } } 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 ea1a238a..e23acf58 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -11,13 +11,7 @@ public record ParliamentaryTableCreateRequest(ParliamentaryTableInfoCreateReques List table) { public ParliamentaryTable toTable(Member member) { - return info.toTable(member, sumOfTime(), info.warningBell(), info().finishBell()); - } - - private int sumOfTime() { - return table.stream() - .mapToInt(ParliamentaryTimeBoxCreateRequest::time) - .sum(); + return info.toTable(member, info.warningBell(), info().finishBell()); } public TimeBoxes toTimeBoxes(ParliamentaryTable parliamentaryTable) { diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java index 621f95d7..905f6343 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java @@ -16,7 +16,7 @@ public record ParliamentaryTableInfoCreateRequest( 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, boolean warningBell, boolean finishBell) { + return new ParliamentaryTable(member, name, agenda, warningBell, finishBell); } } diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java index ad73f58b..d61234f3 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java @@ -11,13 +11,7 @@ public record TimeBasedTableCreateRequest(TimeBasedTableInfoCreateRequest info, List table) { public TimeBasedTable toTable(Member member) { - return info.toTable(member, sumOfTime()); - } - - private int sumOfTime() { - return table.stream() - .mapToInt(TimeBasedTimeBoxCreateRequest::time) - .sum(); + return info.toTable(member); } public TimeBoxes toTimeBoxes(TimeBasedTable timeBasedTable) { diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java index cff9f3d1..51db8b89 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableInfoCreateRequest.java @@ -16,7 +16,7 @@ public record TimeBasedTableInfoCreateRequest( boolean finishBell ) { - public TimeBasedTable toTable(Member member, int duration) { - return new TimeBasedTable(member, name, agenda, duration, warningBell, finishBell); + public TimeBasedTable toTable(Member member) { + return new TimeBasedTable(member, name, agenda, warningBell, finishBell); } } 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/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 4dc7ae58..e1e50bf8 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java @@ -85,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); diff --git a/src/test/java/com/debatetimer/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java index 3b94010a..b8ad484c 100644 --- a/src/test/java/com/debatetimer/domain/DebateTableTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -24,7 +24,7 @@ class Validate { @ParameterizedTest void 테이블_이름은_영문과_한글_숫자_띄어쓰기만_가능하다(String name) { Member member = new Member("default@gmail.com"); - assertThatCode(() -> new DebateTableTestObject(member, name, "agenda", 10, true, true)) + assertThatCode(() -> new DebateTableTestObject(member, name, "agenda", true, true)) .doesNotThrowAnyException(); } @@ -32,7 +32,7 @@ class Validate { @ParameterizedTest void 테이블_이름은_정해진_길이_이내여야_한다(int length) { Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new DebateTableTestObject(member, "f".repeat(length), "agenda", 10, true, true)) + assertThatThrownBy(() -> new DebateTableTestObject(member, "f".repeat(length), "agenda", true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); } @@ -41,7 +41,7 @@ class Validate { @ParameterizedTest void 테이블_이름은_적어도_한_자_있어야_한다(String name) { Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new DebateTableTestObject(member, name, "agenda", 10, true, true)) + assertThatThrownBy(() -> new DebateTableTestObject(member, name, "agenda", true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); } @@ -50,19 +50,10 @@ class Validate { @ParameterizedTest void 허용된_글자_이외의_문자는_불가능하다(String name) { Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new DebateTableTestObject(member, name, "agenda", 10, true, true)) + assertThatThrownBy(() -> new DebateTableTestObject(member, name, "agenda", true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); } - - @ValueSource(ints = {0, -1, -60}) - @ParameterizedTest - void 테이블_시간은_양수만_가능하다(int duration) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new DebateTableTestObject(member, "nickname", "agenda", duration, true, true)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TABLE_TIME.getMessage()); - } } @Nested @@ -71,7 +62,7 @@ class UpdateUsedAt { @Test void 테이블의_사용_시각을_업데이트한다() throws InterruptedException { Member member = new Member("default@gmail.com"); - DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", 10, true, true); + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", true, true); LocalDateTime beforeUsedAt = table.getUsedAt(); Thread.sleep(1); @@ -87,8 +78,8 @@ class Update { @Test void 테이블_정보를_업데이트_할_수_있다() { Member member = new Member("default@gmail.com"); - DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", 10, true, true); - DebateTableTestObject renewTable = new DebateTableTestObject(member, "newName", "newAgenda", 100, false, + DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", true, true); + DebateTableTestObject renewTable = new DebateTableTestObject(member, "newName", "newAgenda", false, false); table.updateTable(renewTable); @@ -96,7 +87,6 @@ class Update { assertAll( () -> assertThat(table.getName()).isEqualTo("newName"), () -> assertThat(table.getAgenda()).isEqualTo("newAgenda"), - () -> assertThat(table.getDuration()).isEqualTo(100), () -> assertThat(table.isWarningBell()).isEqualTo(false), () -> assertThat(table.isFinishBell()).isEqualTo(false) ); @@ -105,8 +95,8 @@ class Update { @Test void 테이블_업데이트_할_때_사용_시간을_변경한다() throws InterruptedException { Member member = new Member("default@gmail.com"); - DebateTableTestObject table = new DebateTableTestObject(member, "tableName", "agenda", 10, true, true); - DebateTableTestObject renewTable = new DebateTableTestObject(member, "newName", "newAgenda", 100, false, + 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); @@ -122,10 +112,9 @@ private static class DebateTableTestObject extends DebateTable { public DebateTableTestObject(Member member, String name, String agenda, - int duration, boolean warningBell, boolean finishBell) { - super(member, name, agenda, duration, warningBell, finishBell); + super(member, name, agenda, warningBell, finishBell); } @Override diff --git a/src/test/java/com/debatetimer/domain/TimeBoxesTest.java b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java index 0f203808..c4534a71 100644 --- a/src/test/java/com/debatetimer/domain/TimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java @@ -20,7 +20,7 @@ class SortedBySequence { @Test void 타임박스의_순서에_따라_정렬된다() { Member member = new Member("default@gmail.com"); - ParliamentaryTable testTable = new ParliamentaryTable(member, "토론 테이블", "주제", 1800, true, true); + 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, 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/TimeBasedTableGenerator.java b/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java index 5d130f3b..51fd2c2a 100644 --- a/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java +++ b/src/test/java/com/debatetimer/fixture/TimeBasedTableGenerator.java @@ -19,7 +19,6 @@ public TimeBasedTable generate(Member member) { member, "토론 테이블", "주제", - 1800, false, false ); From 286bf852c6ba4082bb87953a56a988e0d1d06016 Mon Sep 17 00:00:00 2001 From: leegwichan Date: Mon, 10 Mar 2025 16:53:53 +0900 Subject: [PATCH 12/33] =?UTF-8?q?feat=20:=20=EC=9D=98=ED=9A=8C=EC=8B=9D=20?= =?UTF-8?q?=ED=86=A0=EB=A1=A0=20=EC=A7=84=ED=96=89=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParliamentaryController.java | 10 +++ .../parliamentary/ParliamentaryService.java | 8 ++ .../ParliamentaryControllerTest.java | 27 +++++++ .../ParliamentaryDocumentTest.java | 77 ++++++++++++++++++- .../ParliamentaryServiceTest.java | 34 ++++++++ 5 files changed, 155 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java index 583a0796..4e18d37b 100644 --- a/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java +++ b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java @@ -14,6 +14,7 @@ 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; @@ -56,6 +57,15 @@ public ParliamentaryTableResponse updateTable( return parliamentaryService.updateTable(tableCreateRequest, tableId, member); } + @PatchMapping("/api/table/parliamentary/{tableId}/debate") + @ResponseStatus(HttpStatus.OK) + public ParliamentaryTableResponse doDebate( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return parliamentaryService.updateUsedAt(tableId, member); + } + @DeleteMapping("/api/table/parliamentary/{tableId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteTable( diff --git a/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java index 8b4904e2..24bbd17f 100644 --- a/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java +++ b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java @@ -61,6 +61,14 @@ public ParliamentaryTableResponse updateTable( return new ParliamentaryTableResponse(existingTable, savedTimeBoxes); } + @Transactional + 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()); diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index b5ce0012..ada4e9b8 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -109,6 +109,33 @@ class UpdateTable { } } + @Nested + class DoDebate { + + @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 { diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index 78b9bc16..a4f33d87 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -311,7 +311,7 @@ class UpdateTable { } ) @ParameterizedTest - void 의회식_테이블_생성_실패(ClientErrorCode errorCode) { + void 의회식_테이블_수정_실패(ClientErrorCode errorCode) { long tableId = 5L; ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( new ParliamentaryTableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), @@ -338,6 +338,81 @@ class UpdateTable { } } + @Nested + class DoDebate { + + 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 { diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index b2288903..adbf8d1c 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -16,6 +16,7 @@ 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; @@ -129,6 +130,39 @@ class UpdateTable { } } + @Nested + class UpdateUsedAt { + + @Test + void 의회식_토론_테이블을_수정한다() throws InterruptedException { + Member member = memberGenerator.generate("default@gmail.com"); + ParliamentaryTable table = parliamentaryTableGenerator.generate(member); + LocalDateTime beforeUsedAt = table.getUsedAt(); + Thread.sleep(1); + + 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 { From 916dd92cd9fcd26a9f68e317dd16096a4f67a3f8 Mon Sep 17 00:00:00 2001 From: leegwichan Date: Mon, 10 Mar 2025 16:59:05 +0900 Subject: [PATCH 13/33] =?UTF-8?q?fix=20:=20=EC=8B=9C=EA=B0=84=20=EC=B4=9D?= =?UTF-8?q?=EB=9F=89=EC=A0=9C=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20Http=20Method=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 의회식 토론 테이블과 통일하여 PUT 으로 변경 --- .../debatetimer/controller/timebased/TimeBasedController.java | 4 ++-- .../controller/timebased/TimeBasedControllerTest.java | 2 +- .../controller/timebased/TimeBasedDocumentTest.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java index dbb25c79..3976078b 100644 --- a/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java +++ b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java @@ -10,9 +10,9 @@ 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; @@ -41,7 +41,7 @@ public TimeBasedTableResponse getTable( return timeBasedService.findTable(tableId, member); } - @PatchMapping("/api/table/time-based/{tableId}") + @PutMapping("/api/table/time-based/{tableId}") @ResponseStatus(HttpStatus.OK) public TimeBasedTableResponse updateTable( @Valid @RequestBody TimeBasedTableCreateRequest tableCreateRequest, diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java index 2a01686e..56d0b6b8 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java @@ -97,7 +97,7 @@ class UpdateTable { .pathParam("tableId", bitoTable.getId()) .headers(headers) .body(renewTableRequest) - .when().patch("/api/table/time-based/{tableId}") + .when().put("/api/table/time-based/{tableId}") .then().statusCode(200) .extract().as(TimeBasedTableResponse.class); diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java index 073372ac..d182de40 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java @@ -303,7 +303,7 @@ class UpdateTable { .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) .body(request) - .when().patch("/api/table/time-based/{tableId}") + .when().put("/api/table/time-based/{tableId}") .then().statusCode(200); } @@ -343,7 +343,7 @@ class UpdateTable { .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) .body(request) - .when().patch("/api/table/time-based/{tableId}") + .when().put("/api/table/time-based/{tableId}") .then().statusCode(errorCode.getStatus().value()); } } From 94d4f1a97fb3f611b1e2cd19a691f419237cdd70 Mon Sep 17 00:00:00 2001 From: leegwichan Date: Mon, 10 Mar 2025 17:15:21 +0900 Subject: [PATCH 14/33] =?UTF-8?q?feat=20:=20=EC=8B=9C=EA=B0=84=20=EC=B4=9D?= =?UTF-8?q?=EB=9F=89=EC=A0=9C=20=ED=86=A0=EB=A1=A0=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timebased/TimeBasedController.java | 10 +++ .../service/timebased/TimeBasedService.java | 9 +++ .../ParliamentaryDocumentTest.java | 2 +- .../timebased/TimeBasedControllerTest.java | 27 +++++++ .../timebased/TimeBasedDocumentTest.java | 77 +++++++++++++++++++ .../timebased/TimeBasedServiceTest.java | 40 ++++++++++ 6 files changed, 164 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java index 3976078b..48ef4570 100644 --- a/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java +++ b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java @@ -10,6 +10,7 @@ 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; @@ -51,6 +52,15 @@ public TimeBasedTableResponse updateTable( return timeBasedService.updateTable(tableCreateRequest, tableId, member); } + @PatchMapping("/api/table/time-based/{tableId}/debate") + @ResponseStatus(HttpStatus.OK) + public TimeBasedTableResponse doDebate( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return timeBasedService.updateUsedAt(tableId, member); + } + @DeleteMapping("/api/table/time-based/{tableId}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteTable( diff --git a/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java b/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java index d6a1011d..9d87ef61 100644 --- a/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java +++ b/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java @@ -54,6 +54,15 @@ public TimeBasedTableResponse updateTable( 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()); diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index a4f33d87..7bb53158 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -380,7 +380,7 @@ class DoDebate { ); doReturn(response).when(parliamentaryService).updateUsedAt(eq(tableId), any()); - var document = document("parliamentary/patch-debate", 200) + var document = document("parliamentary/patch_debate", 200) .request(requestDocument) .response(responseDocument) .build(); diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java index 56d0b6b8..67e82aa5 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java @@ -109,6 +109,33 @@ class UpdateTable { } } + @Nested + class DoDebate { + + @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 { diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java index d182de40..3ac490b7 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java @@ -348,6 +348,83 @@ class UpdateTable { } } + @Nested + class DoDebate { + + 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 { diff --git a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java index ffe39fc8..454b0ec0 100644 --- a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java +++ b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java @@ -16,6 +16,7 @@ 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; @@ -140,6 +141,45 @@ class UpdateTable { } } + @Nested + class UpdateUsedAt { + + @Test + void 시간총량제_토론_테이블을_수정한다() throws InterruptedException { + Member member = memberGenerator.generate("default@gmail.com"); + TimeBasedTable table = timeBasedTableGenerator.generate(member); + LocalDateTime beforeUsedAt = table.getUsedAt(); + Thread.sleep(1); + + 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 { From 191bec81c90faf6f08ece4d1e5dca95c53de8365 Mon Sep 17 00:00:00 2001 From: leegwichan Date: Mon, 10 Mar 2025 17:23:35 +0900 Subject: [PATCH 15/33] =?UTF-8?q?refactor=20:=20TimeBoxes=20=EC=9D=98=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParliamentaryTableCreateRequest.java | 3 ++- .../request/TimeBasedTableCreateRequest.java | 3 ++- .../ParliamentaryTimeBoxRepository.java | 4 ++-- .../timebased/TimeBasedTimeBoxRepository.java | 4 ++-- .../parliamentary/ParliamentaryService.java | 20 +++++++++---------- .../service/timebased/TimeBasedService.java | 18 ++++++++--------- 6 files changed, 27 insertions(+), 25 deletions(-) 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 e23acf58..fb5078f2 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -3,6 +3,7 @@ import com.debatetimer.domain.TimeBoxes; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; +import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -14,7 +15,7 @@ public ParliamentaryTable toTable(Member member) { return info.toTable(member, info.warningBell(), info().finishBell()); } - public TimeBoxes 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(), TimeBoxes::new)); diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java index d61234f3..3012958a 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java @@ -3,6 +3,7 @@ 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; @@ -14,7 +15,7 @@ public TimeBasedTable toTable(Member member) { return info.toTable(member); } - public TimeBoxes toTimeBoxes(TimeBasedTable timeBasedTable) { + 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/repository/parliamentary/ParliamentaryTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java index 3915aecf..63120aa6 100644 --- a/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java @@ -22,9 +22,9 @@ default List saveAll(List timeBoxes) List findAllByParliamentaryTable(ParliamentaryTable table); - default TimeBoxes findTableTimeBoxes(ParliamentaryTable table) { + default TimeBoxes findTableTimeBoxes(ParliamentaryTable table) { List timeBoxes = findAllByParliamentaryTable(table); - return new TimeBoxes(timeBoxes); + return new TimeBoxes<>(timeBoxes); } @Query("DELETE FROM ParliamentaryTimeBox ptb WHERE ptb IN :timeBoxes") diff --git a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java index 1657a0ab..a34e4c07 100644 --- a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java @@ -22,9 +22,9 @@ default List saveAll(List timeBoxes) { List findAllByTimeBasedTable(TimeBasedTable table); - default TimeBoxes findTableTimeBoxes(TimeBasedTable table) { + default TimeBoxes findTableTimeBoxes(TimeBasedTable table) { List timeBoxes = findAllByTimeBasedTable(table); - return new TimeBoxes(timeBoxes); + return new TimeBoxes<>(timeBoxes); } @Query("DELETE FROM TimeBasedTimeBox ptb WHERE ptb IN :timeBoxes") diff --git a/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java index 24bbd17f..5b76f221 100644 --- a/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java +++ b/src/main/java/com/debatetimer/service/parliamentary/ParliamentaryService.java @@ -27,21 +27,21 @@ public ParliamentaryTableResponse save(ParliamentaryTableCreateRequest tableCrea ParliamentaryTable table = tableCreateRequest.toTable(member); ParliamentaryTable savedTable = tableRepository.save(table); - TimeBoxes 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()); - TimeBoxes 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); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); return new ParliamentaryTableResponse(table, timeBoxes); } @@ -55,16 +55,16 @@ public ParliamentaryTableResponse updateTable( ParliamentaryTable renewedTable = tableCreateRequest.toTable(member); existingTable.update(renewedTable); - TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); - TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); return new ParliamentaryTableResponse(existingTable, savedTimeBoxes); } @Transactional public ParliamentaryTableResponse updateUsedAt(long tableId, Member member) { ParliamentaryTable table = getOwnerTable(tableId, member.getId()); - TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); table.updateUsedAt(); return new ParliamentaryTableResponse(table, timeBoxes); } @@ -72,18 +72,18 @@ public ParliamentaryTableResponse updateUsedAt(long tableId, Member member) { @Transactional public void deleteTable(long tableId, Member member) { ParliamentaryTable table = getOwnerTable(tableId, member.getId()); - TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); tableRepository.delete(table); } - private TimeBoxes saveTimeBoxes( + private TimeBoxes saveTimeBoxes( ParliamentaryTableCreateRequest tableCreateRequest, ParliamentaryTable table ) { - TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); + TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); List savedTimeBoxes = timeBoxRepository.saveAll(timeBoxes.getTimeBoxes()); - return new TimeBoxes(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 index 9d87ef61..f984b7c8 100644 --- a/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java +++ b/src/main/java/com/debatetimer/service/timebased/TimeBasedService.java @@ -27,14 +27,14 @@ public TimeBasedTableResponse save(TimeBasedTableCreateRequest tableCreateReques TimeBasedTable table = tableCreateRequest.toTable(member); TimeBasedTable savedTable = tableRepository.save(table); - TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, savedTable); + 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); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); return new TimeBasedTableResponse(table, timeBoxes); } @@ -48,16 +48,16 @@ public TimeBasedTableResponse updateTable( TimeBasedTable renewedTable = tableCreateRequest.toTable(member); existingTable.update(renewedTable); - TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); - TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); + 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); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); table.updateUsedAt(); return new TimeBasedTableResponse(table, timeBoxes); @@ -66,18 +66,18 @@ public TimeBasedTableResponse updateUsedAt(long tableId, Member member) { @Transactional public void deleteTable(long tableId, Member member) { TimeBasedTable table = getOwnerTable(tableId, member.getId()); - TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); tableRepository.delete(table); } - private TimeBoxes saveTimeBoxes( + private TimeBoxes saveTimeBoxes( TimeBasedTableCreateRequest tableCreateRequest, TimeBasedTable table ) { - TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); + TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); List savedTimeBoxes = timeBoxRepository.saveAll(timeBoxes.getTimeBoxes()); - return new TimeBoxes(savedTimeBoxes); + return new TimeBoxes<>(savedTimeBoxes); } private TimeBasedTable getOwnerTable(long tableId, long memberId) { From 68742b512f9c2c152bbe491894598664b0949e90 Mon Sep 17 00:00:00 2001 From: leegwichan Date: Mon, 10 Mar 2025 17:27:30 +0900 Subject: [PATCH 16/33] =?UTF-8?q?refactor=20:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=9D=B8=EC=9E=90=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20=ED=95=84=EC=9A=94=20=EC=97=86=EB=8A=94?= =?UTF-8?q?=20Wrapper=20=ED=83=80=EC=9E=85=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parliamentary/ParliamentaryServiceTest.java | 8 +------- .../service/timebased/TimeBasedServiceTest.java | 10 +--------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index adbf8d1c..521cf31b 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -34,12 +34,6 @@ class Save { @Test void 의회식_토론_테이블을_생성한다() { Member chan = memberGenerator.generate("default@gmail.com"); - ParliamentaryTableInfoCreateRequest requestTableInfo = new ParliamentaryTableInfoCreateRequest("커찬의 테이블", - "주제", true, true); - List requestTimeBoxes = List.of( - new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) - ); ParliamentaryTableCreateRequest chanTableRequest = new ParliamentaryTableCreateRequest( new ParliamentaryTableInfoCreateRequest("커찬의 테이블", "주제", true, true), List.of(new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), @@ -190,7 +184,7 @@ class DeleteTable { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); ParliamentaryTable chanTable = parliamentaryTableGenerator.generate(chan); - Long chanTableId = chanTable.getId(); + 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 index 454b0ec0..25cbf8b9 100644 --- a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java +++ b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java @@ -34,14 +34,6 @@ class Save { @Test void 시간총량제_토론_테이블을_생성한다() { Member chan = memberGenerator.generate("default@gmail.com"); - TimeBasedTableInfoCreateRequest requestTableInfo = new TimeBasedTableInfoCreateRequest("커찬의 테이블", - "주제", true, true); - List requestTimeBoxes = List.of( - new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, - 1), - new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, - 60, - 1)); TimeBasedTableCreateRequest chanTableRequest = new TimeBasedTableCreateRequest( new TimeBasedTableInfoCreateRequest("커찬의 테이블", "주제", true, true), List.of(new TimeBasedTimeBoxCreateRequest(Stance.PROS, TimeBasedBoxType.OPENING, 120, null, null, @@ -207,7 +199,7 @@ class DeleteTable { Member chan = memberGenerator.generate("default@gmail.com"); Member coli = memberGenerator.generate("default2@gmail.com"); TimeBasedTable chanTable = timeBasedTableGenerator.generate(chan); - Long chanTableId = chanTable.getId(); + long chanTableId = chanTable.getId(); assertThatThrownBy(() -> timeBasedService.deleteTable(chanTableId, coli)) .isInstanceOf(DTClientErrorException.class) From cdf5775b54c2f7da83d07ba50913e5f5e369d6de Mon Sep 17 00:00:00 2001 From: leegwichan Date: Mon, 10 Mar 2025 18:45:15 +0900 Subject: [PATCH 17/33] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/parliamentary/ParliamentaryControllerTest.java | 2 +- .../controller/timebased/TimeBasedDocumentTest.java | 4 ++-- .../service/parliamentary/ParliamentaryServiceTest.java | 2 +- .../debatetimer/service/timebased/TimeBasedServiceTest.java | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index ada4e9b8..7cb5b8ce 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -113,7 +113,7 @@ class UpdateTable { class DoDebate { @Test - void 토론을_진행한다() { + void 의회식_토론을_진행한다() { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); parliamentaryTimeBoxGenerator.generate(bitoTable, 1); diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java index 3ac490b7..231f11c8 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java @@ -380,7 +380,7 @@ class DoDebate { ); @Test - void 시간총량제_테이블_조회_성공() { + void 시간총량제_토론_진행_성공() { long tableId = 5L; TimeBasedTableResponse response = new TimeBasedTableResponse( 5L, @@ -407,7 +407,7 @@ class DoDebate { @ParameterizedTest @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) - void 시간총량제_테이블_조회_실패(ClientErrorCode errorCode) { + void 시간총량제_토론_진행_실패(ClientErrorCode errorCode) { long tableId = 5L; doThrow(new DTClientErrorException(errorCode)).when(timeBasedService).updateUsedAt(eq(tableId), any()); diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index 521cf31b..1bb3b7ed 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -128,7 +128,7 @@ class UpdateTable { class UpdateUsedAt { @Test - void 의회식_토론_테이블을_수정한다() throws InterruptedException { + void 의회식_토론_테이블의_사용_시각을_최신화한다() throws InterruptedException { Member member = memberGenerator.generate("default@gmail.com"); ParliamentaryTable table = parliamentaryTableGenerator.generate(member); LocalDateTime beforeUsedAt = table.getUsedAt(); diff --git a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java index 25cbf8b9..48fa8d0a 100644 --- a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java +++ b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java @@ -137,7 +137,7 @@ class UpdateTable { class UpdateUsedAt { @Test - void 시간총량제_토론_테이블을_수정한다() throws InterruptedException { + void 시간총량제_토론_테이블의_사용_시각을_최신화한다() throws InterruptedException { Member member = memberGenerator.generate("default@gmail.com"); TimeBasedTable table = timeBasedTableGenerator.generate(member); LocalDateTime beforeUsedAt = table.getUsedAt(); From 961d3678211660668c00f8d4cc7cd5255e384b23 Mon Sep 17 00:00:00 2001 From: leegwichan Date: Tue, 11 Mar 2025 00:07:35 +0900 Subject: [PATCH 18/33] =?UTF-8?q?refactor:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - doDebate -> debate --- .../controller/parliamentary/ParliamentaryController.java | 2 +- .../debatetimer/controller/timebased/TimeBasedController.java | 2 +- .../controller/parliamentary/ParliamentaryControllerTest.java | 2 +- .../controller/parliamentary/ParliamentaryDocumentTest.java | 2 +- .../controller/timebased/TimeBasedControllerTest.java | 2 +- .../debatetimer/controller/timebased/TimeBasedDocumentTest.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java index 4e18d37b..9a2176f2 100644 --- a/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java +++ b/src/main/java/com/debatetimer/controller/parliamentary/ParliamentaryController.java @@ -59,7 +59,7 @@ public ParliamentaryTableResponse updateTable( @PatchMapping("/api/table/parliamentary/{tableId}/debate") @ResponseStatus(HttpStatus.OK) - public ParliamentaryTableResponse doDebate( + public ParliamentaryTableResponse debate( @PathVariable Long tableId, @AuthMember Member member ) { diff --git a/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java index 48ef4570..2e67ca6f 100644 --- a/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java +++ b/src/main/java/com/debatetimer/controller/timebased/TimeBasedController.java @@ -54,7 +54,7 @@ public TimeBasedTableResponse updateTable( @PatchMapping("/api/table/time-based/{tableId}/debate") @ResponseStatus(HttpStatus.OK) - public TimeBasedTableResponse doDebate( + public TimeBasedTableResponse debate( @PathVariable Long tableId, @AuthMember Member member ) { diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index 7cb5b8ce..559f4456 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -110,7 +110,7 @@ class UpdateTable { } @Nested - class DoDebate { + class Debate { @Test void 의회식_토론을_진행한다() { diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index 7bb53158..0e2ef73b 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -339,7 +339,7 @@ class UpdateTable { } @Nested - class DoDebate { + class Debate { private final RestDocumentationRequest requestDocument = request() .summary("의회식 토론 진행") diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java index 67e82aa5..034e1952 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java @@ -110,7 +110,7 @@ class UpdateTable { } @Nested - class DoDebate { + class Debate { @Test void 시간총량제_토론을_시작한다() { diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java index 231f11c8..2c1eb608 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedDocumentTest.java @@ -349,7 +349,7 @@ class UpdateTable { } @Nested - class DoDebate { + class Debate { private final RestDocumentationRequest requestDocument = request() .summary("시간총량제 토론 시작") From c00aed0eaa88f29b84edf2531af15e3f629ea03c Mon Sep 17 00:00:00 2001 From: leegwichan Date: Tue, 11 Mar 2025 00:09:39 +0900 Subject: [PATCH 19/33] =?UTF-8?q?test:=20Service=20Test=20=EC=8B=9C=20Thre?= =?UTF-8?q?ad.sleep()=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/parliamentary/ParliamentaryServiceTest.java | 3 +-- .../debatetimer/service/timebased/TimeBasedServiceTest.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index 1bb3b7ed..fe0df654 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -128,11 +128,10 @@ class UpdateTable { class UpdateUsedAt { @Test - void 의회식_토론_테이블의_사용_시각을_최신화한다() throws InterruptedException { + void 의회식_토론_테이블의_사용_시각을_최신화한다() { Member member = memberGenerator.generate("default@gmail.com"); ParliamentaryTable table = parliamentaryTableGenerator.generate(member); LocalDateTime beforeUsedAt = table.getUsedAt(); - Thread.sleep(1); parliamentaryService.updateUsedAt(table.getId(), member); diff --git a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java index 48fa8d0a..be5410fa 100644 --- a/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java +++ b/src/test/java/com/debatetimer/service/timebased/TimeBasedServiceTest.java @@ -137,11 +137,10 @@ class UpdateTable { class UpdateUsedAt { @Test - void 시간총량제_토론_테이블의_사용_시각을_최신화한다() throws InterruptedException { + void 시간총량제_토론_테이블의_사용_시각을_최신화한다() { Member member = memberGenerator.generate("default@gmail.com"); TimeBasedTable table = timeBasedTableGenerator.generate(member); LocalDateTime beforeUsedAt = table.getUsedAt(); - Thread.sleep(1); timeBasedService.updateUsedAt(table.getId(), member); From 5e5bb3c63a6da8a879db10bd4aac4618ff71b647 Mon Sep 17 00:00:00 2001 From: SANGHUN OH <121424793+unifolio0@users.noreply.github.com> Date: Tue, 11 Mar 2025 10:36:25 +0900 Subject: [PATCH 20/33] =?UTF-8?q?[FEAT]=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95=20=ED=85=8C=EC=9D=B4=EB=B8=94=20Entity=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#118)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/DebateTimeBox.java | 2 +- .../domain/customize/CustomizeBoxType.java | 15 +++ .../domain/customize/CustomizeTable.java | 53 ++++++++ .../domain/customize/CustomizeTimeBox.java | 117 ++++++++++++++++++ .../com/debatetimer/dto/member/TableType.java | 3 +- .../ParliamentaryTableCreateRequest.java | 2 +- .../ParliamentaryTableInfoCreateRequest.java | 2 +- .../db/migration/V5__add_customize_table.sql | 40 ++++++ .../domain/customize/CustomizeTableTest.java | 21 ++++ .../customize/CustomizeTimeBoxTest.java | 86 +++++++++++++ 10 files changed, 337 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/debatetimer/domain/customize/CustomizeBoxType.java create mode 100644 src/main/java/com/debatetimer/domain/customize/CustomizeTable.java create mode 100644 src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java create mode 100644 src/main/resources/db/migration/V5__add_customize_table.sql create mode 100644 src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java create mode 100644 src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java diff --git a/src/main/java/com/debatetimer/domain/DebateTimeBox.java b/src/main/java/com/debatetimer/domain/DebateTimeBox.java index 9f5217b8..0538104a 100644 --- a/src/main/java/com/debatetimer/domain/DebateTimeBox.java +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -24,7 +24,7 @@ public abstract class DebateTimeBox { private int time; private Integer speaker; - public DebateTimeBox(int sequence, Stance stance, int time, Integer speaker) { + protected DebateTimeBox(int sequence, Stance stance, int time, Integer speaker) { validateSequence(sequence); validateTime(time); validateSpeakerNumber(speaker); 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/dto/member/TableType.java b/src/main/java/com/debatetimer/dto/member/TableType.java index 3320e0d1..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; + 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 e23acf58..6dfe90a6 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -11,7 +11,7 @@ public record ParliamentaryTableCreateRequest(ParliamentaryTableInfoCreateReques List table) { public ParliamentaryTable toTable(Member member) { - return info.toTable(member, info.warningBell(), info().finishBell()); + return info.toTable(member); } public TimeBoxes toTimeBoxes(ParliamentaryTable parliamentaryTable) { diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java index 905f6343..764fd62b 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableInfoCreateRequest.java @@ -16,7 +16,7 @@ public record ParliamentaryTableInfoCreateRequest( boolean finishBell ) { - public ParliamentaryTable toTable(Member member, boolean warningBell, boolean finishBell) { + public ParliamentaryTable toTable(Member member) { return new ParliamentaryTable(member, name, agenda, warningBell, finishBell); } } 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/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()); + } + } +} From ead31d66662c44267a5f6d21a2dae62441cebc22 Mon Sep 17 00:00:00 2001 From: SANGHUN OH <121424793+unifolio0@users.noreply.github.com> Date: Wed, 12 Mar 2025 12:04:53 +0900 Subject: [PATCH 21/33] =?UTF-8?q?[REFACTOR]=20agenda=20nullable=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/debatetimer/domain/DebateTable.java | 3 --- .../resources/db/migration/V6__agenda_modify_nullable.sql | 3 +++ src/test/resources/application-flyway.yml | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/db/migration/V6__agenda_modify_nullable.sql diff --git a/src/main/java/com/debatetimer/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java index fd7f1f36..dbbb83f9 100644 --- a/src/main/java/com/debatetimer/domain/DebateTable.java +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -31,11 +31,8 @@ public abstract class DebateTable extends BaseTimeEntity { @NotNull private String name; - @NotNull private String agenda; - private boolean warningBell; - private boolean finishBell; @NotNull 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/test/resources/application-flyway.yml b/src/test/resources/application-flyway.yml index e8fb7f10..bdad3efa 100644 --- a/src/test/resources/application-flyway.yml +++ b/src/test/resources/application-flyway.yml @@ -1,4 +1,9 @@ spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:flyway;MODE=MySQL + username: sa + password: jpa: show-sql: true properties: From ecff34dc22569739f60a5162d5ac8299df97c9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Wed, 19 Mar 2025 04:07:04 +0900 Subject: [PATCH 22/33] =?UTF-8?q?[FEAT]=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95=20=ED=85=8C=EC=9D=B4=EB=B8=94=20CRUD=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../customize/CustomizeController.java | 72 +++ .../com/debatetimer/domain/DebateTimeBox.java | 11 +- .../domain/customize/CustomizeTable.java | 6 + .../domain/customize/CustomizeTimeBox.java | 4 +- .../parliamentary/ParliamentaryTimeBox.java | 9 +- .../domain/timebased/TimeBasedTimeBox.java | 11 +- .../request/CustomizeTableCreateRequest.java | 25 + .../CustomizeTableInfoCreateRequest.java | 28 + .../CustomizeTimeBoxCreateRequest.java | 32 ++ .../response/CustomizeTableInfoResponse.java | 27 + .../response/CustomizeTableResponse.java | 28 + .../response/CustomizeTimeBoxResponse.java | 28 + .../ParliamentaryTimeBoxResponse.java | 2 +- .../response/TimeBasedTimeBoxResponse.java | 2 +- .../customize/CustomizeTableRepository.java | 25 + .../customize/CustomizeTimeBoxRepository.java | 34 ++ .../timebased/TimeBasedTimeBoxRepository.java | 2 +- .../service/customize/CustomizeService.java | 94 ++++ .../migration/V7__speaker_modify_varchar.sql | 3 + .../controller/BaseControllerTest.java | 12 + .../controller/BaseDocumentTest.java | 4 + .../java/com/debatetimer/controller/Tag.java | 4 +- .../customize/CustomizeControllerTest.java | 164 ++++++ .../customize/CustomizeDocumentTest.java | 516 ++++++++++++++++++ .../debatetimer/domain/DebateTimeBoxTest.java | 18 +- .../customize/CustomizeTimeBoxTest.java | 31 +- .../ParliamentaryTimeBoxTest.java | 17 + .../timebased/TimeBasedTimeBoxTest.java | 17 + .../fixture/CustomizeTableGenerator.java | 29 + .../fixture/CustomizeTimeBoxGenerator.java | 31 ++ .../repository/BaseRepositoryTest.java | 21 +- .../CustomizeTableRepositoryTest.java | 58 ++ .../CustomizeTimeBoxRepositoryTest.java | 39 ++ .../debatetimer/service/BaseServiceTest.java | 16 + .../customize/CustomizeServiceTest.java | 220 ++++++++ 35 files changed, 1588 insertions(+), 52 deletions(-) create mode 100644 src/main/java/com/debatetimer/controller/customize/CustomizeController.java create mode 100644 src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java create mode 100644 src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java create mode 100644 src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java create mode 100644 src/main/java/com/debatetimer/dto/customize/response/CustomizeTableInfoResponse.java create mode 100644 src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java create mode 100644 src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java create mode 100644 src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java create mode 100644 src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java create mode 100644 src/main/java/com/debatetimer/service/customize/CustomizeService.java create mode 100644 src/main/resources/db/migration/V7__speaker_modify_varchar.sql create mode 100644 src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java create mode 100644 src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java create mode 100644 src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java create mode 100644 src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java create mode 100644 src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java create mode 100644 src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java create mode 100644 src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java diff --git a/src/main/java/com/debatetimer/controller/customize/CustomizeController.java b/src/main/java/com/debatetimer/controller/customize/CustomizeController.java new file mode 100644 index 00000000..6716d9f1 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/customize/CustomizeController.java @@ -0,0 +1,72 @@ +package com.debatetimer.controller.customize; + +import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.service.customize.CustomizeService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class CustomizeController { + + private final CustomizeService customizeService; + + @PostMapping("/api/table/customize") + @ResponseStatus(HttpStatus.CREATED) + public CustomizeTableResponse save( + @Valid @RequestBody CustomizeTableCreateRequest tableCreateRequest, + @AuthMember Member member + ) { + return customizeService.save(tableCreateRequest, member); + } + + @GetMapping("/api/table/customize/{tableId}") + @ResponseStatus(HttpStatus.OK) + public CustomizeTableResponse getTable( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return customizeService.findTable(tableId, member); + } + + @PutMapping("/api/table/customize/{tableId}") + @ResponseStatus(HttpStatus.OK) + public CustomizeTableResponse updateTable( + @Valid @RequestBody CustomizeTableCreateRequest tableCreateRequest, + @PathVariable Long tableId, + @AuthMember Member member + ) { + return customizeService.updateTable(tableCreateRequest, tableId, member); + } + + @PatchMapping("/api/table/customize/{tableId}/debate") + @ResponseStatus(HttpStatus.OK) + public CustomizeTableResponse debate( + @PathVariable Long tableId, + @AuthMember Member member + ) { + return customizeService.updateUsedAt(tableId, member); + } + + @DeleteMapping("/api/table/customize/{tableId}") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteTable( + @PathVariable Long tableId, + @AuthMember Member member + ) { + customizeService.deleteTable(tableId, member); + } +} diff --git a/src/main/java/com/debatetimer/domain/DebateTimeBox.java b/src/main/java/com/debatetimer/domain/DebateTimeBox.java index 0538104a..168e98f6 100644 --- a/src/main/java/com/debatetimer/domain/DebateTimeBox.java +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -22,12 +22,11 @@ public abstract class DebateTimeBox { private Stance stance; private int time; - private Integer speaker; + private String speaker; - protected DebateTimeBox(int sequence, Stance stance, int time, Integer speaker) { + protected DebateTimeBox(int sequence, Stance stance, int time, String speaker) { validateSequence(sequence); validateTime(time); - validateSpeakerNumber(speaker); this.sequence = sequence; this.stance = stance; @@ -46,10 +45,4 @@ private void validateTime(int time) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); } } - - private void validateSpeakerNumber(Integer speaker) { - if (speaker != null && speaker <= 0) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER); - } - } } diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java index 4b6e43eb..3d36273a 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java @@ -41,6 +41,12 @@ public CustomizeTable( this.consTeamName = consTeamName; } + public void update(CustomizeTable renewTable) { + this.prosTeamName = renewTable.getProsTeamName(); + this.consTeamName = renewTable.getConsTeamName(); + updateTable(renewTable); + } + @Override public long getId() { return id; diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java index 5c3028c4..ff4443d4 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java @@ -50,7 +50,7 @@ public CustomizeTimeBox( String speechType, CustomizeBoxType boxType, int time, - Integer speaker + String speaker ) { super(sequence, stance, time, speaker); validateNotTimeBasedType(boxType); @@ -69,7 +69,7 @@ public CustomizeTimeBox( int time, int timePerTeam, Integer timePerSpeaking, - Integer speaker + String speaker ) { super(sequence, stance, time, speaker); validateTime(timePerTeam, timePerSpeaking); diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java index fa66cff2..e494bc76 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java @@ -44,8 +44,9 @@ public ParliamentaryTimeBox( int time, Integer speaker ) { - super(sequence, stance, time, speaker); + super(sequence, stance, time, String.valueOf(speaker)); validate(stance, type); + validateSpeakerNumber(speaker); this.parliamentaryTable = parliamentaryTable; this.type = type; @@ -56,4 +57,10 @@ private void validate(Stance stance, ParliamentaryBoxType boxType) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); } } + + private void validateSpeakerNumber(Integer speaker) { + if (speaker != null && speaker <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER); + } + } } diff --git a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java index 27184f61..051a7b3f 100644 --- a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java @@ -47,7 +47,7 @@ public TimeBasedTimeBox( int time, Integer speaker ) { - super(sequence, stance, time, speaker); + super(sequence, stance, time, String.valueOf(speaker)); validateStance(stance, type); validateNotTimeBasedType(type); @@ -65,11 +65,12 @@ public TimeBasedTimeBox( int timePerSpeaking, Integer speaker ) { - super(sequence, stance, time, speaker); + super(sequence, stance, time, String.valueOf(speaker)); validateTime(timePerTeam, timePerSpeaking); validateTimeBasedTime(time, timePerTeam); validateStance(stance, type); validateTimeBasedType(type); + validateSpeakerNumber(speaker); this.timeBasedTable = timeBasedTable; this.type = type; @@ -114,4 +115,10 @@ private void validateNotTimeBasedType(TimeBasedBoxType boxType) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); } } + + private void validateSpeakerNumber(Integer speaker) { + if (speaker != null && speaker <= 0) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER); + } + } } diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java new file mode 100644 index 00000000..108c840f --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java @@ -0,0 +1,25 @@ +package com.debatetimer.dto.customize.request; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.member.Member; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public record CustomizeTableCreateRequest( + CustomizeTableInfoCreateRequest info, + List table +) { + + public CustomizeTable toTable(Member member) { + return info.toTable(member); + } + + public TimeBoxes toTimeBoxes(CustomizeTable customizeTable) { + return IntStream.range(0, table.size()) + .mapToObj(i -> table.get(i).toTimeBox(customizeTable, i + 1)) + .collect(Collectors.collectingAndThen(Collectors.toList(), TimeBoxes::new)); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java new file mode 100644 index 00000000..c83a7d5d --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableInfoCreateRequest.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.customize.request; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record CustomizeTableInfoCreateRequest( + @NotBlank + String name, + + @NotNull + String agenda, + + @NotBlank + String prosTeamName, + + @NotBlank + String consTeamName, + + boolean warningBell, + boolean finishBell +) { + + public CustomizeTable toTable(Member member) { + return new CustomizeTable(member, name, agenda, warningBell, finishBell, prosTeamName, consTeamName); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java new file mode 100644 index 00000000..3ce029ae --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java @@ -0,0 +1,32 @@ +package com.debatetimer.dto.customize.request; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import jakarta.validation.constraints.NotBlank; + +public record CustomizeTimeBoxCreateRequest( + @NotBlank + Stance stance, + + @NotBlank + String speechType, + + @NotBlank + CustomizeBoxType boxType, + + int time, + Integer timePerTeam, + Integer timePerSpeaking, + String speaker +) { + + public CustomizeTimeBox toTimeBox(CustomizeTable customizeTable, int sequence) { + if (boxType.isTimeBased()) { + return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, time, timePerTeam, + timePerSpeaking, speaker); + } + return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, time, speaker); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableInfoResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableInfoResponse.java new file mode 100644 index 00000000..380cd7de --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableInfoResponse.java @@ -0,0 +1,27 @@ +package com.debatetimer.dto.customize.response; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.dto.member.TableType; + +public record CustomizeTableInfoResponse( + String name, + TableType type, + String agenda, + String prosTeamName, + String consTeamName, + boolean warningBell, + boolean finishBell +) { + + public CustomizeTableInfoResponse(CustomizeTable customizeTable) { + this( + customizeTable.getName(), + TableType.CUSTOMIZE, + customizeTable.getAgenda(), + customizeTable.getProsTeamName(), + customizeTable.getConsTeamName(), + customizeTable.isWarningBell(), + customizeTable.isFinishBell() + ); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java new file mode 100644 index 00000000..bbd1bf95 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTableResponse.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.customize.response; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import java.util.List; + +public record CustomizeTableResponse(long id, CustomizeTableInfoResponse info, List table) { + + public CustomizeTableResponse( + CustomizeTable customizeTable, + TimeBoxes timeBasedTimeBoxes + ) { + this( + customizeTable.getId(), + new CustomizeTableInfoResponse(customizeTable), + toTimeBoxResponses(timeBasedTimeBoxes) + ); + } + + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List customizeTimeBoxes = timeBoxes.getTimeBoxes(); + return customizeTimeBoxes + .stream() + .map(CustomizeTimeBoxResponse::new) + .toList(); + } +} diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java new file mode 100644 index 00000000..c8b53e17 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.customize.response; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTimeBox; + +public record CustomizeTimeBoxResponse( + Stance stance, + String speechType, + CustomizeBoxType boxType, + Integer time, + Integer timePerTeam, + Integer timePerSpeaking, + String speaker +) { + + public CustomizeTimeBoxResponse(CustomizeTimeBox customizeTimeBox) { + this( + customizeTimeBox.getStance(), + customizeTimeBox.getSpeechType(), + customizeTimeBox.getBoxType(), + customizeTimeBox.getTime(), + customizeTimeBox.getTimePerTeam(), + customizeTimeBox.getTimePerSpeaking(), + String.valueOf(customizeTimeBox.getSpeaker()) + ); + } +} diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java index 70ae83c6..ab674c9c 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java @@ -10,7 +10,7 @@ public ParliamentaryTimeBoxResponse(ParliamentaryTimeBox parliamentaryTimeBox) { this(parliamentaryTimeBox.getStance(), parliamentaryTimeBox.getType(), parliamentaryTimeBox.getTime(), - parliamentaryTimeBox.getSpeaker() + Integer.parseInt(parliamentaryTimeBox.getSpeaker()) ); } } diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java index 1a094aa2..2fab64f1 100644 --- a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java @@ -19,7 +19,7 @@ public TimeBasedTimeBoxResponse(TimeBasedTimeBox timeBasedTimeBox) { timeBasedTimeBox.getTime(), timeBasedTimeBox.getTimePerTeam(), timeBasedTimeBox.getTimePerSpeaking(), - timeBasedTimeBox.getSpeaker() + Integer.parseInt(timeBasedTimeBox.getSpeaker()) ); } } diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java new file mode 100644 index 00000000..e461e58c --- /dev/null +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java @@ -0,0 +1,25 @@ +package com.debatetimer.repository.customize; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.List; +import java.util.Optional; +import org.springframework.data.repository.Repository; + +public interface CustomizeTableRepository extends Repository { + + CustomizeTable save(CustomizeTable customizeTable); + + Optional findById(long id); + + default CustomizeTable getById(long tableId) { + return findById(tableId) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); + } + + List findAllByMember(Member member); + + void delete(CustomizeTable table); +} diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java new file mode 100644 index 00000000..85657e38 --- /dev/null +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java @@ -0,0 +1,34 @@ +package com.debatetimer.repository.customize; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import java.util.List; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.transaction.annotation.Transactional; + +public interface CustomizeTimeBoxRepository extends Repository { + + CustomizeTimeBox save(CustomizeTimeBox timeBox); + + @Transactional + default List saveAll(List timeBoxes) { + return timeBoxes.stream() + .map(this::save) + .toList(); + } + + List findAllByCustomizeTable(CustomizeTable table); + + default TimeBoxes findTableTimeBoxes(CustomizeTable table) { + List timeBoxes = findAllByCustomizeTable(table); + return new TimeBoxes<>(timeBoxes); + } + + @Query("DELETE FROM CustomizeTimeBox ctb WHERE ctb IN :timeBoxes") + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Transactional + void deleteAll(List timeBoxes); +} diff --git a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java index a34e4c07..df4bdbf6 100644 --- a/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/timebased/TimeBasedTimeBoxRepository.java @@ -27,7 +27,7 @@ default TimeBoxes findTableTimeBoxes(TimeBasedTable table) { return new TimeBoxes<>(timeBoxes); } - @Query("DELETE FROM TimeBasedTimeBox ptb WHERE ptb IN :timeBoxes") + @Query("DELETE FROM TimeBasedTimeBox tbtb WHERE tbtb IN :timeBoxes") @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional void deleteAll(List timeBoxes); diff --git a/src/main/java/com/debatetimer/service/customize/CustomizeService.java b/src/main/java/com/debatetimer/service/customize/CustomizeService.java new file mode 100644 index 00000000..6bd3ec6b --- /dev/null +++ b/src/main/java/com/debatetimer/service/customize/CustomizeService.java @@ -0,0 +1,94 @@ +package com.debatetimer.service.customize; + +import com.debatetimer.domain.TimeBoxes; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.customize.CustomizeTableRepository; +import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CustomizeService { + + private final CustomizeTableRepository tableRepository; + private final CustomizeTimeBoxRepository timeBoxRepository; + + @Transactional + public CustomizeTableResponse save(CustomizeTableCreateRequest tableCreateRequest, Member member) { + CustomizeTable table = tableCreateRequest.toTable(member); + CustomizeTable savedTable = tableRepository.save(table); + + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, savedTable); + return new CustomizeTableResponse(savedTable, savedTimeBoxes); + } + + @Transactional(readOnly = true) + public CustomizeTableResponse findTable(long tableId, Member member) { + CustomizeTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + return new CustomizeTableResponse(table, timeBoxes); + } + + @Transactional + public CustomizeTableResponse updateTable( + CustomizeTableCreateRequest tableCreateRequest, + long tableId, + Member member + ) { + CustomizeTable existingTable = getOwnerTable(tableId, member.getId()); + CustomizeTable renewedTable = tableCreateRequest.toTable(member); + existingTable.update(renewedTable); + + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(existingTable); + timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); + TimeBoxes savedTimeBoxes = saveTimeBoxes(tableCreateRequest, existingTable); + return new CustomizeTableResponse(existingTable, savedTimeBoxes); + } + + @Transactional + public CustomizeTableResponse updateUsedAt(long tableId, Member member) { + CustomizeTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + table.updateUsedAt(); + + return new CustomizeTableResponse(table, timeBoxes); + } + + @Transactional + public void deleteTable(long tableId, Member member) { + CustomizeTable table = getOwnerTable(tableId, member.getId()); + TimeBoxes timeBoxes = timeBoxRepository.findTableTimeBoxes(table); + timeBoxRepository.deleteAll(timeBoxes.getTimeBoxes()); + tableRepository.delete(table); + } + + private TimeBoxes saveTimeBoxes( + CustomizeTableCreateRequest tableCreateRequest, + CustomizeTable table + ) { + TimeBoxes timeBoxes = tableCreateRequest.toTimeBoxes(table); + List savedTimeBoxes = timeBoxRepository.saveAll(timeBoxes.getTimeBoxes()); + return new TimeBoxes<>(savedTimeBoxes); + } + + private CustomizeTable getOwnerTable(long tableId, long memberId) { + CustomizeTable foundTable = tableRepository.getById(tableId); + validateOwn(foundTable, memberId); + return foundTable; + } + + private void validateOwn(CustomizeTable table, long memberId) { + if (!table.isOwner(memberId)) { + throw new DTClientErrorException(ClientErrorCode.NOT_TABLE_OWNER); + } + } +} diff --git a/src/main/resources/db/migration/V7__speaker_modify_varchar.sql b/src/main/resources/db/migration/V7__speaker_modify_varchar.sql new file mode 100644 index 00000000..54a7d7a2 --- /dev/null +++ b/src/main/resources/db/migration/V7__speaker_modify_varchar.sql @@ -0,0 +1,3 @@ +alter table parliamentary_time_box modify speaker varchar (255) null; +alter table time_based_time_box modify speaker varchar (255) null; +alter table customize_time_box modify speaker varchar (255) null; diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 0bc58e12..265ace71 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -2,6 +2,8 @@ import com.debatetimer.DataBaseCleaner; import com.debatetimer.client.OAuthClient; +import com.debatetimer.fixture.CustomizeTableGenerator; +import com.debatetimer.fixture.CustomizeTimeBoxGenerator; import com.debatetimer.fixture.HeaderGenerator; import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.ParliamentaryTableGenerator; @@ -9,6 +11,7 @@ import com.debatetimer.fixture.TimeBasedTableGenerator; import com.debatetimer.fixture.TimeBasedTimeBoxGenerator; import com.debatetimer.fixture.TokenGenerator; +import com.debatetimer.repository.customize.CustomizeTableRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; import com.debatetimer.repository.timebased.TimeBasedTableRepository; import io.restassured.RestAssured; @@ -33,6 +36,9 @@ public abstract class BaseControllerTest { @Autowired protected TimeBasedTableRepository timeBasedTableRepository; + @Autowired + protected CustomizeTableRepository customizeTableRepository; + @Autowired protected MemberGenerator memberGenerator; @@ -48,6 +54,12 @@ public abstract class BaseControllerTest { @Autowired protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; + @Autowired + protected CustomizeTableGenerator customizeTableGenerator; + + @Autowired + protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; + @Autowired protected HeaderGenerator headerGenerator; diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index bb92a2ed..4e70556f 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -10,6 +10,7 @@ import com.debatetimer.dto.member.JwtTokenResponse; import com.debatetimer.exception.errorcode.ClientErrorCode; import com.debatetimer.service.auth.AuthService; +import com.debatetimer.service.customize.CustomizeService; import com.debatetimer.service.member.MemberService; import com.debatetimer.service.parliamentary.ParliamentaryService; import com.debatetimer.service.timebased.TimeBasedService; @@ -63,6 +64,9 @@ public abstract class BaseDocumentTest { @MockitoBean protected TimeBasedService timeBasedService; + @MockitoBean + protected CustomizeService customizeService; + @MockitoBean protected AuthService authService; diff --git a/src/test/java/com/debatetimer/controller/Tag.java b/src/test/java/com/debatetimer/controller/Tag.java index d65c7cb2..c7dfa897 100644 --- a/src/test/java/com/debatetimer/controller/Tag.java +++ b/src/test/java/com/debatetimer/controller/Tag.java @@ -4,7 +4,9 @@ public enum Tag { MEMBER_API("Member API"), PARLIAMENTARY_API("Parliamentary Table API"), - TIME_BASED_API("Time Based Table API"); + TIME_BASED_API("Time Based Table API"), + CUSTOMIZE_API("Customize Table API"), + ; private final String displayName; diff --git a/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java b/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java new file mode 100644 index 00000000..d8247a78 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java @@ -0,0 +1,164 @@ +package com.debatetimer.controller.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.controller.BaseControllerTest; +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; +import com.debatetimer.dto.customize.request.CustomizeTableInfoCreateRequest; +import com.debatetimer.dto.customize.request.CustomizeTimeBoxCreateRequest; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import io.restassured.http.ContentType; +import io.restassured.http.Headers; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CustomizeControllerTest extends BaseControllerTest { + + @Nested + class Save { + + @Test + void 사용자_지정_테이블을_생성한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자2") + ) + ); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .headers(headers) + .body(request) + .when().post("/api/table/customize") + .then().statusCode(201) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.info().name()).isEqualTo(request.info().name()), + () -> assertThat(response.table()).hasSize(request.table().size()) + ); + } + } + + @Nested + class GetTable { + + @Test + void 사용자_지정_테이블을_조회한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().get("/api/table/customize/{tableId}") + .then().statusCode(200) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), + () -> assertThat(response.table()).hasSize(2) + ); + } + } + + @Nested + class UpdateTable { + + @Test + void 사용자_지정_토론_테이블을_업데이트한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTableCreateRequest renewTableRequest = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자2") + ) + ); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .body(renewTableRequest) + .when().put("/api/table/customize/{tableId}") + .then().statusCode(200) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), + () -> assertThat(response.info().name()).isEqualTo(renewTableRequest.info().name()), + () -> assertThat(response.table()).hasSize(renewTableRequest.table().size()) + ); + } + } + + @Nested + class Debate { + + @Test + void 사용자_지정_토론을_시작한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().patch("/api/table/customize/{tableId}/debate") + .then().statusCode(200) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), + () -> assertThat(response.info().name()).isEqualTo(bitoTable.getName()), + () -> assertThat(response.table()).hasSize(2) + ); + } + } + + @Nested + class DeleteTable { + + @Test + void 사용자_지정_토론_테이블을_삭제한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .headers(headers) + .when().delete("/api/table/customize/{tableId}") + .then().statusCode(204); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java new file mode 100644 index 00000000..1f3ca5b9 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java @@ -0,0 +1,516 @@ +package com.debatetimer.controller.customize; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.OBJECT; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import com.debatetimer.controller.BaseDocumentTest; +import com.debatetimer.controller.RestDocumentationRequest; +import com.debatetimer.controller.RestDocumentationResponse; +import com.debatetimer.controller.Tag; +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; +import com.debatetimer.dto.customize.request.CustomizeTableInfoCreateRequest; +import com.debatetimer.dto.customize.request.CustomizeTimeBoxCreateRequest; +import com.debatetimer.dto.customize.response.CustomizeTableInfoResponse; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.dto.customize.response.CustomizeTimeBoxResponse; +import com.debatetimer.dto.member.TableType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import io.restassured.http.ContentType; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.http.HttpHeaders; + +public class CustomizeDocumentTest extends BaseDocumentTest { + + @Nested + class Save { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.CUSTOMIZE_API) + .summary("새로운 사용자 지정 토론 시간표 생성") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .requestBodyField( + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"), + fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), + fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), + fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"), + fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), + fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), + fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() + ); + + @Test + void 사용자_지정_테이블_생성_성공() { + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).save(eq(request), any()); + + var document = document("customize/post", 201) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .body(request) + .when().post("/api/table/customize") + .then().statusCode(201); + } + + @EnumSource( + value = ClientErrorCode.class, + names = { + "INVALID_TABLE_NAME_LENGTH", + "INVALID_TABLE_NAME_FORM", + "INVALID_TABLE_TIME", + "INVALID_TIME_BOX_SEQUENCE", + "INVALID_TIME_BOX_TIME", + "INVALID_TIME_BOX_STANCE", + "INVALID_TIME_BOX_FORMAT" + } + ) + @ParameterizedTest + void 사용자_지정_테이블_생성_실패(ClientErrorCode errorCode) { + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doThrow(new DTClientErrorException(errorCode)).when(customizeService).save(eq(request), any()); + + var document = document("customize/post", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .body(request) + .when().post("/api/table/customize") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class GetTable { + + private final RestDocumentationRequest requestDocument = request() + .summary("사용자_지정 토론 시간표 조회") + .tag(Tag.CUSTOMIZE_API) + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"), + fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), + fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), + fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() + ); + + @Test + void 사용자_지정_테이블_조회_성공() { + long tableId = 5L; + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).findTable(eq(tableId), any()); + + var document = document("customize/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().get("/api/table/customize/{tableId}") + .then().statusCode(200); + } + + @ParameterizedTest + @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) + void 사용자_지정_테이블_조회_실패(ClientErrorCode errorCode) { + long tableId = 5L; + doThrow(new DTClientErrorException(errorCode)).when(customizeService).findTable(eq(tableId), any()); + + var document = document("customize/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().get("/api/table/customize/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class UpdateTable { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.CUSTOMIZE_API) + .summary("사용자 지정 토론 시간표 수정") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ) + .requestBodyField( + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"), + fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), + fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), + fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"), + fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), + fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), + fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() + ); + + @Test + void 사용자_지정_토론_테이블_수정() { + long tableId = 5L; + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).updateTable(eq(request), eq(tableId), any()); + + var document = document("customize/patch", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .body(request) + .when().put("/api/table/customize/{tableId}") + .then().statusCode(200); + } + + @EnumSource( + value = ClientErrorCode.class, + names = { + "INVALID_TABLE_NAME_LENGTH", + "INVALID_TABLE_NAME_FORM", + "INVALID_TABLE_TIME", + "INVALID_TIME_BOX_SEQUENCE", + "INVALID_TIME_BOX_TIME", + "INVALID_TIME_BOX_STANCE", + "INVALID_TIME_BOX_FORMAT" + } + ) + @ParameterizedTest + void 사용자_지정_테이블_생성_실패(ClientErrorCode errorCode) { + long tableId = 5L; + CustomizeTableCreateRequest request = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doThrow(new DTClientErrorException(errorCode)).when(customizeService) + .updateTable(eq(request), eq(tableId), any()); + + var document = document("customize/patch", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .body(request) + .when().put("/api/table/customize/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class Debate { + + private final RestDocumentationRequest requestDocument = request() + .summary("사용자 지정 토론 시작") + .tag(Tag.CUSTOMIZE_API) + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)"), + fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), + fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), + fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() + ); + + + @Test + void 사용자_지정_토론_진행_성공() { + long tableId = 5L; + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론1", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxResponse(Stance.PROS, "입론2", CustomizeBoxType.TIME_BASED, + 120, 60, null, "발언자2") + ) + ); + doReturn(response).when(customizeService).updateUsedAt(eq(tableId), any()); + + var document = document("customize/patch_debate", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/customize/{tableId}/debate") + .then().statusCode(200); + } + + @ParameterizedTest + @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) + void 사용자_지정_토론_진행_실패(ClientErrorCode errorCode) { + long tableId = 5L; + doThrow(new DTClientErrorException(errorCode)).when(customizeService).updateUsedAt(eq(tableId), any()); + + var document = document("customize/get", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().patch("/api/table/customize/{tableId}/debate") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class DeleteTable { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.CUSTOMIZE_API) + .summary("사용자 지정 토론 시간표 삭제") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + @Test + void 사용자_지정_테이블_삭제_성공() { + long tableId = 5L; + doNothing().when(customizeService).deleteTable(eq(tableId), any()); + + var document = document("customize/delete", 204) + .request(requestDocument) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().delete("/api/table/customize/{tableId}") + .then().statusCode(204); + } + + @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND", "NOT_TABLE_OWNER"}) + @ParameterizedTest + void 사용자_지정_테이블_삭제_실패(ClientErrorCode errorCode) { + long tableId = 5L; + doThrow(new DTClientErrorException(errorCode)).when(customizeService).deleteTable(eq(tableId), any()); + + var document = document("customize/delete", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", tableId) + .when().delete("/api/table/customize/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java index 52b8dba0..b87b3e06 100644 --- a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java @@ -18,7 +18,7 @@ class Validate { @ValueSource(ints = {0, -1}) @ParameterizedTest void 순서는_양수만_가능하다(int sequence) { - assertThatThrownBy(() -> new DebateTimeBoxTestObject(sequence, Stance.CONS, 60, 1)) + assertThatThrownBy(() -> new DebateTimeBoxTestObject(sequence, Stance.CONS, 60, "발언자")) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); } @@ -27,31 +27,23 @@ class Validate { @ParameterizedTest void 시간은_양수만_가능하다(int time) { assertThatThrownBy( - () -> new DebateTimeBoxTestObject(1, Stance.CONS, time, 1)) + () -> new DebateTimeBoxTestObject(1, Stance.CONS, time, "발언자")) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); } @Test - void 발표자_번호는_빈_값이_허용된다() { - Integer speaker = null; + void 발언자는_빈_값이_허용된다() { + String 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) { + public DebateTimeBoxTestObject(int sequence, Stance stance, int time, String speaker) { super(sequence, stance, time, speaker); } } diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java index c9a65495..21fbcdeb 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java @@ -20,8 +20,8 @@ class ValidateCustomize { CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 150, 120, 60, 1)) - .isInstanceOf(DTClientErrorException.class) + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 150, 120, 60, + "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE.getMessage()); } @@ -30,9 +30,8 @@ class ValidateCustomize { CustomizeTable table = new CustomizeTable(); CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - assertThatCode( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 240, 120, 60, 1)) - .doesNotThrowAnyException(); + assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 240, 120, 60, + "발언자")).doesNotThrowAnyException(); } @Test @@ -40,8 +39,8 @@ class ValidateCustomize { CustomizeTable table = new CustomizeTable(); CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 10, 1)) - .isInstanceOf(DTClientErrorException.class) + assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 10, + "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } @@ -51,8 +50,8 @@ class ValidateCustomize { CustomizeBoxType notTimeBasedBoxType = CustomizeBoxType.NORMAL; assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 240, 120, 60, 1)) - .isInstanceOf(DTClientErrorException.class) + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 240, 120, 60, + "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } @@ -62,11 +61,8 @@ class ValidateCustomize { int timePerTeam = 60; int timePerSpeaking = 59; - assertThatCode( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam * 2, - timePerTeam, timePerSpeaking, 1)) - .doesNotThrowAnyException(); + assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam * 2, timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); } @Test @@ -75,11 +71,8 @@ class ValidateCustomize { int timePerTeam = 60; int timePerSpeaking = 61; - assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam * 2, - timePerTeam, timePerSpeaking, 1)) - .isInstanceOf(DTClientErrorException.class) + assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam * 2, timePerTeam, timePerSpeaking, "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); } } diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java index 59859496..827e414b 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java @@ -8,6 +8,8 @@ import com.debatetimer.exception.errorcode.ClientErrorCode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class ParliamentaryTimeBoxTest { @@ -30,4 +32,19 @@ class ValidateStance { .hasMessage(ClientErrorCode.INVALID_TIME_BOX_STANCE.getMessage()); } } + + @Nested + class ValidateSpeakerNumber { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 의회식_타임박스의_발표자_번호는_양수만_가능하다(int speaker) { + ParliamentaryTable table = new ParliamentaryTable(); + + assertThatThrownBy( + () -> new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); + } + } } diff --git a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java index 48f1336d..ca7842ee 100644 --- a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java @@ -8,6 +8,8 @@ import com.debatetimer.exception.errorcode.ClientErrorCode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class TimeBasedTimeBoxTest { @@ -102,4 +104,19 @@ class ValidateTimeBased { .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); } } + + @Nested + class ValidateSpeakerNumber { + + @ValueSource(ints = {0, -1}) + @ParameterizedTest + void 시간총량제_타임박스의_발표자_번호는_양수만_가능하다(int speaker) { + TimeBasedTable table = new TimeBasedTable(); + + assertThatThrownBy(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, + 240, 120, 60, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); + } + } } diff --git a/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java b/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java new file mode 100644 index 00000000..f7420bf5 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/CustomizeTableGenerator.java @@ -0,0 +1,29 @@ +package com.debatetimer.fixture; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.repository.customize.CustomizeTableRepository; +import org.springframework.stereotype.Component; + +@Component +public class CustomizeTableGenerator { + + private final CustomizeTableRepository customizeTableRepository; + + public CustomizeTableGenerator(CustomizeTableRepository customizeTableRepository) { + this.customizeTableRepository = customizeTableRepository; + } + + public CustomizeTable generate(Member member) { + CustomizeTable table = new CustomizeTable( + member, + "토론 테이블", + "주제", + false, + false, + "찬성", + "반대" + ); + return customizeTableRepository.save(table); + } +} diff --git a/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java new file mode 100644 index 00000000..e23be1ce --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java @@ -0,0 +1,31 @@ +package com.debatetimer.fixture; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; +import org.springframework.stereotype.Component; + +@Component +public class CustomizeTimeBoxGenerator { + + private final CustomizeTimeBoxRepository customizeTimeBoxRepository; + + public CustomizeTimeBoxGenerator(CustomizeTimeBoxRepository customizeTimeBoxRepository) { + this.customizeTimeBoxRepository = customizeTimeBoxRepository; + } + + public CustomizeTimeBox generate(CustomizeTable testTable, CustomizeBoxType boxType, int sequence) { + CustomizeTimeBox timeBox = new CustomizeTimeBox( + testTable, + sequence, + Stance.PROS, + "입론", + boxType, + 180, + "콜리" + ); + return customizeTimeBoxRepository.save(timeBox); + } +} diff --git a/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java b/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java index 91c0e36c..ea98a19a 100644 --- a/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/BaseRepositoryTest.java @@ -1,5 +1,8 @@ package com.debatetimer.repository; +import com.debatetimer.config.JpaAuditingConfig; +import com.debatetimer.fixture.CustomizeTableGenerator; +import com.debatetimer.fixture.CustomizeTimeBoxGenerator; import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.ParliamentaryTableGenerator; import com.debatetimer.fixture.ParliamentaryTimeBoxGenerator; @@ -9,8 +12,16 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -@Import({MemberGenerator.class, ParliamentaryTableGenerator.class, ParliamentaryTimeBoxGenerator.class, - TimeBasedTableGenerator.class, TimeBasedTimeBoxGenerator.class}) +@Import({ + JpaAuditingConfig.class, + MemberGenerator.class, + ParliamentaryTableGenerator.class, + ParliamentaryTimeBoxGenerator.class, + TimeBasedTableGenerator.class, + TimeBasedTimeBoxGenerator.class, + CustomizeTableGenerator.class, + CustomizeTimeBoxGenerator.class +}) @DataJpaTest public abstract class BaseRepositoryTest { @@ -28,4 +39,10 @@ public abstract class BaseRepositoryTest { @Autowired protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; + + @Autowired + protected CustomizeTableGenerator customizeTableGenerator; + + @Autowired + protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; } diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java new file mode 100644 index 00000000..049387ff --- /dev/null +++ b/src/test/java/com/debatetimer/repository/customize/CustomizeTableRepositoryTest.java @@ -0,0 +1,58 @@ +package com.debatetimer.repository.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.member.Member; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.BaseRepositoryTest; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CustomizeTableRepositoryTest extends BaseRepositoryTest { + + @Autowired + private CustomizeTableRepository tableRepository; + + @Nested + class FindAllByMember { + + @Test + void 특정_회원의_테이블만_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member bito = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable1 = customizeTableGenerator.generate(chan); + CustomizeTable chanTable2 = customizeTableGenerator.generate(chan); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + + List foundKeoChanTables = tableRepository.findAllByMember(chan); + + assertThat(foundKeoChanTables).containsExactly(chanTable1, chanTable2); + } + } + + @Nested + class GetById { + + @Test + void 특정_아이디의_테이블을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + + CustomizeTable foundChanTable = tableRepository.getById(chanTable.getId()); + + assertThat(foundChanTable).usingRecursiveComparison().isEqualTo(chanTable); + } + + @Test + void 특정_아이디의_테이블이_없으면_에러를_발생시킨다() { + assertThatThrownBy(() -> tableRepository.getById(1L)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.TABLE_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java new file mode 100644 index 00000000..7afe406d --- /dev/null +++ b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java @@ -0,0 +1,39 @@ +package com.debatetimer.repository.customize; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.member.Member; +import com.debatetimer.repository.BaseRepositoryTest; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CustomizeTimeBoxRepositoryTest extends BaseRepositoryTest { + + @Autowired + private CustomizeTimeBoxRepository customizeTimeBoxRepository; + + @Nested + class FindAllByCustomizeTable { + + @Test + void 특정_테이블의_타임박스를_모두_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member bito = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + CustomizeTable bitoTable = customizeTableGenerator.generate(bito); + CustomizeTimeBox chanBox1 = customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); + CustomizeTimeBox chanBox2 = customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + CustomizeTimeBox bitoBox1 = customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + CustomizeTimeBox bitoBox2 = customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + + List foundBoxes = customizeTimeBoxRepository.findAllByCustomizeTable(chanTable); + + assertThat(foundBoxes).containsExactly(chanBox1, chanBox2); + } + } +} diff --git a/src/test/java/com/debatetimer/service/BaseServiceTest.java b/src/test/java/com/debatetimer/service/BaseServiceTest.java index 4323ae6f..69cb79bc 100644 --- a/src/test/java/com/debatetimer/service/BaseServiceTest.java +++ b/src/test/java/com/debatetimer/service/BaseServiceTest.java @@ -1,11 +1,15 @@ package com.debatetimer.service; import com.debatetimer.DataBaseCleaner; +import com.debatetimer.fixture.CustomizeTableGenerator; +import com.debatetimer.fixture.CustomizeTimeBoxGenerator; import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.ParliamentaryTableGenerator; import com.debatetimer.fixture.ParliamentaryTimeBoxGenerator; import com.debatetimer.fixture.TimeBasedTableGenerator; import com.debatetimer.fixture.TimeBasedTimeBoxGenerator; +import com.debatetimer.repository.customize.CustomizeTableRepository; +import com.debatetimer.repository.customize.CustomizeTimeBoxRepository; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTimeBoxRepository; @@ -34,6 +38,12 @@ public abstract class BaseServiceTest { @Autowired protected TimeBasedTimeBoxRepository timeBasedTimeBoxRepository; + @Autowired + protected CustomizeTableRepository customizeTableRepository; + + @Autowired + protected CustomizeTimeBoxRepository customizeTimeBoxRepository; + @Autowired protected MemberGenerator memberGenerator; @@ -48,4 +58,10 @@ public abstract class BaseServiceTest { @Autowired protected TimeBasedTimeBoxGenerator timeBasedTimeBoxGenerator; + + @Autowired + protected CustomizeTableGenerator customizeTableGenerator; + + @Autowired + protected CustomizeTimeBoxGenerator customizeTimeBoxGenerator; } diff --git a/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java new file mode 100644 index 00000000..3e38b993 --- /dev/null +++ b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java @@ -0,0 +1,220 @@ +package com.debatetimer.service.customize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.debatetimer.domain.Stance; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.request.CustomizeTableCreateRequest; +import com.debatetimer.dto.customize.request.CustomizeTableInfoCreateRequest; +import com.debatetimer.dto.customize.request.CustomizeTimeBoxCreateRequest; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.service.BaseServiceTest; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CustomizeServiceTest extends BaseServiceTest { + + @Autowired + private CustomizeService customizeService; + + @Nested + class Save { + + @Test + void 사용자_지정_토론_테이블을_생성한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableCreateRequest customizeTableCreateRequest = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자2") + ) + ); + + CustomizeTableResponse savedTableResponse = customizeService.save(customizeTableCreateRequest, chan); + Optional foundTable = customizeTableRepository.findById(savedTableResponse.id()); + List foundTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + foundTable.get()); + + assertAll( + () -> assertThat(foundTable.get().getName()).isEqualTo(customizeTableCreateRequest.info().name()), + () -> assertThat(foundTimeBoxes).hasSize(customizeTableCreateRequest.table().size()) + ); + } + } + + @Nested + class FindTable { + + @Test + void 사용자_지정_토론_테이블을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + + CustomizeTableResponse foundResponse = customizeService.findTable(chanTable.getId(), chan); + + assertAll( + () -> assertThat(foundResponse.id()).isEqualTo(chanTable.getId()), + () -> assertThat(foundResponse.table()).hasSize(2) + ); + } + + @Test + void 회원_소유가_아닌_테이블_조회_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> customizeService.findTable(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + + @Nested + class UpdateTable { + + @Test + void 사용자_지정_토론_테이블을_수정한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + CustomizeTableCreateRequest renewTableRequest = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자2") + ) + ); + + customizeService.updateTable(renewTableRequest, chanTable.getId(), chan); + + Optional updatedTable = customizeTableRepository.findById(chanTable.getId()); + List updatedTimeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + updatedTable.get()); + + assertAll( + () -> assertThat(updatedTable.get().getId()).isEqualTo(chanTable.getId()), + () -> assertThat(updatedTable.get().getName()).isEqualTo(renewTableRequest.info().name()), + () -> assertThat(updatedTimeBoxes).hasSize(renewTableRequest.table().size()) + ); + } + + @Test + void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + CustomizeTableCreateRequest renewTableRequest = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자2") + ) + ); + + assertThatThrownBy(() -> customizeService.updateTable(renewTableRequest, chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + + @Nested + class UpdateUsedAt { + + @Test + void 사용자_지정_토론_테이블의_사용_시각을_최신화한다() { + Member member = memberGenerator.generate("default@gmail.com"); + CustomizeTable table = customizeTableGenerator.generate(member); + LocalDateTime beforeUsedAt = table.getUsedAt(); + + customizeService.updateUsedAt(table.getId(), member); + + Optional updatedTable = customizeTableRepository.findById(table.getId()); + assertAll( + () -> assertThat(updatedTable.get().getId()).isEqualTo(table.getId()), + () -> assertThat(updatedTable.get().getUsedAt()).isAfter(beforeUsedAt) + ); + } + + @Test + void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + CustomizeTableCreateRequest renewTableRequest = new CustomizeTableCreateRequest( + new CustomizeTableInfoCreateRequest("자유 테이블", "주제", "찬성", + "반대", true, true), + List.of( + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자1"), + new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, + 120, 60, null, "발언자2") + ) + ); + + assertThatThrownBy(() -> customizeService.updateTable(renewTableRequest, chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } + + @Nested + class DeleteTable { + + @Test + void 사용자_지정_토론_테이블을_삭제한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + + customizeService.deleteTable(chanTable.getId(), chan); + + Optional foundTable = customizeTableRepository.findById(chanTable.getId()); + List timeBoxes = customizeTimeBoxRepository.findAllByCustomizeTable( + chanTable); + + assertAll( + () -> assertThat(foundTable).isEmpty(), + () -> assertThat(timeBoxes).isEmpty() + ); + } + + @Test + void 회원_소유가_아닌_테이블_삭제_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTable chanTable = customizeTableGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> customizeService.deleteTable(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.NOT_TABLE_OWNER.getMessage()); + } + } +} From e17482c377b47a1da4a85c0a4874460a784c6637 Mon Sep 17 00:00:00 2001 From: SANGHUN OH <121424793+unifolio0@users.noreply.github.com> Date: Thu, 27 Mar 2025 16:40:00 +0900 Subject: [PATCH 23/33] =?UTF-8?q?[REFACTOR]=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EB=AA=85,=20=ED=8C=80=20=EC=9D=B4=EB=A6=84=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=88=98=EC=A0=95=20(#129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/DebateTable.java | 2 +- .../domain/customize/CustomizeTable.java | 16 +++++ .../exception/errorcode/ClientErrorCode.java | 15 ++++- .../debatetimer/domain/DebateTableTest.java | 22 +++---- .../domain/customize/CustomizeTableTest.java | 65 +++++++++++++++++++ 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/debatetimer/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java index dbbb83f9..79f2de74 100644 --- a/src/main/java/com/debatetimer/domain/DebateTable.java +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -20,7 +20,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class DebateTable extends BaseTimeEntity { - private static final String NAME_REGEX = "^[a-zA-Z가-힣0-9 ]+$"; + private static final String NAME_REGEX = "^[\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\s]+$"; public static final int NAME_MAX_LENGTH = 20; @NotNull diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java index 3d36273a..90a868a0 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java @@ -3,6 +3,8 @@ import com.debatetimer.domain.DebateTable; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.TableType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -17,6 +19,9 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CustomizeTable extends DebateTable { + private static final String TEAM_NAME_REGEX = "^[\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\s]+$"; + public static final int TEAM_NAME_MAX_LENGTH = 8; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -37,6 +42,8 @@ public CustomizeTable( String consTeamName ) { super(member, name, agenda, warningBell, finishBell); + validateTeamName(prosTeamName); + validateTeamName(consTeamName); this.prosTeamName = prosTeamName; this.consTeamName = consTeamName; } @@ -47,6 +54,15 @@ public void update(CustomizeTable renewTable) { updateTable(renewTable); } + private void validateTeamName(String teamName) { + if (teamName.isBlank() || teamName.length() > TEAM_NAME_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TEAM_NAME_LENGTH); + } + if (!teamName.matches(TEAM_NAME_REGEX)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TEAM_NAME_FORM); + } + } + @Override public long getId() { return id; diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 7134e1cd..73258da9 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -1,6 +1,7 @@ package com.debatetimer.exception.errorcode; -import com.debatetimer.domain.parliamentary.ParliamentaryTable; +import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.customize.CustomizeTable; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -9,11 +10,11 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_TABLE_NAME_LENGTH( HttpStatus.BAD_REQUEST, - "테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(ParliamentaryTable.NAME_MAX_LENGTH) + "테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(DebateTable.NAME_MAX_LENGTH) ), INVALID_TABLE_NAME_FORM( HttpStatus.BAD_REQUEST, - "테이블 이름은 영문/한글/숫자/띄어쓰기만 가능합니다" + "테이블 이름에 이모지를 넣을 수 없습니다" ), INVALID_TABLE_TIME(HttpStatus.BAD_REQUEST, "시간은 양수만 가능합니다"), @@ -24,6 +25,14 @@ public enum ClientErrorCode implements ResponseErrorCode { 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배여야 합니다"), + INVALID_TEAM_NAME_LENGTH( + HttpStatus.BAD_REQUEST, + "팀 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTable.TEAM_NAME_MAX_LENGTH) + ), + INVALID_TEAM_NAME_FORM( + HttpStatus.BAD_REQUEST, + "팀 이름에 이모지를 넣을 수 없습니다" + ), FIELD_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), URL_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), diff --git a/src/test/java/com/debatetimer/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java index b8ad484c..3e35a8d5 100644 --- a/src/test/java/com/debatetimer/domain/DebateTableTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -20,14 +20,23 @@ class DebateTableTest { @Nested class Validate { - @ValueSource(strings = {"a bc가다9", "가0나 다ab"}) + @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfältig"}) @ParameterizedTest - void 테이블_이름은_영문과_한글_숫자_띄어쓰기만_가능하다(String name) { + void 테이블_이름은_이모지를_제외한_글자만_가능하다(String name) { Member member = new Member("default@gmail.com"); assertThatCode(() -> new DebateTableTestObject(member, name, "agenda", true, true)) .doesNotThrowAnyException(); } + @ValueSource(strings = {"a😀bc가다9", "🐥", "🥦"}) + @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()); + } + @ValueSource(ints = {0, DebateTable.NAME_MAX_LENGTH + 1}) @ParameterizedTest void 테이블_이름은_정해진_길이_이내여야_한다(int length) { @@ -45,15 +54,6 @@ class Validate { .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 diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java index 10fb988f..d47b287d 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java @@ -1,10 +1,17 @@ package com.debatetimer.domain.customize; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.TableType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class CustomizeTableTest { @@ -18,4 +25,62 @@ class GetType { assertThat(customizeTable.getType()).isEqualTo(TableType.CUSTOMIZE); } } + + @Nested + class ValidateTeamName { + + @ValueSource(ints = {0, CustomizeTable.TEAM_NAME_MAX_LENGTH + 1}) + @ParameterizedTest + void 찬성_팀_이름은_정해진_길이_이내여야_한다(int length) { + Member member = new Member("default@gmail.com"); + assertThatThrownBy( + () -> new CustomizeTable(member, "name", "agenda", true, true, "f".repeat(length), "cons")) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); + } + + @ValueSource(ints = {0, CustomizeTable.TEAM_NAME_MAX_LENGTH + 1}) + @ParameterizedTest + void 반대_팀_이름은_정해진_길이_이내여야_한다(int length) { + Member member = new Member("default@gmail.com"); + assertThatThrownBy( + () -> new CustomizeTable(member, "name", "agenda", true, true, "pros", "f".repeat(length))) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); + } + + @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfäl"}) + @ParameterizedTest + void 찬성_팀_이름은_이모지를_제외한_글자만_가능하다(String prosName) { + Member member = new Member("default@gmail.com"); + assertThatCode(() -> new CustomizeTable(member, "name", "agenda", true, true, prosName, "cons")) + .doesNotThrowAnyException(); + } + + @ValueSource(strings = {"a😀가다9", "🐥", "🥦"}) + @ParameterizedTest + void 찬성_팀_이름에_이모지를_넣을_수_없다(String prosName) { + Member member = new Member("default@gmail.com"); + assertThatThrownBy(() -> new CustomizeTable(member, "name", "agenda", true, true, prosName, "cons")) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_FORM.getMessage()); + } + + @ValueSource(strings = {"a bc가다9", "가0나 다ab", "ㄱㄷㅇㄹ", "漢字", "にほんご", "vielfäl"}) + @ParameterizedTest + void 반대_팀_이름은_이모지를_제외한_글자만_가능하다(String consName) { + Member member = new Member("default@gmail.com"); + assertThatCode(() -> new CustomizeTable(member, "name", "agenda", true, true, "pros", consName)) + .doesNotThrowAnyException(); + } + + @ValueSource(strings = {"a😀가다9", "🐥", "🥦"}) + @ParameterizedTest + void 반대_팀_이름에_이모지를_넣을_수_없다(String consName) { + Member member = new Member("default@gmail.com"); + assertThatThrownBy(() -> new CustomizeTable(member, "name", "agenda", true, true, "pros", consName)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_FORM.getMessage()); + } + } } From 72439321ee5d24cf5c0a72b46042e4b0f971f7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Fri, 28 Mar 2025 12:09:07 +0900 Subject: [PATCH 24/33] =?UTF-8?q?[FEAT]=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: leegwichan --- build.gradle | 3 ++ .../client/notifier/ConsoleNotifier.java | 15 ++++++ .../client/notifier/DiscordNotifier.java | 54 +++++++++++++++++++ .../client/notifier/DiscordProperties.java | 27 ++++++++++ .../client/notifier/ErrorNotifier.java | 6 +++ .../client/{ => oauth}/OAuthClient.java | 2 +- .../client/{ => oauth}/OAuthProperties.java | 2 +- .../config/AuthenticationConfig.java | 2 +- .../debatetimer/config/NotifierConfig.java | 39 ++++++++++++++ .../errorcode/InitializationErrorCode.java | 2 + .../handler/GlobalExceptionHandler.java | 7 +++ .../debatetimer/service/auth/AuthService.java | 2 +- src/main/resources/application-dev.yml | 4 ++ src/main/resources/application-prod.yml | 4 ++ .../DatabaseSchemaManagerTest.java | 5 +- .../DebateTimerApplicationTest.java | 5 ++ .../client/DiscordPropertiesTest.java | 36 +++++++++++++ .../client/OAuthPropertiesTest.java | 1 + .../controller/BaseControllerTest.java | 2 +- 19 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java create mode 100644 src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java create mode 100644 src/main/java/com/debatetimer/client/notifier/DiscordProperties.java create mode 100644 src/main/java/com/debatetimer/client/notifier/ErrorNotifier.java rename src/main/java/com/debatetimer/client/{ => oauth}/OAuthClient.java (97%) rename src/main/java/com/debatetimer/client/{ => oauth}/OAuthProperties.java (97%) create mode 100644 src/main/java/com/debatetimer/config/NotifierConfig.java create mode 100644 src/test/java/com/debatetimer/client/DiscordPropertiesTest.java diff --git a/build.gradle b/build.gradle index 1c12f1fd..e617a826 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,9 @@ dependencies { // Logging implementation 'org.springframework.boot:spring-boot-starter-log4j2' implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" + + //Discord + implementation 'net.dv8tion:JDA:5.0.0-beta.24' } bootJar { diff --git a/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java b/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java new file mode 100644 index 00000000..38811a6b --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java @@ -0,0 +1,15 @@ +package com.debatetimer.client.notifier; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ConsoleNotifier implements ErrorNotifier { + + private static final String ERROR_SEND_MESSAGE = "에러 정보가 채널로 발송되었습니다."; + + @Override + public void sendErrorMessage(Throwable throwable) { + log.error(ERROR_SEND_MESSAGE); + log.error(throwable.getMessage()); + } +} diff --git a/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java new file mode 100644 index 00000000..90cf1c1c --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java @@ -0,0 +1,54 @@ +package com.debatetimer.client.notifier; + +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import java.util.Arrays; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; + +@Slf4j +public class DiscordNotifier implements ErrorNotifier { + + private static final String NOTIFICATION_PREFIX = ":rotating_light: [**Error 발생!**]\n```\n"; + private static final String DISCORD_LINE_SEPARATOR = "/n"; + private static final int STACK_TRACE_LENGTH = 10; + + private final DiscordProperties properties; + private final JDA jda; + + public DiscordNotifier(DiscordProperties discordProperties) { + this.properties = discordProperties; + this.jda = initializeJda(properties.getToken()); + } + + private JDA initializeJda(String token) { + try { + return JDABuilder.createDefault(token).build().awaitReady(); + } catch (InterruptedException e) { + throw new DTInitializationException(InitializationErrorCode.JDA_INITIALIZATION_FAIL); + } + } + + public void sendErrorMessage(Throwable throwable) { + TextChannel channel = jda.getTextChannelById(properties.getChannelId()); + String errorMessage = throwable.getMessage(); + String stackTrace = getStackTraceAsString(throwable); + + String errorNotification = NOTIFICATION_PREFIX + + errorMessage + + DISCORD_LINE_SEPARATOR + + stackTrace; + channel.sendMessage(errorNotification).queue(); + } + + private String getStackTraceAsString(Throwable throwable) { + return Arrays.stream(throwable.getStackTrace()) + .map(StackTraceElement::toString) + .limit(STACK_TRACE_LENGTH) + .collect(Collectors.joining(System.lineSeparator())); + } +} + diff --git a/src/main/java/com/debatetimer/client/notifier/DiscordProperties.java b/src/main/java/com/debatetimer/client/notifier/DiscordProperties.java new file mode 100644 index 00000000..0012e83e --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/DiscordProperties.java @@ -0,0 +1,27 @@ +package com.debatetimer.client.notifier; + +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@ConfigurationProperties(prefix = "discord") +public class DiscordProperties { + + private final String token; + private final String channelId; + + public DiscordProperties(String token, String channelId) { + validate(token); + validate(channelId); + this.token = token; + this.channelId = channelId; + } + + private void validate(String element) { + if (element == null || element.isBlank()) { + throw new DTInitializationException(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY); + } + } +} diff --git a/src/main/java/com/debatetimer/client/notifier/ErrorNotifier.java b/src/main/java/com/debatetimer/client/notifier/ErrorNotifier.java new file mode 100644 index 00000000..4385976c --- /dev/null +++ b/src/main/java/com/debatetimer/client/notifier/ErrorNotifier.java @@ -0,0 +1,6 @@ +package com.debatetimer.client.notifier; + +public interface ErrorNotifier { + + void sendErrorMessage(Throwable throwable); +} diff --git a/src/main/java/com/debatetimer/client/OAuthClient.java b/src/main/java/com/debatetimer/client/oauth/OAuthClient.java similarity index 97% rename from src/main/java/com/debatetimer/client/OAuthClient.java rename to src/main/java/com/debatetimer/client/oauth/OAuthClient.java index 56523120..3e041f3e 100644 --- a/src/main/java/com/debatetimer/client/OAuthClient.java +++ b/src/main/java/com/debatetimer/client/oauth/OAuthClient.java @@ -1,4 +1,4 @@ -package com.debatetimer.client; +package com.debatetimer.client.oauth; import com.debatetimer.aop.logging.LoggingClient; import com.debatetimer.dto.member.MemberCreateRequest; diff --git a/src/main/java/com/debatetimer/client/OAuthProperties.java b/src/main/java/com/debatetimer/client/oauth/OAuthProperties.java similarity index 97% rename from src/main/java/com/debatetimer/client/OAuthProperties.java rename to src/main/java/com/debatetimer/client/oauth/OAuthProperties.java index bb5ac44c..56598a13 100644 --- a/src/main/java/com/debatetimer/client/OAuthProperties.java +++ b/src/main/java/com/debatetimer/client/oauth/OAuthProperties.java @@ -1,4 +1,4 @@ -package com.debatetimer.client; +package com.debatetimer.client.oauth; import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.exception.custom.DTInitializationException; diff --git a/src/main/java/com/debatetimer/config/AuthenticationConfig.java b/src/main/java/com/debatetimer/config/AuthenticationConfig.java index 3b1073fa..4afdc6cf 100644 --- a/src/main/java/com/debatetimer/config/AuthenticationConfig.java +++ b/src/main/java/com/debatetimer/config/AuthenticationConfig.java @@ -1,6 +1,6 @@ package com.debatetimer.config; -import com.debatetimer.client.OAuthProperties; +import com.debatetimer.client.oauth.OAuthProperties; import com.debatetimer.controller.tool.jwt.JwtTokenProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/debatetimer/config/NotifierConfig.java b/src/main/java/com/debatetimer/config/NotifierConfig.java new file mode 100644 index 00000000..0235a042 --- /dev/null +++ b/src/main/java/com/debatetimer/config/NotifierConfig.java @@ -0,0 +1,39 @@ +package com.debatetimer.config; + + +import com.debatetimer.client.notifier.ConsoleNotifier; +import com.debatetimer.client.notifier.DiscordNotifier; +import com.debatetimer.client.notifier.DiscordProperties; +import com.debatetimer.client.notifier.ErrorNotifier; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +public class NotifierConfig { + + @Profile({"dev", "prod"}) + @Configuration + @RequiredArgsConstructor + @EnableConfigurationProperties(DiscordProperties.class) + public static class DiscordNotifierConfig { + + private final DiscordProperties discordProperties; + + @Bean + public ErrorNotifier discordNotifier() { + return new DiscordNotifier(discordProperties); + } + } + + @Profile({"test", "local"}) + @Configuration + public static class ConsoleNotifierConfig { + + @Bean + public ErrorNotifier consoleNotifier() { + return new ConsoleNotifier(); + } + } +} diff --git a/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java index a037a69f..153067b0 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/InitializationErrorCode.java @@ -8,6 +8,8 @@ public enum InitializationErrorCode { OAUTH_PROPERTIES_EMPTY("OAuth 구성 요소들이 입력되지 않았습니다"), + DISCORD_PROPERTIES_EMPTY("디스코드 봇 구성 요소들이 입력되지 않았습니다"), + JDA_INITIALIZATION_FAIL("디스코드 client 구성에 실패하였습니다"), CORS_ORIGIN_EMPTY("CORS Origin 은 적어도 한 개 있어야 합니다"), CORS_ORIGIN_STRING_BLANK("CORS Origin 에 빈 값이 들어올 수 없습니다"), diff --git a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java index efeaab4c..e17153c0 100644 --- a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.debatetimer.exception.handler; +import com.debatetimer.client.notifier.ErrorNotifier; import com.debatetimer.exception.ErrorResponse; import com.debatetimer.exception.custom.DTClientErrorException; import com.debatetimer.exception.custom.DTServerErrorException; @@ -7,6 +8,7 @@ import com.debatetimer.exception.errorcode.ResponseErrorCode; import com.debatetimer.exception.errorcode.ServerErrorCode; import jakarta.validation.ConstraintViolationException; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; import org.springframework.http.HttpStatus; @@ -21,8 +23,11 @@ @Slf4j @RestControllerAdvice +@RequiredArgsConstructor public class GlobalExceptionHandler { + private final ErrorNotifier errorNotifier; + @ExceptionHandler(BindException.class) public ResponseEntity handleBindingException(BindException exception) { log.warn("message: {}", exception.getMessage()); @@ -78,12 +83,14 @@ public ResponseEntity handleClientException(DTClientErrorExceptio @ExceptionHandler(DTServerErrorException.class) public ResponseEntity handleServerException(DTServerErrorException exception) { log.error("message: {}", exception.getMessage()); + errorNotifier.sendErrorMessage(exception); return toResponse(exception.getHttpStatus(), exception.getMessage()); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception exception) { log.error("exception: {}", exception.getMessage()); + errorNotifier.sendErrorMessage(exception); return toResponse(ServerErrorCode.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/com/debatetimer/service/auth/AuthService.java b/src/main/java/com/debatetimer/service/auth/AuthService.java index aec9e8e1..2e5f37ff 100644 --- a/src/main/java/com/debatetimer/service/auth/AuthService.java +++ b/src/main/java/com/debatetimer/service/auth/AuthService.java @@ -1,6 +1,6 @@ package com.debatetimer.service.auth; -import com.debatetimer.client.OAuthClient; +import com.debatetimer.client.oauth.OAuthClient; import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.dto.member.MemberInfo; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 51b83166..af61d46d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -33,5 +33,9 @@ jwt: access_token_expiration: ${secret.jwt.access_token_expiration} refresh_token_expiration: ${secret.jwt.refresh_token_expiration} +discord: + token: ${secret.discord.token} + channelId: ${secret.discord.channelId} + #logging: # config: classpath:logging/log4j2-dev.yml diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 2af3608c..79040396 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -32,5 +32,9 @@ jwt: access_token_expiration: ${secret.jwt.access_token_expiration} refresh_token_expiration: ${secret.jwt.refresh_token_expiration} +discord: + token: ${secret.discord.token} + channelId: ${secret.discord.channelId} + #logging: # config: classpath:logging/log4j2-prod.yml diff --git a/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java index e89fce15..fb3953ea 100644 --- a/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java +++ b/src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java @@ -6,10 +6,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.context.ActiveProfiles; -@SpringBootTest -@ActiveProfiles("flyway") +@SpringBootTest(webEnvironment = WebEnvironment.NONE) +@ActiveProfiles({"test", "flyway"}) class DatabaseSchemaManagerTest { @Autowired diff --git a/src/test/java/com/debatetimer/DebateTimerApplicationTest.java b/src/test/java/com/debatetimer/DebateTimerApplicationTest.java index fcf32f13..453ef46a 100644 --- a/src/test/java/com/debatetimer/DebateTimerApplicationTest.java +++ b/src/test/java/com/debatetimer/DebateTimerApplicationTest.java @@ -1,11 +1,16 @@ package com.debatetimer; +import com.debatetimer.client.notifier.ErrorNotifier; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; @SpringBootTest class DebateTimerApplicationTest { + @MockitoBean + private ErrorNotifier errorNotifier; + @Test void contextLoads() { } diff --git a/src/test/java/com/debatetimer/client/DiscordPropertiesTest.java b/src/test/java/com/debatetimer/client/DiscordPropertiesTest.java new file mode 100644 index 00000000..85fc72a4 --- /dev/null +++ b/src/test/java/com/debatetimer/client/DiscordPropertiesTest.java @@ -0,0 +1,36 @@ +package com.debatetimer.client; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.client.notifier.DiscordProperties; +import com.debatetimer.exception.custom.DTInitializationException; +import com.debatetimer.exception.errorcode.InitializationErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +class DiscordPropertiesTest { + + @Nested + class Validate { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"\n", "\t "}) + void 디스코드봇_토큰이_비어있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new DiscordProperties(empty, "channelId")) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"\n", "\t "}) + void 디스코드_채널_아이디가_비어있을_경우_예외를_발생시킨다(String empty) { + assertThatThrownBy(() -> new DiscordProperties("token", empty)) + .isInstanceOf(DTInitializationException.class) + .hasMessage(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java b/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java index 1b0250f9..05fbc406 100644 --- a/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java +++ b/src/test/java/com/debatetimer/client/OAuthPropertiesTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.debatetimer.client.oauth.OAuthProperties; import com.debatetimer.exception.custom.DTInitializationException; import com.debatetimer.exception.errorcode.InitializationErrorCode; import org.junit.jupiter.api.Nested; diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 265ace71..c764dd15 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -1,7 +1,7 @@ package com.debatetimer.controller; import com.debatetimer.DataBaseCleaner; -import com.debatetimer.client.OAuthClient; +import com.debatetimer.client.oauth.OAuthClient; import com.debatetimer.fixture.CustomizeTableGenerator; import com.debatetimer.fixture.CustomizeTimeBoxGenerator; import com.debatetimer.fixture.HeaderGenerator; From f3760af7c2843045365cef1ab189661deefaccec Mon Sep 17 00:00:00 2001 From: SANGHUN OH <121424793+unifolio0@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:55:28 +0900 Subject: [PATCH 25/33] =?UTF-8?q?[HOTFIX]=20NullException=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20(#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/CustomizeTimeBoxResponse.java | 2 +- .../response/ParliamentaryTableResponse.java | 6 +++--- .../response/ParliamentaryTimeBoxResponse.java | 9 ++++++++- .../timebased/response/TimeBasedTableResponse.java | 6 +++--- .../response/TimeBasedTimeBoxResponse.java | 9 ++++++++- .../customize/CustomizeControllerTest.java | 10 +++++----- .../parliamentary/ParliamentaryControllerTest.java | 10 +++++----- .../timebased/TimeBasedControllerTest.java | 10 +++++----- .../fixture/CustomizeTimeBoxGenerator.java | 13 +++++++++++++ .../fixture/ParliamentaryTimeBoxGenerator.java | 6 ++++++ .../fixture/TimeBasedTimeBoxGenerator.java | 6 ++++++ 11 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java index c8b53e17..632cf214 100644 --- a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java @@ -22,7 +22,7 @@ public CustomizeTimeBoxResponse(CustomizeTimeBox customizeTimeBox) { customizeTimeBox.getTime(), customizeTimeBox.getTimePerTeam(), customizeTimeBox.getTimePerSpeaking(), - String.valueOf(customizeTimeBox.getSpeaker()) + customizeTimeBox.getSpeaker() ); } } diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java index 6701c51e..0d4dbb63 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTableResponse.java @@ -10,7 +10,7 @@ public record ParliamentaryTableResponse(long id, ParliamentaryTableInfoResponse public ParliamentaryTableResponse( ParliamentaryTable parliamentaryTable, - TimeBoxes parliamentaryTimeBoxes + TimeBoxes parliamentaryTimeBoxes ) { this( parliamentaryTable.getId(), @@ -19,8 +19,8 @@ public ParliamentaryTableResponse( ); } - private static List toTimeBoxResponses(TimeBoxes timeBoxes) { - List parliamentaryTimeBoxes = (List) timeBoxes.getTimeBoxes(); + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List parliamentaryTimeBoxes = timeBoxes.getTimeBoxes(); return parliamentaryTimeBoxes .stream() .map(ParliamentaryTimeBoxResponse::new) diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java index ab674c9c..bad541ea 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java @@ -10,7 +10,14 @@ public ParliamentaryTimeBoxResponse(ParliamentaryTimeBox parliamentaryTimeBox) { this(parliamentaryTimeBox.getStance(), parliamentaryTimeBox.getType(), parliamentaryTimeBox.getTime(), - Integer.parseInt(parliamentaryTimeBox.getSpeaker()) + getSpeakerNumber(parliamentaryTimeBox) ); } + + private static Integer getSpeakerNumber(ParliamentaryTimeBox parliamentaryTimeBox) { + if (parliamentaryTimeBox.getSpeaker() == null || parliamentaryTimeBox.getSpeaker().equals("null")) { + return null; + } + return Integer.parseInt(parliamentaryTimeBox.getSpeaker()); + } } diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java index a1f3d608..1c19a87e 100644 --- a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTableResponse.java @@ -9,7 +9,7 @@ public record TimeBasedTableResponse(long id, TimeBasedTableInfoResponse info, L public TimeBasedTableResponse( TimeBasedTable timeBasedTable, - TimeBoxes timeBasedTimeBoxes + TimeBoxes timeBasedTimeBoxes ) { this( timeBasedTable.getId(), @@ -18,8 +18,8 @@ public TimeBasedTableResponse( ); } - private static List toTimeBoxResponses(TimeBoxes timeBoxes) { - List timeBasedTimeBoxes = (List) timeBoxes.getTimeBoxes(); + private static List toTimeBoxResponses(TimeBoxes timeBoxes) { + List timeBasedTimeBoxes = timeBoxes.getTimeBoxes(); return timeBasedTimeBoxes .stream() .map(TimeBasedTimeBoxResponse::new) diff --git a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java index 2fab64f1..587d95ac 100644 --- a/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/timebased/response/TimeBasedTimeBoxResponse.java @@ -19,7 +19,14 @@ public TimeBasedTimeBoxResponse(TimeBasedTimeBox timeBasedTimeBox) { timeBasedTimeBox.getTime(), timeBasedTimeBox.getTimePerTeam(), timeBasedTimeBox.getTimePerSpeaking(), - Integer.parseInt(timeBasedTimeBox.getSpeaker()) + getSpeakerNumber(timeBasedTimeBox) ); } + + private static Integer getSpeakerNumber(TimeBasedTimeBox timeBasedTimeBox) { + if (timeBasedTimeBox.getSpeaker() == null || timeBasedTimeBox.getSpeaker().equals("null")) { + return null; + } + return Integer.parseInt(timeBasedTimeBox.getSpeaker()); + } } diff --git a/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java b/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java index d8247a78..bbaf87b7 100644 --- a/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeControllerTest.java @@ -33,7 +33,7 @@ class Save { new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, 120, 60, null, "발언자1"), new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자2") + 120, 60, null, null) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -61,7 +61,7 @@ class GetTable { Member bito = memberGenerator.generate("default@gmail.com"); CustomizeTable bitoTable = customizeTableGenerator.generate(bito); customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + customizeTimeBoxGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableResponse response = given() @@ -93,7 +93,7 @@ class UpdateTable { new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론1", CustomizeBoxType.NORMAL, 120, 60, null, "발언자1"), new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론2", CustomizeBoxType.NORMAL, - 120, 60, null, "발언자2") + 120, 60, null, null) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -123,7 +123,7 @@ class Debate { Member bito = memberGenerator.generate("default@gmail.com"); CustomizeTable bitoTable = customizeTableGenerator.generate(bito); customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + customizeTimeBoxGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); CustomizeTableResponse response = given() @@ -150,7 +150,7 @@ class DeleteTable { Member bito = memberGenerator.generate("default@gmail.com"); CustomizeTable bitoTable = customizeTableGenerator.generate(bito); customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); - customizeTimeBoxGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 2); + customizeTimeBoxGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index 559f4456..6e38e278 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -30,7 +30,7 @@ class Save { new ParliamentaryTableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, null) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -58,7 +58,7 @@ class GetTable { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); parliamentaryTimeBoxGenerator.generate(bitoTable, 1); - parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + parliamentaryTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() @@ -87,7 +87,7 @@ class UpdateTable { new ParliamentaryTableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( new ParliamentaryTimeBoxCreateRequest(Stance.PROS, ParliamentaryBoxType.OPENING, 3, 1), - new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, 1) + new ParliamentaryTimeBoxCreateRequest(Stance.CONS, ParliamentaryBoxType.OPENING, 3, null) ) ); Headers headers = headerGenerator.generateAccessTokenHeader(bito); @@ -117,7 +117,7 @@ class Debate { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); parliamentaryTimeBoxGenerator.generate(bitoTable, 1); - parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + parliamentaryTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() @@ -144,7 +144,7 @@ class DeleteTable { Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = parliamentaryTableGenerator.generate(bito); parliamentaryTimeBoxGenerator.generate(bitoTable, 1); - parliamentaryTimeBoxGenerator.generate(bitoTable, 2); + parliamentaryTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() diff --git a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java index 034e1952..e5d41d4a 100644 --- a/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java +++ b/src/test/java/com/debatetimer/controller/timebased/TimeBasedControllerTest.java @@ -32,7 +32,7 @@ class Save { 1), new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, - 1))); + null))); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -58,7 +58,7 @@ class GetTable { Member bito = memberGenerator.generate("default@gmail.com"); TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); timeBasedTimeBoxGenerator.generate(bitoTable, 1); - timeBasedTimeBoxGenerator.generate(bitoTable, 2); + timeBasedTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -89,7 +89,7 @@ class UpdateTable { 1), new TimeBasedTimeBoxCreateRequest(Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, 360, 180, 60, - 1))); + null))); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -117,7 +117,7 @@ class Debate { Member bito = memberGenerator.generate("default@gmail.com"); TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); timeBasedTimeBoxGenerator.generate(bitoTable, 1); - timeBasedTimeBoxGenerator.generate(bitoTable, 2); + timeBasedTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); TimeBasedTableResponse response = given() @@ -144,7 +144,7 @@ class DeleteTable { Member bito = memberGenerator.generate("default@gmail.com"); TimeBasedTable bitoTable = timeBasedTableGenerator.generate(bito); timeBasedTimeBoxGenerator.generate(bitoTable, 1); - timeBasedTimeBoxGenerator.generate(bitoTable, 2); + timeBasedTimeBoxGenerator.generateNotExistSpeaker(bitoTable, 2); Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() diff --git a/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java index e23be1ce..df77fb3d 100644 --- a/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java +++ b/src/test/java/com/debatetimer/fixture/CustomizeTimeBoxGenerator.java @@ -28,4 +28,17 @@ public CustomizeTimeBox generate(CustomizeTable testTable, CustomizeBoxType boxT ); return customizeTimeBoxRepository.save(timeBox); } + + public CustomizeTimeBox generateNotExistSpeaker(CustomizeTable testTable, CustomizeBoxType boxType, int sequence) { + CustomizeTimeBox timeBox = new CustomizeTimeBox( + testTable, + sequence, + Stance.PROS, + "입론", + boxType, + 180, + null + ); + return customizeTimeBoxRepository.save(timeBox); + } } diff --git a/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java index 376c9136..05107cd3 100644 --- a/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java +++ b/src/test/java/com/debatetimer/fixture/ParliamentaryTimeBoxGenerator.java @@ -21,4 +21,10 @@ public ParliamentaryTimeBox generate(ParliamentaryTable testTable, int sequence) ParliamentaryBoxType.OPENING, 180, 1); return parliamentaryTimeBoxRepository.save(timeBox); } + + public ParliamentaryTimeBox generateNotExistSpeaker(ParliamentaryTable testTable, int sequence) { + ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(testTable, sequence, Stance.PROS, + ParliamentaryBoxType.OPENING, 180, null); + return parliamentaryTimeBoxRepository.save(timeBox); + } } diff --git a/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java b/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java index 319dfb6e..f1df0beb 100644 --- a/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java +++ b/src/test/java/com/debatetimer/fixture/TimeBasedTimeBoxGenerator.java @@ -21,4 +21,10 @@ public TimeBasedTimeBox generate(TimeBasedTable testTable, int sequence) { TimeBasedBoxType.OPENING, 180, 1); return timeBasedTimeBoxRepository.save(timeBox); } + + public TimeBasedTimeBox generateNotExistSpeaker(TimeBasedTable testTable, int sequence) { + TimeBasedTimeBox timeBox = new TimeBasedTimeBox(testTable, sequence, Stance.PROS, + TimeBasedBoxType.OPENING, 180, null); + return timeBasedTimeBoxRepository.save(timeBox); + } } From 31d9cded6d8623f082c80fe844b9ff9f5c0dcc0b Mon Sep 17 00:00:00 2001 From: Chung-an Lee <44027393+leegwichan@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:57:38 +0900 Subject: [PATCH 26/33] =?UTF-8?q?[FEAT]=20=ED=86=A0=ED=81=B0=EC=9D=B4=20?= =?UTF-8?q?=EB=B9=84=EC=96=B4=EC=9E=88=EC=9D=84=20=EB=95=8C=20400=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20(#135)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/notifier/ConsoleNotifier.java | 14 +++++++++++--- .../client/notifier/DiscordNotifier.java | 14 ++++++++------ .../exception/errorcode/ClientErrorCode.java | 17 +++++++++-------- .../handler/GlobalExceptionHandler.java | 6 ++++++ .../member/MemberControllerTest.java | 18 ++++++++++++++++++ 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java b/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java index 38811a6b..59ff577b 100644 --- a/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java +++ b/src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java @@ -1,15 +1,23 @@ package com.debatetimer.client.notifier; +import java.util.Arrays; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j public class ConsoleNotifier implements ErrorNotifier { - private static final String ERROR_SEND_MESSAGE = "에러 정보가 채널로 발송되었습니다."; + private static final String ERROR_SEND_MESSAGE = "에러 정보가 채널로 발송되었습니다"; @Override public void sendErrorMessage(Throwable throwable) { - log.error(ERROR_SEND_MESSAGE); - log.error(throwable.getMessage()); + log.error("{} : {}", ERROR_SEND_MESSAGE, throwable); + log.error(getStackTraceAsString(throwable)); + } + + private String getStackTraceAsString(Throwable throwable) { + return Arrays.stream(throwable.getStackTrace()) + .map(StackTraceElement::toString) + .collect(Collectors.joining(System.lineSeparator())); } } diff --git a/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java index 90cf1c1c..c4bb1529 100644 --- a/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java +++ b/src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java @@ -12,8 +12,9 @@ @Slf4j public class DiscordNotifier implements ErrorNotifier { - private static final String NOTIFICATION_PREFIX = ":rotating_light: [**Error 발생!**]\n```\n"; - private static final String DISCORD_LINE_SEPARATOR = "/n"; + private static final String NOTIFICATION_PREFIX = ":rotating_light: [**Error 발생!**]\n"; + private static final String STACK_TRACE_AFFIX = "\n```\n"; + private static final String DISCORD_LINE_SEPARATOR = "\n"; private static final int STACK_TRACE_LENGTH = 10; private final DiscordProperties properties; @@ -34,13 +35,14 @@ private JDA initializeJda(String token) { public void sendErrorMessage(Throwable throwable) { TextChannel channel = jda.getTextChannelById(properties.getChannelId()); - String errorMessage = throwable.getMessage(); + String errorMessage = throwable.toString(); String stackTrace = getStackTraceAsString(throwable); String errorNotification = NOTIFICATION_PREFIX + errorMessage - + DISCORD_LINE_SEPARATOR - + stackTrace; + + STACK_TRACE_AFFIX + + stackTrace + + STACK_TRACE_AFFIX; channel.sendMessage(errorNotification).queue(); } @@ -48,7 +50,7 @@ private String getStackTraceAsString(Throwable throwable) { return Arrays.stream(throwable.getStackTrace()) .map(StackTraceElement::toString) .limit(STACK_TRACE_LENGTH) - .collect(Collectors.joining(System.lineSeparator())); + .collect(Collectors.joining(DISCORD_LINE_SEPARATOR)); } } diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 73258da9..9b902db8 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -34,14 +34,6 @@ public enum ClientErrorCode implements ResponseErrorCode { "팀 이름에 이모지를 넣을 수 없습니다" ), - FIELD_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), - URL_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), - METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "입력한 값의 타입이 잘못되었습니다."), - NO_RESOURCE_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."), - METHOD_NOT_SUPPORTED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 메서드입니다."), - MEDIA_TYPE_NOT_SUPPORTED(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "허용되지 않은 미디어 타입입니다."), - ALREADY_DISCONNECTED(HttpStatus.BAD_REQUEST, "이미 클라이언트에서 요청이 종료되었습니다."), - TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "토론 테이블을 찾을 수 없습니다."), NOT_TABLE_OWNER(HttpStatus.UNAUTHORIZED, "테이블을 소유한 회원이 아닙니다."), UNAUTHORIZED_MEMBER(HttpStatus.UNAUTHORIZED, "접근 권한이 없습니다"), @@ -49,6 +41,15 @@ public enum ClientErrorCode implements ResponseErrorCode { EMPTY_COOKIE(HttpStatus.UNAUTHORIZED, "쿠키에 값이 없습니다"), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원이 존재하지 않습니다"), + + FIELD_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), + URL_PARAMETER_ERROR(HttpStatus.BAD_REQUEST, "입력이 잘못되었습니다."), + METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "입력한 값의 타입이 잘못되었습니다."), + NO_RESOURCE_FOUND(HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다."), + METHOD_NOT_SUPPORTED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 메서드입니다."), + MEDIA_TYPE_NOT_SUPPORTED(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "허용되지 않은 미디어 타입입니다."), + ALREADY_DISCONNECTED(HttpStatus.BAD_REQUEST, "이미 클라이언트에서 요청이 종료되었습니다."), + NO_COOKIE_FOUND(HttpStatus.BAD_REQUEST, "필수 쿠키 값이 존재하지 않습니다.") ; private final HttpStatus status; diff --git a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java index e17153c0..ee411661 100644 --- a/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/debatetimer/exception/handler/GlobalExceptionHandler.java @@ -16,6 +16,7 @@ import org.springframework.validation.BindException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingRequestCookieException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @@ -74,6 +75,11 @@ public ResponseEntity handleNoResourceFoundException(NoResourceFo return toResponse(ClientErrorCode.NO_RESOURCE_FOUND); } + @ExceptionHandler(MissingRequestCookieException.class) + public ResponseEntity handleMissingRequestCookieException(MissingRequestCookieException exception) { + return toResponse(ClientErrorCode.NO_COOKIE_FOUND); + } + @ExceptionHandler(DTClientErrorException.class) public ResponseEntity handleClientException(DTClientErrorException exception) { log.warn("message: {}", exception.getMessage()); diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index 1594468f..e3ae4650 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -71,6 +71,13 @@ class ReissueAccessToken { .when().post("/api/member/reissue") .then().statusCode(200); } + + @Test + void 토큰이_없을_경우_400_에러를_반환한다() { + given() + .when().post("/api/member/reissue") + .then().statusCode(400); + } } @Nested @@ -88,5 +95,16 @@ class Logout { .when().post("/api/member/logout") .then().statusCode(204); } + + @Test + void 토큰이_없을_경우_400_에러를_반환한다() { + Member bito = memberGenerator.generate("bito@gmail.com"); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + + given() + .headers(headers) + .when().post("/api/member/logout") + .then().statusCode(400); + } } } From 754c38c5b58e97ce20c39b6f447c834ded0306d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Wed, 2 Apr 2025 12:19:00 +0900 Subject: [PATCH 27/33] =?UTF-8?q?[FEAT]=20=EB=B0=9C=EC=96=B8=EC=9E=90=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=EB=B0=8F=20=EB=B0=9C=EC=96=B8=EC=9C=A0?= =?UTF-8?q?=ED=98=95=20=EA=B8=B8=EC=9D=B4=20=EA=B2=80=EC=A6=9D=20(#137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/DebateTimeBox.java | 9 ++++++ .../domain/customize/CustomizeTimeBox.java | 10 +++++++ .../exception/errorcode/ClientErrorCode.java | 13 +++++++-- .../debatetimer/domain/DebateTimeBoxTest.java | 22 +++++++++++++- .../customize/CustomizeTimeBoxTest.java | 29 +++++++++++++++---- 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/debatetimer/domain/DebateTimeBox.java b/src/main/java/com/debatetimer/domain/DebateTimeBox.java index 168e98f6..c3115f4a 100644 --- a/src/main/java/com/debatetimer/domain/DebateTimeBox.java +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -15,6 +15,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public abstract class DebateTimeBox { + public static final int SPEAKER_MAX_LENGTH = 5; + private int sequence; @NotNull @@ -25,6 +27,7 @@ public abstract class DebateTimeBox { private String speaker; protected DebateTimeBox(int sequence, Stance stance, int time, String speaker) { + validateSpeaker(speaker); validateSequence(sequence); validateTime(time); @@ -34,6 +37,12 @@ protected DebateTimeBox(int sequence, Stance stance, int time, String speaker) { this.speaker = speaker; } + private void validateSpeaker(String speaker) { + if (speaker != null && (speaker.isBlank() || speaker.length() > SPEAKER_MAX_LENGTH)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH); + } + } + private void validateSequence(int sequence) { if (sequence <= 0) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE); diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java index ff4443d4..065a3377 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java @@ -24,6 +24,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class CustomizeTimeBox extends DebateTimeBox { + public static final int SPEECH_TYPE_MAX_LENGTH = 10; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -54,6 +56,7 @@ public CustomizeTimeBox( ) { super(sequence, stance, time, speaker); validateNotTimeBasedType(boxType); + validateSpeechType(speechType); this.customizeTable = customizeTable; this.speechType = speechType; @@ -75,6 +78,7 @@ public CustomizeTimeBox( validateTime(timePerTeam, timePerSpeaking); validateTimeBasedTime(time, timePerTeam); validateTimeBasedType(boxType); + validateSpeechType(speechType); this.customizeTable = customizeTable; this.speechType = speechType; @@ -114,4 +118,10 @@ private void validateNotTimeBasedType(CustomizeBoxType boxType) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); } } + + private void validateSpeechType(String speechType) { + if (speechType.isBlank() || speechType.length() > SPEECH_TYPE_MAX_LENGTH) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH); + } + } } diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 9b902db8..2e6405d2 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -1,7 +1,9 @@ package com.debatetimer.exception.errorcode; import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.DebateTimeBox; import com.debatetimer.domain.customize.CustomizeTable; +import com.debatetimer.domain.customize.CustomizeTimeBox; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -25,6 +27,14 @@ public enum ClientErrorCode implements ResponseErrorCode { 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배여야 합니다"), + INVALID_TIME_BOX_SPEECH_TYPE_LENGTH( + HttpStatus.BAD_REQUEST, + "발언 유형 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH) + ), + INVALID_TIME_BOX_SPEAKER_LENGTH( + HttpStatus.BAD_REQUEST, + "발언자 이름은 1자 이상 %d자 이하여야 합니다.".formatted(DebateTimeBox.SPEAKER_MAX_LENGTH) + ), INVALID_TEAM_NAME_LENGTH( HttpStatus.BAD_REQUEST, "팀 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTable.TEAM_NAME_MAX_LENGTH) @@ -49,8 +59,7 @@ public enum ClientErrorCode implements ResponseErrorCode { METHOD_NOT_SUPPORTED(HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 메서드입니다."), MEDIA_TYPE_NOT_SUPPORTED(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "허용되지 않은 미디어 타입입니다."), ALREADY_DISCONNECTED(HttpStatus.BAD_REQUEST, "이미 클라이언트에서 요청이 종료되었습니다."), - NO_COOKIE_FOUND(HttpStatus.BAD_REQUEST, "필수 쿠키 값이 존재하지 않습니다.") - ; + NO_COOKIE_FOUND(HttpStatus.BAD_REQUEST, "필수 쿠키 값이 존재하지 않습니다."); private final HttpStatus status; private final String message; diff --git a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java index b87b3e06..f9f694c8 100644 --- a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java @@ -13,7 +13,7 @@ class DebateTimeBoxTest { @Nested - class Validate { + class ValidateSequence { @ValueSource(ints = {0, -1}) @ParameterizedTest @@ -23,6 +23,12 @@ class Validate { .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SEQUENCE.getMessage()); } + + } + + @Nested + class ValidateTime { + @ValueSource(ints = {0, -1}) @ParameterizedTest void 시간은_양수만_가능하다(int time) { @@ -31,6 +37,20 @@ class Validate { .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_TIME.getMessage()); } + } + + @Nested + class ValidateSpeaker { + + @ParameterizedTest + @ValueSource(ints = {0, DebateTimeBox.SPEAKER_MAX_LENGTH + 1}) + void 발언자는_일정길이_이내로_허용된다(int length) { + String speaker = "k".repeat(length); + + assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH.getMessage()); + } @Test void 발언자는_빈_값이_허용된다() { diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java index 21fbcdeb..911016b1 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java @@ -8,20 +8,24 @@ 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 CustomizeTimeBoxTest { @Nested - class ValidateCustomize { + class ValidateCustomizeTime { @Test void 자유토론_타입은_총_시간이_팀_발언_시간의_2배여야_한다() { CustomizeTable table = new CustomizeTable(); CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 150, 120, 60, - "발언자")).isInstanceOf(DTClientErrorException.class) + int totalTime = 150; + int timePerTeam = 120; + + assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, totalTime, + timePerTeam, 60, "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE.getMessage()); } @@ -30,8 +34,9 @@ class ValidateCustomize { CustomizeTable table = new CustomizeTable(); CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, 240, 120, 60, - "발언자")).doesNotThrowAnyException(); + assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", + customizeBoxType, 240, 120, 60, "발언자") + ).doesNotThrowAnyException(); } @Test @@ -75,5 +80,17 @@ class ValidateCustomize { timePerTeam * 2, timePerTeam, timePerSpeaking, "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); } + + @ParameterizedTest + @ValueSource(ints = {0, CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH + 1}) + void 발언_유형의_길이는_일정_범위_이내여야_한다(int length) { + CustomizeTable table = new CustomizeTable(); + String longSpeechType = "s".repeat(length); + + assertThatThrownBy( + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, longSpeechType, CustomizeBoxType.TIME_BASED, + 240, 120, 60, "발언자")).isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH.getMessage()); + } } } From 10c9c374b15423d0bd1b5e4d0cf20fc586af6001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Wed, 2 Apr 2025 18:34:59 +0900 Subject: [PATCH 28/33] =?UTF-8?q?[REFACTOR]=20Blank=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9D=84=20DTO=EC=97=90=EC=84=9C=20=EC=9D=BC=EA=B4=80=EC=84=B1?= =?UTF-8?q?=EC=9E=88=EA=B2=8C=20=EC=88=98=ED=96=89=20(#141)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/domain/DebateTable.java | 2 +- .../com/debatetimer/domain/DebateTimeBox.java | 11 +++++++-- .../domain/customize/CustomizeTable.java | 2 +- .../domain/customize/CustomizeTimeBox.java | 2 +- .../request/CustomizeTableCreateRequest.java | 5 ++-- .../CustomizeTimeBoxCreateRequest.java | 5 ++-- .../ParliamentaryTableCreateRequest.java | 7 ++++-- .../ParliamentaryTimeBoxCreateRequest.java | 8 +++---- .../request/TimeBasedTableCreateRequest.java | 7 ++++-- .../TimeBasedTimeBoxCreateRequest.java | 6 ++--- .../debatetimer/domain/DebateTableTest.java | 16 ++++--------- .../debatetimer/domain/DebateTimeBoxTest.java | 24 ++++++++++++------- .../domain/customize/CustomizeTableTest.java | 18 +++++++------- .../customize/CustomizeTimeBoxTest.java | 9 +++---- 14 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/debatetimer/domain/DebateTable.java b/src/main/java/com/debatetimer/domain/DebateTable.java index 79f2de74..41161031 100644 --- a/src/main/java/com/debatetimer/domain/DebateTable.java +++ b/src/main/java/com/debatetimer/domain/DebateTable.java @@ -68,7 +68,7 @@ protected final void updateTable(DebateTable renewTable) { } private void validate(String name) { - if (name.isBlank() || name.length() > NAME_MAX_LENGTH) { + if (name.length() > NAME_MAX_LENGTH) { throw new DTClientErrorException(ClientErrorCode.INVALID_TABLE_NAME_LENGTH); } if (!name.matches(NAME_REGEX)) { diff --git a/src/main/java/com/debatetimer/domain/DebateTimeBox.java b/src/main/java/com/debatetimer/domain/DebateTimeBox.java index c3115f4a..c0409032 100644 --- a/src/main/java/com/debatetimer/domain/DebateTimeBox.java +++ b/src/main/java/com/debatetimer/domain/DebateTimeBox.java @@ -34,11 +34,18 @@ protected DebateTimeBox(int sequence, Stance stance, int time, String speaker) { this.sequence = sequence; this.stance = stance; this.time = time; - this.speaker = speaker; + this.speaker = initializeSpeaker(speaker); + } + + private String initializeSpeaker(String speaker) { + if (speaker == null || speaker.isBlank()) { + return null; + } + return speaker; } private void validateSpeaker(String speaker) { - if (speaker != null && (speaker.isBlank() || speaker.length() > SPEAKER_MAX_LENGTH)) { + if (speaker != null && speaker.length() > SPEAKER_MAX_LENGTH) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH); } } diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java index 90a868a0..0bc38a83 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTable.java @@ -55,7 +55,7 @@ public void update(CustomizeTable renewTable) { } private void validateTeamName(String teamName) { - if (teamName.isBlank() || teamName.length() > TEAM_NAME_MAX_LENGTH) { + if (teamName.length() > TEAM_NAME_MAX_LENGTH) { throw new DTClientErrorException(ClientErrorCode.INVALID_TEAM_NAME_LENGTH); } if (!teamName.matches(TEAM_NAME_REGEX)) { diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java index 065a3377..a64217b8 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java @@ -120,7 +120,7 @@ private void validateNotTimeBasedType(CustomizeBoxType boxType) { } private void validateSpeechType(String speechType) { - if (speechType.isBlank() || speechType.length() > SPEECH_TYPE_MAX_LENGTH) { + if (speechType.length() > SPEECH_TYPE_MAX_LENGTH) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH); } } diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java index 108c840f..da62fb26 100644 --- a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTableCreateRequest.java @@ -4,13 +4,14 @@ import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.customize.CustomizeTimeBox; import com.debatetimer.domain.member.Member; +import jakarta.validation.Valid; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; public record CustomizeTableCreateRequest( - CustomizeTableInfoCreateRequest info, - List table + @Valid CustomizeTableInfoCreateRequest info, + @Valid List table ) { public CustomizeTable toTable(Member member) { diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java index 3ce029ae..a040e47e 100644 --- a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java @@ -5,15 +5,16 @@ import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.customize.CustomizeTimeBox; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record CustomizeTimeBoxCreateRequest( - @NotBlank + @NotNull Stance stance, @NotBlank String speechType, - @NotBlank + @NotNull CustomizeBoxType boxType, int time, diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java index 5c193733..9e86c58a 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -4,12 +4,15 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; +import jakarta.validation.Valid; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; -public record ParliamentaryTableCreateRequest(ParliamentaryTableInfoCreateRequest info, - List table) { +public record ParliamentaryTableCreateRequest( + @Valid ParliamentaryTableInfoCreateRequest info, + @Valid List table +) { public ParliamentaryTable toTable(Member member) { return info.toTable(member); diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java index 9313ea90..7cf15f01 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTimeBoxCreateRequest.java @@ -1,17 +1,17 @@ package com.debatetimer.dto.parliamentary.request; -import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.Stance; +import com.debatetimer.domain.parliamentary.ParliamentaryBoxType; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.domain.parliamentary.ParliamentaryTimeBox; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; public record ParliamentaryTimeBoxCreateRequest( - @NotBlank + @NotNull Stance stance, - @NotBlank + @NotNull ParliamentaryBoxType type, @Positive diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java index 3012958a..c051f94c 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTableCreateRequest.java @@ -4,12 +4,15 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.domain.timebased.TimeBasedTimeBox; +import jakarta.validation.Valid; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; -public record TimeBasedTableCreateRequest(TimeBasedTableInfoCreateRequest info, - List table) { +public record TimeBasedTableCreateRequest( + @Valid TimeBasedTableInfoCreateRequest info, + @Valid List table +) { public TimeBasedTable toTable(Member member) { return info.toTable(member); diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java index d9419190..10dea4e1 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java @@ -4,13 +4,13 @@ import com.debatetimer.domain.timebased.TimeBasedBoxType; import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.domain.timebased.TimeBasedTimeBox; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record TimeBasedTimeBoxCreateRequest( - @NotBlank + @NotNull Stance stance, - @NotBlank + @NotNull TimeBasedBoxType type, int time, diff --git a/src/test/java/com/debatetimer/domain/DebateTableTest.java b/src/test/java/com/debatetimer/domain/DebateTableTest.java index 3e35a8d5..3035f0c8 100644 --- a/src/test/java/com/debatetimer/domain/DebateTableTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTableTest.java @@ -37,20 +37,12 @@ class Validate { .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); } - @ValueSource(ints = {0, DebateTable.NAME_MAX_LENGTH + 1}) - @ParameterizedTest - void 테이블_이름은_정해진_길이_이내여야_한다(int length) { + @Test + void 테이블_이름은_정해진_길이_이내여야_한다() { 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()); - } + String longTableName = "f".repeat(DebateTable.NAME_MAX_LENGTH + 1); - @ValueSource(strings = {"", "\t", "\n"}) - @ParameterizedTest - void 테이블_이름은_적어도_한_자_있어야_한다(String name) { - Member member = new Member("default@gmail.com"); - assertThatThrownBy(() -> new DebateTableTestObject(member, name, "agenda", true, true)) + assertThatThrownBy(() -> new DebateTableTestObject(member, longTableName, "agenda", true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); } diff --git a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java index f9f694c8..d6ea6af1 100644 --- a/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/DebateTimeBoxTest.java @@ -1,5 +1,6 @@ package com.debatetimer.domain; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -8,6 +9,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; class DebateTimeBoxTest { @@ -42,23 +44,29 @@ class ValidateTime { @Nested class ValidateSpeaker { - @ParameterizedTest - @ValueSource(ints = {0, DebateTimeBox.SPEAKER_MAX_LENGTH + 1}) - void 발언자는_일정길이_이내로_허용된다(int length) { - String speaker = "k".repeat(length); + @Test + void 발언자_이름은_일정길이_이내로_허용된다() { + String speaker = "k".repeat(DebateTimeBox.SPEAKER_MAX_LENGTH + 1); assertThatThrownBy(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER_LENGTH.getMessage()); } - @Test - void 발언자는_빈_값이_허용된다() { - String speaker = null; - + @NullSource + @ParameterizedTest + void 발언자는_빈_값이_허용된다(String speaker) { assertThatCode(() -> new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker)) .doesNotThrowAnyException(); } + + @ValueSource(strings = {" ", " "}) + @ParameterizedTest + void 발언자는_공백이_입력되면_null로_저장된다(String speaker) { + DebateTimeBoxTestObject timeBox = new DebateTimeBoxTestObject(1, Stance.CONS, 60, speaker); + + assertThat(timeBox.getSpeaker()).isNull(); + } } private static class DebateTimeBoxTestObject extends DebateTimeBox { diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java index d47b287d..e2f5d389 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTableTest.java @@ -29,22 +29,24 @@ class GetType { @Nested class ValidateTeamName { - @ValueSource(ints = {0, CustomizeTable.TEAM_NAME_MAX_LENGTH + 1}) - @ParameterizedTest - void 찬성_팀_이름은_정해진_길이_이내여야_한다(int length) { + @Test + void 찬성_팀_이름은_정해진_길이_이내여야_한다() { Member member = new Member("default@gmail.com"); + String longProsTeamName = "f".repeat(CustomizeTable.TEAM_NAME_MAX_LENGTH + 1); + assertThatThrownBy( - () -> new CustomizeTable(member, "name", "agenda", true, true, "f".repeat(length), "cons")) + () -> new CustomizeTable(member, "name", "agenda", true, true, longProsTeamName, "cons")) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); } - @ValueSource(ints = {0, CustomizeTable.TEAM_NAME_MAX_LENGTH + 1}) - @ParameterizedTest - void 반대_팀_이름은_정해진_길이_이내여야_한다(int length) { + @Test + void 반대_팀_이름은_정해진_길이_이내여야_한다() { Member member = new Member("default@gmail.com"); + String longConsTeamName = "f".repeat(CustomizeTable.TEAM_NAME_MAX_LENGTH + 1); + assertThatThrownBy( - () -> new CustomizeTable(member, "name", "agenda", true, true, "pros", "f".repeat(length))) + () -> new CustomizeTable(member, "name", "agenda", true, true, "pros", longConsTeamName)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TEAM_NAME_LENGTH.getMessage()); } diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java index 911016b1..1b7780f6 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java @@ -8,8 +8,6 @@ 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 CustomizeTimeBoxTest { @@ -81,11 +79,10 @@ class ValidateCustomizeTime { .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); } - @ParameterizedTest - @ValueSource(ints = {0, CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH + 1}) - void 발언_유형의_길이는_일정_범위_이내여야_한다(int length) { + @Test + void 발언_유형의_길이는_일정_범위_이내여야_한다() { CustomizeTable table = new CustomizeTable(); - String longSpeechType = "s".repeat(length); + String longSpeechType = "s".repeat(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH + 1); assertThatThrownBy( () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, longSpeechType, CustomizeBoxType.TIME_BASED, From 4094cc3c2c3d625c7f43b843389961c6bd8d25ad Mon Sep 17 00:00:00 2001 From: Chung-an Lee <44027393+leegwichan@users.noreply.github.com> Date: Fri, 4 Apr 2025 23:24:37 +0900 Subject: [PATCH 29/33] =?UTF-8?q?[FEAT]=20=EC=9E=90=EC=9C=A0=20=ED=86=A0?= =?UTF-8?q?=EB=A1=A0=20=ED=83=80=EC=9E=84=20=EB=B0=95=EC=8A=A4=20nullable?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/customize/CustomizeTimeBox.java | 34 +++++++------ .../domain/timebased/TimeBasedTimeBox.java | 12 ++--- .../CustomizeTimeBoxCreateRequest.java | 12 ++++- .../TimeBasedTimeBoxCreateRequest.java | 2 +- .../exception/errorcode/ClientErrorCode.java | 1 - .../customize/CustomizeTimeBoxTest.java | 49 ++++++++++++------- .../timebased/TimeBasedTimeBoxTest.java | 37 ++++++++------ 7 files changed, 85 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java index a64217b8..cc98e171 100644 --- a/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java +++ b/src/main/java/com/debatetimer/domain/customize/CustomizeTimeBox.java @@ -25,6 +25,7 @@ public class CustomizeTimeBox extends DebateTimeBox { public static final int SPEECH_TYPE_MAX_LENGTH = 10; + public static final int TIME_MULTIPLIER = 2; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -51,7 +52,7 @@ public CustomizeTimeBox( Stance stance, String speechType, CustomizeBoxType boxType, - int time, + Integer time, String speaker ) { super(sequence, stance, time, speaker); @@ -69,14 +70,12 @@ public CustomizeTimeBox( Stance stance, String speechType, CustomizeBoxType boxType, - int time, - int timePerTeam, + Integer timePerTeam, Integer timePerSpeaking, String speaker ) { - super(sequence, stance, time, speaker); - validateTime(timePerTeam, timePerSpeaking); - validateTimeBasedTime(time, timePerTeam); + super(sequence, stance, convertToTime(timePerTeam), speaker); + validateTimeBasedTimes(timePerTeam, timePerSpeaking); validateTimeBasedType(boxType); validateSpeechType(speechType); @@ -87,26 +86,31 @@ public CustomizeTimeBox( this.timePerSpeaking = timePerSpeaking; } - private void validateTime(int time) { - if (time <= 0) { + private static int convertToTime(Integer timePerTeam) { + if (timePerTeam == null) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); + } + return timePerTeam * TIME_MULTIPLIER; + } + + private void validateTime(Integer time) { + if (time == null || time <= 0) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_TIME); } } - private void validateTime(int timePerTeam, int timePerSpeaking) { + private void validateTimeBasedTimes(Integer timePerTeam, Integer timePerSpeaking) { validateTime(timePerTeam); + if (timePerSpeaking == null) { + return; + } + validateTime(timePerSpeaking); if (timePerTeam < timePerSpeaking) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME); } } - private void validateTimeBasedTime(int time, int timePerTeam) { - if (time != timePerTeam * 2) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE); - } - } - private void validateTimeBasedType(CustomizeBoxType boxType) { if (boxType.isNotTimeBased()) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_FORMAT); diff --git a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java index 051a7b3f..0995acf0 100644 --- a/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java +++ b/src/main/java/com/debatetimer/domain/timebased/TimeBasedTimeBox.java @@ -23,6 +23,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class TimeBasedTimeBox extends DebateTimeBox { + public static final int TIME_MULTIPLIER = 2; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -60,14 +62,12 @@ public TimeBasedTimeBox( int sequence, Stance stance, TimeBasedBoxType type, - int time, int timePerTeam, int timePerSpeaking, Integer speaker ) { - super(sequence, stance, time, String.valueOf(speaker)); + super(sequence, stance, timePerTeam * TIME_MULTIPLIER, String.valueOf(speaker)); validateTime(timePerTeam, timePerSpeaking); - validateTimeBasedTime(time, timePerTeam); validateStance(stance, type); validateTimeBasedType(type); validateSpeakerNumber(speaker); @@ -92,12 +92,6 @@ private void validateTime(int timePerTeam, int timePerSpeaking) { } } - private void validateTimeBasedTime(int time, int timePerTeam) { - if (time != timePerTeam * 2) { - throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE); - } - } - private void validateStance(Stance stance, TimeBasedBoxType boxType) { if (!boxType.isAvailable(stance)) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); diff --git a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java index a040e47e..d5c6d320 100644 --- a/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/customize/request/CustomizeTimeBoxCreateRequest.java @@ -6,6 +6,7 @@ import com.debatetimer.domain.customize.CustomizeTimeBox; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import org.springframework.lang.Nullable; public record CustomizeTimeBoxCreateRequest( @NotNull @@ -17,15 +18,22 @@ public record CustomizeTimeBoxCreateRequest( @NotNull CustomizeBoxType boxType, - int time, + @Nullable + Integer time, + + @Nullable Integer timePerTeam, + + @Nullable Integer timePerSpeaking, + + @Nullable String speaker ) { public CustomizeTimeBox toTimeBox(CustomizeTable customizeTable, int sequence) { if (boxType.isTimeBased()) { - return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, time, timePerTeam, + return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, timePerTeam, timePerSpeaking, speaker); } return new CustomizeTimeBox(customizeTable, sequence, stance, speechType, boxType, time, speaker); diff --git a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java index 10dea4e1..d3da4518 100644 --- a/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/timebased/request/TimeBasedTimeBoxCreateRequest.java @@ -21,7 +21,7 @@ public record TimeBasedTimeBoxCreateRequest( public TimeBasedTimeBox toTimeBox(TimeBasedTable timeBasedTable, int sequence) { if (type.isTimeBased()) { - return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, time, timePerTeam, timePerSpeaking, + return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, timePerTeam, timePerSpeaking, speakerNumber); } return new TimeBasedTimeBox(timeBasedTable, sequence, stance, type, time, speakerNumber); diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 2e6405d2..72e772c7 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -26,7 +26,6 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_TIME_BOX_STANCE(HttpStatus.BAD_REQUEST, "타임박스 유형과 일치하지 않는 입장입니다."), INVALID_TIME_BOX_FORMAT(HttpStatus.BAD_REQUEST, "타임박스 유형과 일치하지 않는 형식입니다"), INVALID_TIME_BASED_TIME(HttpStatus.BAD_REQUEST, "팀 발언 시간은 개인 발언 시간보다 길어야합니다"), - INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE(HttpStatus.BAD_REQUEST, "총 시간은 팀 발언 시간의 2배여야 합니다"), INVALID_TIME_BOX_SPEECH_TYPE_LENGTH( HttpStatus.BAD_REQUEST, "발언 유형 이름은 1자 이상 %d자 이하여야 합니다.".formatted(CustomizeTimeBox.SPEECH_TYPE_MAX_LENGTH) diff --git a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java index 1b7780f6..4d482d1d 100644 --- a/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/customize/CustomizeTimeBoxTest.java @@ -1,5 +1,6 @@ package com.debatetimer.domain.customize; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -14,26 +15,13 @@ class CustomizeTimeBoxTest { @Nested class ValidateCustomizeTime { - @Test - void 자유토론_타입은_총_시간이_팀_발언_시간의_2배여야_한다() { - CustomizeTable table = new CustomizeTable(); - CustomizeBoxType customizeBoxType = CustomizeBoxType.TIME_BASED; - - int totalTime = 150; - int timePerTeam = 120; - - assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", customizeBoxType, totalTime, - timePerTeam, 60, "발언자")).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, "발언자") + customizeBoxType, 120, 60, "발언자") ).doesNotThrowAnyException(); } @@ -53,11 +41,21 @@ class ValidateCustomizeTime { CustomizeBoxType notTimeBasedBoxType = CustomizeBoxType.NORMAL; assertThatThrownBy( - () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 240, 120, 60, + () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", notTimeBasedBoxType, 120, 60, "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } + @Test + void 팀_발언_시간은_있으며_개인_발언_시간은_없을_수_있다() { + CustomizeTable table = new CustomizeTable(); + Integer timePerTeam = 60; + Integer timePerSpeaking = null; + + assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); + } + @Test void 개인_발언_시간은_팀_발언_시간보다_적거나_같아야_한다() { CustomizeTable table = new CustomizeTable(); @@ -65,7 +63,7 @@ class ValidateCustomizeTime { int timePerSpeaking = 59; assertThatCode(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam * 2, timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); + timePerTeam, timePerSpeaking, "발언자")).doesNotThrowAnyException(); } @Test @@ -75,7 +73,7 @@ class ValidateCustomizeTime { int timePerSpeaking = 61; assertThatThrownBy(() -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, "자유토론", CustomizeBoxType.TIME_BASED, - timePerTeam * 2, timePerTeam, timePerSpeaking, "발언자")).isInstanceOf(DTClientErrorException.class) + timePerTeam, timePerSpeaking, "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); } @@ -86,8 +84,23 @@ class ValidateCustomizeTime { assertThatThrownBy( () -> new CustomizeTimeBox(table, 1, Stance.NEUTRAL, longSpeechType, CustomizeBoxType.TIME_BASED, - 240, 120, 60, "발언자")).isInstanceOf(DTClientErrorException.class) + 120, 60, "발언자")).isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEECH_TYPE_LENGTH.getMessage()); } } + + @Nested + class getTime { + + @Test + void 자유_토론_타임_박스의_시간은_팀_당_발언_시간의_배수이어야_한다() { + int timePerTeam = 300; + int timePerSpeaking = 120; + CustomizeTable table = new CustomizeTable(); + CustomizeTimeBox timeBasedTimeBox = new CustomizeTimeBox(table, 1, Stance.CONS, "자유 토론", + CustomizeBoxType.TIME_BASED, timePerTeam, timePerSpeaking, "콜리"); + + assertThat(timeBasedTimeBox.getTime()).isEqualTo(timePerTeam * CustomizeTimeBox.TIME_MULTIPLIER); + } + } } diff --git a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java index ca7842ee..5ea3b7ae 100644 --- a/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/timebased/TimeBasedTimeBoxTest.java @@ -1,5 +1,6 @@ package com.debatetimer.domain.timebased; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -38,23 +39,12 @@ class ValidateStance { @Nested class ValidateTimeBased { - @Test - void 시간총량제_타입은_총_시간이_팀_발언_시간의_2배여야_한다() { - TimeBasedTable table = new TimeBasedTable(); - TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; - - assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 150, 120, 60, 1)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME_IS_NOT_DOUBLE.getMessage()); - } - @Test void 시간총량제_타입은_개인_발언_시간과_팀_발언_시간을_입력해야_한다() { TimeBasedTable table = new TimeBasedTable(); TimeBasedBoxType timeBasedBoxType = TimeBasedBoxType.TIME_BASED; - assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 240, 120, 60, 1)) + assertThatCode(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, timeBasedBoxType, 120, 60, 1)) .doesNotThrowAnyException(); } @@ -74,7 +64,7 @@ class ValidateTimeBased { TimeBasedBoxType notTimeBasedBoxType = TimeBasedBoxType.TIME_OUT; assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, notTimeBasedBoxType, 240, 120, 60, 1)) + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, notTimeBasedBoxType, 120, 60, 1)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_FORMAT.getMessage()); } @@ -86,7 +76,7 @@ class ValidateTimeBased { int timePerSpeaking = 59; assertThatCode( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam * 2, + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam, timePerSpeaking, 1)) .doesNotThrowAnyException(); } @@ -98,7 +88,7 @@ class ValidateTimeBased { int timePerSpeaking = 61; assertThatThrownBy( - () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam * 2, + () -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, timePerTeam, timePerSpeaking, 1)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIME.getMessage()); @@ -114,9 +104,24 @@ class ValidateSpeakerNumber { TimeBasedTable table = new TimeBasedTable(); assertThatThrownBy(() -> new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, - 240, 120, 60, speaker)) + 120, 60, speaker)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); } } + + @Nested + class getTime { + + @Test + void 자유_토론_타임_박스의_시간은_팀_당_발언_시간의_배수이어야_한다() { + int timePerTeam = 300; + int timePerSpeaking = 120; + TimeBasedTable table = new TimeBasedTable(); + TimeBasedTimeBox timeBasedTimeBox = new TimeBasedTimeBox(table, 1, Stance.NEUTRAL, TimeBasedBoxType.TIME_BASED, + timePerTeam, timePerSpeaking, 1); + + assertThat(timeBasedTimeBox.getTime()).isEqualTo(timePerTeam * TimeBasedTimeBox.TIME_MULTIPLIER); + } + } } From cf8f2ca631a8f373170ed5385e9f08f7fe5c1dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Fri, 4 Apr 2025 23:54:43 +0900 Subject: [PATCH 30/33] =?UTF-8?q?[REFACTOR]=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20?= =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=20(#143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/member/TableResponses.java | 19 ++++++++++++------- .../service/member/MemberService.java | 10 +++++----- .../member/MemberControllerTest.java | 4 +++- .../service/member/MemberServiceTest.java | 6 +++--- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/debatetimer/dto/member/TableResponses.java b/src/main/java/com/debatetimer/dto/member/TableResponses.java index 651acfbc..51cbde50 100644 --- a/src/main/java/com/debatetimer/dto/member/TableResponses.java +++ b/src/main/java/com/debatetimer/dto/member/TableResponses.java @@ -1,8 +1,8 @@ package com.debatetimer.dto.member; import com.debatetimer.domain.DebateTable; +import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.timebased.TimeBasedTable; import java.util.Comparator; import java.util.List; import java.util.stream.Stream; @@ -13,14 +13,19 @@ public record TableResponses(List tables) { .comparing(DebateTable::getUsedAt) .reversed(); - public TableResponses(List parliamentaryTables, - List timeBasedTables) { - this(toTableResponses(parliamentaryTables, timeBasedTables)); + public TableResponses( + List parliamentaryTables, + List customizeTables + ) { + this(toTableResponses(parliamentaryTables, customizeTables)); } - private static List toTableResponses(List parliamentaryTables, - List timeBasedTables) { - return Stream.concat(parliamentaryTables.stream(), timeBasedTables.stream()) + private static List toTableResponses( + List parliamentaryTables, + List customizeTables + ) { + return Stream.of(parliamentaryTables, customizeTables) + .flatMap(List::stream) .sorted(DEBATE_TABLE_COMPARATOR) .map(TableResponse::new) .toList(); diff --git a/src/main/java/com/debatetimer/service/member/MemberService.java b/src/main/java/com/debatetimer/service/member/MemberService.java index 67bb3b7d..210b2067 100644 --- a/src/main/java/com/debatetimer/service/member/MemberService.java +++ b/src/main/java/com/debatetimer/service/member/MemberService.java @@ -1,14 +1,14 @@ package com.debatetimer.service.member; +import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.dto.member.MemberCreateResponse; import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; +import com.debatetimer.repository.customize.CustomizeTableRepository; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; -import com.debatetimer.repository.timebased.TimeBasedTableRepository; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -20,14 +20,14 @@ public class MemberService { private final MemberRepository memberRepository; private final ParliamentaryTableRepository parliamentaryTableRepository; - private final TimeBasedTableRepository timeBasedTableRepository; + private final CustomizeTableRepository customizeTableRepository; @Transactional(readOnly = true) public TableResponses getTables(long memberId) { Member member = memberRepository.getById(memberId); List parliamentaryTables = parliamentaryTableRepository.findAllByMember(member); - List timeBasedTables = timeBasedTableRepository.findAllByMember(member); - return new TableResponses(parliamentaryTables, timeBasedTables); + List customizeTables = customizeTableRepository.findAllByMember(member); + return new TableResponses(parliamentaryTables, customizeTables); } @Transactional diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index e3ae4650..0028d5aa 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.doReturn; import com.debatetimer.controller.BaseControllerTest; +import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.dto.member.MemberCreateRequest; @@ -24,7 +25,8 @@ class GetTables { void 회원의_전체_토론_시간표를_조회한다() { Member member = memberGenerator.generate("default@gmail.com"); parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", false, false)); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", false, false)); + customizeTableRepository.save(new CustomizeTable(member, "커스텀 테이블", "주제", false, false, + "찬성", "반대")); Headers headers = headerGenerator.generateAccessTokenHeader(member); diff --git a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java index 08e3b496..17c6000c 100644 --- a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java +++ b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java @@ -3,9 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import com.debatetimer.domain.customize.CustomizeTable; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.domain.timebased.TimeBasedTable; import com.debatetimer.dto.member.MemberCreateResponse; import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; @@ -54,7 +54,7 @@ class GetTables { void 회원의_전체_토론_시간표를_조회한다() { Member member = memberGenerator.generate("default@gmail.com"); parliamentaryTableGenerator.generate(member); - timeBasedTableGenerator.generate(member); + customizeTableGenerator.generate(member); TableResponses response = memberService.getTables(member.getId()); @@ -65,7 +65,7 @@ class GetTables { void 회원의_전체_토론_시간표는_정해진_순서대로_반환한다() throws InterruptedException { Member member = memberGenerator.generate("default@gmail.com"); ParliamentaryTable table1 = parliamentaryTableGenerator.generate(member); - TimeBasedTable table2 = timeBasedTableGenerator.generate(member); + CustomizeTable table2 = customizeTableGenerator.generate(member); Thread.sleep(1); table1.updateUsedAt(); From f41ac2961e0e602e26b16c3abff33d0c8f409794 Mon Sep 17 00:00:00 2001 From: Chung-an Lee <44027393+leegwichan@users.noreply.github.com> Date: Fri, 4 Apr 2025 23:55:03 +0900 Subject: [PATCH 31/33] =?UTF-8?q?[REFACTOR]=20speakerNumber=20parseInt?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20NPE=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parliamentary/ParliamentaryTimeBox.java | 17 +++++++++- .../ParliamentaryTimeBoxResponse.java | 9 +----- .../ParliamentaryTimeBoxTest.java | 31 +++++++++++++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java index e494bc76..a987552d 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBox.java @@ -44,7 +44,7 @@ public ParliamentaryTimeBox( int time, Integer speaker ) { - super(sequence, stance, time, String.valueOf(speaker)); + super(sequence, stance, time, convertToSpeaker(speaker)); validate(stance, type); validateSpeakerNumber(speaker); @@ -52,6 +52,13 @@ public ParliamentaryTimeBox( this.type = type; } + private static String convertToSpeaker(Integer speakerNumber) { + if (speakerNumber == null) { + return null; + } + return String.valueOf(speakerNumber); + } + private void validate(Stance stance, ParliamentaryBoxType boxType) { if (!boxType.isAvailable(stance)) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_STANCE); @@ -63,4 +70,12 @@ private void validateSpeakerNumber(Integer speaker) { throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BOX_SPEAKER); } } + + public Integer getSpeakerNumber() { + String speaker = getSpeaker(); + if (speaker == null || speaker.isBlank() || speaker.equals("null")) { + return null; + } + return Integer.parseInt(speaker); + } } diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java index bad541ea..d30cfade 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/ParliamentaryTimeBoxResponse.java @@ -10,14 +10,7 @@ public ParliamentaryTimeBoxResponse(ParliamentaryTimeBox parliamentaryTimeBox) { this(parliamentaryTimeBox.getStance(), parliamentaryTimeBox.getType(), parliamentaryTimeBox.getTime(), - getSpeakerNumber(parliamentaryTimeBox) + parliamentaryTimeBox.getSpeakerNumber() ); } - - private static Integer getSpeakerNumber(ParliamentaryTimeBox parliamentaryTimeBox) { - if (parliamentaryTimeBox.getSpeaker() == null || parliamentaryTimeBox.getSpeaker().equals("null")) { - return null; - } - return Integer.parseInt(parliamentaryTimeBox.getSpeaker()); - } } diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java index 827e414b..dcd6cbb8 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxTest.java @@ -1,5 +1,6 @@ package com.debatetimer.domain.parliamentary; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -38,13 +39,39 @@ class ValidateSpeakerNumber { @ValueSource(ints = {0, -1}) @ParameterizedTest - void 의회식_타임박스의_발표자_번호는_양수만_가능하다(int speaker) { + void 의회식_타임박스의_발표자_번호_음수는_불가능하다(int speakerNumber) { ParliamentaryTable table = new ParliamentaryTable(); assertThatThrownBy( - () -> new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speaker)) + () -> new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speakerNumber)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TIME_BOX_SPEAKER.getMessage()); } } + + @Nested + class getSpeakerNumber { + + @ValueSource(ints = {1, 5}) + @ParameterizedTest + void 의회식_타임박스의_발표자_번호는_양수만_가능하다(int speakerNumber) { + ParliamentaryTable table = new ParliamentaryTable(); + ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speakerNumber); + + Integer actual = timeBox.getSpeakerNumber(); + + assertThat(actual).isEqualTo(speakerNumber); + } + + @Test + void 의회식_타임박스의_발표자는_비어있을_수_있다() { + ParliamentaryTable table = new ParliamentaryTable(); + Integer speakerNumber = null; + ParliamentaryTimeBox timeBox = new ParliamentaryTimeBox(table, 1, Stance.PROS, ParliamentaryBoxType.OPENING, 10, speakerNumber); + + Integer actual = timeBox.getSpeakerNumber(); + + assertThat(actual).isEqualTo(speakerNumber); + } + } } From 2d092c533f6747e72ba947f4073aa7c826dd98aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Sat, 5 Apr 2025 11:14:03 +0900 Subject: [PATCH 32/33] =?UTF-8?q?[REFACTOR]=20time=5Fbased=20=EC=9D=BC=20?= =?UTF-8?q?=EB=95=8C=20time=EC=9D=84=20null=EB=A1=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/customize/response/CustomizeTimeBoxResponse.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java index 632cf214..113bd23e 100644 --- a/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java +++ b/src/main/java/com/debatetimer/dto/customize/response/CustomizeTimeBoxResponse.java @@ -19,10 +19,17 @@ public CustomizeTimeBoxResponse(CustomizeTimeBox customizeTimeBox) { customizeTimeBox.getStance(), customizeTimeBox.getSpeechType(), customizeTimeBox.getBoxType(), - customizeTimeBox.getTime(), + convertTime(customizeTimeBox), customizeTimeBox.getTimePerTeam(), customizeTimeBox.getTimePerSpeaking(), customizeTimeBox.getSpeaker() ); } + + private static Integer convertTime(CustomizeTimeBox customizeTimeBox) { + if (customizeTimeBox.getBoxType() == CustomizeBoxType.TIME_BASED) { + return null; + } + return customizeTimeBox.getTime(); + } } From f7ebdfb3400548b1577a73acd1f73249ac7f63d4 Mon Sep 17 00:00:00 2001 From: Chung-an Lee <44027393+leegwichan@users.noreply.github.com> Date: Tue, 8 Apr 2025 23:17:26 +0900 Subject: [PATCH 33/33] =?UTF-8?q?[CHORE]=20=EB=AA=A8=EB=8B=88=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20(#149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 18 ++++++++------ scripts/dev/replace-new-version.sh | 2 +- scripts/prod/replace-new-version.sh | 2 +- src/main/resources/application-monitor.yml | 24 +++++++++++++++++++ .../com/debatetimer/domain/TimeBoxesTest.java | 2 +- 5 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/application-monitor.yml diff --git a/build.gradle b/build.gradle index e617a826..c35390a3 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,17 @@ dependencies { implementation 'org.apache.poi:poi-ooxml:5.2.3' implementation 'org.apache.poi:poi:5.2.3' + // Logging + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" + + // Discord + implementation 'net.dv8tion:JDA:5.0.0-beta.24' + + // Monitoring + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' + // DB schema manager implementation 'org.flywaydb:flyway-mysql' @@ -64,13 +75,6 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2' - - // Logging - implementation 'org.springframework.boot:spring-boot-starter-log4j2' - implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml" - - //Discord - implementation 'net.dv8tion:JDA:5.0.0-beta.24' } bootJar { diff --git a/scripts/dev/replace-new-version.sh b/scripts/dev/replace-new-version.sh index e86b27e8..3fd434fb 100644 --- a/scripts/dev/replace-new-version.sh +++ b/scripts/dev/replace-new-version.sh @@ -19,4 +19,4 @@ fi JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1) -sudo nohup java -Dspring.profiles.active=dev -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & +sudo nohup java -Dspring.profiles.active=dev,monitor -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & diff --git a/scripts/prod/replace-new-version.sh b/scripts/prod/replace-new-version.sh index d78b646f..ea5d2914 100644 --- a/scripts/prod/replace-new-version.sh +++ b/scripts/prod/replace-new-version.sh @@ -19,4 +19,4 @@ fi JAR_FILE=$(ls /home/ubuntu/app/*.jar | head -n 1) -sudo nohup java -Dspring.profiles.active=prod -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & +sudo nohup java -Dspring.profiles.active=prod,monitor -Duser.timezone=Asia/Seoul -Dserver.port=8080 -jar "$JAR_FILE" & diff --git a/src/main/resources/application-monitor.yml b/src/main/resources/application-monitor.yml new file mode 100644 index 00000000..d9809cb1 --- /dev/null +++ b/src/main/resources/application-monitor.yml @@ -0,0 +1,24 @@ +management: + server: + port: 8083 + + endpoints: + web: + exposure: + include: '*' + base-path: /monitoring + + endpoint: + health: + show-components: always + + info: + java: + enabled: true + os: + enabled: true + +server: + tomcat: + mbeanregistry: + enabled: true diff --git a/src/test/java/com/debatetimer/domain/TimeBoxesTest.java b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java index c4534a71..c3a80805 100644 --- a/src/test/java/com/debatetimer/domain/TimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/TimeBoxesTest.java @@ -27,7 +27,7 @@ class SortedBySequence { ParliamentaryBoxType.OPENING, 300, 1); List timeBoxes = new ArrayList<>(Arrays.asList(secondBox, firstBox)); - TimeBoxes actual = new TimeBoxes(timeBoxes); + TimeBoxes actual = new TimeBoxes<>(timeBoxes); assertThat(actual.getTimeBoxes()).containsExactly(firstBox, secondBox); }