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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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/21] =?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: