diff --git a/.github/workflows/Dev_CD.yml b/.github/workflows/Dev_CD.yml index 1aae3280..159dc8b7 100644 --- a/.github/workflows/Dev_CD.yml +++ b/.github/workflows/Dev_CD.yml @@ -16,7 +16,7 @@ jobs: - name: Setting dev-secret.yml run: | - echo "${{ secrets.DEV_SECRET_YML }}" > ./src/main/resources/dev-secret.yml + echo "${{ secrets.DEV_SECRET_TEMP_YML }}" > ./src/main/resources/dev-secret.yml - name: Set up JDK 17 uses: actions/setup-java@v4 @@ -52,7 +52,7 @@ jobs: deploy: needs: build - runs-on: dev + runs-on: dev-temp steps: - name: Download artifact file diff --git a/.github/workflows/Prod_CD.yml b/.github/workflows/Prod_CD.yml index dad56d3a..ac8d5179 100644 --- a/.github/workflows/Prod_CD.yml +++ b/.github/workflows/Prod_CD.yml @@ -16,7 +16,7 @@ jobs: - name: Setting prod-secret.yml run: | - echo "${{ secrets.PROD_SECRET_YML }}" > ./src/main/resources/prod-secret.yml + echo "${{ secrets.PROD_SECRET_TEMP_YML }}" > ./src/main/resources/prod-secret.yml - name: Set up JDK 17 uses: actions/setup-java@v4 @@ -52,7 +52,7 @@ jobs: deploy: needs: build - runs-on: prod + runs-on: prod-temp steps: - name: Download artifact file diff --git a/scripts/dev/zero-downtime-deploy.sh b/scripts/dev/zero-downtime-deploy.sh index 992252c3..af1d0b52 100644 --- a/scripts/dev/zero-downtime-deploy.sh +++ b/scripts/dev/zero-downtime-deploy.sh @@ -163,7 +163,7 @@ switch_nginx_upstream() { log "Switching nginx upstream to port $new_port" sudo cp "$nginx_conf" "$backup_conf" - sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$new_port;/" "$nginx_conf" > "$temp_conf" + sed "s|proxy_pass http://127\.0\.0\.1:[0-9]\+;|proxy_pass http://127.0.0.1:$new_port;|" "$nginx_conf" > "$temp_conf" sudo cp "$temp_conf" "$nginx_conf" if ! sudo nginx -t 2>/dev/null; then diff --git a/scripts/nginx-switch-port.sh b/scripts/nginx-switch-port.sh index 586f5c67..fe814a15 100644 --- a/scripts/nginx-switch-port.sh +++ b/scripts/nginx-switch-port.sh @@ -33,7 +33,7 @@ log "Backing up current nginx configuration" sudo cp "$NGINX_CONF" "$BACKUP_CONF" log "Updating nginx upstream to port $NEW_PORT" -sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$NEW_PORT;/" "$NGINX_CONF" > "$TEMP_CONF" +sed "s|proxy_pass http://127\.0\.0\.1:[0-9]\+;|proxy_pass http://127.0.0.1:$NEW_PORT;|" "$NGINX_CONF" > "$TEMP_CONF" log "Configuration changes:" diff "$NGINX_CONF" "$TEMP_CONF" || true diff --git a/scripts/prod/zero-downtime-deploy.sh b/scripts/prod/zero-downtime-deploy.sh index c4b41432..e60ae48b 100644 --- a/scripts/prod/zero-downtime-deploy.sh +++ b/scripts/prod/zero-downtime-deploy.sh @@ -163,7 +163,7 @@ switch_nginx_upstream() { log "Switching nginx upstream to port $new_port" sudo cp "$nginx_conf" "$backup_conf" - sed "s/server 127\.0\.0\.1:[0-9]\+;/server 127.0.0.1:$new_port;/" "$nginx_conf" > "$temp_conf" + sed "s|proxy_pass http://127\.0\.0\.1:[0-9]\+;|proxy_pass http://127.0.0.1:$new_port;|" "$nginx_conf" > "$temp_conf" sudo cp "$temp_conf" "$nginx_conf" if ! sudo nginx -t 2>/dev/null; then diff --git a/src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java b/src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java index de16ac9f..19c76364 100644 --- a/src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java +++ b/src/main/java/com/debatetimer/config/sharing/WebSocketAuthMemberResolver.java @@ -34,7 +34,7 @@ public Object resolveArgument(MethodParameter parameter, Message message) { throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER); } - String email = authManager.resolveAccessToken(token); + String email = authManager.resolveChairmanToken(token); return authService.getMember(email); } } diff --git a/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java index b2ee31a6..aeb9c819 100644 --- a/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java +++ b/src/main/java/com/debatetimer/config/sharing/WebSocketConfig.java @@ -1,11 +1,14 @@ package com.debatetimer.config.sharing; import com.debatetimer.config.CorsProperties; +import java.time.Duration; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @@ -15,6 +18,12 @@ @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + private static final long SERVER_TO_CLIENT_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis(); + private static final long CLIENT_TO_SERVER_HEARTBEAT_DURATION = Duration.ofSeconds(10).toMillis(); + private static final long SOCKJS_HEART_BEAT_DURATION = Duration.ofSeconds(10).toMillis(); + private static final String HEART_BEAT_THREAD_PREFIX = "wss-heartbeat-"; + private static final int HEART_BEAT_THREAD_COUNT = 1; + private final CorsProperties corsProperties; private final WebSocketAuthMemberResolver webSocketAuthMemberResolver; @@ -25,7 +34,9 @@ public void addArgumentResolvers(List resolvers) @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.enableSimpleBroker("/room", "/chairman"); + registry.enableSimpleBroker("/room", "/chairman") + .setHeartbeatValue(new long[]{SERVER_TO_CLIENT_HEARTBEAT_DURATION, CLIENT_TO_SERVER_HEARTBEAT_DURATION}) + .setTaskScheduler(heartBeatScheduler()); registry.setApplicationDestinationPrefixes("/app"); } @@ -33,6 +44,16 @@ public void configureMessageBroker(MessageBrokerRegistry registry) { public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .setAllowedOriginPatterns(corsProperties.getCorsOrigin()) - .withSockJS(); + .withSockJS() + .setHeartbeatTime(SOCKJS_HEART_BEAT_DURATION); + } + + @Bean + public ThreadPoolTaskScheduler heartBeatScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(HEART_BEAT_THREAD_COUNT); + scheduler.setThreadNamePrefix(HEART_BEAT_THREAD_PREFIX); + scheduler.initialize(); + return scheduler; } } diff --git a/src/main/java/com/debatetimer/controller/customize/CustomizeController.java b/src/main/java/com/debatetimer/controller/customize/CustomizeController.java index 6716d9f1..5577cec7 100644 --- a/src/main/java/com/debatetimer/controller/customize/CustomizeController.java +++ b/src/main/java/com/debatetimer/controller/customize/CustomizeController.java @@ -39,7 +39,7 @@ public CustomizeTableResponse getTable( @PathVariable Long tableId, @AuthMember Member member ) { - return customizeService.findTable(tableId, member); + return customizeService.findMemberTable(tableId, member); } @PutMapping("/api/table/customize/{tableId}") diff --git a/src/main/java/com/debatetimer/controller/organization/OrganizationController.java b/src/main/java/com/debatetimer/controller/organization/OrganizationController.java index 46efeae6..625adb37 100644 --- a/src/main/java/com/debatetimer/controller/organization/OrganizationController.java +++ b/src/main/java/com/debatetimer/controller/organization/OrganizationController.java @@ -1,10 +1,12 @@ package com.debatetimer.controller.organization; +import com.debatetimer.domain.organization.Language; import com.debatetimer.dto.organization.OrganizationResponses; import com.debatetimer.service.organization.OrganizationService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -14,7 +16,8 @@ public class OrganizationController { private final OrganizationService organizationService; @GetMapping("/api/organizations/templates") - public ResponseEntity getOrganizationTemplates() { - return ResponseEntity.ok(organizationService.findAll()); + public ResponseEntity getOrganizationTemplates( + @RequestParam(defaultValue = "KO_KR") Language language) { + return ResponseEntity.ok(organizationService.findAll(language)); } } diff --git a/src/main/java/com/debatetimer/controller/sharing/SharingRestController.java b/src/main/java/com/debatetimer/controller/sharing/SharingRestController.java new file mode 100644 index 00000000..1fbc659e --- /dev/null +++ b/src/main/java/com/debatetimer/controller/sharing/SharingRestController.java @@ -0,0 +1,40 @@ +package com.debatetimer.controller.sharing; + +import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.controller.tool.jwt.AuthManager; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.dto.sharing.response.ChairmanTokenResponse; +import com.debatetimer.service.customize.CustomizeService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class SharingRestController { + + private final CustomizeService customizeService; + private final AuthManager authManager; + + @GetMapping("/api/live/{tableId}/chairman-token") + public ChairmanTokenResponse issueChairmanToken( + @AuthMember Member member, + @PathVariable("tableId") long tableId + ) { + long debateTime = customizeService.findDebateTime(tableId, member); + String chairmanToken = authManager.issueChairmanToken(member, debateTime * 2); + return new ChairmanTokenResponse(chairmanToken); + } + + @GetMapping("/api/live/table/customize/{tableId}") + @ResponseStatus(HttpStatus.OK) + public CustomizeTableResponse getTable( + @PathVariable long tableId + ) { + return customizeService.findTable(tableId); + } +} diff --git a/src/main/java/com/debatetimer/controller/sharing/SharingController.java b/src/main/java/com/debatetimer/controller/sharing/SharingWebSocketController.java similarity index 70% rename from src/main/java/com/debatetimer/controller/sharing/SharingController.java rename to src/main/java/com/debatetimer/controller/sharing/SharingWebSocketController.java index edacf8e6..92ca689d 100644 --- a/src/main/java/com/debatetimer/controller/sharing/SharingController.java +++ b/src/main/java/com/debatetimer/controller/sharing/SharingWebSocketController.java @@ -4,6 +4,9 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.dto.sharing.request.SharingRequest; import com.debatetimer.dto.sharing.response.SharingResponse; +import com.debatetimer.service.sharing.SharingService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; @@ -11,15 +14,18 @@ import org.springframework.stereotype.Controller; @Controller -public class SharingController { +@RequiredArgsConstructor +public class SharingWebSocketController { + + private final SharingService sharingService; @MessageMapping("/event/{roomId}") @SendTo("/room/{roomId}") public SharingResponse share( @AuthMember Member member, @DestinationVariable(value = "roomId") long roomId, - @Payload SharingRequest request + @Valid @Payload SharingRequest request ) { - return new SharingResponse(request.time()); + return sharingService.share(request); } } 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 a614224f..13ae9c42 100644 --- a/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java +++ b/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java @@ -1,5 +1,6 @@ package com.debatetimer.controller.tool.jwt; +import com.debatetimer.domain.member.Member; import com.debatetimer.dto.member.JwtTokenResponse; import com.debatetimer.dto.member.MemberInfo; import java.time.Duration; @@ -20,6 +21,11 @@ public JwtTokenResponse issueToken(MemberInfo memberInfo) { return new JwtTokenResponse(accessToken, refreshToken, refreshTokenExpiration); } + public String issueChairmanToken(Member member, long expiration) { + MemberInfo memberInfo = new MemberInfo(member.getEmail()); + return jwtTokenProvider.createChairmanToken(memberInfo, expiration); + } + public JwtTokenResponse reissueToken(String refreshToken) { String email = jwtTokenResolver.resolveRefreshToken(refreshToken); MemberInfo memberInfo = new MemberInfo(email); @@ -37,4 +43,8 @@ public String resolveAccessToken(String accessToken) { public String resolveRefreshToken(String refreshToken) { return jwtTokenResolver.resolveRefreshToken(refreshToken); } + + public String resolveChairmanToken(String chairmanToken) { + return jwtTokenResolver.resolveChairmanToken(chairmanToken); + } } 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 ffd28bca..9f3ef75b 100644 --- a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java +++ b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java @@ -23,6 +23,10 @@ public String createRefreshToken(MemberInfo memberInfo) { return createToken(memberInfo, refreshTokenExpiration, TokenType.REFRESH_TOKEN); } + public String createChairmanToken(MemberInfo memberInfo, long expirationSeconds) { + return createToken(memberInfo, Duration.ofSeconds(expirationSeconds), TokenType.CHAIRMAN_TOKEN); + } + private String createToken(MemberInfo memberInfo, Duration expiration, TokenType tokenType) { Date now = new Date(); Date expiredDate = new Date(now.getTime() + expiration.toMillis()); diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java index de5d33f7..15e0d186 100644 --- a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java +++ b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java @@ -23,6 +23,10 @@ public String resolveRefreshToken(String refreshToken) { return resolveToken(refreshToken, TokenType.REFRESH_TOKEN); } + public String resolveChairmanToken(String chairmanToken) { + return resolveToken(chairmanToken, TokenType.CHAIRMAN_TOKEN); + } + private String resolveToken(String token, TokenType tokenType) { try { Claims claims = Jwts.parserBuilder() diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java b/src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java index 1112e4b9..9d3a4d0c 100644 --- a/src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java +++ b/src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java @@ -3,5 +3,6 @@ public enum TokenType { ACCESS_TOKEN, - REFRESH_TOKEN + REFRESH_TOKEN, + CHAIRMAN_TOKEN } diff --git a/src/main/java/com/debatetimer/domain/organization/Language.java b/src/main/java/com/debatetimer/domain/organization/Language.java new file mode 100644 index 00000000..6bb758b7 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/organization/Language.java @@ -0,0 +1,7 @@ +package com.debatetimer.domain.organization; + +public enum Language { + US_EN, + KO_KR, + ; +} diff --git a/src/main/java/com/debatetimer/domain/sharing/TimerEvent.java b/src/main/java/com/debatetimer/domain/sharing/TimerEvent.java new file mode 100644 index 00000000..e32a9a20 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/sharing/TimerEvent.java @@ -0,0 +1,28 @@ +package com.debatetimer.domain.sharing; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class TimerEvent { + + @NotNull + private final TimerEventType eventType; + + @Nullable + private final TimerEventData timerEventData; + + public TimerEvent( + TimerEventType eventType, + @Nullable TimerEventData timerEventData + ) { + eventType.validateEventData(timerEventData); + this.eventType = eventType; + this.timerEventData = timerEventData; + } + + public TimerEvent(TimerEventType eventType) { + this(eventType, null); + } +} diff --git a/src/main/java/com/debatetimer/domain/sharing/TimerEventData.java b/src/main/java/com/debatetimer/domain/sharing/TimerEventData.java new file mode 100644 index 00000000..85de556b --- /dev/null +++ b/src/main/java/com/debatetimer/domain/sharing/TimerEventData.java @@ -0,0 +1,46 @@ +package com.debatetimer.domain.sharing; + +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class TimerEventData { + + @NotNull + private final CustomizeBoxType timerType; + + private final int sequence; + + @Nullable + private final Stance currentTeam; + + private final long remainingTime; + + public TimerEventData( + CustomizeBoxType timerType, + int sequence, + @Nullable Stance currentTeam, + long remainingTime + ) { + validateCurrentTeam(timerType, currentTeam); + this.timerType = timerType; + this.sequence = sequence; + this.currentTeam = currentTeam; + this.remainingTime = remainingTime; + } + + private void validateCurrentTeam(CustomizeBoxType timerType, Stance currentTeam) { + if (timerType.isTimeBased() && currentTeam == null) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIME_BASED_TIMER_EVENT_DATA); + } + + if (!timerType.isTimeBased() && currentTeam != null) { + throw new DTClientErrorException(ClientErrorCode.INVALID_NORMAL_TIMER_EVENT_DATA); + } + } +} diff --git a/src/main/java/com/debatetimer/domain/sharing/TimerEventType.java b/src/main/java/com/debatetimer/domain/sharing/TimerEventType.java new file mode 100644 index 00000000..ca1a5427 --- /dev/null +++ b/src/main/java/com/debatetimer/domain/sharing/TimerEventType.java @@ -0,0 +1,28 @@ +package com.debatetimer.domain.sharing; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import java.util.Objects; +import java.util.function.Predicate; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum TimerEventType { + + NEXT(Objects::nonNull), + BEFORE(Objects::nonNull), + STOP(Objects::nonNull), + PLAY(Objects::nonNull), + RESET(Objects::nonNull), + TEAM_SWITCH(Objects::nonNull), + FINISHED(Objects::isNull), + ; + + private final Predicate eventDataValidator; + + public void validateEventData(Object eventData) { + if (!eventDataValidator.test(eventData)) { + throw new DTClientErrorException(ClientErrorCode.INVALID_TIMER_EVENT); + } + } +} diff --git a/src/main/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepository.java index 2c2e96e7..1f1c1e04 100644 --- a/src/main/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepository.java @@ -54,13 +54,32 @@ public CustomizeTable getByIdAndMember(long tableId, Member member) { } @Transactional(readOnly = true) - public List getCustomizeTimeBoxes(long tableId, Member member) { + public CustomizeTable getById(long tableId) { + return tableRepository.getById(tableId) + .toDomain(); + } + + @Transactional(readOnly = true) + public List getMemberCustomizeTimeBoxes(long tableId, Member member) { CustomizeTableEntity tableEntity = tableRepository.getByIdAndMember(tableId, member); List timeBoxEntityList = timeBoxRepository.findAllByCustomizeTable(tableEntity); List bellEntityList = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntityList); return toCustomizeTimeBoxes(timeBoxEntityList, bellEntityList); } + @Transactional(readOnly = true) + public List getCustomizeTimeBoxes(long tableId) { + CustomizeTableEntity tableEntity = tableRepository.getById(tableId); + List timeBoxEntityList = timeBoxRepository.findAllByCustomizeTable(tableEntity); + List bellEntityList = bellRepository.findAllByCustomizeTimeBoxIn(timeBoxEntityList); + return toCustomizeTimeBoxes(timeBoxEntityList, bellEntityList); + } + + @Transactional(readOnly = true) + public long getTotalTimeBoxTimes(long tableId) { + return timeBoxRepository.sumTimeByTableId(tableId); + } + private List toCustomizeTimeBoxes( List timeBoxEntities, List bellEntities diff --git a/src/main/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepository.java b/src/main/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepository.java index 917de8cc..46080303 100644 --- a/src/main/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepository.java +++ b/src/main/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepository.java @@ -1,5 +1,6 @@ package com.debatetimer.domainrepository.organization; +import com.debatetimer.domain.organization.Language; import com.debatetimer.domain.organization.Organization; import com.debatetimer.domain.organization.OrganizationTemplate; import com.debatetimer.entity.organization.OrganizationTemplateEntity; @@ -19,15 +20,15 @@ public class OrganizationDomainRepository { private final OrganizationRepository organizationRepository; private final OrganizationTemplateRepository organizationTemplateRepository; - public List findAll() { - Map> idToTemplatesEntity = organizationTemplateRepository.findAll() + public List findAllByLanguage(Language language) { + Map> idToTemplatesEntity = organizationTemplateRepository.findAllByLanguage(language) .stream() .collect(Collectors.groupingBy( OrganizationTemplateEntity::getOrganizationId, Collectors.mapping(OrganizationTemplateEntity::toDomain, Collectors.toList()) )); - return organizationRepository.findAll() + return organizationRepository.findAllByLanguage(language) .stream() .map(entity -> entity.toDomain( idToTemplatesEntity.getOrDefault(entity.getId(), Collections.emptyList()) diff --git a/src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java b/src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java index b6063711..dab8299a 100644 --- a/src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java +++ b/src/main/java/com/debatetimer/dto/sharing/request/SharingRequest.java @@ -1,9 +1,25 @@ package com.debatetimer.dto.sharing.request; -import java.time.LocalDateTime; +import com.debatetimer.domain.sharing.TimerEvent; +import com.debatetimer.domain.sharing.TimerEventType; +import jakarta.annotation.Nullable; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.Optional; public record SharingRequest( - LocalDateTime time + @NotNull + TimerEventType eventType, + + @Valid + @Nullable + TimerEventInfoRequest data ) { + public TimerEvent toTimerEvent() { + return Optional.ofNullable(data) + .map(TimerEventInfoRequest::toTimerEventInfo) + .map(eventData -> new TimerEvent(eventType, eventData)) + .orElseGet(() -> new TimerEvent(eventType)); + } } diff --git a/src/main/java/com/debatetimer/dto/sharing/request/TimerEventInfoRequest.java b/src/main/java/com/debatetimer/dto/sharing/request/TimerEventInfoRequest.java new file mode 100644 index 00000000..6a4c591e --- /dev/null +++ b/src/main/java/com/debatetimer/dto/sharing/request/TimerEventInfoRequest.java @@ -0,0 +1,28 @@ +package com.debatetimer.dto.sharing.request; + +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.domain.sharing.TimerEventData; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; + +public record TimerEventInfoRequest( + @NotNull + CustomizeBoxType timerType, + + @Nullable + Stance currentTeam, + + int sequence, + long remainingTime +) { + + public TimerEventData toTimerEventInfo() { + return new TimerEventData( + timerType, + sequence, + currentTeam, + remainingTime + ); + } +} diff --git a/src/main/java/com/debatetimer/dto/sharing/response/ChairmanTokenResponse.java b/src/main/java/com/debatetimer/dto/sharing/response/ChairmanTokenResponse.java new file mode 100644 index 00000000..c10fc79e --- /dev/null +++ b/src/main/java/com/debatetimer/dto/sharing/response/ChairmanTokenResponse.java @@ -0,0 +1,7 @@ +package com.debatetimer.dto.sharing.response; + +public record ChairmanTokenResponse( + String chairmanToken +) { + +} diff --git a/src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java b/src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java index 704384d1..2973fb94 100644 --- a/src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java +++ b/src/main/java/com/debatetimer/dto/sharing/response/SharingResponse.java @@ -1,9 +1,16 @@ package com.debatetimer.dto.sharing.response; -import java.time.LocalDateTime; +import com.debatetimer.domain.sharing.TimerEventType; +import jakarta.annotation.Nullable; public record SharingResponse( - LocalDateTime time + TimerEventType eventType, + + @Nullable + TimerEventDataResponse data ) { + public SharingResponse(TimerEventType eventType) { + this(eventType, null); + } } diff --git a/src/main/java/com/debatetimer/dto/sharing/response/TimerEventDataResponse.java b/src/main/java/com/debatetimer/dto/sharing/response/TimerEventDataResponse.java new file mode 100644 index 00000000..5fde093a --- /dev/null +++ b/src/main/java/com/debatetimer/dto/sharing/response/TimerEventDataResponse.java @@ -0,0 +1,29 @@ +package com.debatetimer.dto.sharing.response; + + +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.domain.sharing.TimerEventData; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; + +public record TimerEventDataResponse( + @NotNull + CustomizeBoxType timerType, + + @Nullable + Stance currentTeam, + + int sequence, + long remainingTime +) { + + public TimerEventDataResponse(TimerEventData timerEventInfo) { + this( + timerEventInfo.getTimerType(), + timerEventInfo.getCurrentTeam(), + timerEventInfo.getSequence(), + timerEventInfo.getRemainingTime() + ); + } +} diff --git a/src/main/java/com/debatetimer/entity/organization/OrganizationEntity.java b/src/main/java/com/debatetimer/entity/organization/OrganizationEntity.java index 326e289e..6d5d891b 100644 --- a/src/main/java/com/debatetimer/entity/organization/OrganizationEntity.java +++ b/src/main/java/com/debatetimer/entity/organization/OrganizationEntity.java @@ -1,9 +1,12 @@ package com.debatetimer.entity.organization; +import com.debatetimer.domain.organization.Language; import com.debatetimer.domain.organization.Organization; import com.debatetimer.domain.organization.OrganizationTemplate; import com.debatetimer.entity.BaseTimeEntity; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -34,10 +37,15 @@ public class OrganizationEntity extends BaseTimeEntity { @NotBlank private String iconPath; - public OrganizationEntity(String name, String affiliation, String iconPath) { + @NotNull + @Enumerated(EnumType.STRING) + private Language language; + + public OrganizationEntity(String name, String affiliation, String iconPath, Language language) { this.name = name; this.affiliation = affiliation; this.iconPath = iconPath; + this.language = language; } public Organization toDomain(List templates) { diff --git a/src/main/java/com/debatetimer/entity/organization/OrganizationTemplateEntity.java b/src/main/java/com/debatetimer/entity/organization/OrganizationTemplateEntity.java index 96753279..26b7c686 100644 --- a/src/main/java/com/debatetimer/entity/organization/OrganizationTemplateEntity.java +++ b/src/main/java/com/debatetimer/entity/organization/OrganizationTemplateEntity.java @@ -1,10 +1,13 @@ package com.debatetimer.entity.organization; +import com.debatetimer.domain.organization.Language; import com.debatetimer.domain.organization.OrganizationTemplate; import com.debatetimer.entity.BaseTimeEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -40,10 +43,15 @@ public class OrganizationTemplateEntity extends BaseTimeEntity { @Column(length = 8191) private String data; - public OrganizationTemplateEntity(OrganizationEntity organization, String name, String data) { + @NotNull + @Enumerated(EnumType.STRING) + private Language language; + + public OrganizationTemplateEntity(OrganizationEntity organization, String name, String data, Language language) { this.organization = organization; this.name = name; this.data = data; + this.language = language; } public OrganizationTemplate toDomain() { diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index 5bf7d05b..6079ba2f 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -77,6 +77,10 @@ public enum ClientErrorCode implements ResponseErrorCode { INVALID_BELL_TIME(HttpStatus.BAD_REQUEST, "벨 시간은 0 이상의 정수여야 합니다."), INVALID_BELL_COUNT(HttpStatus.BAD_REQUEST, "벨 카운트는 1 이상 %d 이하의 정수여야 합니다.".formatted(Bell.MAX_BELL_COUNT)), + + INVALID_NORMAL_TIMER_EVENT_DATA(HttpStatus.BAD_REQUEST, "잘못된 일반 타이머 이벤트 데이터입니다"), + INVALID_TIME_BASED_TIMER_EVENT_DATA(HttpStatus.BAD_REQUEST, "잘못된 자유토론 타이머 이벤트 데이터입니다"), + INVALID_TIMER_EVENT(HttpStatus.BAD_REQUEST, "잘못된 타이머 이벤트 데이터입니다"), ; private final HttpStatus status; diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java index bf00c3c8..00f32b0f 100644 --- a/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTableRepository.java @@ -23,5 +23,10 @@ default CustomizeTableEntity getByIdAndMember(long tableId, Member member) { .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); } + default CustomizeTableEntity getById(long tableId) { + return findById(tableId) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.TABLE_NOT_FOUND)); + } + void delete(CustomizeTableEntity table); } diff --git a/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java index 1a0e524d..f7059b1b 100644 --- a/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepository.java @@ -13,6 +13,9 @@ public interface CustomizeTimeBoxRepository extends Repository findAllByCustomizeTable(CustomizeTableEntity table); + @Query("SELECT COALESCE(SUM(ctb.time), 0) FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId") + long sumTimeByTableId(long tableId); + @Query("DELETE FROM CustomizeTimeBoxEntity ctb WHERE ctb.customizeTable.id = :tableId") @Modifying(clearAutomatically = true, flushAutomatically = true) void deleteAllByTable(long tableId); diff --git a/src/main/java/com/debatetimer/repository/organization/OrganizationRepository.java b/src/main/java/com/debatetimer/repository/organization/OrganizationRepository.java index 5c6ef65a..5b70760c 100644 --- a/src/main/java/com/debatetimer/repository/organization/OrganizationRepository.java +++ b/src/main/java/com/debatetimer/repository/organization/OrganizationRepository.java @@ -1,12 +1,13 @@ package com.debatetimer.repository.organization; +import com.debatetimer.domain.organization.Language; import com.debatetimer.entity.organization.OrganizationEntity; import java.util.List; import org.springframework.data.repository.Repository; public interface OrganizationRepository extends Repository { - List findAll(); + List findAllByLanguage(Language language); OrganizationEntity save(OrganizationEntity organizationEntity); } diff --git a/src/main/java/com/debatetimer/repository/organization/OrganizationTemplateRepository.java b/src/main/java/com/debatetimer/repository/organization/OrganizationTemplateRepository.java index 4c5b7472..0a11cc06 100644 --- a/src/main/java/com/debatetimer/repository/organization/OrganizationTemplateRepository.java +++ b/src/main/java/com/debatetimer/repository/organization/OrganizationTemplateRepository.java @@ -1,12 +1,13 @@ package com.debatetimer.repository.organization; +import com.debatetimer.domain.organization.Language; import com.debatetimer.entity.organization.OrganizationTemplateEntity; import java.util.List; import org.springframework.data.repository.Repository; public interface OrganizationTemplateRepository extends Repository { - List findAll(); + List findAllByLanguage(Language language); OrganizationTemplateEntity save(OrganizationTemplateEntity entity); } diff --git a/src/main/java/com/debatetimer/service/customize/CustomizeService.java b/src/main/java/com/debatetimer/service/customize/CustomizeService.java index 1670036f..d5e27d7c 100644 --- a/src/main/java/com/debatetimer/service/customize/CustomizeService.java +++ b/src/main/java/com/debatetimer/service/customize/CustomizeService.java @@ -26,12 +26,25 @@ public CustomizeTableResponse save(CustomizeTableCreateRequest tableCreateReques } @Transactional(readOnly = true) - public CustomizeTableResponse findTable(long tableId, Member member) { + public CustomizeTableResponse findMemberTable(long tableId, Member member) { CustomizeTable table = customizeTableDomainRepository.getByIdAndMember(tableId, member); - List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes(tableId, member); + List timeBoxes = customizeTableDomainRepository.getMemberCustomizeTimeBoxes(tableId, member); return new CustomizeTableResponse(table, timeBoxes); } + @Transactional(readOnly = true) + public CustomizeTableResponse findTable(long tableId) { + CustomizeTable table = customizeTableDomainRepository.getById(tableId); + List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes(tableId); + return new CustomizeTableResponse(table, timeBoxes); + } + + @Transactional(readOnly = true) + public long findDebateTime(long tableId, Member member) { + CustomizeTable customizeTable = customizeTableDomainRepository.getByIdAndMember(tableId, member); + return customizeTableDomainRepository.getTotalTimeBoxTimes(customizeTable.getId()); + } + @Transactional public CustomizeTableResponse updateTable( CustomizeTableCreateRequest tableCreateRequest, @@ -48,7 +61,7 @@ public CustomizeTableResponse updateTable( @Transactional public CustomizeTableResponse updateUsedAt(long tableId, Member member) { CustomizeTable table = customizeTableDomainRepository.updateUsedAt(tableId, member); - List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes(tableId, member); + List timeBoxes = customizeTableDomainRepository.getMemberCustomizeTimeBoxes(tableId, member); return new CustomizeTableResponse(table, timeBoxes); } diff --git a/src/main/java/com/debatetimer/service/organization/OrganizationService.java b/src/main/java/com/debatetimer/service/organization/OrganizationService.java index ab07e4b5..3cd30af5 100644 --- a/src/main/java/com/debatetimer/service/organization/OrganizationService.java +++ b/src/main/java/com/debatetimer/service/organization/OrganizationService.java @@ -1,5 +1,6 @@ package com.debatetimer.service.organization; +import com.debatetimer.domain.organization.Language; import com.debatetimer.domainrepository.organization.OrganizationDomainRepository; import com.debatetimer.dto.organization.OrganizationResponses; import lombok.RequiredArgsConstructor; @@ -11,7 +12,7 @@ public class OrganizationService { private final OrganizationDomainRepository organizationDomainRepository; - public OrganizationResponses findAll() { - return OrganizationResponses.from(organizationDomainRepository.findAll()); + public OrganizationResponses findAll(Language language) { + return OrganizationResponses.from(organizationDomainRepository.findAllByLanguage(language)); } } diff --git a/src/main/java/com/debatetimer/service/sharing/SharingService.java b/src/main/java/com/debatetimer/service/sharing/SharingService.java new file mode 100644 index 00000000..863981af --- /dev/null +++ b/src/main/java/com/debatetimer/service/sharing/SharingService.java @@ -0,0 +1,24 @@ +package com.debatetimer.service.sharing; + +import com.debatetimer.domain.sharing.TimerEvent; +import com.debatetimer.dto.sharing.request.SharingRequest; +import com.debatetimer.dto.sharing.response.SharingResponse; +import com.debatetimer.dto.sharing.response.TimerEventDataResponse; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SharingService { + + public SharingResponse share(SharingRequest request) { + TimerEvent timerEvent = request.toTimerEvent(); + return Optional.ofNullable(timerEvent.getTimerEventData()) + .map(eventData -> new SharingResponse( + request.eventType(), + new TimerEventDataResponse(eventData) + )) + .orElse(new SharingResponse(request.eventType(), null)); + } +} diff --git a/src/main/resources/db/migration/V16__add_language_into_organization.sql b/src/main/resources/db/migration/V16__add_language_into_organization.sql new file mode 100644 index 00000000..7192dcd1 --- /dev/null +++ b/src/main/resources/db/migration/V16__add_language_into_organization.sql @@ -0,0 +1,5 @@ +alter table organization + add column language varchar(255) not null default 'KO_KR'; + +alter table organization_template + add column language varchar(255) not null default 'KO_KR'; diff --git a/src/main/resources/static/icon/ap.png b/src/main/resources/static/icon/ap.png new file mode 100644 index 00000000..e595cace Binary files /dev/null and b/src/main/resources/static/icon/ap.png differ diff --git a/src/main/resources/static/icon/bp.png b/src/main/resources/static/icon/bp.png new file mode 100644 index 00000000..51e0a21d Binary files /dev/null and b/src/main/resources/static/icon/bp.png differ diff --git a/src/main/resources/static/icon/ceda.png b/src/main/resources/static/icon/ceda.png new file mode 100644 index 00000000..1b2d0ba1 Binary files /dev/null and b/src/main/resources/static/icon/ceda.png differ diff --git a/src/main/resources/static/icon/kp.png b/src/main/resources/static/icon/kp.png new file mode 100644 index 00000000..9b20266d Binary files /dev/null and b/src/main/resources/static/icon/kp.png differ diff --git a/src/main/resources/static/icon/lincon.png b/src/main/resources/static/icon/lincon.png new file mode 100644 index 00000000..12fcf1be Binary files /dev/null and b/src/main/resources/static/icon/lincon.png differ diff --git a/src/main/resources/static/icon/public-forum.png b/src/main/resources/static/icon/public-forum.png new file mode 100644 index 00000000..cccc91c5 Binary files /dev/null and b/src/main/resources/static/icon/public-forum.png differ diff --git a/src/main/resources/static/icon/wsdc.png b/src/main/resources/static/icon/wsdc.png new file mode 100644 index 00000000..2fd870a8 Binary files /dev/null and b/src/main/resources/static/icon/wsdc.png differ diff --git a/src/test/java/com/debatetimer/BaseStompTest.java b/src/test/java/com/debatetimer/BaseStompTest.java index 4f0410c0..cf38a6e4 100644 --- a/src/test/java/com/debatetimer/BaseStompTest.java +++ b/src/test/java/com/debatetimer/BaseStompTest.java @@ -10,6 +10,7 @@ import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; @@ -23,6 +24,7 @@ import org.springframework.web.socket.sockjs.client.Transport; import org.springframework.web.socket.sockjs.client.WebSocketTransport; +@ExtendWith(DataBaseCleaner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class BaseStompTest { diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index bbac2b81..3c139034 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -15,6 +15,7 @@ import com.debatetimer.service.organization.OrganizationService; import com.debatetimer.service.poll.PollService; import com.debatetimer.service.poll.VoteService; +import com.debatetimer.service.sharing.SharingService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; @@ -71,6 +72,9 @@ public abstract class BaseDocumentTest { @MockitoBean protected VoteService voteService; + @MockitoBean + protected SharingService sharingService; + @MockitoBean protected OrganizationService organizationService; diff --git a/src/test/java/com/debatetimer/controller/Tag.java b/src/test/java/com/debatetimer/controller/Tag.java index 5da09bb2..12ac3339 100644 --- a/src/test/java/com/debatetimer/controller/Tag.java +++ b/src/test/java/com/debatetimer/controller/Tag.java @@ -6,6 +6,7 @@ public enum Tag { PARLIAMENTARY_API("Parliamentary Table API"), TIME_BASED_API("Time Based Table API"), CUSTOMIZE_API("Customize Table API"), + SHARING_API("Sharing API"), POLL_API("Poll API"), ORGANIZATION_API("Organization API"); diff --git a/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java index e2613aa0..1350bfaa 100644 --- a/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/customize/CustomizeDocumentTest.java @@ -114,7 +114,8 @@ class Save { new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, @@ -129,7 +130,8 @@ class Save { new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), + new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, @@ -173,7 +175,8 @@ class Save { new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, @@ -252,14 +255,15 @@ class GetTable { new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), + new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, null, null, 360, null, null) ) ); - doReturn(response).when(customizeService).findTable(eq(tableId), any()); + doReturn(response).when(customizeService).findMemberTable(eq(tableId), any()); var document = document("customize/get", 200) .request(requestDocument) @@ -278,7 +282,7 @@ class GetTable { @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND"}) void 사용자_지정_테이블_조회_실패(ClientErrorCode errorCode) { long tableId = 5L; - doThrow(new DTClientErrorException(errorCode)).when(customizeService).findTable(eq(tableId), any()); + doThrow(new DTClientErrorException(errorCode)).when(customizeService).findMemberTable(eq(tableId), any()); var document = document("customize/get", errorCode) .request(requestDocument) @@ -370,7 +374,8 @@ class UpdateTable { new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, @@ -385,7 +390,8 @@ class UpdateTable { new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), + new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, @@ -432,7 +438,8 @@ class UpdateTable { new CustomizeTimeBoxCreateRequest(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxCreateRequest(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellRequest(BellType.AFTER_START, 90, 1), + new BellRequest(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxCreateRequest(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, @@ -514,7 +521,8 @@ class Debate { new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, - 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), + new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, null, null, 360, 120, null), new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, diff --git a/src/test/java/com/debatetimer/controller/organization/OrganizationControllerTest.java b/src/test/java/com/debatetimer/controller/organization/OrganizationControllerTest.java index 1c6cbd25..dfd7975a 100644 --- a/src/test/java/com/debatetimer/controller/organization/OrganizationControllerTest.java +++ b/src/test/java/com/debatetimer/controller/organization/OrganizationControllerTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.debatetimer.controller.BaseControllerTest; +import com.debatetimer.domain.organization.Language; import com.debatetimer.dto.organization.OrganizationResponses; import com.debatetimer.entity.organization.OrganizationEntity; import io.restassured.http.ContentType; @@ -17,12 +18,14 @@ class OrganizationControllerTest extends BaseControllerTest { class GetOrganizationTemplates { @Test - void 모든_기관의_토론_템플릿을_조회할_수_있다() { - OrganizationEntity organization1 = organizationEntityGenerator.generate("한앎", "한양대"); - OrganizationEntity organization2 = organizationEntityGenerator.generate("한모름", "양한대"); - organizationTemplateEntityGenerator.generate(organization1, "템플릿1"); - organizationTemplateEntityGenerator.generate(organization1, "템플릿2"); - organizationTemplateEntityGenerator.generate(organization2, "릿플템1"); + void language_파라미터가_없으면_KR_기관의_토론_템플릿을_조회한다() { + OrganizationEntity korean1 = organizationEntityGenerator.generate("한앎", "한양대", Language.KO_KR); + OrganizationEntity korean2 = organizationEntityGenerator.generate("한모름", "양한대", Language.KO_KR); + OrganizationEntity english = organizationEntityGenerator.generate("english", "hanyang", Language.US_EN); + organizationTemplateEntityGenerator.generate(korean1, "템플릿1", Language.KO_KR); + organizationTemplateEntityGenerator.generate(korean1, "템플릿2", Language.KO_KR); + organizationTemplateEntityGenerator.generate(korean2, "릿플템1", Language.KO_KR); + organizationTemplateEntityGenerator.generate(english, "template1", Language.US_EN); OrganizationResponses response = given() .contentType(ContentType.JSON) @@ -36,5 +39,36 @@ class GetOrganizationTemplates { () -> assertThat(response.organizations().get(1).templates()).hasSize(1) ); } + + @Test + void 요청한_언어의_기관_토론_템플릿만_조회한다() { + OrganizationEntity korean = organizationEntityGenerator.generate("한앎", "한양대", Language.KO_KR); + OrganizationEntity english = organizationEntityGenerator.generate("english", "hanyang", Language.US_EN); + organizationTemplateEntityGenerator.generate(korean, "템플릿1", Language.KO_KR); + organizationTemplateEntityGenerator.generate(english, "template1", Language.US_EN); + organizationTemplateEntityGenerator.generate(english, "template2", Language.US_EN); + + OrganizationResponses response = given() + .contentType(ContentType.JSON) + .queryParam("language", "US_EN") + .when().get("/api/organizations/templates") + .then().statusCode(HttpStatus.OK.value()) + .extract().as(OrganizationResponses.class); + + assertAll( + () -> assertThat(response.organizations()).hasSize(1), + () -> assertThat(response.organizations().get(0).organization()).isEqualTo("english"), + () -> assertThat(response.organizations().get(0).templates()).hasSize(2) + ); + } + + @Test + void 지원하지_않는_언어를_요청하면_400을_반환한다() { + given() + .contentType(ContentType.JSON) + .queryParam("language", "JP") + .when().get("/api/organizations/templates") + .then().statusCode(HttpStatus.BAD_REQUEST.value()); + } } } diff --git a/src/test/java/com/debatetimer/controller/organization/OrganizationDocumentTest.java b/src/test/java/com/debatetimer/controller/organization/OrganizationDocumentTest.java index 1933b7db..40a3eeaa 100644 --- a/src/test/java/com/debatetimer/controller/organization/OrganizationDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/organization/OrganizationDocumentTest.java @@ -1,9 +1,11 @@ package com.debatetimer.controller.organization; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; 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; @@ -24,7 +26,11 @@ class GetOrganizationTemplates { private final RestDocumentationRequest requestDocument = request() .tag(Tag.ORGANIZATION_API) - .summary("기관별 템플릿 조회"); + .summary("기관별 템플릿 조회") + .queryParameter( + parameterWithName("language").optional() + .description("조회 언어 (US_EN | KO_KR, 기본값 KO_KR)") + ); private final RestDocumentationResponse responseDocument = response() .responseBodyField( @@ -51,7 +57,7 @@ class GetOrganizationTemplates { new OrganizationTemplateResponse("템플릿2", DEFAULT_TEMPLATE_CONTENT) )) )); - doReturn(response).when(organizationService).findAll(); + doReturn(response).when(organizationService).findAll(any()); var document = document("organization/get-templates", 200) .request(requestDocument) @@ -60,6 +66,7 @@ class GetOrganizationTemplates { given(document) .contentType(ContentType.JSON) + .queryParam("language", "KO_KR") .when().get("/api/organizations/templates") .then().statusCode(200); } diff --git a/src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java b/src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java deleted file mode 100644 index c1362bc5..00000000 --- a/src/test/java/com/debatetimer/controller/sharing/SharingControllerTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.debatetimer.controller.sharing; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.debatetimer.BaseStompTest; -import com.debatetimer.MessageFrameHandler; -import com.debatetimer.domain.member.Member; -import com.debatetimer.dto.sharing.request.SharingRequest; -import com.debatetimer.dto.sharing.response.SharingResponse; -import java.time.LocalDateTime; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.messaging.simp.stomp.StompHeaders; - -class SharingControllerTest extends BaseStompTest { - - @Nested - class Share { - - @Test - void 사회자가_발생시킨_이벤트를_청중이_공유받는다() throws ExecutionException, InterruptedException, TimeoutException { - long roomId = 1L; - LocalDateTime time = LocalDateTime.now(); - MessageFrameHandler handler = new MessageFrameHandler<>(SharingResponse.class); - Member member = memberGenerator.generate("example@email.com"); - StompHeaders headers = headerGenerator.generateAccessTokenHeader("/app/event/" + roomId, member); - stompSession.subscribe("/room/" + roomId, handler); //청중의 구독 - - stompSession.send(headers, new SharingRequest(time)); //사회자의 이벤트 발생 - - SharingResponse response = handler.getCompletableFuture() - .get(3L, TimeUnit.SECONDS); - assertThat(response.time()).isEqualTo(time); - } - } -} diff --git a/src/test/java/com/debatetimer/controller/sharing/SharingDocumentTest.java b/src/test/java/com/debatetimer/controller/sharing/SharingDocumentTest.java new file mode 100644 index 00000000..afa7873a --- /dev/null +++ b/src/test/java/com/debatetimer/controller/sharing/SharingDocumentTest.java @@ -0,0 +1,171 @@ +package com.debatetimer.controller.sharing; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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.customize.BellType; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.response.BellResponse; +import com.debatetimer.dto.customize.response.CustomizeTableInfoResponse; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.dto.customize.response.CustomizeTimeBoxResponse; +import com.debatetimer.dto.member.TableType; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import io.restassured.http.ContentType; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.http.HttpHeaders; + +public class SharingDocumentTest extends BaseDocumentTest { + + @Nested + class IssueChairmanToken { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.SHARING_API) + .summary("사회자용 토큰 발급") + .requestHeader(headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰")) + .pathParameter(parameterWithName("tableId").description("테이블 id")); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("chairmanToken").type(STRING).description("사회자용 토큰") + ); + + @Test + void 사회자_용_토큰_생성_성공() { + long requestTableId = 1L; + long debateTime = 500L; + doReturn(debateTime).when(customizeService) + .findDebateTime(eq(requestTableId), any(Member.class)); + doReturn("testToken").when(authManager) + .issueChairmanToken(any(Member.class), eq(debateTime * 2)); + + var document = document("sharing/get", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) + .pathParam("tableId", String.valueOf(requestTableId)) + .when().get("/api/live/{tableId}/chairman-token") + .then().statusCode(200); + } + } + + @Nested + class GetTable { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.SHARING_API) + .summary("비회원용 사용자 지정 토론 시간표 조회") + .description(""" + ### 타임 박스 종류에 따른 웅답 값 + | 타임 박스 종류 | 필수 입력 | 선택 입력 | null 입력 | + | :---: | ---| --- | --- | + | 커스텀 타임 박스 | stance, speechType, boxType, time | speaker | timePerTeam, timePerSpeaking | + | 자유 토론 타임 박스 | stance, speechType, boxType, timePerTeam | speaker, timePerSpeaking | time | + """) + .pathParameter( + parameterWithName("tableId").description("테이블 ID") + ); + + private final RestDocumentationResponse responseDocument = response() + .responseBodyField( + fieldWithPath("id").type(NUMBER).description("테이블 ID"), + fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), + fieldWithPath("info.name").type(STRING).description("테이블 이름"), + fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.type").type(STRING).description("토론 테이블 유형"), + fieldWithPath("info.prosTeamName").type(STRING).description("찬성팀 팀명"), + fieldWithPath("info.consTeamName").type(STRING).description("반대팀 팀명"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), + fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), + fieldWithPath("table[].stance").type(STRING).description("입장"), + fieldWithPath("table[].speechType").type(STRING).description("발언 유형"), + fieldWithPath("table[].boxType").type(STRING).description("타임 박스 유형"), + fieldWithPath("table[].time").type(NUMBER).description("발언 시간(초)").optional(), + fieldWithPath("table[].bell").type(ARRAY).description("종소리 정보").optional(), + fieldWithPath("table[].bell[].type").type(STRING).description("종소리 종류"), + fieldWithPath("table[].bell[].time").type(NUMBER).description("종소리 울릴 시간(초)"), + fieldWithPath("table[].bell[].count").type(NUMBER).description("종소리 횟수"), + fieldWithPath("table[].timePerTeam").type(NUMBER).description("팀당 발언 시간 (초)").optional(), + fieldWithPath("table[].timePerSpeaking").type(NUMBER).description("1회 발언 시간 (초)").optional(), + fieldWithPath("table[].speaker").type(STRING).description("발언자 이름").optional() + ); + + @Test + void 비회원_사용자_지정_테이블_조회_성공() { + long tableId = 5L; + CustomizeTableResponse response = new CustomizeTableResponse( + 5L, + new CustomizeTableInfoResponse("나의 테이블", TableType.CUSTOMIZE, "토론 주제", + "찬성", "반대", true, true), + List.of( + new CustomizeTimeBoxResponse(Stance.PROS, "입론", CustomizeBoxType.NORMAL, + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1)), null, null, "콜리"), + new CustomizeTimeBoxResponse(Stance.CONS, "입론", CustomizeBoxType.NORMAL, + 120, List.of(new BellResponse(BellType.AFTER_START, 90, 1), + new BellResponse(BellType.AFTER_START, 120, 2)), null, null, "비토"), + new CustomizeTimeBoxResponse(Stance.NEUTRAL, "난상 토론", CustomizeBoxType.TIME_BASED, + null, null, 360, 120, null), + new CustomizeTimeBoxResponse(Stance.NEUTRAL, "존중 토론", CustomizeBoxType.TIME_BASED, + null, null, 360, null, null) + ) + ); + doReturn(response).when(customizeService).findTable(eq(tableId)); + + var document = document("sharing/get_table", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .contentType(ContentType.JSON) + .pathParam("tableId", tableId) + .when().get("/api/live/table/customize/{tableId}") + .then().statusCode(200); + } + + @ParameterizedTest + @EnumSource(value = ClientErrorCode.class, names = {"TABLE_NOT_FOUND"}) + void 비회원_사용자_지정_테이블_조회_실패(ClientErrorCode errorCode) { + long tableId = 5L; + doThrow(new DTClientErrorException(errorCode)).when(customizeService).findTable(eq(tableId)); + + var document = document("sharing/get_table", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .contentType(ContentType.JSON) + .pathParam("tableId", tableId) + .when().get("/api/live/table/customize/{tableId}") + .then().statusCode(errorCode.getStatus().value()); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/sharing/SharingRestControllerTest.java b/src/test/java/com/debatetimer/controller/sharing/SharingRestControllerTest.java new file mode 100644 index 00000000..4187f8fb --- /dev/null +++ b/src/test/java/com/debatetimer/controller/sharing/SharingRestControllerTest.java @@ -0,0 +1,51 @@ +package com.debatetimer.controller.sharing; + +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.customize.CustomizeBoxType; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.customize.response.CustomizeTableResponse; +import com.debatetimer.entity.customize.CustomizeTableEntity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class SharingRestControllerTest extends BaseControllerTest { + + @Nested + class GetTable { + + @Test + void 비회원이_사용자_지정_테이블을_조회한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity bitoTable = customizeTableEntityGenerator.generate(bito); + customizeTimeBoxEntityGenerator.generate(bitoTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generateNotExistSpeaker(bitoTable, CustomizeBoxType.NORMAL, 2); + + CustomizeTableResponse response = given() + .contentType(ContentType.JSON) + .pathParam("tableId", bitoTable.getId()) + .when().get("/api/live/table/customize/{tableId}") + .then().statusCode(200) + .extract().as(CustomizeTableResponse.class); + + assertAll( + () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), + () -> assertThat(response.table()).hasSize(2) + ); + } + + @Test + void 존재하지_않는_테이블을_조회하면_예외가_발생한다() { + long notExistTableId = 999L; + + given() + .contentType(ContentType.JSON) + .pathParam("tableId", notExistTableId) + .when().get("/api/live/table/customize/{tableId}") + .then().statusCode(404); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/sharing/SharingWebSocketControllerTest.java b/src/test/java/com/debatetimer/controller/sharing/SharingWebSocketControllerTest.java new file mode 100644 index 00000000..88775492 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/sharing/SharingWebSocketControllerTest.java @@ -0,0 +1,128 @@ +package com.debatetimer.controller.sharing; + +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.BaseStompTest; +import com.debatetimer.MessageFrameHandler; +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.member.Member; +import com.debatetimer.domain.sharing.TimerEventType; +import com.debatetimer.dto.sharing.request.SharingRequest; +import com.debatetimer.dto.sharing.request.TimerEventInfoRequest; +import com.debatetimer.dto.sharing.response.SharingResponse; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.springframework.messaging.simp.stomp.StompHeaders; + +class SharingWebSocketControllerTest extends BaseStompTest { + + @Nested + class Share { + + @Test + void 사회자가_발생시킨_이벤트를_청중이_공유받는다() throws ExecutionException, InterruptedException, TimeoutException { + long roomId = 1L; + MessageFrameHandler handler = new MessageFrameHandler<>(SharingResponse.class); + Member member = memberGenerator.generate("example@email.com"); + StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member); + SharingRequest request = new SharingRequest( + TimerEventType.NEXT, + new TimerEventInfoRequest( + CustomizeBoxType.NORMAL, + null, + 2, + 30 + ) + ); + stompSession.subscribe("/room/" + roomId, handler); //청중의 구독 + + stompSession.send(headers, request); //사회자의 이벤트 발생 + + SharingResponse response = handler.getCompletableFuture() + .get(3L, TimeUnit.SECONDS); + + assertAll( + () -> assertThat(response.eventType()).isEqualTo(request.eventType()), + () -> assertThat(response.data()).isNotNull(), + () -> assertThat(response.data().timerType()).isEqualTo(request.data().timerType()), + () -> assertThat(response.data().sequence()).isEqualTo(request.data().sequence()), + () -> assertThat(response.data().currentTeam()).isEqualTo(request.data().currentTeam()), + () -> assertThat(response.data().remainingTime()).isEqualTo(request.data().remainingTime()) + ); + } + + @Test + void 사회자가_아니면_이벤트를_발행할_수_없다() throws ExecutionException, InterruptedException, TimeoutException { + long roomId = 1L; + MessageFrameHandler handler = new MessageFrameHandler<>(SharingResponse.class); + Member member = memberGenerator.generate("example@email.com"); + SharingRequest request = new SharingRequest( + TimerEventType.NEXT, + new TimerEventInfoRequest( + CustomizeBoxType.NORMAL, + null, + 2, + 30 + ) + ); + stompSession.subscribe("/room/" + roomId, handler); //청중의 구독 + stompSession.send("/app/event/" + roomId, request); //사회자의 이벤트 발생 + + assertThatThrownBy(() -> handler.getCompletableFuture() + .get(2L, TimeUnit.SECONDS)) + .isInstanceOf(TimeoutException.class); + } + + @Test + void 사회자가_발생시킨_토론_종료_이벤트를_청중이_공유받는다() throws ExecutionException, InterruptedException, TimeoutException { + long roomId = 1L; + MessageFrameHandler handler = new MessageFrameHandler<>(SharingResponse.class); + Member member = memberGenerator.generate("example@email.com"); + StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member); + SharingRequest request = new SharingRequest(TimerEventType.FINISHED, null); + stompSession.subscribe("/room/" + roomId, handler); //청중의 구독 + + stompSession.send(headers, request); //사회자의 이벤트 발생 + + SharingResponse response = handler.getCompletableFuture() + .get(3L, TimeUnit.SECONDS); + + assertAll( + () -> assertThat(response.eventType()).isEqualTo(request.eventType()), + () -> assertThat(response.data()).isNull() + ); + } + + @ParameterizedTest + @NullSource + void 타이머_타입은_빈_값일_수_없다(CustomizeBoxType boxType) { + long roomId = 1L; + MessageFrameHandler handler = new MessageFrameHandler<>(SharingResponse.class); + Member member = memberGenerator.generate("example@email.com"); + StompHeaders headers = headerGenerator.generateChairmanTokenHeader("/app/event/" + roomId, member); + SharingRequest request = new SharingRequest( + TimerEventType.NEXT, + new TimerEventInfoRequest( + boxType, + null, + 2, + 30 + ) + ); + stompSession.subscribe("/room/" + roomId, handler); //청중의 구독 + + stompSession.send(headers, request); //사회자의 이벤트 발생 + + assertThatThrownBy(() -> handler.getCompletableFuture() + .get(2L, TimeUnit.SECONDS)) + .isInstanceOf(TimeoutException.class); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/sharing/TimerEventDataTest.java b/src/test/java/com/debatetimer/domain/sharing/TimerEventDataTest.java new file mode 100644 index 00000000..1811b00e --- /dev/null +++ b/src/test/java/com/debatetimer/domain/sharing/TimerEventDataTest.java @@ -0,0 +1,39 @@ +package com.debatetimer.domain.sharing; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.customize.CustomizeBoxType; +import com.debatetimer.domain.customize.Stance; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class TimerEventDataTest { + + @Nested + class ValidateCurrentTeam { + + @Test + void 일반_타이머의_경우_현재_발언팀이_입력되면_안된다() { + assertThatThrownBy(() -> new TimerEventData( + CustomizeBoxType.NORMAL, + 2, + Stance.CONS, + 30L + )).isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_NORMAL_TIMER_EVENT_DATA.getMessage()); + } + + @Test + void 자유토론_타이머의_경우_현재_발언팀이_입력되어야_한다() { + assertThatThrownBy(() -> new TimerEventData( + CustomizeBoxType.TIME_BASED, + 2, + null, + 30L + )).isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIME_BASED_TIMER_EVENT_DATA.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/sharing/TimerEventTypeTest.java b/src/test/java/com/debatetimer/domain/sharing/TimerEventTypeTest.java new file mode 100644 index 00000000..b080c112 --- /dev/null +++ b/src/test/java/com/debatetimer/domain/sharing/TimerEventTypeTest.java @@ -0,0 +1,50 @@ +package com.debatetimer.domain.sharing; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.customize.CustomizeBoxType; +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.EnumSource; + +class TimerEventTypeTest { + + @Nested + class ValidateData { + + @EnumSource( + value = TimerEventType.class, + names = { + "NEXT", + "BEFORE", + "STOP", + "PLAY", + "RESET", + "TEAM_SWITCH", + } + ) + @ParameterizedTest + void 타이머_이벤트_데이터가_존재하여야_한다(TimerEventType eventType) { + assertThatThrownBy(() -> eventType.validateEventData(null)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIMER_EVENT.getMessage()); + + } + + @EnumSource(value = TimerEventType.class, names = {"FINISHED"}) + @ParameterizedTest + void 타이머_이벤트_데이터가_존재하지_않아야_한다(TimerEventType eventType) { + TimerEventData timerEventData = new TimerEventData( + CustomizeBoxType.NORMAL, + 2, + null, + 30L + ); + assertThatThrownBy(() -> eventType.validateEventData(timerEventData)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.INVALID_TIMER_EVENT.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepositoryTest.java index e1dbbf53..08a3df7c 100644 --- a/src/test/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepositoryTest.java +++ b/src/test/java/com/debatetimer/domainrepository/customize/CustomizeTableDomainRepositoryTest.java @@ -75,7 +75,7 @@ class GetCustomizeTimeBoxes { timeBoxEntityGenerator.generate(tableEntity, CustomizeBoxType.NORMAL, 2, 180); timeBoxEntityGenerator.generate(tableEntity, CustomizeBoxType.NORMAL, 3, 120); - List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes( + List timeBoxes = customizeTableDomainRepository.getMemberCustomizeTimeBoxes( tableEntity.getId(), member); assertThat(timeBoxes).hasSize(3) @@ -95,7 +95,7 @@ class GetCustomizeTimeBoxes { bellEntityGenerator.generate(timeBoxEntity1, BellType.BEFORE_END, 30, 1); bellEntityGenerator.generate(timeBoxEntity2, BellType.BEFORE_END, 10, 1); - List timeBoxes = customizeTableDomainRepository.getCustomizeTimeBoxes( + List timeBoxes = customizeTableDomainRepository.getMemberCustomizeTimeBoxes( tableEntity.getId(), member); assertAll( diff --git a/src/test/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepositoryTest.java b/src/test/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepositoryTest.java index 8e28eb07..821f7626 100644 --- a/src/test/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepositoryTest.java +++ b/src/test/java/com/debatetimer/domainrepository/organization/OrganizationDomainRepositoryTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import com.debatetimer.domain.organization.Language; import com.debatetimer.domain.organization.Organization; import com.debatetimer.domainrepository.BaseDomainRepositoryTest; import com.debatetimer.entity.organization.OrganizationEntity; @@ -17,17 +18,17 @@ class OrganizationDomainRepositoryTest extends BaseDomainRepositoryTest { private OrganizationDomainRepository organizationDomainRepository; @Nested - class FindAll { + class FindAllByLanguage { @Test - void 모든_조직_템플릿을_가져온다() { - OrganizationEntity organization1 = organizationEntityGenerator.generate("한앎", "한양대"); - OrganizationEntity organization2 = organizationEntityGenerator.generate("한모름", "양한대"); - organizationTemplateEntityGenerator.generate(organization1, "템플릿1"); - organizationTemplateEntityGenerator.generate(organization1, "템플릿2"); - organizationTemplateEntityGenerator.generate(organization2, "릿플템1"); + void 해당_언어의_모든_조직_템플릿을_가져온다() { + OrganizationEntity organization1 = organizationEntityGenerator.generate("한앎", "한양대", Language.KO_KR); + OrganizationEntity organization2 = organizationEntityGenerator.generate("한모름", "양한대", Language.KO_KR); + organizationTemplateEntityGenerator.generate(organization1, "템플릿1", Language.KO_KR); + organizationTemplateEntityGenerator.generate(organization1, "템플릿2", Language.KO_KR); + organizationTemplateEntityGenerator.generate(organization2, "릿플템1", Language.KO_KR); - List organizations = organizationDomainRepository.findAll(); + List organizations = organizationDomainRepository.findAllByLanguage(Language.KO_KR); assertAll( () -> assertThat(organizations).hasSize(2), @@ -35,5 +36,22 @@ class FindAll { () -> assertThat(organizations.get(1).getTemplates()).hasSize(1) ); } + + @Test + void 요청한_언어의_조직_템플릿만_반환한다() { + OrganizationEntity korean = organizationEntityGenerator.generate("한앎", "한양대", Language.KO_KR); + OrganizationEntity english = organizationEntityGenerator.generate("english", "hanyang", Language.US_EN); + organizationTemplateEntityGenerator.generate(korean, "템플릿1", Language.KO_KR); + organizationTemplateEntityGenerator.generate(english, "template1", Language.US_EN); + organizationTemplateEntityGenerator.generate(english, "template2", Language.US_EN); + + List englishOrganizations = organizationDomainRepository.findAllByLanguage(Language.US_EN); + + assertAll( + () -> assertThat(englishOrganizations).hasSize(1), + () -> assertThat(englishOrganizations.get(0).getName()).isEqualTo("english"), + () -> assertThat(englishOrganizations.get(0).getTemplates()).hasSize(2) + ); + } } } diff --git a/src/test/java/com/debatetimer/fixture/HeaderGenerator.java b/src/test/java/com/debatetimer/fixture/HeaderGenerator.java index 70362980..5a76eaac 100644 --- a/src/test/java/com/debatetimer/fixture/HeaderGenerator.java +++ b/src/test/java/com/debatetimer/fixture/HeaderGenerator.java @@ -23,11 +23,11 @@ public Headers generateAccessTokenHeader(Member member) { return new Headers(new Header(HttpHeaders.AUTHORIZATION, accessToken)); } - public StompHeaders generateAccessTokenHeader(String destination, Member member) { - String accessToken = jwtTokenProvider.createAccessToken(new MemberInfo(member)); + public StompHeaders generateChairmanTokenHeader(String destination, Member member) { + String chairmanToken = jwtTokenProvider.createChairmanToken(new MemberInfo(member), 5L); StompHeaders stompHeaders = new StompHeaders(); stompHeaders.setDestination(destination); - stompHeaders.add(HttpHeaders.AUTHORIZATION, accessToken); + stompHeaders.add(HttpHeaders.AUTHORIZATION, chairmanToken); return stompHeaders; } } diff --git a/src/test/java/com/debatetimer/fixture/entity/OrganizationEntityGenerator.java b/src/test/java/com/debatetimer/fixture/entity/OrganizationEntityGenerator.java index f193aeca..7eb66b34 100644 --- a/src/test/java/com/debatetimer/fixture/entity/OrganizationEntityGenerator.java +++ b/src/test/java/com/debatetimer/fixture/entity/OrganizationEntityGenerator.java @@ -1,5 +1,6 @@ package com.debatetimer.fixture.entity; +import com.debatetimer.domain.organization.Language; import com.debatetimer.entity.organization.OrganizationEntity; import com.debatetimer.repository.organization.OrganizationRepository; import org.springframework.stereotype.Component; @@ -16,7 +17,11 @@ public OrganizationEntityGenerator(OrganizationRepository organizationRepository } public OrganizationEntity generate(String name, String affiliation) { - OrganizationEntity organization = new OrganizationEntity(name, affiliation, DEFAULT_ICON_PATH); + return generate(name, affiliation, Language.KO_KR); + } + + public OrganizationEntity generate(String name, String affiliation, Language language) { + OrganizationEntity organization = new OrganizationEntity(name, affiliation, DEFAULT_ICON_PATH, language); return organizationRepository.save(organization); } } diff --git a/src/test/java/com/debatetimer/fixture/entity/OrganizationTemplateEntityGenerator.java b/src/test/java/com/debatetimer/fixture/entity/OrganizationTemplateEntityGenerator.java index 45242cd7..12d588a8 100644 --- a/src/test/java/com/debatetimer/fixture/entity/OrganizationTemplateEntityGenerator.java +++ b/src/test/java/com/debatetimer/fixture/entity/OrganizationTemplateEntityGenerator.java @@ -1,5 +1,6 @@ package com.debatetimer.fixture.entity; +import com.debatetimer.domain.organization.Language; import com.debatetimer.entity.organization.OrganizationEntity; import com.debatetimer.entity.organization.OrganizationTemplateEntity; import com.debatetimer.repository.organization.OrganizationTemplateRepository; @@ -17,8 +18,12 @@ public OrganizationTemplateEntityGenerator(OrganizationTemplateRepository organi } public OrganizationTemplateEntity generate(OrganizationEntity organization, String name) { + return generate(organization, name, Language.KO_KR); + } + + public OrganizationTemplateEntity generate(OrganizationEntity organization, String name, Language language) { OrganizationTemplateEntity template = - new OrganizationTemplateEntity(organization, name, DEFAULT_TEMPLATE_CONTENT); + new OrganizationTemplateEntity(organization, name, DEFAULT_TEMPLATE_CONTENT, language); return organizationTemplateRepository.save(template); } } diff --git a/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java index 5fcf6b39..93268c2d 100644 --- a/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/customize/CustomizeTimeBoxRepositoryTest.java @@ -80,4 +80,22 @@ class DeleteAllByTable { .doesNotThrowAnyException(); } } + + @Nested + class SumTimeByTableId { + + @Test + void 특정_테이블의_타임_박스시간의_합을_반환한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity debateTable = customizeTableEntityGenerator.generate(chan); + CustomizeTimeBoxEntity timeBox1 = customizeTimeBoxEntityGenerator.generate(debateTable, + CustomizeBoxType.NORMAL, 1, 10); + CustomizeTimeBoxEntity timeBox2 = customizeTimeBoxEntityGenerator.generate(debateTable, + CustomizeBoxType.NORMAL, 2, 20); + + long summedTimeByTableId = customizeTimeBoxRepository.sumTimeByTableId(debateTable.getId()); + + assertThat(summedTimeByTableId).isEqualTo(timeBox1.getTime() + timeBox2.getTime()); + } + } } diff --git a/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java index 664a50c5..2175ac6c 100644 --- a/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java +++ b/src/test/java/com/debatetimer/service/customize/CustomizeServiceTest.java @@ -76,7 +76,7 @@ class FindTable { bellEntityGenerator.generate(customizeTimeBox, BellType.AFTER_START, 1, 1); bellEntityGenerator.generate(customizeTimeBox, BellType.AFTER_START, 1, 2); - CustomizeTableResponse foundResponse = customizeService.findTable(chanTable.getId(), chan); + CustomizeTableResponse foundResponse = customizeService.findMemberTable(chanTable.getId(), chan); assertAll( () -> assertThat(foundResponse.id()).isEqualTo(chanTable.getId()), @@ -93,7 +93,68 @@ class FindTable { CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); long chanTableId = chanTable.getId(); - assertThatThrownBy(() -> customizeService.findTable(chanTableId, coli)) + assertThatThrownBy(() -> customizeService.findMemberTable(chanTableId, coli)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.TABLE_NOT_FOUND.getMessage()); + } + } + + @Nested + class FindTableForGuest { + + @Test + void 비회원이_사용자_지정_토론_테이블을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); + CustomizeTimeBoxEntity customizeTimeBox = customizeTimeBoxEntityGenerator.generate( + chanTable, CustomizeBoxType.NORMAL, 1); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2); + bellEntityGenerator.generate(customizeTimeBox, BellType.AFTER_START, 1, 1); + bellEntityGenerator.generate(customizeTimeBox, BellType.AFTER_START, 1, 2); + + CustomizeTableResponse foundResponse = customizeService.findTable(chanTable.getId()); + + assertAll( + () -> assertThat(foundResponse.id()).isEqualTo(chanTable.getId()), + () -> assertThat(foundResponse.table()).hasSize(2), + () -> assertThat(foundResponse.table().get(0).bell()).hasSize(2), + () -> assertThat(foundResponse.table().get(1).bell()).isNull() + ); + } + + @Test + void 존재하지_않는_테이블_조회_시_예외를_발생시킨다() { + long notExistTableId = 999L; + + assertThatThrownBy(() -> customizeService.findTable(notExistTableId)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.TABLE_NOT_FOUND.getMessage()); + } + } + + @Nested + class FindDebateTime { + + @Test + void 사용자_지정_토론_테이블의_총_토론_시간을_조회한다() { + Member chan = memberGenerator.generate("default@gmail.com"); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 1, 120); + customizeTimeBoxEntityGenerator.generate(chanTable, CustomizeBoxType.NORMAL, 2, 180); + + long debateTime = customizeService.findDebateTime(chanTable.getId(), chan); + + assertThat(debateTime).isEqualTo(300); + } + + @Test + void 회원_소유가_아닌_테이블_조회_시_예외를_발생시킨다() { + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); + CustomizeTableEntity chanTable = customizeTableEntityGenerator.generate(chan); + long chanTableId = chanTable.getId(); + + assertThatThrownBy(() -> customizeService.findDebateTime(chanTableId, coli)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.TABLE_NOT_FOUND.getMessage()); } diff --git a/src/test/java/com/debatetimer/service/organization/OrganizationServiceTest.java b/src/test/java/com/debatetimer/service/organization/OrganizationServiceTest.java index 86e0b251..9a6ab163 100644 --- a/src/test/java/com/debatetimer/service/organization/OrganizationServiceTest.java +++ b/src/test/java/com/debatetimer/service/organization/OrganizationServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import com.debatetimer.domain.organization.Language; import com.debatetimer.dto.organization.OrganizationResponses; import com.debatetimer.entity.organization.OrganizationEntity; import com.debatetimer.service.BaseServiceTest; @@ -26,7 +27,7 @@ class FindAll { organizationTemplateEntityGenerator.generate(organization1, "템플릿2"); organizationTemplateEntityGenerator.generate(organization2, "릿플템1"); - OrganizationResponses response = organizationService.findAll(); + OrganizationResponses response = organizationService.findAll(Language.KO_KR); assertAll( () -> assertThat(response.organizations()).hasSize(2), @@ -35,9 +36,24 @@ class FindAll { ); } + @Test + void 요청한_언어의_기관_템플릿만_반환한다() { + OrganizationEntity korean = organizationEntityGenerator.generate("한앎", "한양대", Language.KO_KR); + OrganizationEntity english = organizationEntityGenerator.generate("english", "hanyang", Language.US_EN); + organizationTemplateEntityGenerator.generate(korean, "템플릿1", Language.KO_KR); + organizationTemplateEntityGenerator.generate(english, "template1", Language.US_EN); + + OrganizationResponses response = organizationService.findAll(Language.US_EN); + + assertAll( + () -> assertThat(response.organizations()).hasSize(1), + () -> assertThat(response.organizations().get(0).organization()).isEqualTo("english") + ); + } + @Test void 비어있을_경우_빈_객체를_반환한다() { - OrganizationResponses response = organizationService.findAll(); + OrganizationResponses response = organizationService.findAll(Language.KO_KR); assertThat(response.organizations()).isEmpty(); }