diff --git a/.gitmodules b/.gitmodules index 9557fa59..00ceb973 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "backend/src/main/resources/submodule"] path = backend/src/main/resources/submodule url = https://github.com/haru-study/submodule.git +[submodule "frontend/env-submodule"] + path = frontend/env-submodule + url = https://github.com/haru-study/env-submodule.git diff --git a/backend/build.gradle b/backend/build.gradle index 87776225..3bb619ab 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -25,12 +25,21 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' + + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'mysql:mysql-connector-java:8.0.33' compileOnly 'org.projectlombok:lombok' diff --git a/backend/src/main/java/harustudy/backend/auth/AuthArgumentResolver.java b/backend/src/main/java/harustudy/backend/auth/AuthArgumentResolver.java new file mode 100644 index 00000000..0294c100 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/AuthArgumentResolver.java @@ -0,0 +1,37 @@ +package harustudy.backend.auth; + +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.service.AuthService; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +@Component +public class AuthArgumentResolver implements HandlerMethodArgumentResolver { + + private static final int ACCESS_TOKEN_LOCATION = 1; + + private final AuthService authService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Authenticated.class); + } + + @Override + public AuthMember resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + String authorizationHeader = webRequest.getHeader(HttpHeaders.AUTHORIZATION); + Objects.requireNonNull(authorizationHeader); + String accessToken = authorizationHeader.split(" ")[ACCESS_TOKEN_LOCATION]; + long memberId = Long.parseLong(authService.parseMemberId(accessToken)); + return new AuthMember(memberId); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/AuthInterceptor.java b/backend/src/main/java/harustudy/backend/auth/AuthInterceptor.java new file mode 100644 index 00000000..b862ff21 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/AuthInterceptor.java @@ -0,0 +1,35 @@ +package harustudy.backend.auth; + +import harustudy.backend.auth.exception.InvalidAuthorizationHeaderException; +import harustudy.backend.auth.service.AuthService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@RequiredArgsConstructor +@Component +public class AuthInterceptor implements HandlerInterceptor { + + private final AuthService authService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + if (HttpMethod.OPTIONS.matches(request.getMethod())) { + return true; + } + String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + String[] splitAuthorizationHeader = authorizationHeader.split(" "); + if (splitAuthorizationHeader.length != 2 || + !splitAuthorizationHeader[0].equals("Bearer")) { + throw new InvalidAuthorizationHeaderException(); + } + String accessToken = authorizationHeader.split(" ")[1]; + authService.validateAccessToken(accessToken); + return HandlerInterceptor.super.preHandle(request, response, handler); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/Authenticated.java b/backend/src/main/java/harustudy/backend/auth/Authenticated.java new file mode 100644 index 00000000..1a88b103 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/Authenticated.java @@ -0,0 +1,14 @@ +package harustudy.backend.auth; + +import io.swagger.v3.oas.annotations.Hidden; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Hidden +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Authenticated { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/config/OauthProperties.java b/backend/src/main/java/harustudy/backend/auth/config/OauthProperties.java new file mode 100644 index 00000000..9f841f5a --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/config/OauthProperties.java @@ -0,0 +1,18 @@ +package harustudy.backend.auth.config; + +import java.util.Map; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Setter +@Configuration +@ConfigurationProperties(prefix = "oauth2") +public class OauthProperties { + + private Map oauthProperties; + + public OauthProperty get(String providerName) { + return oauthProperties.get(providerName); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/config/OauthProperty.java b/backend/src/main/java/harustudy/backend/auth/config/OauthProperty.java new file mode 100644 index 00000000..8d9b58d1 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/config/OauthProperty.java @@ -0,0 +1,15 @@ +package harustudy.backend.auth.config; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OauthProperty { + + private String clientId; + private String clientSecret; + private String redirectUri; + private String tokenUri; + private String userInfoUri; +} diff --git a/backend/src/main/java/harustudy/backend/auth/config/TokenConfig.java b/backend/src/main/java/harustudy/backend/auth/config/TokenConfig.java new file mode 100644 index 00000000..25d2f068 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/config/TokenConfig.java @@ -0,0 +1,13 @@ +package harustudy.backend.auth.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public record TokenConfig( + @Value("${jwt.secret-key}") String secretKey, + @Value("${jwt.expire-length}") long accessTokenExpireLength, + @Value("${jwt.guest-expire-length}") long guestAccessTokenExpireLength, + @Value("${refresh-token.expire-length}") long refreshTokenExpireLength) { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/controller/AuthController.java b/backend/src/main/java/harustudy/backend/auth/controller/AuthController.java new file mode 100644 index 00000000..f3f2c863 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/controller/AuthController.java @@ -0,0 +1,75 @@ +package harustudy.backend.auth.controller; + +import harustudy.backend.auth.dto.OauthLoginRequest; +import harustudy.backend.auth.dto.TokenResponse; +import harustudy.backend.auth.exception.RefreshTokenCookieNotExistsException; +import harustudy.backend.auth.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "인증 관련 기능") +@RequiredArgsConstructor +@RequestMapping("api/auth") +@RestController +public class AuthController { + + @Value("${refresh-token.expire-length}") + private Long refreshTokenExpireLength; + + private final AuthService authService; + + @Operation(summary = "소셜 로그인 요청") + @PostMapping("/login") + public ResponseEntity oauthLogin( + HttpServletResponse httpServletResponse, + @RequestBody OauthLoginRequest request + ) { + TokenResponse tokenResponse = authService.oauthLogin(request); + Cookie cookie = new Cookie("refreshToken", tokenResponse.refreshToken().toString()); + cookie.setMaxAge(refreshTokenExpireLength.intValue()); + cookie.setPath("/"); + httpServletResponse.addCookie(cookie); + return ResponseEntity.ok(tokenResponse); + } + + @Operation(summary = "비회원 로그인 요청") + @PostMapping("/guest") + public ResponseEntity guestLogin() { + TokenResponse tokenResponse = authService.guestLogin(); + return ResponseEntity.ok(tokenResponse); + } + + @Operation(summary = "access 토큰, refresh 토큰 갱신") + @PostMapping("/refresh") + public ResponseEntity refresh( + HttpServletRequest httpServletRequest, + HttpServletResponse httpServletResponse + ) { + String refreshToken = extractRefreshTokenFromCookie(httpServletRequest); + TokenResponse tokenResponse = authService.refresh(refreshToken); + Cookie cookie = new Cookie("refreshToken", tokenResponse.refreshToken().toString()); + cookie.setMaxAge(refreshTokenExpireLength.intValue()); + cookie.setPath("/"); + httpServletResponse.addCookie(cookie); + return ResponseEntity.ok(tokenResponse); + } + + private String extractRefreshTokenFromCookie(HttpServletRequest httpServletRequest) { + return Arrays.stream(httpServletRequest.getCookies()) + .filter(cookie -> cookie.getName().equals("refreshToken")) + .map(Cookie::getValue) + .findAny() + .orElseThrow(RefreshTokenCookieNotExistsException::new); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/domain/RefreshToken.java b/backend/src/main/java/harustudy/backend/auth/domain/RefreshToken.java new file mode 100644 index 00000000..3344dd66 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/domain/RefreshToken.java @@ -0,0 +1,60 @@ +package harustudy.backend.auth.domain; + +import harustudy.backend.auth.exception.RefreshTokenExpiredException; +import harustudy.backend.common.BaseTimeEntity; +import harustudy.backend.member.domain.Member; +import jakarta.persistence.Column; +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.OneToOne; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class RefreshToken extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(columnDefinition = "BINARY(16)") + private UUID uuid; + + private LocalDateTime expireDateTime; + + public RefreshToken(Member member, long expireLength) { + this.member = member; + this.uuid = UUID.randomUUID(); + this.expireDateTime = LocalDateTime.now().plus(expireLength, ChronoUnit.MILLIS); + } + + public void validateExpired() { + if (expireDateTime.isBefore(LocalDateTime.now())) { + throw new RefreshTokenExpiredException(); + } + } + + public RefreshToken updateExpireDateTime(long expireLength) { + this.expireDateTime = LocalDateTime.now().plus(expireLength, ChronoUnit.MILLIS); + return this; + } + + public void updateUuidAndExpireDateTime(long expireLength) { + this.uuid = UUID.randomUUID(); + this.expireDateTime = LocalDateTime.now().plus(expireLength, ChronoUnit.MILLIS); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/dto/AuthMember.java b/backend/src/main/java/harustudy/backend/auth/dto/AuthMember.java new file mode 100644 index 00000000..5474797e --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/dto/AuthMember.java @@ -0,0 +1,5 @@ +package harustudy.backend.auth.dto; + +public record AuthMember(Long id){ + +} diff --git a/backend/src/main/java/harustudy/backend/auth/dto/OauthLoginRequest.java b/backend/src/main/java/harustudy/backend/auth/dto/OauthLoginRequest.java new file mode 100644 index 00000000..d3cf0752 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/dto/OauthLoginRequest.java @@ -0,0 +1,5 @@ +package harustudy.backend.auth.dto; + +public record OauthLoginRequest(String oauthProvider, String code) { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/dto/OauthTokenResponse.java b/backend/src/main/java/harustudy/backend/auth/dto/OauthTokenResponse.java new file mode 100644 index 00000000..36d271b3 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/dto/OauthTokenResponse.java @@ -0,0 +1,12 @@ +package harustudy.backend.auth.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record OauthTokenResponse( + @JsonProperty("token_type") String tokenType, + @JsonProperty("access_token") String accessToken, + String scope) { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/dto/RefreshTokenRequest.java b/backend/src/main/java/harustudy/backend/auth/dto/RefreshTokenRequest.java new file mode 100644 index 00000000..9481bafb --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/dto/RefreshTokenRequest.java @@ -0,0 +1,7 @@ +package harustudy.backend.auth.dto; + +import java.util.UUID; + +public record RefreshTokenRequest(UUID refreshTokenUuid) { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/dto/TokenResponse.java b/backend/src/main/java/harustudy/backend/auth/dto/TokenResponse.java new file mode 100644 index 00000000..64745fd2 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/dto/TokenResponse.java @@ -0,0 +1,19 @@ +package harustudy.backend.auth.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import harustudy.backend.auth.domain.RefreshToken; +import java.util.UUID; + +@JsonInclude(Include.NON_NULL) +public record TokenResponse(String accessToken, @JsonIgnore UUID refreshToken) { + + public static TokenResponse forLoggedIn(String accessToken, RefreshToken refreshToken) { + return new TokenResponse(accessToken, refreshToken.getUuid()); + } + + public static TokenResponse forGuest(String accessToken) { + return new TokenResponse(accessToken, null); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/dto/UserInfo.java b/backend/src/main/java/harustudy/backend/auth/dto/UserInfo.java new file mode 100644 index 00000000..1fbc0200 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/dto/UserInfo.java @@ -0,0 +1,13 @@ +package harustudy.backend.auth.dto; + +import harustudy.backend.member.domain.LoginType; +import harustudy.backend.member.domain.Member; +import lombok.Builder; + +@Builder +public record UserInfo(String name, String email, String imageUrl) { + + public Member toMember(LoginType loginType) { + return new Member(name, email, imageUrl, loginType); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/exception/AuthorizationException.java b/backend/src/main/java/harustudy/backend/auth/exception/AuthorizationException.java new file mode 100644 index 00000000..d3013a52 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/AuthorizationException.java @@ -0,0 +1,7 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.HaruStudyException; + +public class AuthorizationException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/exception/InvalidAccessTokenException.java b/backend/src/main/java/harustudy/backend/auth/exception/InvalidAccessTokenException.java new file mode 100644 index 00000000..82f2bc93 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidAccessTokenException.java @@ -0,0 +1,8 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.HaruStudyException; + +public class InvalidAccessTokenException extends + HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/exception/InvalidAuthorizationHeaderException.java b/backend/src/main/java/harustudy/backend/auth/exception/InvalidAuthorizationHeaderException.java new file mode 100644 index 00000000..036d9ca4 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidAuthorizationHeaderException.java @@ -0,0 +1,8 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.HaruStudyException; + +public class InvalidAuthorizationHeaderException extends + HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/exception/InvalidProviderNameException.java b/backend/src/main/java/harustudy/backend/auth/exception/InvalidProviderNameException.java new file mode 100644 index 00000000..0408caf8 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidProviderNameException.java @@ -0,0 +1,7 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.HaruStudyException; + +public class InvalidProviderNameException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/exception/InvalidRefreshTokenException.java b/backend/src/main/java/harustudy/backend/auth/exception/InvalidRefreshTokenException.java new file mode 100644 index 00000000..cee73292 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidRefreshTokenException.java @@ -0,0 +1,7 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.HaruStudyException; + +public class InvalidRefreshTokenException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenCookieNotExistsException.java b/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenCookieNotExistsException.java new file mode 100644 index 00000000..5d2e1183 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenCookieNotExistsException.java @@ -0,0 +1,7 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.HaruStudyException; + +public class RefreshTokenCookieNotExistsException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenExpiredException.java b/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenExpiredException.java new file mode 100644 index 00000000..bdf594e1 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenExpiredException.java @@ -0,0 +1,7 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.HaruStudyException; + +public class RefreshTokenExpiredException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/infrastructure/GoogleOauthClient.java b/backend/src/main/java/harustudy/backend/auth/infrastructure/GoogleOauthClient.java new file mode 100644 index 00000000..0e968771 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/infrastructure/GoogleOauthClient.java @@ -0,0 +1,54 @@ +package harustudy.backend.auth.infrastructure; + +import harustudy.backend.auth.config.OauthProperty; +import harustudy.backend.auth.dto.OauthTokenResponse; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.client.WebClient; + +@Component +public class GoogleOauthClient { + + public OauthTokenResponse requestOauthToken(String code, OauthProperty oauthProperty) { + return WebClient.create() + .post() + .uri(oauthProperty.getTokenUri()) + .headers(header -> { + header.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + header.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + header.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8)); + }) + .bodyValue(setupFormData(code, oauthProperty)) + .retrieve() + .bodyToMono(OauthTokenResponse.class) + .block(); + } + + private MultiValueMap setupFormData(String code, OauthProperty oauthProperty) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("client_id", oauthProperty.getClientId()); + formData.add("client_secret", oauthProperty.getClientSecret()); + formData.add("code", URLDecoder.decode(code, StandardCharsets.UTF_8)); + formData.add("grant_type", "authorization_code"); + formData.add("redirect_uri", oauthProperty.getRedirectUri()); + return formData; + } + + public Map requestOauthUserInfo(OauthProperty oauthProperty, String accessToken) { + return WebClient.create() + .get() + .uri(oauthProperty.getUserInfoUri()) + .headers(header -> header.setBearerAuth(accessToken)) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() { + }) + .block(); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/repository/RefreshTokenRepository.java b/backend/src/main/java/harustudy/backend/auth/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..873f18a6 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,14 @@ +package harustudy.backend.auth.repository; + +import harustudy.backend.auth.domain.RefreshToken; +import harustudy.backend.member.domain.Member; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { + + Optional findByUuid(UUID refreshToken); + + Optional findByMember(Member member); +} diff --git a/backend/src/main/java/harustudy/backend/auth/service/AuthService.java b/backend/src/main/java/harustudy/backend/auth/service/AuthService.java new file mode 100644 index 00000000..408b6107 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/service/AuthService.java @@ -0,0 +1,113 @@ +package harustudy.backend.auth.service; + +import harustudy.backend.auth.config.OauthProperties; +import harustudy.backend.auth.config.OauthProperty; +import harustudy.backend.auth.config.TokenConfig; +import harustudy.backend.auth.domain.RefreshToken; +import harustudy.backend.auth.dto.OauthLoginRequest; +import harustudy.backend.auth.dto.OauthTokenResponse; +import harustudy.backend.auth.dto.TokenResponse; +import harustudy.backend.auth.dto.UserInfo; +import harustudy.backend.auth.exception.InvalidAccessTokenException; +import harustudy.backend.auth.exception.InvalidRefreshTokenException; +import harustudy.backend.auth.infrastructure.GoogleOauthClient; +import harustudy.backend.auth.repository.RefreshTokenRepository; +import harustudy.backend.auth.util.JwtTokenProvider; +import harustudy.backend.auth.util.OauthUserInfoExtractor; +import harustudy.backend.member.domain.LoginType; +import harustudy.backend.member.domain.Member; +import harustudy.backend.member.repository.MemberRepository; +import io.jsonwebtoken.JwtException; +import java.util.Map; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class AuthService { + + private final OauthProperties oauthProperties; + private final GoogleOauthClient googleOauthClient; + private final JwtTokenProvider jwtTokenProvider; + private final TokenConfig tokenConfig; + private final MemberRepository memberRepository; + private final RefreshTokenRepository refreshTokenRepository; + + public TokenResponse oauthLogin(OauthLoginRequest request) { + UserInfo userInfo = requestUserInfo(request.oauthProvider(), request.code()); + Member member = saveOrUpdateMember(request.oauthProvider(), userInfo); // TODO: 트랜잭션 분리 + String accessToken = generateAccessToken(member.getId()); + RefreshToken refreshToken = saveRefreshTokenOf(member); + return TokenResponse.forLoggedIn(accessToken, refreshToken); + } + + private UserInfo requestUserInfo(String oauthProvider, String code) { + OauthProperty oauthProperty = oauthProperties.get(oauthProvider); + OauthTokenResponse oauthToken = googleOauthClient.requestOauthToken(code, oauthProperty); + Map oauthUserInfo = + googleOauthClient.requestOauthUserInfo(oauthProperty, oauthToken.accessToken()); + return OauthUserInfoExtractor.extract(oauthProvider, oauthUserInfo); + } + + private Member saveOrUpdateMember(String oauthProvider, UserInfo userInfo) { + Member member = memberRepository.findByEmail(userInfo.email()) + .map(entity -> entity.updateUserInfo(userInfo.name(), userInfo.email(), userInfo.imageUrl())) + .orElseGet(() -> userInfo.toMember(LoginType.from(oauthProvider))); + return memberRepository.save(member); + } + + private String generateAccessToken(Long memberId) { + return jwtTokenProvider.builder() + .subject(String.valueOf(memberId)) + .accessTokenExpireLength(tokenConfig.accessTokenExpireLength()) + .secretKey(tokenConfig.secretKey()) + .build(); + } + + private RefreshToken saveRefreshTokenOf(Member member) { + RefreshToken refreshToken = refreshTokenRepository.findByMember(member) + .map(entity -> entity.updateExpireDateTime(tokenConfig.refreshTokenExpireLength())) + .orElseGet(() -> new RefreshToken(member, tokenConfig.refreshTokenExpireLength())); + refreshTokenRepository.save(refreshToken); + return refreshToken; + } + + public TokenResponse guestLogin() { + Member member = Member.guest(); + memberRepository.save(member); + String accessToken = generateGuestAccessToken(member.getId()); + return TokenResponse.forGuest(accessToken); + } + + private String generateGuestAccessToken(Long memberId) { + return jwtTokenProvider.builder() + .subject(String.valueOf(memberId)) + .accessTokenExpireLength(tokenConfig.guestAccessTokenExpireLength()) + .secretKey(tokenConfig.secretKey()) + .build(); + } + + public TokenResponse refresh(String refreshTokenRequest) { + RefreshToken refreshToken = refreshTokenRepository.findByUuid(UUID.fromString(refreshTokenRequest)) + .orElseThrow(InvalidRefreshTokenException::new); + refreshToken.validateExpired(); + refreshToken.updateUuidAndExpireDateTime(tokenConfig.refreshTokenExpireLength()); + String accessToken = generateAccessToken(refreshToken.getMember().getId()); + return TokenResponse.forLoggedIn(accessToken, refreshToken); + } + + public void validateAccessToken(String accessToken) { + try { + jwtTokenProvider.validateAccessToken(accessToken, tokenConfig.secretKey()); + } catch (JwtException e) { + throw new InvalidAccessTokenException(); + } + } + + public String parseMemberId(String accessToken) { + return jwtTokenProvider.parseSubject(accessToken, tokenConfig.secretKey()); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/util/JwtTokenProvider.java b/backend/src/main/java/harustudy/backend/auth/util/JwtTokenProvider.java new file mode 100644 index 00000000..99281506 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/util/JwtTokenProvider.java @@ -0,0 +1,55 @@ +package harustudy.backend.auth.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class JwtTokenProvider { + + @Builder + public String createAccessToken(String subject, Long accessTokenExpireLength, String secretKey) { + Claims claims = generateClaims(subject, accessTokenExpireLength); + + return Jwts.builder() + .setHeaderParam("alg", "HS256") + .setHeaderParam("typ", "JWT") + .setClaims(claims) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)), + SignatureAlgorithm.HS256) + .compact(); + } + + private Claims generateClaims(String subject, Long accessTokenExpireLength) { + Date now = new Date(); + Date expiredAt = new Date(now.getTime() + accessTokenExpireLength); + + return Jwts.claims() + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(expiredAt); + } + + public void validateAccessToken(String accessToken, String secretKey) { + Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(accessToken); + } + + public String parseSubject(String accessToken, String secretKey) { + return Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(accessToken) + .getBody() + .getSubject(); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/util/OauthUserInfoExtractor.java b/backend/src/main/java/harustudy/backend/auth/util/OauthUserInfoExtractor.java new file mode 100644 index 00000000..fe03e88b --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/util/OauthUserInfoExtractor.java @@ -0,0 +1,34 @@ +package harustudy.backend.auth.util; + +import harustudy.backend.auth.dto.UserInfo; +import harustudy.backend.auth.exception.InvalidProviderNameException; +import java.util.Arrays; +import java.util.Map; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum OauthUserInfoExtractor { + + GOOGLE("google") { + @Override + public UserInfo from(Map attributes) { + return UserInfo.builder() + .name(String.valueOf(attributes.get("name"))) + .email(String.valueOf(attributes.get("email"))) + .imageUrl(String.valueOf(attributes.get("picture"))) + .build(); + } + }; + + private final String providerName; + + public static UserInfo extract(String providerName, Map attributes) { + return Arrays.stream(values()) + .filter(provider -> providerName.equals(provider.providerName)) + .findFirst() + .orElseThrow(InvalidProviderNameException::new) + .from(attributes); + } + + public abstract UserInfo from(Map attributes); +} diff --git a/backend/src/main/java/harustudy/backend/common/CachingFilter.java b/backend/src/main/java/harustudy/backend/common/CachingFilter.java new file mode 100644 index 00000000..77833a66 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/common/CachingFilter.java @@ -0,0 +1,28 @@ +package harustudy.backend.common; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +@Component +public class CachingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request); + ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper(response); + + chain.doFilter(wrappingRequest, wrappingResponse); + + wrappingResponse.copyBodyToResponse(); + } +} diff --git a/backend/src/main/java/harustudy/backend/common/EntityNotFoundException.java b/backend/src/main/java/harustudy/backend/common/EntityNotFoundException.java deleted file mode 100644 index 7947fbf6..00000000 --- a/backend/src/main/java/harustudy/backend/common/EntityNotFoundException.java +++ /dev/null @@ -1,25 +0,0 @@ -package harustudy.backend.common; - -// TODO: 예외 나누기 -public class EntityNotFoundException extends RuntimeException { - - public static class RoomNotFound extends EntityNotFoundException { - - } - - public static class MemberNotFound extends EntityNotFoundException { - - } - - public static class PomodoroProgressNotFound extends EntityNotFoundException { - - } - - public static class PomodoroContentNotFound extends EntityNotFoundException { - - } - - public static class ParticipantCodeNotFound extends EntityNotFoundException { - - } -} diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionAdvice.java b/backend/src/main/java/harustudy/backend/common/ExceptionAdvice.java index 1eb5f4e0..7e50c7d6 100644 --- a/backend/src/main/java/harustudy/backend/common/ExceptionAdvice.java +++ b/backend/src/main/java/harustudy/backend/common/ExceptionAdvice.java @@ -1,37 +1,30 @@ package harustudy.backend.common; -import harustudy.backend.member.exception.MemberNotParticipatedException; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -@Slf4j @RestControllerAdvice public class ExceptionAdvice { - @ExceptionHandler(MemberNotParticipatedException.class) - public ResponseEntity handleMemberNotParticipatedException( - MemberNotParticipatedException e) { - log.info(e.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(new ExceptionResponse(MemberNotParticipatedException.CODE, - MemberNotParticipatedException.MESSAGE)); - } + Logger defaultLog = LoggerFactory.getLogger(ExceptionAdvice.class); + Logger exceptionLog = LoggerFactory.getLogger("ExceptionLogger"); - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleBindException( - MethodArgumentNotValidException e) { - log.info(e.getMessage()); - return ResponseEntity.badRequest() - .body(new ExceptionResponse(null, e.getMessage())); + @ExceptionHandler(HaruStudyException.class) + public ResponseEntity handleHaruStudyException(HaruStudyException e) { + ExceptionSituation exceptionSituation = ExceptionMapper.getSituationOf(e); + defaultLog.warn(exceptionSituation.getMessage()); + exceptionLog.warn(exceptionSituation.getMessage(), e); + return ResponseEntity.status(exceptionSituation.getStatusCode()) + .body(ExceptionResponse.from(exceptionSituation)); } @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception e) { - log.info(e.getMessage()); + defaultLog.error(e.getMessage()); + exceptionLog.error(e.getMessage(), e); return ResponseEntity.internalServerError().build(); } } diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionMapper.java b/backend/src/main/java/harustudy/backend/common/ExceptionMapper.java new file mode 100644 index 00000000..fc2173a1 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/common/ExceptionMapper.java @@ -0,0 +1,99 @@ +package harustudy.backend.common; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.FORBIDDEN; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +import harustudy.backend.auth.exception.AuthorizationException; +import harustudy.backend.auth.exception.InvalidAccessTokenException; +import harustudy.backend.auth.exception.InvalidAuthorizationHeaderException; +import harustudy.backend.auth.exception.InvalidProviderNameException; +import harustudy.backend.auth.exception.InvalidRefreshTokenException; +import harustudy.backend.auth.exception.RefreshTokenExpiredException; +import harustudy.backend.content.exception.PomodoroContentNotFoundException; +import harustudy.backend.member.exception.MemberNotFoundException; +import harustudy.backend.progress.exception.NicknameLengthException; +import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; +import harustudy.backend.progress.exception.PomodoroProgressStatusException; +import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; +import harustudy.backend.room.exception.ParticipantCodeExpiredException; +import harustudy.backend.room.exception.ParticipantCodeNotFoundException; +import harustudy.backend.room.exception.PomodoroRoomNameLengthException; +import harustudy.backend.room.exception.PomodoroTimePerCycleException; +import harustudy.backend.room.exception.PomodoroTotalCycleException; +import harustudy.backend.room.exception.RoomNotFoundException; +import java.util.HashMap; +import java.util.Map; + +public class ExceptionMapper { + + private static final Map, ExceptionSituation> mapper = new HashMap<>(); + + static { + setUpMemberException(); + setUpPomodoroContentException(); + setUpPomodoroProgressException(); + setUpRoomException(); + setUpAuthenticationException(); + setUpAuthorizationException(); + } + + private static void setUpMemberException() { + mapper.put(MemberNotFoundException.class, + ExceptionSituation.of("해당하는 멤버가 없습니다.", NOT_FOUND, 1002)); + } + + private static void setUpPomodoroContentException() { + mapper.put(PomodoroContentNotFoundException.class, + ExceptionSituation.of("해당하는 컨텐츠가 없습니다.", NOT_FOUND, 1100)); + } + + private static void setUpPomodoroProgressException() { + mapper.put(PomodoroProgressNotFoundException.class, + ExceptionSituation.of("해당 스터디에 참여한 상태가 아닙니다.", NOT_FOUND, 1201)); + mapper.put(PomodoroProgressStatusException.class, + ExceptionSituation.of("스터디 진행 상태가 적절하지 않습니다.", BAD_REQUEST, 1202)); + mapper.put(ProgressNotBelongToRoomException.class, + ExceptionSituation.of("해당 스터디에 참여한 기록이 없습니다.", BAD_REQUEST, 1203)); + mapper.put(NicknameLengthException.class, + ExceptionSituation.of("닉네임 길이가 유효하지 않습니다.", BAD_REQUEST, 1204)); + } + + private static void setUpRoomException() { + mapper.put(ParticipantCodeNotFoundException.class, + ExceptionSituation.of("해당하는 참여코드가 없습니다.", NOT_FOUND, 1300)); + mapper.put(ParticipantCodeExpiredException.class, + ExceptionSituation.of("만료된 참여코드입니다.", BAD_REQUEST, 1301)); + mapper.put(RoomNotFoundException.class, + ExceptionSituation.of("해당하는 스터디가 없습니다.", NOT_FOUND, 1302)); + mapper.put(PomodoroRoomNameLengthException.class, + ExceptionSituation.of("스터디 이름의 길이가 적절하지 않습니다.", BAD_REQUEST, 1304)); + mapper.put(PomodoroTimePerCycleException.class, + ExceptionSituation.of("시간 당 사이클 횟수가 적절하지 않습니다.", BAD_REQUEST, 1305)); + mapper.put(PomodoroTotalCycleException.class, + ExceptionSituation.of("총 사이클 횟수가 적절하지 않습니다.", BAD_REQUEST, 1306)); + } + + private static void setUpAuthenticationException() { + mapper.put(InvalidProviderNameException.class, + ExceptionSituation.of("유효하지 않은 프로바이더 이름입니다.", BAD_REQUEST, 1400)); + mapper.put(InvalidRefreshTokenException.class, + ExceptionSituation.of("유효하지 않은 갱신 토큰입니다.", BAD_REQUEST, 1401)); + mapper.put(RefreshTokenExpiredException.class, + ExceptionSituation.of("만료된 갱신 토큰입니다.", BAD_REQUEST, 1402)); + mapper.put(InvalidAccessTokenException.class, + ExceptionSituation.of("유효하지 않은 액세스 토큰입니다", UNAUTHORIZED, 1403)); + mapper.put(InvalidAuthorizationHeaderException.class, + ExceptionSituation.of("유효하지 않은 인증 헤더 형식입니다.", BAD_REQUEST, 1404)); + } + + private static void setUpAuthorizationException() { + mapper.put(AuthorizationException.class, + ExceptionSituation.of("권한이 없습니다.", FORBIDDEN, 1500)); + } + + public static ExceptionSituation getSituationOf(Exception exception) { + return mapper.get(exception.getClass()); + } +} diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionResponse.java b/backend/src/main/java/harustudy/backend/common/ExceptionResponse.java index 8558facc..e0d6125a 100644 --- a/backend/src/main/java/harustudy/backend/common/ExceptionResponse.java +++ b/backend/src/main/java/harustudy/backend/common/ExceptionResponse.java @@ -1,5 +1,11 @@ package harustudy.backend.common; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) public record ExceptionResponse(Integer code, String message) { + public static ExceptionResponse from(ExceptionSituation exceptionSituation) { + return new ExceptionResponse(exceptionSituation.getErrorCode(), exceptionSituation.getMessage()); + } } diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionSituation.java b/backend/src/main/java/harustudy/backend/common/ExceptionSituation.java new file mode 100644 index 00000000..77c07014 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/common/ExceptionSituation.java @@ -0,0 +1,26 @@ +package harustudy.backend.common; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class ExceptionSituation { + + private final String message; + private final HttpStatus statusCode; + private final Integer errorCode; + + private ExceptionSituation(String message, HttpStatus statusCode, Integer errorCode) { + this.message = message; + this.statusCode = statusCode; + this.errorCode = errorCode; + } + + public static ExceptionSituation of(String message, HttpStatus statusCode) { + return of(message, statusCode, null); + } + + public static ExceptionSituation of(String message, HttpStatus statusCode, Integer errorCode) { + return new ExceptionSituation(message, statusCode, errorCode); + } +} diff --git a/backend/src/main/java/harustudy/backend/common/HaruStudyException.java b/backend/src/main/java/harustudy/backend/common/HaruStudyException.java new file mode 100644 index 00000000..cadf5f33 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/common/HaruStudyException.java @@ -0,0 +1,5 @@ +package harustudy.backend.common; + +public class HaruStudyException extends RuntimeException { + +} diff --git a/backend/src/main/java/harustudy/backend/common/LoggingInterceptor.java b/backend/src/main/java/harustudy/backend/common/LoggingInterceptor.java new file mode 100644 index 00000000..e45f4d28 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/common/LoggingInterceptor.java @@ -0,0 +1,52 @@ +package harustudy.backend.common; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +@Slf4j(topic = "HttpLogger") +@RequiredArgsConstructor +@Component +public class LoggingInterceptor implements HandlerInterceptor { + + public static final String HTTP_LOG_FORMAT = """ + + request: + requestURI: {} {} + QueryString: {} + Authorization: {} + Body: {} + Handler: {} + ================ + response: + statusCode: {} + Body: {} + """; + + private final ObjectMapper objectMapper; + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request; + ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response; + log.info( + HTTP_LOG_FORMAT, + request.getMethod(), + request.getRequestURI(), + request.getQueryString(), + request.getHeader("Authorization"), + objectMapper.readTree(cachingRequest.getContentAsByteArray()), + handler, + + response.getStatus(), + objectMapper.readTree(cachingResponse.getContentAsByteArray()) + ); + } +} diff --git a/backend/src/main/java/harustudy/backend/common/SwaggerExceptionResponse.java b/backend/src/main/java/harustudy/backend/common/SwaggerExceptionResponse.java new file mode 100644 index 00000000..973f095a --- /dev/null +++ b/backend/src/main/java/harustudy/backend/common/SwaggerExceptionResponse.java @@ -0,0 +1,13 @@ +package harustudy.backend.common; + +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 SwaggerExceptionResponse { + + Class[] value(); +} \ No newline at end of file diff --git a/backend/src/main/java/harustudy/backend/config/BeanConfig.java b/backend/src/main/java/harustudy/backend/config/BeanConfig.java index d1ec9568..05f849f7 100644 --- a/backend/src/main/java/harustudy/backend/config/BeanConfig.java +++ b/backend/src/main/java/harustudy/backend/config/BeanConfig.java @@ -1,7 +1,7 @@ package harustudy.backend.config; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.GenerationStrategy; +import harustudy.backend.room.domain.CodeGenerationStrategy; +import harustudy.backend.room.domain.GenerationStrategy; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/harustudy/backend/config/WebMvcConfig.java b/backend/src/main/java/harustudy/backend/config/WebMvcConfig.java new file mode 100644 index 00000000..17db8334 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/config/WebMvcConfig.java @@ -0,0 +1,48 @@ +package harustudy.backend.config; + +import harustudy.backend.auth.AuthArgumentResolver; +import harustudy.backend.auth.AuthInterceptor; +import harustudy.backend.common.LoggingInterceptor; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final LoggingInterceptor loggingInterceptor; + private final AuthInterceptor authInterceptor; + private final AuthArgumentResolver authArgumentResolver; + + @Value("${cors-allow-origin}") + private String[] corsAllowOrigins; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(loggingInterceptor) + .addPathPatterns("/api/**"); + + registry.addInterceptor(authInterceptor) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/auth/**"); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/**") + .allowedOriginPatterns(corsAllowOrigins) + .allowedMethods("*") + .allowCredentials(true); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(authArgumentResolver); + } +} diff --git a/backend/src/main/java/harustudy/backend/config/OpenApiConfiguration.java b/backend/src/main/java/harustudy/backend/config/swagger/OpenApiConfiguration.java similarity index 82% rename from backend/src/main/java/harustudy/backend/config/OpenApiConfiguration.java rename to backend/src/main/java/harustudy/backend/config/swagger/OpenApiConfiguration.java index a4b3c78a..05b535a6 100644 --- a/backend/src/main/java/harustudy/backend/config/OpenApiConfiguration.java +++ b/backend/src/main/java/harustudy/backend/config/swagger/OpenApiConfiguration.java @@ -1,4 +1,4 @@ -package harustudy.backend.config; +package harustudy.backend.config.swagger; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; @@ -9,7 +9,8 @@ import org.springframework.context.annotation.Configuration; @OpenAPIDefinition(servers = { - @Server(url = "https://haru-study.com", description = "하루스터디"), + @Server(url = "https://dev.haru-study.com", description = "하루스터디 개발 서버"), + @Server(url = "https://haru-study.com", description = "하루스터디 운영 서버") }) @Configuration public class OpenApiConfiguration { diff --git a/backend/src/main/java/harustudy/backend/config/swagger/SwaggerConfig.java b/backend/src/main/java/harustudy/backend/config/swagger/SwaggerConfig.java new file mode 100644 index 00000000..5159f177 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/config/swagger/SwaggerConfig.java @@ -0,0 +1,91 @@ +package harustudy.backend.config.swagger; + +import harustudy.backend.common.ExceptionMapper; +import harustudy.backend.common.ExceptionSituation; +import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.SwaggerExceptionResponse; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.HandlerMethod; + +@Configuration +public class SwaggerConfig { + + @Bean + public OperationCustomizer customize() { + return (Operation operation, HandlerMethod handlerMethod) -> { + SwaggerExceptionResponse exceptionResponse = handlerMethod.getMethodAnnotation( + SwaggerExceptionResponse.class); + + if (!Objects.isNull(exceptionResponse)) { + Class[] exceptionClasses = exceptionResponse.value(); + ApiResponses responses = operation.getResponses(); + setUpApiResponses(exceptionClasses, responses); + } + return operation; + }; + } + + private void setUpApiResponses(Class[] exceptionClasses, + ApiResponses responses) { + Arrays.stream(exceptionClasses) + .forEach(exceptionClass -> setApiResponseFrom(exceptionClass, responses)); + } + + private void setApiResponseFrom(Class exceptionClass, + ApiResponses responses) { + HaruStudyException exception = extractExceptionFrom(exceptionClass); + ApiResponse apiResponse = setupApiResponse(exception); + responses.addApiResponse(removePostfix(exceptionClass), apiResponse); + } + + + private HaruStudyException extractExceptionFrom( + Class exceptionClass) { + try { + Constructor constructor = exceptionClass.getConstructor(); + return constructor.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private ApiResponse setupApiResponse(HaruStudyException exception) { + ObjectSchema objectSchema = setupObjectSchema(exception); + MediaType mediaType = new MediaType().schema(objectSchema); + Content content = new Content().addMediaType("application/json", mediaType); + + ExceptionSituation situation = ExceptionMapper.getSituationOf(exception); + ApiResponse apiResponse = new ApiResponse(); + apiResponse.setContent(content); + apiResponse.description(situation.getStatusCode().toString()); + return apiResponse; + } + + private ObjectSchema setupObjectSchema(HaruStudyException exception) { + ExceptionSituation situation = ExceptionMapper.getSituationOf(exception); + ObjectSchema responseObject = new ObjectSchema(); + responseObject.addProperty("message", new StringSchema().example(situation.getMessage())); + Optional.ofNullable(situation.getErrorCode()) + .ifPresent(code -> responseObject.addProperty("code", + new StringSchema().example(code))); + return responseObject; + } + + private String removePostfix(Class exceptionClass) { + String exceptionClassName = exceptionClass.getSimpleName(); + return exceptionClassName.substring(0, exceptionClassName.indexOf("Exception")); + } +} diff --git a/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentController.java b/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentController.java index e5eab439..1560c4a6 100644 --- a/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentController.java +++ b/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentController.java @@ -1,60 +1,85 @@ package harustudy.backend.content.controller; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.Authenticated; +import harustudy.backend.auth.exception.AuthorizationException; +import harustudy.backend.common.SwaggerExceptionResponse; import harustudy.backend.content.dto.PomodoroContentsResponse; +import harustudy.backend.content.dto.WritePlanRequest; +import harustudy.backend.content.dto.WriteRetrospectRequest; +import harustudy.backend.content.exception.PomodoroContentNotFoundException; import harustudy.backend.content.service.PomodoroContentService; -import jakarta.validation.constraints.NotNull; -import java.util.Map; +import harustudy.backend.member.exception.MemberNotFoundException; +import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; +import harustudy.backend.progress.exception.PomodoroProgressStatusException; +import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; +import harustudy.backend.room.exception.RoomNotFoundException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -// TODO: 콘텐츠 길이 검증 필요 +@Tag(name = "컨텐츠 관련 기능") @RequiredArgsConstructor @RestController -@Deprecated public class PomodoroContentController { private final PomodoroContentService pomodoroContentService; - @Deprecated - @PostMapping("/api/studies/{studyId}/members/{memberId}/content/plans") - public ResponseEntity writePlan( - @PathVariable("studyId") Long studyId, - @PathVariable("memberId") Long memberId, - @RequestBody Map plan - ) { - pomodoroContentService.writePlan(studyId, memberId, plan); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } - - @Deprecated - @PostMapping("/api/studies/{studyId}/members/{memberId}/content/retrospects") - public ResponseEntity writeRetrospect( - @PathVariable("studyId") Long studyId, - @PathVariable("memberId") Long memberId, - @RequestBody Map retrospect + @SwaggerExceptionResponse({ + RoomNotFoundException.class, + MemberNotFoundException.class, + AuthorizationException.class, + PomodoroProgressNotFoundException.class, + PomodoroContentNotFoundException.class + }) + @Operation(summary = "필터링 조건으로 멤버 컨텐츠 조회") + @GetMapping("/api/studies/{studyId}/contents") + public ResponseEntity findMemberContentsWithFilter( + @Authenticated AuthMember authMember, + @PathVariable Long studyId, + @RequestParam Long progressId, + @RequestParam(required = false) Integer cycle ) { - pomodoroContentService.writeRetrospect(studyId, memberId, retrospect); - return ResponseEntity.status(HttpStatus.CREATED).build(); + PomodoroContentsResponse response = pomodoroContentService.findContentsWithFilter(authMember, studyId, progressId, cycle); + return ResponseEntity.ok(response); } - @Deprecated - @GetMapping("/api/studies/{studyId}/members/{memberId}/content/plans") - public ResponseEntity> findCurrentCyclePlan( + @SwaggerExceptionResponse({ + RoomNotFoundException.class, + MemberNotFoundException.class, + AuthorizationException.class, + PomodoroProgressNotFoundException.class, + ProgressNotBelongToRoomException.class + }) + @Operation(summary = "스터디 계획 작성") + @PostMapping("/api/studies/{studyId}/contents/write-plan") + public ResponseEntity writePlan( + @Authenticated AuthMember authMember, @PathVariable Long studyId, - @PathVariable Long memberId, - @RequestParam @NotNull Integer cycle + @RequestBody WritePlanRequest request ) { - return ResponseEntity.ok(pomodoroContentService.findCurrentCyclePlan(studyId, memberId, cycle)); + pomodoroContentService.writePlan(authMember, studyId, request); + return ResponseEntity.ok().build(); } - @Deprecated - @GetMapping("/api/studies/{studyId}/members/{memberId}/content") - public ResponseEntity findMemberContent( + @SwaggerExceptionResponse({ + MemberNotFoundException.class, + RoomNotFoundException.class, + PomodoroProgressNotFoundException.class, + AuthorizationException.class, + PomodoroProgressStatusException.class, + PomodoroContentNotFoundException.class + }) + @Operation(summary = "스터디 회고 작성") + @PostMapping("/api/studies/{studyId}/contents/write-retrospect") + public ResponseEntity writeRetrospect( + @Authenticated AuthMember authMember, @PathVariable Long studyId, - @PathVariable Long memberId + @RequestBody WriteRetrospectRequest request ) { - return ResponseEntity.ok(pomodoroContentService.findMemberContent(studyId, memberId)); + pomodoroContentService.writeRetrospect(authMember, studyId, request); + return ResponseEntity.ok().build(); } } diff --git a/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentControllerV2.java b/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentControllerV2.java deleted file mode 100644 index 9bcbc66f..00000000 --- a/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentControllerV2.java +++ /dev/null @@ -1,48 +0,0 @@ -package harustudy.backend.content.controller; - -import harustudy.backend.content.dto.PomodoroContentsResponse; -import harustudy.backend.content.dto.WritePlanRequest; -import harustudy.backend.content.dto.WriteRetrospectRequest; -import harustudy.backend.content.service.PomodoroContentServiceV2; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -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.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RequiredArgsConstructor -@RestController -public class PomodoroContentControllerV2 { - - private final PomodoroContentServiceV2 pomodoroContentService; - - @GetMapping("/api/v2/studies/{studyId}/contents") - public ResponseEntity findMemberContent( - @PathVariable("studyId") Long studyId, - @RequestParam("memberId") Long memberId, - @RequestParam(name = "cycle", required = false) Integer cycle - ) { - return ResponseEntity.ok(pomodoroContentService.findMemberContentWithCycleFilter(studyId, memberId, cycle)); - } - - @PostMapping("/api/v2/studies/{studyId}/contents/write-plan") - public ResponseEntity writePlan( - @PathVariable("studyId") Long studyId, - @RequestBody WritePlanRequest request - ) { - pomodoroContentService.writePlan(studyId, request); - return ResponseEntity.ok().build(); - } - - @PostMapping("/api/v2/studies/{studyId}/contents/write-retrospect") - public ResponseEntity writeRetrospect( - @PathVariable("studyId") Long studyId, - @RequestBody WriteRetrospectRequest request - ) { - pomodoroContentService.writeRetrospect(studyId, request); - return ResponseEntity.ok().build(); - } -} diff --git a/backend/src/main/java/harustudy/backend/content/dto/WritePlanRequest.java b/backend/src/main/java/harustudy/backend/content/dto/WritePlanRequest.java index a35676c7..78d7d9f7 100644 --- a/backend/src/main/java/harustudy/backend/content/dto/WritePlanRequest.java +++ b/backend/src/main/java/harustudy/backend/content/dto/WritePlanRequest.java @@ -2,6 +2,6 @@ import java.util.Map; -public record WritePlanRequest(Long memberId, Map plan) { +public record WritePlanRequest(Long progressId, Map plan) { } diff --git a/backend/src/main/java/harustudy/backend/content/dto/WriteRetrospectRequest.java b/backend/src/main/java/harustudy/backend/content/dto/WriteRetrospectRequest.java index 767396bb..7fccd7dc 100644 --- a/backend/src/main/java/harustudy/backend/content/dto/WriteRetrospectRequest.java +++ b/backend/src/main/java/harustudy/backend/content/dto/WriteRetrospectRequest.java @@ -2,6 +2,6 @@ import java.util.Map; -public record WriteRetrospectRequest(Long memberId, Map retrospect) { +public record WriteRetrospectRequest(Long progressId, Map retrospect) { } diff --git a/backend/src/main/java/harustudy/backend/content/exception/PomodoroContentNotFoundException.java b/backend/src/main/java/harustudy/backend/content/exception/PomodoroContentNotFoundException.java new file mode 100644 index 00000000..1df8f5a0 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/content/exception/PomodoroContentNotFoundException.java @@ -0,0 +1,8 @@ +package harustudy.backend.content.exception; + +import harustudy.backend.common.HaruStudyException; + +public class PomodoroContentNotFoundException extends HaruStudyException { + +} + diff --git a/backend/src/main/java/harustudy/backend/content/service/PomodoroContentService.java b/backend/src/main/java/harustudy/backend/content/service/PomodoroContentService.java index 5c90a90f..bf60d5ff 100644 --- a/backend/src/main/java/harustudy/backend/content/service/PomodoroContentService.java +++ b/backend/src/main/java/harustudy/backend/content/service/PomodoroContentService.java @@ -1,23 +1,27 @@ package harustudy.backend.content.service; -import harustudy.backend.common.EntityNotFoundException.MemberNotFound; -import harustudy.backend.common.EntityNotFoundException.PomodoroProgressNotFound; -import harustudy.backend.common.EntityNotFoundException.PomodoroContentNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.exception.AuthorizationException; import harustudy.backend.content.domain.PomodoroContent; import harustudy.backend.content.dto.PomodoroContentResponse; import harustudy.backend.content.dto.PomodoroContentsResponse; +import harustudy.backend.content.dto.WritePlanRequest; +import harustudy.backend.content.dto.WriteRetrospectRequest; +import harustudy.backend.content.exception.PomodoroContentNotFoundException; import harustudy.backend.content.repository.PomodoroContentRepository; import harustudy.backend.member.domain.Member; import harustudy.backend.member.repository.MemberRepository; import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.exception.InvalidPomodoroProgressException.UnavailableToProceed; +import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; import harustudy.backend.progress.exception.PomodoroProgressStatusException; +import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; import harustudy.backend.progress.repository.PomodoroProgressRepository; import harustudy.backend.room.domain.PomodoroRoom; +import harustudy.backend.room.exception.RoomNotFoundException; import harustudy.backend.room.repository.PomodoroRoomRepository; +import jakarta.annotation.Nullable; import java.util.List; -import java.util.Map; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,7 +29,6 @@ @RequiredArgsConstructor @Transactional @Service -@Deprecated public class PomodoroContentService { private final PomodoroRoomRepository pomodoroRoomRepository; @@ -33,93 +36,128 @@ public class PomodoroContentService { private final PomodoroProgressRepository pomodoroProgressRepository; private final PomodoroContentRepository pomodoroContentRepository; - public Map findCurrentCyclePlan(Long roomId, Long memberId, - Integer cycle) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId).orElseThrow(IllegalArgumentException::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); - - PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByPomodoroRoomAndMember( - pomodoroRoom, member) - .orElseThrow(IllegalArgumentException::new); + public PomodoroContentsResponse findContentsWithFilter( + AuthMember authMember, Long roomId, Long progressId, @Nullable Integer cycle + ) { + List pomodoroProgresses = getProgressesIfAuthorized( + authMember, roomId); + PomodoroProgress pomodoroProgress = filterSingleProgressById( + pomodoroProgresses, progressId); + + List pomodoroContents = pomodoroProgress.getPomodoroContents(); + if (Objects.isNull(cycle)) { + return getPomodoroContentsResponseWithoutCycleFilter(pomodoroContents); + } + return getPomodoroContentsResponseWithCycleFilter(pomodoroContents, cycle); + } - return pomodoroProgress.findPomodoroRecordByCycle(cycle).getPlan(); + private List getProgressesIfAuthorized(AuthMember authMember, Long roomId) { + PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) + .orElseThrow(RoomNotFoundException::new); + List pomodoroProgresses = pomodoroProgressRepository.findAllByPomodoroRoomFetchMember( + pomodoroRoom); + Member member = memberRepository.findByIdIfExists(authMember.id()); + if (isProgressNotRelatedToMember(pomodoroProgresses, member)) { + throw new AuthorizationException(); + } + return pomodoroProgresses; } - public void writePlan(Long roomId, Long memberId, Map plan) { - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, memberId); - PomodoroContent recentRecord = findRecordWithSameCycle(pomodoroProgress); - validateProgressIsPlanning(pomodoroProgress); - pomodoroProgress.proceed(); - recentRecord.changePlan(plan); + private boolean isProgressNotRelatedToMember(List pomodoroProgresses, + Member member) { + return pomodoroProgresses.stream() + .noneMatch(pomodoroProgress -> pomodoroProgress.isOwnedBy(member)); } - private void validateProgressIsPlanning(PomodoroProgress pomodoroProgress) { - if (pomodoroProgress.isNotPlanning()) { - throw new PomodoroProgressStatusException(); - } + private PomodoroProgress filterSingleProgressById( + List pomodoroProgresses, Long progressId) { + return pomodoroProgresses.stream() + .filter(progress -> progress.getId().equals(progressId)) + .findFirst() + .orElseThrow(PomodoroProgressNotFoundException::new); } - public PomodoroContentsResponse findMemberContent(Long roomId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId).orElseThrow(IllegalArgumentException::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + private PomodoroContentsResponse getPomodoroContentsResponseWithoutCycleFilter(List pomodoroContents) { + List pomodoroContentResponses = pomodoroContents.stream() + .map(PomodoroContentResponse::from) + .toList(); + return PomodoroContentsResponse.from(pomodoroContentResponses); + } - PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByPomodoroRoomAndMember( - pomodoroRoom, member) - .orElseThrow(IllegalArgumentException::new); + private PomodoroContentsResponse getPomodoroContentsResponseWithCycleFilter( + List pomodoroContents, Integer cycle) { + List pomodoroContentResponses = pomodoroContents.stream() + .filter(content -> content.getCycle().equals(cycle)) + .map(PomodoroContentResponse::from) + .toList(); + return PomodoroContentsResponse.from(pomodoroContentResponses); + } - List pomodoroRecords = pomodoroProgress.getPomodoroRecords(); + public void writePlan(AuthMember authMember, Long roomId, WritePlanRequest request) { + Member member = memberRepository.findByIdIfExists(authMember.id()); + PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, request.progressId()); + validateMemberOwnsProgress(member, pomodoroProgress); + validateProgressIsPlanning(pomodoroProgress); + PomodoroContent recentContent = findContentWithSameCycle(pomodoroProgress); + recentContent.changePlan(request.plan()); + } - List pomodoroContentRespons = pomodoroRecords.stream() - .map(record -> new PomodoroContentResponse(record.getCycle(), record.getPlan(), - record.getRetrospect())) - .toList(); + private PomodoroProgress findPomodoroProgressFrom(Long roomId, Long progressId) { + PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) + .orElseThrow(RoomNotFoundException::new); + PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findById(progressId) + .orElseThrow(PomodoroProgressNotFoundException::new); + validateProgressBelongsToRoom(pomodoroRoom, pomodoroProgress); + return pomodoroProgress; + } - return new PomodoroContentsResponse(pomodoroContentRespons); + private void validateProgressBelongsToRoom(PomodoroRoom pomodoroRoom, PomodoroProgress pomodoroProgress) { + if (!pomodoroProgress.isProgressOf(pomodoroRoom)) { + throw new ProgressNotBelongToRoomException(); + } } - public void writeRetrospect(Long roomId, Long memberId, Map retrospect) { - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, memberId); - PomodoroContent recentRecord = findRecordWithSameCycle(pomodoroProgress); - validateProgressIsRetrospect(pomodoroProgress); - validateIsPlanFilled(recentRecord); - recentRecord.changeRetrospect(retrospect); - int totalCycle = pomodoroProgress.getPomodoroRoom().getTotalCycle(); - if (pomodoroProgress.getCurrentCycle().equals(totalCycle)) { - pomodoroProgress.setDone(); - return; + private void validateProgressIsPlanning(PomodoroProgress pomodoroProgress) { + if (pomodoroProgress.isNotPlanning()) { + throw new PomodoroProgressStatusException(); } - pomodoroProgress.proceed(); } - private PomodoroContent findRecordWithSameCycle(PomodoroProgress pomodoroProgress) { - List pomodoroRecords = pomodoroContentRepository.findByPomodoroProgress( + private PomodoroContent findContentWithSameCycle(PomodoroProgress pomodoroProgress) { + List pomodoroContents = pomodoroContentRepository.findByPomodoroProgress( pomodoroProgress); - return pomodoroRecords.stream() - .filter(pomodoroRecord -> pomodoroRecord.hasSameCycleWith(pomodoroProgress)) + return pomodoroContents.stream() + .filter(pomodoroContent -> pomodoroContent.hasSameCycleWith(pomodoroProgress)) .findAny() - .orElseThrow(PomodoroContentNotFound::new); + .orElseThrow(PomodoroContentNotFoundException::new); } - private PomodoroProgress findPomodoroProgressFrom(Long roomId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFound::new); - return pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) - .orElseThrow(PomodoroProgressNotFound::new); + public void writeRetrospect(AuthMember authMember, Long roomId, WriteRetrospectRequest request) { + Member member = memberRepository.findByIdIfExists(authMember.id()); + PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, request.progressId()); + validateMemberOwnsProgress(member, pomodoroProgress); + validateProgressIsRetrospect(pomodoroProgress); + PomodoroContent recentContent = findContentWithSameCycle(pomodoroProgress); + validateIsPlanFilled(recentContent); + + recentContent.changeRetrospect(request.retrospect()); + } + + private void validateMemberOwnsProgress(Member member, PomodoroProgress pomodoroProgress) { + if (!pomodoroProgress.isOwnedBy(member)) { + throw new AuthorizationException(); + } } private void validateProgressIsRetrospect(PomodoroProgress pomodoroProgress) { if (pomodoroProgress.isNotRetrospect()) { - throw new UnavailableToProceed(); // TODO: 예외 세분화 + throw new PomodoroProgressStatusException(); } } - private void validateIsPlanFilled(PomodoroContent recentRecord) { - if (recentRecord.getPlan().isEmpty()) { + private void validateIsPlanFilled(PomodoroContent recentContent) { + if (recentContent.hasEmptyPlan()) { throw new PomodoroProgressStatusException(); } } diff --git a/backend/src/main/java/harustudy/backend/content/service/PomodoroContentServiceV2.java b/backend/src/main/java/harustudy/backend/content/service/PomodoroContentServiceV2.java deleted file mode 100644 index cf3c604d..00000000 --- a/backend/src/main/java/harustudy/backend/content/service/PomodoroContentServiceV2.java +++ /dev/null @@ -1,112 +0,0 @@ -package harustudy.backend.content.service; - -import harustudy.backend.common.EntityNotFoundException.MemberNotFound; -import harustudy.backend.common.EntityNotFoundException.PomodoroContentNotFound; -import harustudy.backend.common.EntityNotFoundException.PomodoroProgressNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.content.dto.PomodoroContentResponse; -import harustudy.backend.content.dto.PomodoroContentsResponse; -import harustudy.backend.content.dto.WritePlanRequest; -import harustudy.backend.content.dto.WriteRetrospectRequest; -import harustudy.backend.content.repository.PomodoroContentRepository; -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.exception.PomodoroProgressStatusException; -import harustudy.backend.progress.repository.PomodoroProgressRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.repository.PomodoroRoomRepository; -import java.util.List; -import java.util.Objects; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Transactional -@Service -public class PomodoroContentServiceV2 { - - private final PomodoroRoomRepository pomodoroRoomRepository; - private final MemberRepository memberRepository; - private final PomodoroProgressRepository pomodoroProgressRepository; - private final PomodoroContentRepository pomodoroContentRepository; - - public void writePlan(Long roomId, WritePlanRequest request) { - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, request.memberId()); - PomodoroContent recentContent = findContentWithSameCycle(pomodoroProgress); - validateProgressIsPlanning(pomodoroProgress); - recentContent.changePlan(request.plan()); - } - - private void validateProgressIsPlanning(PomodoroProgress pomodoroProgress) { - if (pomodoroProgress.isNotPlanning()) { - throw new PomodoroProgressStatusException(); - } - } - - private PomodoroContent findContentWithSameCycle(PomodoroProgress pomodoroProgress) { - List pomodoroContents = pomodoroContentRepository.findByPomodoroProgress( - pomodoroProgress); - - return pomodoroContents.stream() - .filter(pomodoroContent -> pomodoroContent.hasSameCycleWith(pomodoroProgress)) - .findAny() - .orElseThrow(PomodoroContentNotFound::new); - } - - public void writeRetrospect(Long roomId, WriteRetrospectRequest request) { - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, request.memberId()); - PomodoroContent recentContent = findContentWithSameCycle(pomodoroProgress); - validateProgressIsRetrospect(pomodoroProgress); - validateIsPlanFilled(recentContent); - recentContent.changeRetrospect(request.retrospect()); - } - - private void validateProgressIsRetrospect(PomodoroProgress pomodoroProgress) { - if (pomodoroProgress.isNotRetrospect()) { - throw new PomodoroProgressStatusException(); - } - } - - private void validateIsPlanFilled(PomodoroContent recentContent) { - if (recentContent.hasEmptyPlan()) { - throw new PomodoroProgressStatusException(); - } - } - - // TODO: 이후 동적쿼리로 변경해서 repository로 로직 밀어넣기 - public PomodoroContentsResponse findMemberContentWithCycleFilter(Long roomId, Long memberId, Integer cycle) { - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, memberId); - List pomodoroContents = pomodoroProgress.getPomodoroContents(); - if (Objects.isNull(cycle)) { - return getPomodoroContentsResponseWithoutCycleFilter(pomodoroContents); - } - return getPomodoroContentsResponseWithCycleFilter(pomodoroContents, cycle); - } - - private PomodoroContentsResponse getPomodoroContentsResponseWithoutCycleFilter(List pomodoroContents) { - List pomodoroContentResponses = pomodoroContents.stream() - .map(PomodoroContentResponse::from) - .toList(); - return PomodoroContentsResponse.from(pomodoroContentResponses); - } - - private PomodoroContentsResponse getPomodoroContentsResponseWithCycleFilter(List pomodoroContents, Integer cycle) { - List pomodoroContentResponses = pomodoroContents.stream() - .filter(content -> content.getCycle().equals(cycle)) - .map(PomodoroContentResponse::from) - .toList(); - return PomodoroContentsResponse.from(pomodoroContentResponses); - } - - private PomodoroProgress findPomodoroProgressFrom(Long roomId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFound::new); - return pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) - .orElseThrow(PomodoroProgressNotFound::new); - } -} diff --git a/backend/src/main/java/harustudy/backend/member/controller/MemberController.java b/backend/src/main/java/harustudy/backend/member/controller/MemberController.java index 5d0d65ad..836c3876 100644 --- a/backend/src/main/java/harustudy/backend/member/controller/MemberController.java +++ b/backend/src/main/java/harustudy/backend/member/controller/MemberController.java @@ -1,25 +1,34 @@ package harustudy.backend.member.controller; -import harustudy.backend.member.dto.NicknameResponse; +import harustudy.backend.auth.Authenticated; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.common.SwaggerExceptionResponse; +import harustudy.backend.member.dto.MemberResponse; +import harustudy.backend.member.exception.MemberNotFoundException; import harustudy.backend.member.service.MemberService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -@Deprecated +@Tag(name = "멤버 관련 기능") @RequiredArgsConstructor @RestController public class MemberController { private final MemberService memberService; - @Deprecated - @GetMapping("/api/studies/{studyId}/members/{memberId}") - public ResponseEntity findNickname(@PathVariable Long studyId, - @PathVariable Long memberId) { - NicknameResponse response = memberService.findParticipatedMemberNickname(studyId, memberId); + @SwaggerExceptionResponse({ + MemberNotFoundException.class + }) + @Operation(summary = "멤버 Oauth 프로필 정보 조회") + @GetMapping("/api/me") + public ResponseEntity findOauthProfile( + @Authenticated AuthMember authMember + ) { + MemberResponse response = memberService.findOauthProfile(authMember); return ResponseEntity.ok(response); } } diff --git a/backend/src/main/java/harustudy/backend/member/controller/MemberControllerV2.java b/backend/src/main/java/harustudy/backend/member/controller/MemberControllerV2.java deleted file mode 100644 index 2ad97bbe..00000000 --- a/backend/src/main/java/harustudy/backend/member/controller/MemberControllerV2.java +++ /dev/null @@ -1,48 +0,0 @@ -package harustudy.backend.member.controller; - -import harustudy.backend.member.dto.GuestRegisterRequest; -import harustudy.backend.member.dto.MemberResponseV2; -import harustudy.backend.member.dto.MembersResponseV2; -import harustudy.backend.member.service.MemberServiceV2; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; -import java.net.URI; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -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.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RequiredArgsConstructor -@RestController -public class MemberControllerV2 { - - private final MemberServiceV2 memberService; - - @GetMapping("/api/v2/members/{memberId}") - public ResponseEntity findMember(@PathVariable Long memberId) { - MemberResponseV2 response = memberService.findMember(memberId); - return ResponseEntity.ok(response); - } - - @GetMapping("/api/v2/members") - public ResponseEntity findParticipatedMembers( - @RequestParam("studyId") Long studyId) { - MembersResponseV2 response = memberService.findParticipatedMembers(studyId); - return ResponseEntity.ok(response); - } - - @PostMapping("/api/v2/members/guest") - public ResponseEntity registerGuest(HttpServletResponse servletResponse, - @RequestBody GuestRegisterRequest request) { - Long memberId = memberService.register(request); - - //TODO: cookie 수명 설정해주기 - Cookie cookie = new Cookie("memberId", String.valueOf(memberId)); - servletResponse.addCookie(cookie); - return ResponseEntity.created(URI.create("/api/v2/members/" + memberId)).build(); - } -} diff --git a/backend/src/main/java/harustudy/backend/member/domain/LoginType.java b/backend/src/main/java/harustudy/backend/member/domain/LoginType.java new file mode 100644 index 00000000..e57bf7a9 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/member/domain/LoginType.java @@ -0,0 +1,11 @@ +package harustudy.backend.member.domain; + +public enum LoginType { + + GUEST, + GOOGLE; + + public static LoginType from(String name) { + return valueOf(name.toUpperCase()); + } +} diff --git a/backend/src/main/java/harustudy/backend/member/domain/Member.java b/backend/src/main/java/harustudy/backend/member/domain/Member.java index cba4ef05..06d1c121 100644 --- a/backend/src/main/java/harustudy/backend/member/domain/Member.java +++ b/backend/src/main/java/harustudy/backend/member/domain/Member.java @@ -1,13 +1,13 @@ package harustudy.backend.member.domain; import harustudy.backend.common.BaseTimeEntity; -import harustudy.backend.member.exception.MemberNameLengthException; -import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.validation.constraints.NotNull; +import java.util.Objects; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -21,22 +21,51 @@ public class Member extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @NotNull - @Column(length = 10) - private String nickname; + private String name; - public Member(@NotNull String nickname) { - validateLength(nickname); - this.nickname = nickname; + private String email; + + private String imageUrl; + + @Enumerated(EnumType.STRING) + private LoginType loginType; + + public Member(String name, String email, String imageUrl, LoginType loginType) { + this.name = name; + this.email = email; + this.imageUrl = imageUrl; + this.loginType = loginType; + } + + public static Member guest() { + return new Member("guest", null, null, LoginType.GUEST); } - private void validateLength(String nickname) { - if (nickname.length() < 1 || nickname.length() > 10) { - throw new MemberNameLengthException(); + public Member updateUserInfo(String name, String email, String imageUrl) { + this.name = name; + this.email = email; + this.imageUrl = imageUrl; + return this; + } + + public boolean isDifferentMember(Member other) { + return !this.equals(other); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; } + Member otherMember = (Member) other; + return getId().equals(otherMember.getId()); } - public boolean hasSameNickname(Member member) { - return nickname.equals(member.nickname); + @Override + public int hashCode() { + return Objects.hash(getId()); } } diff --git a/backend/src/main/java/harustudy/backend/member/dto/GuestRegisterRequest.java b/backend/src/main/java/harustudy/backend/member/dto/GuestRegisterRequest.java deleted file mode 100644 index fa3a2c39..00000000 --- a/backend/src/main/java/harustudy/backend/member/dto/GuestRegisterRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package harustudy.backend.member.dto; - -public record GuestRegisterRequest(Long studyId, String nickname) { - -} diff --git a/backend/src/main/java/harustudy/backend/member/dto/MemberResponse.java b/backend/src/main/java/harustudy/backend/member/dto/MemberResponse.java new file mode 100644 index 00000000..10d26d83 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/member/dto/MemberResponse.java @@ -0,0 +1,12 @@ +package harustudy.backend.member.dto; + +import harustudy.backend.member.domain.Member; + +public record MemberResponse(Long memberId, String name, String email, String imageUrl, + String loginType) { + + public static MemberResponse from(Member member) { + return new MemberResponse(member.getId(), member.getName(), member.getEmail(), + member.getImageUrl(), member.getLoginType().name().toLowerCase()); + } +} diff --git a/backend/src/main/java/harustudy/backend/member/dto/MemberResponseV2.java b/backend/src/main/java/harustudy/backend/member/dto/MemberResponseV2.java deleted file mode 100644 index e81323c7..00000000 --- a/backend/src/main/java/harustudy/backend/member/dto/MemberResponseV2.java +++ /dev/null @@ -1,10 +0,0 @@ -package harustudy.backend.member.dto; - -import harustudy.backend.member.domain.Member; - -public record MemberResponseV2(Long id, String nickname) { - - public static MemberResponseV2 from(Member member) { - return new MemberResponseV2(member.getId(), member.getNickname()); - } -} diff --git a/backend/src/main/java/harustudy/backend/member/dto/MembersResponseV2.java b/backend/src/main/java/harustudy/backend/member/dto/MembersResponseV2.java deleted file mode 100644 index e631d29f..00000000 --- a/backend/src/main/java/harustudy/backend/member/dto/MembersResponseV2.java +++ /dev/null @@ -1,7 +0,0 @@ -package harustudy.backend.member.dto; - -import java.util.List; - -public record MembersResponseV2(List members) { - -} diff --git a/backend/src/main/java/harustudy/backend/member/dto/NicknameResponse.java b/backend/src/main/java/harustudy/backend/member/dto/NicknameResponse.java deleted file mode 100644 index 3f8ec3b1..00000000 --- a/backend/src/main/java/harustudy/backend/member/dto/NicknameResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package harustudy.backend.member.dto; - -public record NicknameResponse(String nickname) { - -} diff --git a/backend/src/main/java/harustudy/backend/member/exception/MemberNameLengthException.java b/backend/src/main/java/harustudy/backend/member/exception/MemberNameLengthException.java deleted file mode 100644 index 9027db26..00000000 --- a/backend/src/main/java/harustudy/backend/member/exception/MemberNameLengthException.java +++ /dev/null @@ -1,5 +0,0 @@ -package harustudy.backend.member.exception; - -public class MemberNameLengthException extends RuntimeException { - -} diff --git a/backend/src/main/java/harustudy/backend/member/exception/MemberNotFoundException.java b/backend/src/main/java/harustudy/backend/member/exception/MemberNotFoundException.java new file mode 100644 index 00000000..6d772c13 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/member/exception/MemberNotFoundException.java @@ -0,0 +1,7 @@ +package harustudy.backend.member.exception; + +import harustudy.backend.common.HaruStudyException; + +public class MemberNotFoundException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/member/exception/MemberNotParticipatedException.java b/backend/src/main/java/harustudy/backend/member/exception/MemberNotParticipatedException.java deleted file mode 100644 index c0698058..00000000 --- a/backend/src/main/java/harustudy/backend/member/exception/MemberNotParticipatedException.java +++ /dev/null @@ -1,7 +0,0 @@ -package harustudy.backend.member.exception; - -public class MemberNotParticipatedException extends RuntimeException { - - public static final int CODE = 1000; - public static final String MESSAGE = "스터디에 참여하지 않은 멤버의 닉네임입니다."; -} diff --git a/backend/src/main/java/harustudy/backend/member/repository/MemberRepository.java b/backend/src/main/java/harustudy/backend/member/repository/MemberRepository.java index 64e5cb38..ac2e5ea4 100644 --- a/backend/src/main/java/harustudy/backend/member/repository/MemberRepository.java +++ b/backend/src/main/java/harustudy/backend/member/repository/MemberRepository.java @@ -1,8 +1,16 @@ package harustudy.backend.member.repository; import harustudy.backend.member.domain.Member; +import harustudy.backend.member.exception.MemberNotFoundException; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberRepository extends JpaRepository { + default Member findByIdIfExists(Long id) { + return findById(id) + .orElseThrow(MemberNotFoundException::new); + } + + Optional findByEmail(String email); } diff --git a/backend/src/main/java/harustudy/backend/member/service/MemberService.java b/backend/src/main/java/harustudy/backend/member/service/MemberService.java index 3403c0d8..fbd8a9fb 100644 --- a/backend/src/main/java/harustudy/backend/member/service/MemberService.java +++ b/backend/src/main/java/harustudy/backend/member/service/MemberService.java @@ -1,34 +1,25 @@ package harustudy.backend.member.service; -import harustudy.backend.common.EntityNotFoundException.MemberNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; +import harustudy.backend.auth.dto.AuthMember; import harustudy.backend.member.domain.Member; -import harustudy.backend.member.dto.NicknameResponse; +import harustudy.backend.member.dto.MemberResponse; +import harustudy.backend.member.exception.MemberNotFoundException; import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.repository.PomodoroRoomRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@Deprecated @RequiredArgsConstructor -@Transactional(readOnly = true) +@Transactional @Service public class MemberService { - private final PomodoroRoomRepository pomodoroRoomRepository; private final MemberRepository memberRepository; - public NicknameResponse findParticipatedMemberNickname(Long roomId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFound::new); + public MemberResponse findOauthProfile(AuthMember authMember) { + Member authorizedMember = memberRepository.findById(authMember.id()) + .orElseThrow(MemberNotFoundException::new); - if (pomodoroRoom.isParticipatedMember(member)) { - return new NicknameResponse(member.getNickname()); - } - return new NicknameResponse(null); + return MemberResponse.from(authorizedMember); } } diff --git a/backend/src/main/java/harustudy/backend/member/service/MemberServiceV2.java b/backend/src/main/java/harustudy/backend/member/service/MemberServiceV2.java deleted file mode 100644 index aa4ce0d3..00000000 --- a/backend/src/main/java/harustudy/backend/member/service/MemberServiceV2.java +++ /dev/null @@ -1,62 +0,0 @@ -package harustudy.backend.member.service; - -import harustudy.backend.common.EntityNotFoundException.MemberNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.content.repository.PomodoroContentRepository; -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.dto.GuestRegisterRequest; -import harustudy.backend.member.dto.MemberResponseV2; -import harustudy.backend.member.dto.MembersResponseV2; -import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.repository.PomodoroProgressRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.repository.PomodoroRoomRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Transactional -@Service -public class MemberServiceV2 { - - private final MemberRepository memberRepository; - private final PomodoroRoomRepository pomodoroRoomRepository; - private final PomodoroProgressRepository pomodoroProgressRepository; - private final PomodoroContentRepository pomodoroContentRepository; - - public MemberResponseV2 findMember(Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFound::new); - return MemberResponseV2.from(member); - } - - public MembersResponseV2 findParticipatedMembers(Long roomId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - List pomodoroProgresses = - pomodoroProgressRepository.findAllByPomodoroRoomFetchMember(pomodoroRoom); - List memberResponses = pomodoroProgresses.stream() - .map(PomodoroProgress::getMember) - .map(MemberResponseV2::from) - .toList(); - return new MembersResponseV2(memberResponses); - } - - public Long register(GuestRegisterRequest request) { - Member member = memberRepository.save(new Member(request.nickname())); - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(request.studyId()) - .orElseThrow(RoomNotFound::new); - Integer totalCycle = pomodoroRoom.getTotalCycle(); - - PomodoroProgress pomodoroProgress = pomodoroProgressRepository.save( - new PomodoroProgress(pomodoroRoom, member)); - for (int i = 1; i <= totalCycle; i++) { - pomodoroContentRepository.save(new PomodoroContent(pomodoroProgress, i)); - } - return member.getId(); - } -} diff --git a/backend/src/main/java/harustudy/backend/participantcode/controller/ParticipantCodeController.java b/backend/src/main/java/harustudy/backend/participantcode/controller/ParticipantCodeController.java deleted file mode 100644 index cd7ab3b0..00000000 --- a/backend/src/main/java/harustudy/backend/participantcode/controller/ParticipantCodeController.java +++ /dev/null @@ -1,24 +0,0 @@ -package harustudy.backend.participantcode.controller; - -import harustudy.backend.participantcode.dto.FindRoomRequest; -import harustudy.backend.participantcode.dto.FindRoomResponse; -import harustudy.backend.participantcode.service.ParticipantCodeService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -@RequiredArgsConstructor -@RestController -public class ParticipantCodeController { - - private final ParticipantCodeService participantCodeService; - - @Deprecated - @PostMapping("/api/studies/authenticate") - public ResponseEntity checkAuth(@Valid @RequestBody FindRoomRequest request) { - return ResponseEntity.ok(participantCodeService.findRoomByCode(request.participantCode())); - } -} diff --git a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomAndNicknameRequest.java b/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomAndNicknameRequest.java deleted file mode 100644 index 2404493c..00000000 --- a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomAndNicknameRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package harustudy.backend.participantcode.dto; - -public record FindRoomAndNicknameRequest(String participantCode, Long memberId) { - -} diff --git a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomAndNicknameResponse.java b/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomAndNicknameResponse.java deleted file mode 100644 index ddb5e6c7..00000000 --- a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomAndNicknameResponse.java +++ /dev/null @@ -1,8 +0,0 @@ -package harustudy.backend.participantcode.dto; - -public record FindRoomAndNicknameResponse(Long studyId, String studyName, String nickname) { - - public FindRoomAndNicknameResponse(Long studyId, String studyName) { - this(studyId, studyName, null); - } -} diff --git a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomRequest.java b/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomRequest.java deleted file mode 100644 index a00e4c10..00000000 --- a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomRequest.java +++ /dev/null @@ -1,7 +0,0 @@ -package harustudy.backend.participantcode.dto; - -import jakarta.validation.constraints.NotNull; - -public record FindRoomRequest(@NotNull String participantCode) { - -} diff --git a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomResponse.java b/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomResponse.java deleted file mode 100644 index f237273e..00000000 --- a/backend/src/main/java/harustudy/backend/participantcode/dto/FindRoomResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package harustudy.backend.participantcode.dto; - -public record FindRoomResponse(Long studyId, String studyName) { - -} diff --git a/backend/src/main/java/harustudy/backend/participantcode/service/ParticipantCodeService.java b/backend/src/main/java/harustudy/backend/participantcode/service/ParticipantCodeService.java deleted file mode 100644 index bc7f6fba..00000000 --- a/backend/src/main/java/harustudy/backend/participantcode/service/ParticipantCodeService.java +++ /dev/null @@ -1,49 +0,0 @@ -package harustudy.backend.participantcode.service; - -import static harustudy.backend.common.EntityNotFoundException.RoomNotFound; - -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.participantcode.dto.FindRoomAndNicknameResponse; -import harustudy.backend.participantcode.dto.FindRoomResponse; -import harustudy.backend.participantcode.repository.ParticipantCodeRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.repository.PomodoroRoomRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -// TODO: @Transactional(readonly = true) -@RequiredArgsConstructor -@Transactional -@Service -public class ParticipantCodeService { - - private final PomodoroRoomRepository pomodoroRoomRepository; - private final ParticipantCodeRepository participantCodeRepository; - private final MemberRepository memberRepository; - - public FindRoomAndNicknameResponse findRoomByCodeWithMemberId(String code, Long memberId) { - ParticipantCode participantCode = participantCodeRepository.findByCode(code) - .orElseThrow(IllegalArgumentException::new); - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByParticipantCode(participantCode) - .orElseThrow(RoomNotFound::new); - - Member member = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); - - if (!pomodoroRoom.isParticipatedMember(member)) { - throw new IllegalArgumentException(); - } - return new FindRoomAndNicknameResponse(pomodoroRoom.getId(), pomodoroRoom.getName(), member.getNickname()); - } - - public FindRoomResponse findRoomByCode(String code) { - ParticipantCode participantCode = participantCodeRepository.findByCode(code) - .orElseThrow(IllegalArgumentException::new); - PomodoroRoom room = pomodoroRoomRepository.findByParticipantCode(participantCode) - .orElseThrow(RoomNotFound::new); - return new FindRoomResponse(room.getId(), room.getName()); - } -} diff --git a/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressController.java b/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressController.java index b62598a6..f64b7bbe 100644 --- a/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressController.java +++ b/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressController.java @@ -1,46 +1,105 @@ package harustudy.backend.progress.controller; +import harustudy.backend.auth.Authenticated; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.exception.AuthorizationException; +import harustudy.backend.common.SwaggerExceptionResponse; +import harustudy.backend.member.exception.MemberNotFoundException; +import harustudy.backend.progress.dto.ParticipateStudyRequest; import harustudy.backend.progress.dto.PomodoroProgressResponse; -import harustudy.backend.progress.dto.RoomAndProgressStepResponse; +import harustudy.backend.progress.dto.PomodoroProgressesResponse; +import harustudy.backend.progress.exception.NicknameLengthException; +import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; +import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; import harustudy.backend.progress.service.PomodoroProgressService; -import jakarta.validation.constraints.NotNull; +import harustudy.backend.room.exception.RoomNotFoundException; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +@Tag(name = "진행 관련 기능") @RequiredArgsConstructor @RestController -@Deprecated public class PomodoroProgressController { private final PomodoroProgressService pomodoroProgressService; - @Deprecated - @PostMapping("api/studies/{studyId}/members/{memberId}/next-step") - public ResponseEntity proceed(@PathVariable("studyId") @NotNull Long studyId, - @PathVariable("memberId") @NotNull Long memberId) { - pomodoroProgressService.proceedToRetrospect(studyId, memberId); - return ResponseEntity.ok().build(); + @SwaggerExceptionResponse({ + MemberNotFoundException.class, + RoomNotFoundException.class, + PomodoroProgressNotFoundException.class, + AuthorizationException.class, + ProgressNotBelongToRoomException.class + }) + @Operation(summary = "단일 스터디 진행도 조회") + @GetMapping("/api/studies/{studyId}/progresses/{progressId}") + public ResponseEntity findPomodoroProgress( + @Authenticated AuthMember authMember, + @PathVariable Long studyId, + @PathVariable Long progressId + ) { + PomodoroProgressResponse response = + pomodoroProgressService.findPomodoroProgress(authMember, studyId, progressId); + return ResponseEntity.ok(response); } - @Deprecated - @GetMapping("/api/studies/{studyId}/members/{memberId}/metadata") - public ResponseEntity findMemberStudyMetaData( + @SwaggerExceptionResponse({ + RoomNotFoundException.class, + MemberNotFoundException.class, + PomodoroProgressNotFoundException.class, + AuthorizationException.class + }) + @Operation(summary = "필터링 조건으로 스터디 진행도 조회") + @GetMapping("/api/studies/{studyId}/progresses") + public ResponseEntity findPomodoroProgressesWithFilter( + @Authenticated AuthMember authMember, @PathVariable Long studyId, - @PathVariable Long memberId + @RequestParam(required = false) Long memberId ) { - return ResponseEntity.ok(pomodoroProgressService.findMemberMetaData(studyId, memberId)); + PomodoroProgressesResponse response = + pomodoroProgressService.findPomodoroProgressWithFilter(authMember, studyId, memberId); + return ResponseEntity.ok(response); } - @Deprecated - @GetMapping("api/studies/{studyId}/members/{memberId}/progress") - public ResponseEntity findPomodoroProgress( + @SwaggerExceptionResponse({ + MemberNotFoundException.class, + PomodoroProgressNotFoundException.class, + RoomNotFoundException.class, + AuthorizationException.class, + ProgressNotBelongToRoomException.class + }) + @Operation(summary = "다음 스터디 단계로 이동") + @ApiResponse(responseCode = "204") + @PostMapping("/api/studies/{studyId}/progresses/{progressId}/next-step") + public ResponseEntity proceed( + @Authenticated AuthMember authMember, + @PathVariable Long studyId, + @PathVariable Long progressId + ) { + pomodoroProgressService.proceed(authMember, studyId, progressId); + return ResponseEntity.noContent().build(); + } + + @SwaggerExceptionResponse({ + MemberNotFoundException.class, + RoomNotFoundException.class, + AuthorizationException.class, + NicknameLengthException.class + }) + @Operation(summary = "스터디 참여") + @ApiResponse(responseCode = "201") + @PostMapping("/api/studies/{studyId}/progresses") + public ResponseEntity participate( + @Authenticated AuthMember authMember, @PathVariable Long studyId, - @PathVariable Long memberId + @RequestBody ParticipateStudyRequest request ) { - return ResponseEntity.ok(pomodoroProgressService.findPomodoroProgress(studyId, memberId)); + Long progressId = pomodoroProgressService.participateStudy(authMember, studyId, request); + return ResponseEntity.created( + URI.create("/api/studies/" + studyId + "/progresses/" + progressId)).build(); } } diff --git a/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressControllerV2.java b/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressControllerV2.java deleted file mode 100644 index b09be194..00000000 --- a/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressControllerV2.java +++ /dev/null @@ -1,35 +0,0 @@ -package harustudy.backend.progress.controller; - -import harustudy.backend.progress.dto.PomodoroProgressResponseV2; -import harustudy.backend.progress.service.PomodoroProgressServiceV2; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RequiredArgsConstructor -@RestController -public class PomodoroProgressControllerV2 { - - private final PomodoroProgressServiceV2 pomodoroProgressServiceV2; - - @GetMapping("/api/v2/studies/{studyId}/progresses") - public ResponseEntity findProgress(@PathVariable("studyId") Long studyId, - @RequestParam("memberId") Long memberId) { - PomodoroProgressResponseV2 response = pomodoroProgressServiceV2.findProgress(studyId, - memberId); - return ResponseEntity.ok(response); - } - - @PostMapping("/api/v2/studies/{studyId}/progresses/{progressId}/next-step") - public ResponseEntity proceed( - @PathVariable("studyId") Long studyId, - @PathVariable("progressId") Long progressId - ) { - pomodoroProgressServiceV2.proceed(studyId, progressId); - return ResponseEntity.noContent().build(); - } -} diff --git a/backend/src/main/java/harustudy/backend/progress/domain/PomodoroProgress.java b/backend/src/main/java/harustudy/backend/progress/domain/PomodoroProgress.java index acdfe40e..d40e07eb 100644 --- a/backend/src/main/java/harustudy/backend/progress/domain/PomodoroProgress.java +++ b/backend/src/main/java/harustudy/backend/progress/domain/PomodoroProgress.java @@ -3,7 +3,9 @@ import harustudy.backend.common.BaseTimeEntity; import harustudy.backend.content.domain.PomodoroContent; import harustudy.backend.member.domain.Member; +import harustudy.backend.progress.exception.NicknameLengthException; import harustudy.backend.room.domain.PomodoroRoom; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -39,10 +41,10 @@ public class PomodoroProgress extends BaseTimeEntity { @JoinColumn(name = "member_id") private Member member; - @OneToMany(mappedBy = "pomodoroProgress") + @OneToMany(mappedBy = "pomodoroProgress", cascade = CascadeType.PERSIST) private List pomodoroContents = new ArrayList<>(); - private boolean isDone = false; + private String nickname; @NotNull private Integer currentCycle; @@ -50,55 +52,30 @@ public class PomodoroProgress extends BaseTimeEntity { @Enumerated(value = EnumType.STRING) private PomodoroStatus pomodoroStatus; - public PomodoroProgress(PomodoroRoom pomodoroRoom, Member member) { + public PomodoroProgress(PomodoroRoom pomodoroRoom, Member member, String nickname) { this.pomodoroRoom = pomodoroRoom; this.member = member; + this.nickname = nickname; this.currentCycle = 1; this.pomodoroStatus = PomodoroStatus.PLANNING; - } - - // TODO: 없애기 - public PomodoroProgress(PomodoroRoom pomodoroRoom, Member member, @NotNull Integer currentCycle, - PomodoroStatus pomodoroStatus) { - this.pomodoroRoom = pomodoroRoom; - this.member = member; - this.currentCycle = currentCycle; - this.pomodoroStatus = pomodoroStatus; - } - - public boolean isOwnedBy(Member member) { - return getMember().equals(member); - } - - public boolean hasSameNicknameMember(Member member) { - return getMember().hasSameNickname(member); - } - public void setDone() { - this.isDone = true; + validateNicknameLength(nickname); } - public PomodoroContent findPomodoroRecordByCycle(Integer cycle) { - return getPomodoroContents().stream() - .filter(pomodoro -> pomodoro.getCycle().equals(cycle)) - .findAny() - .orElseThrow(IllegalArgumentException::new); - } - - public List getPomodoroRecords() { - return getPomodoroContents().stream() - .toList(); + private void validateNicknameLength(String nickname) { + if (nickname.length() < 1 || nickname.length() > 10) { + throw new NicknameLengthException(); + } } - @Deprecated - public void proceed() { - if (isRetrospect()) { - currentCycle++; + public void generateContents(int totalCycle) { + for (int cycle = 1; cycle <= totalCycle; cycle++) { + PomodoroContent pomodoroContent = new PomodoroContent(this, cycle); + pomodoroContents.add(pomodoroContent); } - pomodoroStatus = pomodoroStatus.getNext(); } - public void proceedV2() { + public void proceed() { // TODO: 서비스로 뺄지 말지(일관성을 위해) if (pomodoroStatus.equals(PomodoroStatus.RETROSPECT)) { if (currentCycle.equals(pomodoroRoom.getTotalCycle())) { @@ -110,6 +87,18 @@ public void proceedV2() { pomodoroStatus = pomodoroStatus.getNext(); } + public boolean isProgressOf(PomodoroRoom pomodoroRoom) { + return this.pomodoroRoom.getId().equals(pomodoroRoom.getId()); + } + + public boolean isOwnedBy(Member member) { + return this.member.getId().equals(member.getId()); + } + + public boolean hasSameNicknameWith(PomodoroProgress pomodoroProgress) { + return this.nickname.equals(pomodoroProgress.nickname); + } + public boolean isRetrospect() { return pomodoroStatus == PomodoroStatus.RETROSPECT; } @@ -118,11 +107,11 @@ public boolean isNotPlanning() { return pomodoroStatus != PomodoroStatus.PLANNING; } - public boolean isNotStudying() { - return pomodoroStatus != PomodoroStatus.STUDYING; - } - public boolean isNotRetrospect() { return pomodoroStatus != PomodoroStatus.RETROSPECT; } + + public boolean isNotIncludedIn(PomodoroRoom other) { + return !pomodoroRoom.getId().equals(other.getId()); + } } diff --git a/backend/src/main/java/harustudy/backend/progress/dto/ParticipateStudyRequest.java b/backend/src/main/java/harustudy/backend/progress/dto/ParticipateStudyRequest.java new file mode 100644 index 00000000..8d9d0fc1 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/progress/dto/ParticipateStudyRequest.java @@ -0,0 +1,5 @@ +package harustudy.backend.progress.dto; + +public record ParticipateStudyRequest(Long memberId, String nickname) { + +} diff --git a/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressResponse.java b/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressResponse.java index c605d644..5d040005 100644 --- a/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressResponse.java +++ b/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressResponse.java @@ -2,11 +2,11 @@ import harustudy.backend.progress.domain.PomodoroProgress; -@Deprecated -public record PomodoroProgressResponse(Boolean isDone, Integer currentCycle, String step) { +public record PomodoroProgressResponse(Long progressId, String nickname, Integer currentCycle, + String step) { public static PomodoroProgressResponse from(PomodoroProgress pomodoroProgress) { - return new PomodoroProgressResponse(pomodoroProgress.isDone(), pomodoroProgress.getCurrentCycle(), - pomodoroProgress.getPomodoroStatus().name().toLowerCase()); + return new PomodoroProgressResponse(pomodoroProgress.getId(), pomodoroProgress.getNickname(), + pomodoroProgress.getCurrentCycle(), pomodoroProgress.getPomodoroStatus().name().toLowerCase()); } } diff --git a/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressResponseV2.java b/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressResponseV2.java deleted file mode 100644 index 90484790..00000000 --- a/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressResponseV2.java +++ /dev/null @@ -1,11 +0,0 @@ -package harustudy.backend.progress.dto; - -import harustudy.backend.progress.domain.PomodoroProgress; - -public record PomodoroProgressResponseV2(Integer currentCycle, String step) { - - public static PomodoroProgressResponseV2 from(PomodoroProgress pomodoroProgress) { - return new PomodoroProgressResponseV2(pomodoroProgress.getCurrentCycle(), - pomodoroProgress.getPomodoroStatus().name().toLowerCase()); - } -} diff --git a/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressesResponse.java b/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressesResponse.java new file mode 100644 index 00000000..d61cc496 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/progress/dto/PomodoroProgressesResponse.java @@ -0,0 +1,10 @@ +package harustudy.backend.progress.dto; + +import java.util.List; + +public record PomodoroProgressesResponse(List progresses) { + + public static PomodoroProgressesResponse from(List responses) { + return new PomodoroProgressesResponse(responses); + } +} diff --git a/backend/src/main/java/harustudy/backend/progress/dto/RoomAndProgressStepResponse.java b/backend/src/main/java/harustudy/backend/progress/dto/RoomAndProgressStepResponse.java deleted file mode 100644 index f70faadd..00000000 --- a/backend/src/main/java/harustudy/backend/progress/dto/RoomAndProgressStepResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package harustudy.backend.progress.dto; - -import harustudy.backend.progress.domain.PomodoroStatus; - -@Deprecated -public record RoomAndProgressStepResponse(String studyName, Integer totalCycle, - Integer currentCycle, - Integer timePerCycle, String step) { - - public RoomAndProgressStepResponse(String studyName, Integer totalCycle, Integer currentCycle, - Integer timePerCycle, PomodoroStatus step) { - this(studyName, totalCycle, currentCycle, timePerCycle, step.name().toLowerCase()); - } -} diff --git a/backend/src/main/java/harustudy/backend/progress/exception/IllegalProgressStateException.java b/backend/src/main/java/harustudy/backend/progress/exception/IllegalProgressStateException.java deleted file mode 100644 index bc3b3d5b..00000000 --- a/backend/src/main/java/harustudy/backend/progress/exception/IllegalProgressStateException.java +++ /dev/null @@ -1,5 +0,0 @@ -package harustudy.backend.progress.exception; - -public class IllegalProgressStateException extends RuntimeException { - -} diff --git a/backend/src/main/java/harustudy/backend/progress/exception/InvalidPomodoroProgressException.java b/backend/src/main/java/harustudy/backend/progress/exception/InvalidPomodoroProgressException.java deleted file mode 100644 index 0f996eb7..00000000 --- a/backend/src/main/java/harustudy/backend/progress/exception/InvalidPomodoroProgressException.java +++ /dev/null @@ -1,8 +0,0 @@ -package harustudy.backend.progress.exception; - -public class InvalidPomodoroProgressException extends RuntimeException { - - public static class UnavailableToProceed extends InvalidPomodoroProgressException { - - } -} diff --git a/backend/src/main/java/harustudy/backend/progress/exception/NicknameLengthException.java b/backend/src/main/java/harustudy/backend/progress/exception/NicknameLengthException.java new file mode 100644 index 00000000..67a2bef7 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/progress/exception/NicknameLengthException.java @@ -0,0 +1,7 @@ +package harustudy.backend.progress.exception; + +import harustudy.backend.common.HaruStudyException; + +public class NicknameLengthException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressNotFoundException.java b/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressNotFoundException.java new file mode 100644 index 00000000..5136e6cf --- /dev/null +++ b/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressNotFoundException.java @@ -0,0 +1,7 @@ +package harustudy.backend.progress.exception; + +import harustudy.backend.common.HaruStudyException; + +public class PomodoroProgressNotFoundException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressStatusException.java b/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressStatusException.java index 4fdf9ea9..9f238756 100644 --- a/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressStatusException.java +++ b/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressStatusException.java @@ -1,5 +1,7 @@ package harustudy.backend.progress.exception; -public class PomodoroProgressStatusException extends RuntimeException { +import harustudy.backend.common.HaruStudyException; + +public class PomodoroProgressStatusException extends HaruStudyException { } diff --git a/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToRoomException.java b/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToRoomException.java index 1775e6c0..64e8772d 100644 --- a/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToRoomException.java +++ b/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToRoomException.java @@ -1,4 +1,6 @@ package harustudy.backend.progress.exception; -public class ProgressNotBelongToRoomException extends RuntimeException { +import harustudy.backend.common.HaruStudyException; + +public class ProgressNotBelongToRoomException extends HaruStudyException { } diff --git a/backend/src/main/java/harustudy/backend/progress/repository/PomodoroProgressRepository.java b/backend/src/main/java/harustudy/backend/progress/repository/PomodoroProgressRepository.java index a8ab78cf..32bfec27 100644 --- a/backend/src/main/java/harustudy/backend/progress/repository/PomodoroProgressRepository.java +++ b/backend/src/main/java/harustudy/backend/progress/repository/PomodoroProgressRepository.java @@ -2,6 +2,7 @@ import harustudy.backend.member.domain.Member; import harustudy.backend.progress.domain.PomodoroProgress; +import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; import harustudy.backend.room.domain.PomodoroRoom; import java.util.List; import java.util.Optional; @@ -14,10 +15,17 @@ public interface PomodoroProgressRepository extends JpaRepository findByPomodoroRoomAndMember(PomodoroRoom pomodoroRoom, Member member); - @Deprecated - List findAllByPomodoroRoom(PomodoroRoom pomodoroRoom); - @Query("select p from PomodoroProgress p join fetch p.member where p.pomodoroRoom = :pomodoroRoom") List findAllByPomodoroRoomFetchMember( @Param("pomodoroRoom") PomodoroRoom pomodoroRoom); + + + List findByMember(Member member); + + List findByPomodoroRoom(PomodoroRoom pomodoroRoom); + + default PomodoroProgress findByIdIfExists(Long id) { + return findById(id) + .orElseThrow(PomodoroProgressNotFoundException::new); + } } diff --git a/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressService.java b/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressService.java index 11bf378b..091b8c86 100644 --- a/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressService.java +++ b/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressService.java @@ -1,73 +1,119 @@ package harustudy.backend.progress.service; -import harustudy.backend.common.EntityNotFoundException.MemberNotFound; -import harustudy.backend.common.EntityNotFoundException.PomodoroProgressNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.exception.AuthorizationException; import harustudy.backend.member.domain.Member; import harustudy.backend.member.repository.MemberRepository; import harustudy.backend.progress.domain.PomodoroProgress; +import harustudy.backend.progress.dto.ParticipateStudyRequest; import harustudy.backend.progress.dto.PomodoroProgressResponse; -import harustudy.backend.progress.dto.RoomAndProgressStepResponse; -import harustudy.backend.progress.exception.InvalidPomodoroProgressException.UnavailableToProceed; +import harustudy.backend.progress.dto.PomodoroProgressesResponse; +import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; +import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; import harustudy.backend.progress.repository.PomodoroProgressRepository; import harustudy.backend.room.domain.PomodoroRoom; import harustudy.backend.room.repository.PomodoroRoomRepository; -import lombok.AllArgsConstructor; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -@AllArgsConstructor +@RequiredArgsConstructor @Transactional -@Deprecated public class PomodoroProgressService { + private final MemberRepository memberRepository; private final PomodoroProgressRepository pomodoroProgressRepository; private final PomodoroRoomRepository pomodoroRoomRepository; - private final MemberRepository memberRepository; - public RoomAndProgressStepResponse findMemberMetaData(Long studyId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(studyId).orElseThrow(IllegalArgumentException::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + public PomodoroProgressResponse findPomodoroProgress( + AuthMember authMember, Long studyId, Long progressId + ) { + Member member = memberRepository.findByIdIfExists(authMember.id()); + PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); + PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId); + validateProgressIsRelatedWith(pomodoroProgress, member, pomodoroRoom); + return PomodoroProgressResponse.from(pomodoroProgress); + } + + // TODO: 동적쿼리로 변경(memberId 유무에 따른 분기처리) + public PomodoroProgressesResponse findPomodoroProgressWithFilter( + AuthMember authMember, Long studyId, @Nullable Long memberId + ) { + PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); + if (Objects.isNull(memberId)) { + validateEverParticipated(authMember, pomodoroRoom); + return getPomodoroProgressesResponseWithoutMemberFilter(pomodoroRoom); + } + Member member = memberRepository.findByIdIfExists(memberId); + validateIsSameMemberId(authMember, memberId); + return getPomodoroProgressesResponseWithMemberFilter(pomodoroRoom, member); + } + + private void validateEverParticipated(AuthMember authMember, PomodoroRoom pomodoroRoom) { + Member member = memberRepository.findByIdIfExists(authMember.id()); + pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) + .orElseThrow(AuthorizationException::new); + } - PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByPomodoroRoomAndMember( - pomodoroRoom, member) - .orElseThrow(IllegalArgumentException::new); + private PomodoroProgressesResponse getPomodoroProgressesResponseWithoutMemberFilter( + PomodoroRoom pomodoroRoom + ) { + List responses = + pomodoroProgressRepository.findByPomodoroRoom(pomodoroRoom) + .stream() + .map(PomodoroProgressResponse::from) + .collect(Collectors.toList()); + return PomodoroProgressesResponse.from(responses); + } - return new RoomAndProgressStepResponse(pomodoroRoom.getName(), pomodoroRoom.getTotalCycle(), - pomodoroProgress.getCurrentCycle(), - pomodoroRoom.getTimePerCycle(), pomodoroProgress.getPomodoroStatus()); + private PomodoroProgressesResponse getPomodoroProgressesResponseWithMemberFilter( + PomodoroRoom pomodoroRoom, Member member + ) { + PomodoroProgressResponse response = + pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) + .map(PomodoroProgressResponse::from) + .orElseThrow(PomodoroProgressNotFoundException::new); + return PomodoroProgressesResponse.from(List.of(response)); } - public void proceedToRetrospect(Long studyId, Long memberId) { - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(studyId, memberId); - validateProgressIsStudying(pomodoroProgress); + public void proceed(AuthMember authMember, Long studyId, Long progressId) { + Member member = memberRepository.findByIdIfExists(authMember.id()); + PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId); + PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); + + validateProgressIsRelatedWith(pomodoroProgress, member, pomodoroRoom); pomodoroProgress.proceed(); } - private void validateProgressIsStudying(PomodoroProgress pomodoroProgress) { - if (pomodoroProgress.isNotStudying()) { - throw new UnavailableToProceed(); - } + public Long participateStudy(AuthMember authMember, Long studyId, ParticipateStudyRequest request) { + Member member = memberRepository.findByIdIfExists(request.memberId()); + validateIsSameMemberId(authMember, request.memberId()); + PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, request.nickname()); + pomodoroProgress.generateContents(pomodoroRoom.getTotalCycle()); + PomodoroProgress saved = pomodoroProgressRepository.save(pomodoroProgress); + return saved.getId(); } - private PomodoroProgress findPomodoroProgressFrom(Long studyId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(studyId) - .orElseThrow(RoomNotFound::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFound::new); - return pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) - .orElseThrow(PomodoroProgressNotFound::new); + private void validateIsSameMemberId(AuthMember authMember, Long memberId) { + if (!(authMember.id().equals(memberId))) { + throw new AuthorizationException(); + } } - public PomodoroProgressResponse findPomodoroProgress(Long roomId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFound::new); - PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) - .orElseThrow(PomodoroProgressNotFound::new); - return PomodoroProgressResponse.from(pomodoroProgress); + private void validateProgressIsRelatedWith( + PomodoroProgress pomodoroProgress, Member member, PomodoroRoom pomodoroRoom + ) { + if (!pomodoroProgress.isOwnedBy(member)) { + throw new AuthorizationException(); + } + if (pomodoroProgress.isNotIncludedIn(pomodoroRoom)) { + throw new ProgressNotBelongToRoomException(); + } } } diff --git a/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressServiceV2.java b/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressServiceV2.java deleted file mode 100644 index ebec603b..00000000 --- a/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressServiceV2.java +++ /dev/null @@ -1,54 +0,0 @@ -package harustudy.backend.progress.service; - -import static harustudy.backend.common.EntityNotFoundException.PomodoroProgressNotFound; - -import harustudy.backend.common.EntityNotFoundException.MemberNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.exception.MemberNotParticipatedException; -import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.dto.PomodoroProgressResponseV2; -import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; -import harustudy.backend.progress.repository.PomodoroProgressRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.repository.PomodoroRoomRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Transactional -@Service -public class PomodoroProgressServiceV2 { - - private final PomodoroRoomRepository pomodoroRoomRepository; - private final MemberRepository memberRepository; - private final PomodoroProgressRepository pomodoroProgressRepository; - - public PomodoroProgressResponseV2 findProgress(Long roomId, Long memberId) { - PomodoroProgress pomodoroProgress = findPomodoroProgress(roomId, memberId); - return PomodoroProgressResponseV2.from(pomodoroProgress); - } - - public void proceed(Long roomId, Long progressId) { - PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findById(progressId) - .orElseThrow(PomodoroProgressNotFound::new); - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - if (!pomodoroProgress.getPomodoroRoom().equals(pomodoroRoom)) { - throw new ProgressNotBelongToRoomException(); - } - pomodoroProgress.proceedV2(); - } - - private PomodoroProgress findPomodoroProgress(Long roomId, Long memberId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - Member member = memberRepository.findById(memberId) - .orElseThrow(MemberNotFound::new); - return pomodoroProgressRepository.findByPomodoroRoomAndMember( - pomodoroRoom, member) - .orElseThrow(MemberNotParticipatedException::new); - } -} diff --git a/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomController.java b/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomController.java index 9d9b94ec..96b53de4 100644 --- a/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomController.java +++ b/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomController.java @@ -1,45 +1,83 @@ package harustudy.backend.room.controller; -import harustudy.backend.room.dto.*; +import harustudy.backend.auth.Authenticated; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.common.SwaggerExceptionResponse; +import harustudy.backend.member.exception.MemberNotFoundException; +import harustudy.backend.room.dto.CreatePomodoroRoomRequest; +import harustudy.backend.room.dto.CreatePomodoroRoomResponse; +import harustudy.backend.room.dto.PomodoroRoomResponse; +import harustudy.backend.room.dto.PomodoroRoomsResponse; +import harustudy.backend.room.exception.ParticipantCodeNotFoundException; +import harustudy.backend.room.exception.PomodoroRoomNameLengthException; +import harustudy.backend.room.exception.PomodoroTimePerCycleException; +import harustudy.backend.room.exception.PomodoroTotalCycleException; +import harustudy.backend.room.exception.RoomNotFoundException; import harustudy.backend.room.service.PomodoroRoomService; -import jakarta.validation.Valid; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; -@Deprecated +@Tag(name = "스터디 관련 기능") @RequiredArgsConstructor @RestController public class PomodoroRoomController { private final PomodoroRoomService pomodoroRoomService; - @Deprecated - @GetMapping("/api/studies/{studyId}/metadata") - public ResponseEntity findStudyMetaData(@PathVariable Long studyId) { - return ResponseEntity.ok(pomodoroRoomService.findPomodoroRoomMetadata(studyId)); + @SwaggerExceptionResponse({ + RoomNotFoundException.class + }) + @Operation(summary = "단일 스터디 정보 조회") + @GetMapping("/api/studies/{studyId}") + public ResponseEntity findStudy( + @Authenticated AuthMember authMember, + @PathVariable Long studyId + ) { + PomodoroRoomResponse response = pomodoroRoomService.findPomodoroRoom(studyId); + return ResponseEntity.ok(response); } - @Deprecated - @PostMapping("/api/studies") - public ResponseEntity createStudy( - @Valid @RequestBody CreatePomodoroRoomRequest request + @SwaggerExceptionResponse({ + ParticipantCodeNotFoundException.class, + RoomNotFoundException.class, + MemberNotFoundException.class, + }) + @Operation(summary = "필터링 조건으로 스터디 조회") + @GetMapping("/api/studies") + public ResponseEntity findStudiesWithFilter( + @Authenticated AuthMember authMember, + @RequestParam(required = false) Long memberId, + @RequestParam(required = false) String participantCode ) { - CreatePomodoroRoomDto createPomodoroRoomDto = pomodoroRoomService.createPomodoroRoom(request); - return ResponseEntity.created( - URI.create("/api/studies/" + createPomodoroRoomDto.studyId())) - .body(CreatePomodoroRoomResponse.from(createPomodoroRoomDto)); + PomodoroRoomsResponse response = pomodoroRoomService.findPomodoroRoomWithFilter( + memberId, participantCode); + return ResponseEntity.ok(response); } - @Deprecated - @PostMapping("/api/studies/{studyId}/members") - public ResponseEntity participate( - @PathVariable Long studyId, - @Valid @RequestBody ParticipateRequest request + @SwaggerExceptionResponse({ + PomodoroRoomNameLengthException.class, + PomodoroTotalCycleException.class, + PomodoroTimePerCycleException.class + }) + @Operation(summary = "스터디 생성") + @ApiResponse(responseCode = "201") + @PostMapping("/api/studies") + public ResponseEntity createStudy( + @Authenticated AuthMember authMember, + @RequestBody CreatePomodoroRoomRequest request ) { - Long memberId = pomodoroRoomService.participate(studyId, request.nickname()); - return ResponseEntity.created( - URI.create("/api/studies/" + studyId + "/members/" + memberId)).build(); + CreatePomodoroRoomResponse response = pomodoroRoomService.createPomodoroRoom(request); + return ResponseEntity.created(URI.create("/api/studies/" + response.studyId())) + .body(response); } } diff --git a/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomControllerV2.java b/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomControllerV2.java deleted file mode 100644 index 30f74565..00000000 --- a/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomControllerV2.java +++ /dev/null @@ -1,47 +0,0 @@ -package harustudy.backend.room.controller; - -import harustudy.backend.room.dto.CreatePomodoroRoomDto; -import harustudy.backend.room.dto.CreatePomodoroRoomRequest; -import harustudy.backend.room.dto.CreatePomodoroRoomResponse; -import harustudy.backend.room.dto.PomodoroRoomResponseV2; -import harustudy.backend.room.service.PomodoroRoomServiceV2; -import jakarta.validation.Valid; -import java.net.URI; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -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.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RequiredArgsConstructor -@RestController -public class PomodoroRoomControllerV2 { - - private final PomodoroRoomServiceV2 pomodoroRoomService; - - @GetMapping("/api/v2/studies/{studyId}") - public ResponseEntity findStudy(@PathVariable Long studyId) { - return ResponseEntity.ok(pomodoroRoomService.findPomodoroRoom(studyId)); - } - - @GetMapping("/api/v2/studies") - public ResponseEntity findStudy( - @RequestParam("participantCode") String participantCode) { - return ResponseEntity.ok( - pomodoroRoomService.findPomodoroRoomByParticipantCode(participantCode)); - } - - @PostMapping("/api/v2/studies") - public ResponseEntity createStudy( - @Valid @RequestBody CreatePomodoroRoomRequest request - ) { - CreatePomodoroRoomDto createPomodoroRoomDto = pomodoroRoomService.createPomodoroRoom( - request); - return ResponseEntity.created( - URI.create("/api/studies/" + createPomodoroRoomDto.studyId())) - .body(CreatePomodoroRoomResponse.from(createPomodoroRoomDto)); - } -} diff --git a/backend/src/main/java/harustudy/backend/participantcode/domain/CodeGenerationStrategy.java b/backend/src/main/java/harustudy/backend/room/domain/CodeGenerationStrategy.java similarity index 87% rename from backend/src/main/java/harustudy/backend/participantcode/domain/CodeGenerationStrategy.java rename to backend/src/main/java/harustudy/backend/room/domain/CodeGenerationStrategy.java index f2478f38..894b8441 100644 --- a/backend/src/main/java/harustudy/backend/participantcode/domain/CodeGenerationStrategy.java +++ b/backend/src/main/java/harustudy/backend/room/domain/CodeGenerationStrategy.java @@ -1,4 +1,4 @@ -package harustudy.backend.participantcode.domain; +package harustudy.backend.room.domain; public class CodeGenerationStrategy implements GenerationStrategy { diff --git a/backend/src/main/java/harustudy/backend/participantcode/domain/GenerationStrategy.java b/backend/src/main/java/harustudy/backend/room/domain/GenerationStrategy.java similarity index 56% rename from backend/src/main/java/harustudy/backend/participantcode/domain/GenerationStrategy.java rename to backend/src/main/java/harustudy/backend/room/domain/GenerationStrategy.java index 66939bd2..cf4182ed 100644 --- a/backend/src/main/java/harustudy/backend/participantcode/domain/GenerationStrategy.java +++ b/backend/src/main/java/harustudy/backend/room/domain/GenerationStrategy.java @@ -1,4 +1,4 @@ -package harustudy.backend.participantcode.domain; +package harustudy.backend.room.domain; public interface GenerationStrategy { diff --git a/backend/src/main/java/harustudy/backend/participantcode/domain/ParticipantCode.java b/backend/src/main/java/harustudy/backend/room/domain/ParticipantCode.java similarity index 95% rename from backend/src/main/java/harustudy/backend/participantcode/domain/ParticipantCode.java rename to backend/src/main/java/harustudy/backend/room/domain/ParticipantCode.java index 83b1aab7..80f6ef9c 100644 --- a/backend/src/main/java/harustudy/backend/participantcode/domain/ParticipantCode.java +++ b/backend/src/main/java/harustudy/backend/room/domain/ParticipantCode.java @@ -1,4 +1,4 @@ -package harustudy.backend.participantcode.domain; +package harustudy.backend.room.domain; import harustudy.backend.common.BaseTimeEntity; import jakarta.persistence.*; diff --git a/backend/src/main/java/harustudy/backend/room/domain/PomodoroRoom.java b/backend/src/main/java/harustudy/backend/room/domain/PomodoroRoom.java index e599244d..323e899d 100644 --- a/backend/src/main/java/harustudy/backend/room/domain/PomodoroRoom.java +++ b/backend/src/main/java/harustudy/backend/room/domain/PomodoroRoom.java @@ -1,14 +1,19 @@ package harustudy.backend.room.domain; import harustudy.backend.common.BaseTimeEntity; -import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.ParticipantCode; import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.room.exception.DuplicatedNicknameException; +import harustudy.backend.room.exception.PomodoroRoomNameLengthException; import harustudy.backend.room.exception.PomodoroTimePerCycleException; import harustudy.backend.room.exception.PomodoroTotalCycleException; -import harustudy.backend.room.exception.PomodoroRoomNameLengthException; -import jakarta.persistence.*; +import jakarta.persistence.Column; +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.OneToMany; +import jakarta.persistence.OneToOne; import jakarta.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; @@ -24,12 +29,11 @@ public class PomodoroRoom extends BaseTimeEntity { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 10; - // TODO: 순서 조정 - private final static int MIN_TOTAL_CYCLE = 1; - private final static int MAX_TOTAL_CYCLE = 8; + private static final int MIN_TOTAL_CYCLE = 1; + private static final int MAX_TOTAL_CYCLE = 8; - private final static int MIN_TIME_PER_CYCLE = 20; - private final static int MAX_TIME_PER_CYCLE = 60; + private static final int MIN_TIME_PER_CYCLE = 20; + private static final int MAX_TIME_PER_CYCLE = 60; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -84,16 +88,4 @@ private void validateTimePerCycle(Integer timePerCycle) { throw new PomodoroTimePerCycleException(); } } - - public boolean isParticipatedMember(Member member) { - return pomodoroProgresses.stream() - .anyMatch(memberProgress -> memberProgress.isOwnedBy(member)); - } - - public void validateDuplicatedNickname(Member member) { - if (pomodoroProgresses.stream() - .anyMatch(memberProgress -> memberProgress.hasSameNicknameMember(member))) { - throw new DuplicatedNicknameException(); - } - } } diff --git a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomDto.java b/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomDto.java deleted file mode 100644 index 03dcd08b..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package harustudy.backend.room.dto; - -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.room.domain.PomodoroRoom; - -public record CreatePomodoroRoomDto(Long studyId, String participantCode) { - - public static CreatePomodoroRoomDto from(PomodoroRoom pomodoroRoom, ParticipantCode participantCode) { - return new CreatePomodoroRoomDto(pomodoroRoom.getId(), participantCode.getCode()); - } -} diff --git a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomResponse.java b/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomResponse.java index 417efd03..df86d163 100644 --- a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomResponse.java +++ b/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomResponse.java @@ -1,8 +1,13 @@ package harustudy.backend.room.dto; -public record CreatePomodoroRoomResponse(String participantCode) { +import com.fasterxml.jackson.annotation.JsonIgnore; +import harustudy.backend.room.domain.ParticipantCode; +import harustudy.backend.room.domain.PomodoroRoom; - public static CreatePomodoroRoomResponse from(CreatePomodoroRoomDto createPomodoroRoomDto) { - return new CreatePomodoroRoomResponse(createPomodoroRoomDto.participantCode()); +public record CreatePomodoroRoomResponse(@JsonIgnore Long studyId, String participantCode) { + + public static CreatePomodoroRoomResponse from(PomodoroRoom savedRoom, + ParticipantCode participantCode) { + return new CreatePomodoroRoomResponse(savedRoom.getId(), participantCode.getCode()); } } diff --git a/backend/src/main/java/harustudy/backend/room/dto/MemberDto.java b/backend/src/main/java/harustudy/backend/room/dto/MemberDto.java deleted file mode 100644 index ace9e97d..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/MemberDto.java +++ /dev/null @@ -1,6 +0,0 @@ -package harustudy.backend.room.dto; - -@Deprecated -public record MemberDto(Long memberId, String nickname) { - -} diff --git a/backend/src/main/java/harustudy/backend/room/dto/ParticipateRequest.java b/backend/src/main/java/harustudy/backend/room/dto/ParticipateRequest.java deleted file mode 100644 index d098dd1c..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/ParticipateRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package harustudy.backend.room.dto; - -import jakarta.validation.constraints.NotNull; - -@Deprecated -public record ParticipateRequest(@NotNull String nickname) { - -} diff --git a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomAndMembersResponse.java b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomAndMembersResponse.java deleted file mode 100644 index eb69ac38..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomAndMembersResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package harustudy.backend.room.dto; - -import java.util.List; - -@Deprecated -public record PomodoroRoomAndMembersResponse(String studyName, Integer totalCycle, Integer timePerCycle, - List members) { - -} diff --git a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponse.java b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponse.java new file mode 100644 index 00000000..552a3e2c --- /dev/null +++ b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponse.java @@ -0,0 +1,14 @@ +package harustudy.backend.room.dto; + +import harustudy.backend.room.domain.PomodoroRoom; +import java.time.LocalDateTime; + +public record PomodoroRoomResponse(Long studyId, String name, Integer totalCycle, + Integer timePerCycle, LocalDateTime createdDateTime) { + + public static PomodoroRoomResponse from(PomodoroRoom pomodoroRoom) { + return new PomodoroRoomResponse(pomodoroRoom.getId(), pomodoroRoom.getName(), + pomodoroRoom.getTotalCycle(), pomodoroRoom.getTimePerCycle(), + pomodoroRoom.getCreatedDate()); + } +} diff --git a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponseV2.java b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponseV2.java deleted file mode 100644 index 8681142a..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponseV2.java +++ /dev/null @@ -1,11 +0,0 @@ -package harustudy.backend.room.dto; - -import harustudy.backend.room.domain.PomodoroRoom; - -public record PomodoroRoomResponseV2(Long studyId, String name, Integer totalCycle, Integer timePerCycle) { - - public static PomodoroRoomResponseV2 from(PomodoroRoom pomodoroRoom) { - return new PomodoroRoomResponseV2(pomodoroRoom.getId(), pomodoroRoom.getName(), - pomodoroRoom.getTotalCycle(), pomodoroRoom.getTimePerCycle()); - } -} diff --git a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomsResponse.java b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomsResponse.java new file mode 100644 index 00000000..fbc93625 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomsResponse.java @@ -0,0 +1,13 @@ +package harustudy.backend.room.dto; + +import harustudy.backend.room.domain.PomodoroRoom; +import java.util.List; + +public record PomodoroRoomsResponse(List studies) { + + public static PomodoroRoomsResponse from(List pomodoroRooms) { + return new PomodoroRoomsResponse(pomodoroRooms.stream() + .map(PomodoroRoomResponse::from) + .toList()); + } +} diff --git a/backend/src/main/java/harustudy/backend/room/exception/DuplicatedNicknameException.java b/backend/src/main/java/harustudy/backend/room/exception/DuplicatedNicknameException.java deleted file mode 100644 index 38043237..00000000 --- a/backend/src/main/java/harustudy/backend/room/exception/DuplicatedNicknameException.java +++ /dev/null @@ -1,5 +0,0 @@ -package harustudy.backend.room.exception; - -public class DuplicatedNicknameException extends RuntimeException { - -} diff --git a/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeExpiredException.java b/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeExpiredException.java new file mode 100644 index 00000000..090c85c6 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeExpiredException.java @@ -0,0 +1,7 @@ +package harustudy.backend.room.exception; + +import harustudy.backend.common.HaruStudyException; + +public class ParticipantCodeExpiredException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeNotFoundException.java b/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeNotFoundException.java new file mode 100644 index 00000000..1533baab --- /dev/null +++ b/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeNotFoundException.java @@ -0,0 +1,7 @@ +package harustudy.backend.room.exception; + +import harustudy.backend.common.HaruStudyException; + +public class ParticipantCodeNotFoundException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/room/exception/PomodoroRoomNameLengthException.java b/backend/src/main/java/harustudy/backend/room/exception/PomodoroRoomNameLengthException.java index 2b8b4e30..4e5f80d0 100644 --- a/backend/src/main/java/harustudy/backend/room/exception/PomodoroRoomNameLengthException.java +++ b/backend/src/main/java/harustudy/backend/room/exception/PomodoroRoomNameLengthException.java @@ -1,5 +1,7 @@ package harustudy.backend.room.exception; -public class PomodoroRoomNameLengthException extends RuntimeException { +import harustudy.backend.common.HaruStudyException; + +public class PomodoroRoomNameLengthException extends HaruStudyException { } diff --git a/backend/src/main/java/harustudy/backend/room/exception/PomodoroTimePerCycleException.java b/backend/src/main/java/harustudy/backend/room/exception/PomodoroTimePerCycleException.java index 10cfaae0..07fc82c3 100644 --- a/backend/src/main/java/harustudy/backend/room/exception/PomodoroTimePerCycleException.java +++ b/backend/src/main/java/harustudy/backend/room/exception/PomodoroTimePerCycleException.java @@ -1,5 +1,7 @@ package harustudy.backend.room.exception; -public class PomodoroTimePerCycleException extends RuntimeException { +import harustudy.backend.common.HaruStudyException; + +public class PomodoroTimePerCycleException extends HaruStudyException { } diff --git a/backend/src/main/java/harustudy/backend/room/exception/PomodoroTotalCycleException.java b/backend/src/main/java/harustudy/backend/room/exception/PomodoroTotalCycleException.java index 95f5096d..549d2187 100644 --- a/backend/src/main/java/harustudy/backend/room/exception/PomodoroTotalCycleException.java +++ b/backend/src/main/java/harustudy/backend/room/exception/PomodoroTotalCycleException.java @@ -1,5 +1,7 @@ package harustudy.backend.room.exception; -public class PomodoroTotalCycleException extends RuntimeException { +import harustudy.backend.common.HaruStudyException; + +public class PomodoroTotalCycleException extends HaruStudyException { } diff --git a/backend/src/main/java/harustudy/backend/room/exception/RoomNotFoundException.java b/backend/src/main/java/harustudy/backend/room/exception/RoomNotFoundException.java new file mode 100644 index 00000000..ea3b3d6c --- /dev/null +++ b/backend/src/main/java/harustudy/backend/room/exception/RoomNotFoundException.java @@ -0,0 +1,7 @@ +package harustudy.backend.room.exception; + +import harustudy.backend.common.HaruStudyException; + +public class RoomNotFoundException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java b/backend/src/main/java/harustudy/backend/room/repository/ParticipantCodeRepository.java similarity index 63% rename from backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java rename to backend/src/main/java/harustudy/backend/room/repository/ParticipantCodeRepository.java index 293f4f6c..094c1f4e 100644 --- a/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java +++ b/backend/src/main/java/harustudy/backend/room/repository/ParticipantCodeRepository.java @@ -1,12 +1,10 @@ -package harustudy.backend.participantcode.repository; +package harustudy.backend.room.repository; -import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.room.domain.ParticipantCode; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; -@Repository public interface ParticipantCodeRepository extends JpaRepository { Optional findByCode(@Param("code") String code); diff --git a/backend/src/main/java/harustudy/backend/room/repository/PomodoroRoomRepository.java b/backend/src/main/java/harustudy/backend/room/repository/PomodoroRoomRepository.java index b4759e39..d6b88fdb 100644 --- a/backend/src/main/java/harustudy/backend/room/repository/PomodoroRoomRepository.java +++ b/backend/src/main/java/harustudy/backend/room/repository/PomodoroRoomRepository.java @@ -1,11 +1,19 @@ package harustudy.backend.room.repository; -import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.room.domain.ParticipantCode; import harustudy.backend.room.domain.PomodoroRoom; +import java.util.List; +import harustudy.backend.room.exception.RoomNotFoundException; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface PomodoroRoomRepository extends JpaRepository { - - Optional findByParticipantCode(ParticipantCode participantCode); + + // TODO: Optional로 변경 + List findByParticipantCode(ParticipantCode participantCode); + + default PomodoroRoom findByIdIfExists(Long id) { + return findById(id) + .orElseThrow(RoomNotFoundException::new); + } } diff --git a/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomService.java b/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomService.java index ec3ec887..1691cf70 100644 --- a/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomService.java +++ b/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomService.java @@ -1,21 +1,23 @@ package harustudy.backend.room.service; -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.content.repository.PomodoroContentRepository; import harustudy.backend.member.domain.Member; +import harustudy.backend.member.exception.MemberNotFoundException; import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.participantcode.domain.GenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.participantcode.repository.ParticipantCodeRepository; import harustudy.backend.progress.domain.PomodoroProgress; import harustudy.backend.progress.repository.PomodoroProgressRepository; +import harustudy.backend.room.domain.GenerationStrategy; +import harustudy.backend.room.domain.ParticipantCode; import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.dto.CreatePomodoroRoomDto; import harustudy.backend.room.dto.CreatePomodoroRoomRequest; -import harustudy.backend.room.dto.MemberDto; -import harustudy.backend.room.dto.PomodoroRoomAndMembersResponse; +import harustudy.backend.room.dto.CreatePomodoroRoomResponse; +import harustudy.backend.room.dto.PomodoroRoomResponse; +import harustudy.backend.room.dto.PomodoroRoomsResponse; +import harustudy.backend.room.exception.ParticipantCodeNotFoundException; +import harustudy.backend.room.exception.RoomNotFoundException; +import harustudy.backend.room.repository.ParticipantCodeRepository; import harustudy.backend.room.repository.PomodoroRoomRepository; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,15 +27,57 @@ @Service public class PomodoroRoomService { - private final ParticipantCodeRepository participantCodeRepository; + private final PomodoroRoomRepository pomodoroRoomRepository; private final PomodoroProgressRepository pomodoroProgressRepository; + private final ParticipantCodeRepository participantCodeRepository; private final MemberRepository memberRepository; - private final PomodoroRoomRepository pomodoroRoomRepository; private final GenerationStrategy generationStrategy; - private final PomodoroContentRepository pomodoroContentRepository; - @Deprecated - public CreatePomodoroRoomDto createPomodoroRoom(CreatePomodoroRoomRequest request) { + public PomodoroRoomResponse findPomodoroRoom(Long roomId) { + return PomodoroRoomResponse.from(pomodoroRoomRepository.findById(roomId) + .orElseThrow(RoomNotFoundException::new)); + } + + public PomodoroRoomsResponse findPomodoroRoomWithFilter(Long memberId, String code) { + if (Objects.nonNull(code)) { + ParticipantCode participantCode = participantCodeRepository.findByCode(code) + .orElseThrow(ParticipantCodeNotFoundException::new); + List pomodoroRooms = pomodoroRoomRepository.findByParticipantCode( + participantCode); + validateIsPresent(pomodoroRooms); + + return PomodoroRoomsResponse.from(pomodoroRooms); + } + if (Objects.nonNull(memberId)) { + return findPomodoroRoomByMemberId(memberId); + } + + return PomodoroRoomsResponse.from(pomodoroRoomRepository.findAll()); + } + + private void validateIsPresent(List pomodoroRooms) { + if (pomodoroRooms.isEmpty()) { + throw new RoomNotFoundException(); + } + } + + private PomodoroRoomsResponse findPomodoroRoomByMemberId(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + List pomodoroProgresses = pomodoroProgressRepository.findByMember(member); + + List pomodoroRooms = mapToPomodoroRooms(pomodoroProgresses); + + return PomodoroRoomsResponse.from(pomodoroRooms); + } + + private List mapToPomodoroRooms(List pomodoroProgresses) { + return pomodoroProgresses.stream() + .map(PomodoroProgress::getPomodoroRoom) + .toList(); + } + + public CreatePomodoroRoomResponse createPomodoroRoom(CreatePomodoroRoomRequest request) { ParticipantCode participantCode = regenerateUniqueCode(); participantCodeRepository.save(participantCode); @@ -41,10 +85,9 @@ public CreatePomodoroRoomDto createPomodoroRoom(CreatePomodoroRoomRequest reques request.timePerCycle(), participantCode); PomodoroRoom savedRoom = pomodoroRoomRepository.save(pomodoroRoom); - return CreatePomodoroRoomDto.from(savedRoom, participantCode); + return CreatePomodoroRoomResponse.from(savedRoom, participantCode); } - @Deprecated private ParticipantCode regenerateUniqueCode() { ParticipantCode participantCode = new ParticipantCode(generationStrategy); while (isParticipantCodePresent(participantCode)) { @@ -53,50 +96,8 @@ private ParticipantCode regenerateUniqueCode() { return participantCode; } - @Deprecated private boolean isParticipantCodePresent(ParticipantCode participantCode) { return participantCodeRepository.findByCode(participantCode.getCode()) .isPresent(); } - - @Deprecated - public Long participate(Long roomId, String nickname) { - Member member = memberRepository.save(new Member(nickname)); - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(IllegalArgumentException::new); - pomodoroRoom.validateDuplicatedNickname(member); - - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); - pomodoroProgressRepository.save(pomodoroProgress); - - int totalCycle = pomodoroRoom.getTotalCycle(); - for (int i = 1; i <= totalCycle; i++) { - PomodoroContent content = new PomodoroContent(pomodoroProgress, i); - pomodoroContentRepository.save(content); - } - return member.getId(); - } - - @Deprecated - public PomodoroRoomAndMembersResponse findPomodoroRoomMetadata(Long roomId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId).orElseThrow(IllegalArgumentException::new); - List pomodoroProgresses = pomodoroProgressRepository.findAllByPomodoroRoom(pomodoroRoom); - - if (pomodoroProgresses.isEmpty()) { - throw new IllegalArgumentException(); - } - - List members = pomodoroProgresses.stream() - .map(pomodoroProgress -> new MemberDto( - pomodoroProgress.getMember().getId(), - pomodoroProgress.getMember().getNickname())) - .toList(); - - return new PomodoroRoomAndMembersResponse( - pomodoroRoom.getName(), - pomodoroRoom.getTotalCycle(), - pomodoroRoom.getTimePerCycle(), - members - ); - } } diff --git a/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomServiceV2.java b/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomServiceV2.java deleted file mode 100644 index 38f127ec..00000000 --- a/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomServiceV2.java +++ /dev/null @@ -1,63 +0,0 @@ -package harustudy.backend.room.service; - -import harustudy.backend.common.EntityNotFoundException.ParticipantCodeNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; -import harustudy.backend.participantcode.domain.GenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.participantcode.repository.ParticipantCodeRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.dto.CreatePomodoroRoomDto; -import harustudy.backend.room.dto.CreatePomodoroRoomRequest; -import harustudy.backend.room.dto.PomodoroRoomResponseV2; -import harustudy.backend.room.repository.PomodoroRoomRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Transactional -@Service -public class PomodoroRoomServiceV2 { - - private final PomodoroRoomRepository pomodoroRoomRepository; - private final ParticipantCodeRepository participantCodeRepository; - private final GenerationStrategy generationStrategy; - - public PomodoroRoomResponseV2 findPomodoroRoom(Long roomId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFound::new); - return PomodoroRoomResponseV2.from(pomodoroRoom); - } - - public CreatePomodoroRoomDto createPomodoroRoom(CreatePomodoroRoomRequest request) { - ParticipantCode participantCode = regenerateUniqueCode(); - participantCodeRepository.save(participantCode); - - PomodoroRoom pomodoroRoom = new PomodoroRoom(request.name(), request.totalCycle(), - request.timePerCycle(), participantCode); - PomodoroRoom savedRoom = pomodoroRoomRepository.save(pomodoroRoom); - - return CreatePomodoroRoomDto.from(savedRoom, participantCode); - } - - private ParticipantCode regenerateUniqueCode() { - ParticipantCode participantCode = new ParticipantCode(generationStrategy); - while (isParticipantCodePresent(participantCode)) { - participantCode.regenerate(); - } - return participantCode; - } - - private boolean isParticipantCodePresent(ParticipantCode participantCode) { - return participantCodeRepository.findByCode(participantCode.getCode()) - .isPresent(); - } - - public PomodoroRoomResponseV2 findPomodoroRoomByParticipantCode(String code) { - ParticipantCode participantCode = participantCodeRepository.findByCode(code) - .orElseThrow(ParticipantCodeNotFound::new); - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByParticipantCode(participantCode) - .orElseThrow(RoomNotFound::new); - return PomodoroRoomResponseV2.from(pomodoroRoom); - } -} diff --git a/backend/src/main/resources/application-develop.yml b/backend/src/main/resources/application-develop.yml index 67256046..ef4884ad 100644 --- a/backend/src/main/resources/application-develop.yml +++ b/backend/src/main/resources/application-develop.yml @@ -7,11 +7,11 @@ spring: dialect: org.hibernate.dialect.MySQL8Dialect flyway: - enabled: true + enabled: false config: import: - - classpath:/submodule/application-auth.yml + - classpath:/submodule/application-auth-develop.yml - classpath:/submodule/application-db-develop.yml springdoc: diff --git a/backend/src/main/resources/application-production.yml b/backend/src/main/resources/application-production.yml index c61d4d55..b8f7aa42 100644 --- a/backend/src/main/resources/application-production.yml +++ b/backend/src/main/resources/application-production.yml @@ -11,7 +11,7 @@ spring: config: import: - - classpath:/submodule/application-auth.yml + - classpath:/submodule/application-auth-production.yml - classpath:/submodule/application-db-production.yml springdoc: diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml deleted file mode 100644 index 5ce3ed11..00000000 --- a/backend/src/main/resources/application-test.yml +++ /dev/null @@ -1,14 +0,0 @@ -spring: - jpa: - hibernate: - ddl-auto: create - properties: - hibernate: - format_sql: true - show-sql: true - - datasource: - url: jdbc:h2:mem:testdb;MODE=MySQL - username: sa - password: - driver-class-name: org.h2.Driver diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 986140e9..e529fca4 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,20 +1,21 @@ spring: jpa: hibernate: - ddl-auto: validate + ddl-auto: create-drop properties: hibernate: format_sql: true - dialect: org.hibernate.dialect.MySQL8Dialect show-sql: true - flyway: - enabled: true + datasource: + url: jdbc:h2:mem:testdb;MODE=MySQL + username: sa + password: + driver-class-name: org.h2.Driver - config: - import: classpath:/submodule/application.yml - -springdoc: - default-consumes-media-type: application/json - default-produces-media-type: application/json + h2: + console: + enabled: true + config: + import: classpath:/submodule/application-auth-develop.yml diff --git a/backend/src/main/resources/db/migration/V1.1__add-auth.sql b/backend/src/main/resources/db/migration/V1.1__add-auth.sql new file mode 100644 index 00000000..a21ad902 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1.1__add-auth.sql @@ -0,0 +1,23 @@ +alter table pomodoro_progress add column nickname varchar(255); +alter table pomodoro_progress modify nickname varchar (255) not null; +alter table member drop column nickname; +alter table member + add column name varchar(255), + add column email varchar(255), + add column image_url varchar(255), + add column login_type enum ('GUEST', 'GOOGLE'); +alter table member modify name varchar (255) not null; +alter table member modify login_type enum ('GUEST', 'GOOGLE') not null; +alter table pomodoro_progress modify pomodoro_status enum ('PLANNING','RETROSPECT','STUDYING', 'DONE') not null; +alter table pomodoro_progress drop column is_done; + +CREATE TABLE `refresh_token` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `member_id` bigint DEFAULT NULL, + `UUID` binary(16) NOT NULL, + `expire_date_time` datetime(6) NOT NULL, + `created_date` datetime(6) NOT NULL, + `last_modified_date` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +); diff --git a/backend/src/main/resources/default-appender.xml b/backend/src/main/resources/default-appender.xml new file mode 100644 index 00000000..141fb8dc --- /dev/null +++ b/backend/src/main/resources/default-appender.xml @@ -0,0 +1,19 @@ + + + ${LOG_PATH}/default/default.log + + INFO + + + ${FILE_LOG_PATTERN} + + + ${LOG_PATH}/default/default.%d{yyyy-MM-dd}.%i.log + + 20MB + + 30 + 100MB + + + diff --git a/backend/src/main/resources/error-appender.xml b/backend/src/main/resources/error-appender.xml new file mode 100644 index 00000000..ed66f0a9 --- /dev/null +++ b/backend/src/main/resources/error-appender.xml @@ -0,0 +1,19 @@ + + + ${LOG_PATH}/warn-error/warn-error.log + + WARN + + + ${FILE_LOG_PATTERN} + + + ${LOG_PATH}/warn-error/warn-error.%d{yyyy-MM-dd}.%i.log + + 5MB + + 30 + 20MB + + + diff --git a/backend/src/main/resources/http-appender.xml b/backend/src/main/resources/http-appender.xml new file mode 100644 index 00000000..2f5ba112 --- /dev/null +++ b/backend/src/main/resources/http-appender.xml @@ -0,0 +1,21 @@ + + + ${LOG_PATH}/http/http.log + + INFO + ACCEPT + DENY + + + ${FILE_LOG_PATTERN} + + + ${LOG_PATH}/http/http.%d{yyyy-MM-dd}.%i.log + + 20MB + + 30 + 100MB + + + diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..44666b80 --- /dev/null +++ b/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/submodule b/backend/src/main/resources/submodule index 1b3a861d..ef5be0f3 160000 --- a/backend/src/main/resources/submodule +++ b/backend/src/main/resources/submodule @@ -1 +1 @@ -Subproject commit 1b3a861d99321461dc364618834e1db64574d88a +Subproject commit ef5be0f3e8680c68de057b2817ca8b1087485d83 diff --git a/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java b/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java index 4c33212a..f665e810 100644 --- a/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java @@ -1,124 +1,124 @@ -package harustudy.backend.acceptance; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.databind.ObjectMapper; -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.content.repository.PomodoroContentRepository; -import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.domain.PomodoroStatus; -import harustudy.backend.room.domain.PomodoroRoom; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.util.List; -import java.util.Map; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.context.WebApplicationContext; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -public class AcceptanceTest { - - @PersistenceContext - private EntityManager entityManager; - @Autowired - private ObjectMapper objectMapper; - @Autowired - private PomodoroContentRepository pomodoroContentRepository; - @Autowired - private MockMvc mockMvc; - - @Autowired - private WebApplicationContext webApplicationContext; - - @BeforeEach - void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - } - - @Test - void 스터디를_진행한다() throws Exception { - Long 스터디_아이디 = 스터디를_개설한다(); - Long 멤버_아이디 = 스터디에_참여한다(스터디_아이디); - 스터디_계획을_작성한다(스터디_아이디, 멤버_아이디); - 스터디_상태를_진행에서_회고로_넘긴다(스터디_아이디, 멤버_아이디); - 스터디_회고를_작성한다(스터디_아이디, 멤버_아이디); - } - - private Long 스터디를_개설한다() { - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - entityManager.persist(participantCode); - PomodoroRoom pomodoroRoom = new PomodoroRoom("studyName", 1, 20, participantCode); - entityManager.persist(pomodoroRoom); - return pomodoroRoom.getId(); - } - - private Long 스터디에_참여한다(Long studyId) { - // TODO: 스터디 참여 기능 생성되면 대체 - PomodoroRoom pomodoroRoom = entityManager.find(PomodoroRoom.class, studyId); - Member member = new Member("nickname"); - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, - PomodoroStatus.PLANNING); - PomodoroContent pomodoroRecord = new PomodoroContent(pomodoroProgress, 1); - entityManager.persist(member); - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroRecord); - return member.getId(); - } - - private void 스터디_계획을_작성한다(Long studyId, Long memberId) throws Exception { - Map plan = Map.of("plan", "test"); - String jsonRequest = objectMapper.writeValueAsString(plan); - - mockMvc.perform(post("/api/studies/{studyId}/members/{memberId}/content/plans", - studyId, memberId) - .contentType(MediaType.APPLICATION_JSON) - .content(jsonRequest)) - .andExpect(status().isCreated()); - } - - private void 스터디_상태를_진행에서_회고로_넘긴다(Long studyId, Long memberId) throws Exception { - mockMvc.perform(post("/api/studies/{studyId}/members/{memberId}/next-step", - studyId, memberId)) - .andExpect(status().isOk()); - } - - private void 스터디_회고를_작성한다(Long studyId, Long memberId) throws Exception { - Map retrospect = Map.of("retrospect", "test"); - String jsonRequest = objectMapper.writeValueAsString(retrospect); - - mockMvc.perform(post("/api/studies/{studyId}/members/{memberId}/content/retrospects", - studyId, memberId) - .contentType(MediaType.APPLICATION_JSON) - .content(jsonRequest)) - .andExpect(status().isCreated()); - - List pomodoroRecords = pomodoroContentRepository.findAll(); - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(pomodoroRecords.size()).isOne(); - softly.assertThat(pomodoroRecords.get(0).getPlan()) - .containsAllEntriesOf(Map.of("plan", "test")); - softly.assertThat(pomodoroRecords.get(0).getRetrospect()) - .containsAllEntriesOf(Map.of("retrospect", "test")); - } - ); - } -} +//package harustudy.backend.acceptance; +// +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import harustudy.backend.content.domain.PomodoroContent; +//import harustudy.backend.content.repository.PomodoroContentRepository; +//import harustudy.backend.member.domain.Member; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.progress.domain.PomodoroProgress; +//import harustudy.backend.progress.domain.PomodoroStatus; +//import harustudy.backend.room.domain.PomodoroRoom; +//import jakarta.persistence.EntityManager; +//import jakarta.persistence.PersistenceContext; +//import java.util.List; +//import java.util.Map; +//import org.assertj.core.api.SoftAssertions; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.http.MediaType; +//import org.springframework.test.web.servlet.MockMvc; +//import org.springframework.test.web.servlet.setup.MockMvcBuilders; +//import org.springframework.transaction.annotation.Transactional; +//import org.springframework.web.context.WebApplicationContext; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(ReplaceUnderscores.class) +//@SpringBootTest +//@AutoConfigureMockMvc +//@Transactional +//public class AcceptanceTest { +// +// @PersistenceContext +// private EntityManager entityManager; +// @Autowired +// private ObjectMapper objectMapper; +// @Autowired +// private PomodoroContentRepository pomodoroContentRepository; +// @Autowired +// private MockMvc mockMvc; +// +// @Autowired +// private WebApplicationContext webApplicationContext; +// +// @BeforeEach +// void setUp() { +// this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); +// } +// +// @Test +// void 스터디를_진행한다() throws Exception { +// Long 스터디_아이디 = 스터디를_개설한다(); +// Long 멤버_아이디 = 스터디에_참여한다(스터디_아이디); +// 스터디_계획을_작성한다(스터디_아이디, 멤버_아이디); +// 스터디_상태를_진행에서_회고로_넘긴다(스터디_아이디, 멤버_아이디); +// 스터디_회고를_작성한다(스터디_아이디, 멤버_아이디); +// } +// +// private Long 스터디를_개설한다() { +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// entityManager.persist(participantCode); +// PomodoroRoom pomodoroRoom = new PomodoroRoom("studyName", 1, 20, participantCode); +// entityManager.persist(pomodoroRoom); +// return pomodoroRoom.getId(); +// } +// +// private Long 스터디에_참여한다(Long studyId) { +// // TODO: 스터디 참여 기능 생성되면 대체 +// PomodoroRoom pomodoroRoom = entityManager.find(PomodoroRoom.class, studyId); +// Member member = new Member("nickname"); +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, +// PomodoroStatus.PLANNING); +// PomodoroContent pomodoroRecord = new PomodoroContent(pomodoroProgress, 1); +// entityManager.persist(member); +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroRecord); +// return member.getId(); +// } +// +// private void 스터디_계획을_작성한다(Long studyId, Long memberId) throws Exception { +// Map plan = Map.of("plan", "test"); +// String jsonRequest = objectMapper.writeValueAsString(plan); +// +// mockMvc.perform(post("/api/studies/{studyId}/members/{memberId}/content/plans", +// studyId, memberId) +// .contentType(MediaType.APPLICATION_JSON) +// .content(jsonRequest)) +// .andExpect(status().isCreated()); +// } +// +// private void 스터디_상태를_진행에서_회고로_넘긴다(Long studyId, Long memberId) throws Exception { +// mockMvc.perform(post("/api/studies/{studyId}/members/{memberId}/next-step", +// studyId, memberId)) +// .andExpect(status().isOk()); +// } +// +// private void 스터디_회고를_작성한다(Long studyId, Long memberId) throws Exception { +// Map retrospect = Map.of("retrospect", "test"); +// String jsonRequest = objectMapper.writeValueAsString(retrospect); +// +// mockMvc.perform(post("/api/studies/{studyId}/members/{memberId}/content/retrospects", +// studyId, memberId) +// .contentType(MediaType.APPLICATION_JSON) +// .content(jsonRequest)) +// .andExpect(status().isCreated()); +// +// List pomodoroRecords = pomodoroContentRepository.findAll(); +// SoftAssertions.assertSoftly(softly -> { +// softly.assertThat(pomodoroRecords.size()).isOne(); +// softly.assertThat(pomodoroRecords.get(0).getPlan()) +// .containsAllEntriesOf(Map.of("plan", "test")); +// softly.assertThat(pomodoroRecords.get(0).getRetrospect()) +// .containsAllEntriesOf(Map.of("retrospect", "test")); +// } +// ); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/content/repository/PomodoroContentRepositoryTest.java b/backend/src/test/java/harustudy/backend/content/repository/PomodoroContentRepositoryTest.java index a124e734..53d9cdd5 100644 --- a/backend/src/test/java/harustudy/backend/content/repository/PomodoroContentRepositoryTest.java +++ b/backend/src/test/java/harustudy/backend/content/repository/PomodoroContentRepositoryTest.java @@ -1,90 +1,90 @@ -package harustudy.backend.content.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.domain.PomodoroStatus; -import harustudy.backend.room.domain.PomodoroRoom; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@DataJpaTest -class PomodoroContentRepositoryTest { - - @Autowired - private TestEntityManager testEntityManager; - @Autowired - private PomodoroContentRepository pomodoroContentRepository; - - private PomodoroProgress pomodoroProgress; - - @BeforeEach - void setUp() { - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 3, 20, participantCode); - Member member = new Member("member"); - pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.STUDYING); - - testEntityManager.persist(participantCode); - testEntityManager.persist(pomodoroRoom); - testEntityManager.persist(member); - testEntityManager.persist(pomodoroProgress); - } - - @Test - void 스터디_계획을_저장할_수_있다() { - // given - Map plan = Map.of("completionCondition", "완료조건", "expectedProbability", "80%"); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - pomodoroContent.changePlan(plan); - - // when - testEntityManager.persist(pomodoroContent); - - testEntityManager.flush(); - testEntityManager.clear(); - - List found = pomodoroContentRepository.findByPomodoroProgress(pomodoroProgress); - - // then - assertThat(found.get(0).getPlan()).containsAllEntriesOf(plan); - assertThat(found.get(0).getRetrospect()).isEmpty(); - } - - @Test - void 스터디_회고를_작성할_수_있다() { - // given - Map plan = Map.of("completionCondition", "완료조건", - "expectedProbability", "80%"); - Map retrospect = Map.of("doneAsExpected", "예상했던 결과", - "experiencedDifficulty", "겪었던 어려움"); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - pomodoroContent.changePlan(plan); - pomodoroContent.changeRetrospect(retrospect); - - // when - testEntityManager.persist(pomodoroContent); - - testEntityManager.flush(); - testEntityManager.clear(); - - List found = pomodoroContentRepository.findByPomodoroProgress(pomodoroProgress); - // then - - assertThat(found.get(0).getPlan()).containsAllEntriesOf(plan); - assertThat(found.get(0).getRetrospect()).containsAllEntriesOf(retrospect); - } -} +//package harustudy.backend.content.repository; +// +//import static org.assertj.core.api.Assertions.assertThat; +// +//import harustudy.backend.content.domain.PomodoroContent; +//import harustudy.backend.member.domain.Member; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.progress.domain.PomodoroProgress; +//import harustudy.backend.progress.domain.PomodoroStatus; +//import harustudy.backend.room.domain.PomodoroRoom; +//import java.util.List; +//import java.util.Map; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +//import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(ReplaceUnderscores.class) +//@DataJpaTest +//class PomodoroContentRepositoryTest { +// +// @Autowired +// private TestEntityManager testEntityManager; +// @Autowired +// private PomodoroContentRepository pomodoroContentRepository; +// +// private PomodoroProgress pomodoroProgress; +// +// @BeforeEach +// void setUp() { +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 3, 20, participantCode); +// Member member = new Member("member"); +// pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.STUDYING); +// +// testEntityManager.persist(participantCode); +// testEntityManager.persist(pomodoroRoom); +// testEntityManager.persist(member); +// testEntityManager.persist(pomodoroProgress); +// } +// +// @Test +// void 스터디_계획을_저장할_수_있다() { +// // given +// Map plan = Map.of("completionCondition", "완료조건", "expectedProbability", "80%"); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// pomodoroContent.changePlan(plan); +// +// // when +// testEntityManager.persist(pomodoroContent); +// +// testEntityManager.flush(); +// testEntityManager.clear(); +// +// List found = pomodoroContentRepository.findByPomodoroProgress(pomodoroProgress); +// +// // then +// assertThat(found.get(0).getPlan()).containsAllEntriesOf(plan); +// assertThat(found.get(0).getRetrospect()).isEmpty(); +// } +// +// @Test +// void 스터디_회고를_작성할_수_있다() { +// // given +// Map plan = Map.of("completionCondition", "완료조건", +// "expectedProbability", "80%"); +// Map retrospect = Map.of("doneAsExpected", "예상했던 결과", +// "experiencedDifficulty", "겪었던 어려움"); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// pomodoroContent.changePlan(plan); +// pomodoroContent.changeRetrospect(retrospect); +// +// // when +// testEntityManager.persist(pomodoroContent); +// +// testEntityManager.flush(); +// testEntityManager.clear(); +// +// List found = pomodoroContentRepository.findByPomodoroProgress(pomodoroProgress); +// // then +// +// assertThat(found.get(0).getPlan()).containsAllEntriesOf(plan); +// assertThat(found.get(0).getRetrospect()).containsAllEntriesOf(retrospect); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/content/service/PomodoroContentServiceTest.java b/backend/src/test/java/harustudy/backend/content/service/PomodoroContentServiceTest.java index 18e1bae0..70b52f97 100644 --- a/backend/src/test/java/harustudy/backend/content/service/PomodoroContentServiceTest.java +++ b/backend/src/test/java/harustudy/backend/content/service/PomodoroContentServiceTest.java @@ -1,223 +1,223 @@ -package harustudy.backend.content.service; - -import static harustudy.backend.common.EntityNotFoundException.MemberNotFound; -import static harustudy.backend.common.EntityNotFoundException.RoomNotFound; -import static org.assertj.core.api.Assertions.*; - -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.content.dto.PomodoroContentResponse; -import harustudy.backend.content.dto.PomodoroContentsResponse; -import harustudy.backend.content.dto.WritePlanRequest; -import harustudy.backend.content.dto.WriteRetrospectRequest; -import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.domain.PomodoroStatus; -import harustudy.backend.progress.exception.PomodoroProgressStatusException; -import harustudy.backend.room.domain.PomodoroRoom; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@Transactional -@SpringBootTest -class PomodoroContentServiceTest { - - @PersistenceContext - private EntityManager entityManager; - @Autowired - private PomodoroContentServiceV2 pomodoroContentService; - - private PomodoroRoom pomodoroRoom; - private Member member; - - @BeforeEach - void setUp() { - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); - member = new Member("nickname"); - - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - entityManager.persist(member); - } - - - @Test - void 계획_단계가_아닐_때_계획을_작성하려_하면_예외를_던진다() { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - WritePlanRequest request = new WritePlanRequest(member.getId(), Map.of("plan", "abc")); - - // when, then - assertThatThrownBy(() -> pomodoroContentService.writePlan(pomodoroRoom.getId(), request)) - .isInstanceOf(PomodoroProgressStatusException.class); - } - - - @Test - void 계획_단계에서는_계획을_작성할_수_있다() { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.PLANNING); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - WritePlanRequest request = new WritePlanRequest(member.getId(), Map.of("plan", "abc")); - - // when, then - assertThatCode(() -> pomodoroContentService.writePlan(pomodoroRoom.getId(), request)) - .doesNotThrowAnyException(); - } - - @Test - void 계획이_작성되어_있지_않은_경우_회고를_작성하려_하면_예외를_던진다() { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "abc")); - - // when, then - assertThatThrownBy(() -> pomodoroContentService.writeRetrospect(pomodoroRoom.getId(), request)) - .isInstanceOf(PomodoroProgressStatusException.class); - } - - @Test - void 회고_작성_단계가_아닐_때_회고를_작성하려_하면_예외를_던진다() { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.STUDYING); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "abc")); - - // when, then - assertThatThrownBy(() -> pomodoroContentService.writeRetrospect(pomodoroRoom.getId(), request)) - .isInstanceOf(PomodoroProgressStatusException.class); - } - - @Test - void 회고_단계에서는_회고를_작성할_수_있다() { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - pomodoroContent.changePlan(Map.of("plan", "abc")); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "abc")); - - // when, then - assertThatCode(() -> pomodoroContentService.writeRetrospect(pomodoroRoom.getId(), request)) - .doesNotThrowAnyException(); - } - - @Test - void 스터디에_참여한_스터디원의_특정_사이클의_콘텐츠를_조회할_수_있다() { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.DONE); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - Map plan = Map.of("plan", "abc"); - Map retrospect = Map.of("retrospect", "abc"); - pomodoroContent.changePlan(plan); - pomodoroContent.changeRetrospect(retrospect); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - // when - PomodoroContentsResponse pomodoroContentsResponse = pomodoroContentService.findMemberContentWithCycleFilter( - pomodoroRoom.getId(), member.getId(), 1); - PomodoroContentResponse expectedPomodoroContentResponse = new PomodoroContentResponse(1, plan, - retrospect); - List content = pomodoroContentsResponse.content(); - - // then - assertThat(content).containsExactly(expectedPomodoroContentResponse); - } - - @Test - void 스터디에_참여한_스터디원의_모든_사이클의_콘텐츠를_조회할_수_있다() { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 2, PomodoroStatus.DONE); - - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - Map plan = Map.of("plan", "abc"); - Map retrospect = Map.of("retrospect", "abc"); - pomodoroContent.changePlan(plan); - pomodoroContent.changeRetrospect(retrospect); - - PomodoroContent anotherPomodoroContent = new PomodoroContent(pomodoroProgress, 2); - Map anotherCyclePlan = Map.of("plan", "abc"); - Map anotherCycleRetrospect = Map.of("retrospect", "abc"); - anotherPomodoroContent.changePlan(anotherCyclePlan); - anotherPomodoroContent.changeRetrospect(anotherCycleRetrospect); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - entityManager.persist(anotherPomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - // when - PomodoroContentsResponse pomodoroContentsResponse = pomodoroContentService.findMemberContentWithCycleFilter( - pomodoroRoom.getId(), member.getId(), null); - - List expectedPomodoroContentResponses = List.of( - new PomodoroContentResponse(pomodoroContent.getCycle(), plan, retrospect), - new PomodoroContentResponse(anotherPomodoroContent.getCycle(), anotherCyclePlan, anotherCycleRetrospect) - ); - List content = pomodoroContentsResponse.content(); - - // then - assertThat(content).isEqualTo(expectedPomodoroContentResponses); - } - - @Test - void 스터디에_참여한_특정_스터디원의_콘텐츠를_조회시_스터디가_없으면_예외를_던진다() { - // given, when, then - assertThatThrownBy(() -> pomodoroContentService.findMemberContentWithCycleFilter(999L, member.getId(), null)) - .isInstanceOf(RoomNotFound.class); - } - - @Test - void 스터디에_참여한_특정_스터디원의_콘텐츠를_조회_시_멤버가_없으면_예외를_던진다() { - // given, when, then - assertThatThrownBy(() -> pomodoroContentService.findMemberContentWithCycleFilter(pomodoroRoom.getId(), 999L, null)) - .isInstanceOf(MemberNotFound.class); - } - - void FLUSH_AND_CLEAR_CONTEXT() { - entityManager.flush(); - entityManager.clear(); - } -} +//package harustudy.backend.content.service; +// +//import static org.assertj.core.api.Assertions.*; +// +//import harustudy.backend.content.domain.PomodoroContent; +//import harustudy.backend.content.dto.PomodoroContentResponse; +//import harustudy.backend.content.dto.PomodoroContentsResponse; +//import harustudy.backend.content.dto.WritePlanRequest; +//import harustudy.backend.content.dto.WriteRetrospectRequest; +//import harustudy.backend.member.domain.Member; +//import harustudy.backend.member.exception.MemberNotFoundException; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.progress.domain.PomodoroProgress; +//import harustudy.backend.progress.domain.PomodoroStatus; +//import harustudy.backend.progress.exception.PomodoroProgressStatusException; +//import harustudy.backend.room.domain.PomodoroRoom; +//import harustudy.backend.room.exception.RoomNotFoundException; +//import jakarta.persistence.EntityManager; +//import jakarta.persistence.PersistenceContext; +//import java.util.List; +//import java.util.Map; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.transaction.annotation.Transactional; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(ReplaceUnderscores.class) +//@Transactional +//@SpringBootTest +//class PomodoroContentServiceTest { +// +// @PersistenceContext +// private EntityManager entityManager; +// @Autowired +// private PomodoroContentServiceV2 pomodoroContentService; +// +// private PomodoroRoom pomodoroRoom; +// private Member member; +// +// @BeforeEach +// void setUp() { +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); +// member = new Member("nickname"); +// +// entityManager.persist(participantCode); +// entityManager.persist(pomodoroRoom); +// entityManager.persist(member); +// } +// +// +// @Test +// void 계획_단계가_아닐_때_계획을_작성하려_하면_예외를_던진다() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// WritePlanRequest request = new WritePlanRequest(member.getId(), Map.of("plan", "abc")); +// +// // when, then +// assertThatThrownBy(() -> pomodoroContentService.writePlan(pomodoroRoom.getId(), request)) +// .isInstanceOf(PomodoroProgressStatusException.class); +// } +// +// +// @Test +// void 계획_단계에서는_계획을_작성할_수_있다() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.PLANNING); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// WritePlanRequest request = new WritePlanRequest(member.getId(), Map.of("plan", "abc")); +// +// // when, then +// assertThatCode(() -> pomodoroContentService.writePlan(pomodoroRoom.getId(), request)) +// .doesNotThrowAnyException(); +// } +// +// @Test +// void 계획이_작성되어_있지_않은_경우_회고를_작성하려_하면_예외를_던진다() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "abc")); +// +// // when, then +// assertThatThrownBy(() -> pomodoroContentService.writeRetrospect(pomodoroRoom.getId(), request)) +// .isInstanceOf(PomodoroProgressStatusException.class); +// } +// +// @Test +// void 회고_작성_단계가_아닐_때_회고를_작성하려_하면_예외를_던진다() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.STUDYING); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "abc")); +// +// // when, then +// assertThatThrownBy(() -> pomodoroContentService.writeRetrospect(pomodoroRoom.getId(), request)) +// .isInstanceOf(PomodoroProgressStatusException.class); +// } +// +// @Test +// void 회고_단계에서는_회고를_작성할_수_있다() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// pomodoroContent.changePlan(Map.of("plan", "abc")); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "abc")); +// +// // when, then +// assertThatCode(() -> pomodoroContentService.writeRetrospect(pomodoroRoom.getId(), request)) +// .doesNotThrowAnyException(); +// } +// +// @Test +// void 스터디에_참여한_스터디원의_특정_사이클의_콘텐츠를_조회할_수_있다() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.DONE); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// Map plan = Map.of("plan", "abc"); +// Map retrospect = Map.of("retrospect", "abc"); +// pomodoroContent.changePlan(plan); +// pomodoroContent.changeRetrospect(retrospect); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// // when +// PomodoroContentsResponse pomodoroContentsResponse = pomodoroContentService.findMemberContentWithCycleFilter( +// pomodoroRoom.getId(), member.getId(), 1); +// PomodoroContentResponse expectedPomodoroContentResponse = new PomodoroContentResponse(1, plan, +// retrospect); +// List content = pomodoroContentsResponse.content(); +// +// // then +// assertThat(content).containsExactly(expectedPomodoroContentResponse); +// } +// +// @Test +// void 스터디에_참여한_스터디원의_모든_사이클의_콘텐츠를_조회할_수_있다() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 2, PomodoroStatus.DONE); +// +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// Map plan = Map.of("plan", "abc"); +// Map retrospect = Map.of("retrospect", "abc"); +// pomodoroContent.changePlan(plan); +// pomodoroContent.changeRetrospect(retrospect); +// +// PomodoroContent anotherPomodoroContent = new PomodoroContent(pomodoroProgress, 2); +// Map anotherCyclePlan = Map.of("plan", "abc"); +// Map anotherCycleRetrospect = Map.of("retrospect", "abc"); +// anotherPomodoroContent.changePlan(anotherCyclePlan); +// anotherPomodoroContent.changeRetrospect(anotherCycleRetrospect); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// entityManager.persist(anotherPomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// // when +// PomodoroContentsResponse pomodoroContentsResponse = pomodoroContentService.findMemberContentWithCycleFilter( +// pomodoroRoom.getId(), member.getId(), null); +// +// List expectedPomodoroContentResponses = List.of( +// new PomodoroContentResponse(pomodoroContent.getCycle(), plan, retrospect), +// new PomodoroContentResponse(anotherPomodoroContent.getCycle(), anotherCyclePlan, anotherCycleRetrospect) +// ); +// List content = pomodoroContentsResponse.content(); +// +// // then +// assertThat(content).isEqualTo(expectedPomodoroContentResponses); +// } +// +// @Test +// void 스터디에_참여한_특정_스터디원의_콘텐츠를_조회시_스터디가_없으면_예외를_던진다() { +// // given, when, then +// assertThatThrownBy(() -> pomodoroContentService.findMemberContentWithCycleFilter(999L, member.getId(), null)) +// .isInstanceOf(RoomNotFoundException.class); +// } +// +// @Test +// void 스터디에_참여한_특정_스터디원의_콘텐츠를_조회_시_멤버가_없으면_예외를_던진다() { +// // given, when, then +// assertThatThrownBy(() -> pomodoroContentService.findMemberContentWithCycleFilter(pomodoroRoom.getId(), 999L, null)) +// .isInstanceOf(MemberNotFoundException.class); +// } +// +// void FLUSH_AND_CLEAR_CONTEXT() { +// entityManager.flush(); +// entityManager.clear(); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java index 8bf222c2..993bb811 100644 --- a/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java @@ -1,42 +1,42 @@ -package harustudy.backend.integration; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.context.WebApplicationContext; - -@AutoConfigureMockMvc -@Transactional -@SpringBootTest -public class IntegrationTest { - - @PersistenceContext - protected EntityManager entityManager; - - @Autowired - protected ObjectMapper objectMapper; - - protected MockMvc mockMvc; - - @Autowired - private WebApplicationContext webApplicationContext; - - void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - } - - void FLUSH_AND_CLEAR_CONTEXT() { - entityManager.flush(); - entityManager.clear(); - } - - protected void setMockMvc() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - } -} +//package harustudy.backend.integration; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import jakarta.persistence.EntityManager; +//import jakarta.persistence.PersistenceContext; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.test.web.servlet.MockMvc; +//import org.springframework.test.web.servlet.setup.MockMvcBuilders; +//import org.springframework.transaction.annotation.Transactional; +//import org.springframework.web.context.WebApplicationContext; +// +//@AutoConfigureMockMvc +//@Transactional +//@SpringBootTest +//public class IntegrationTest { +// +// @PersistenceContext +// protected EntityManager entityManager; +// +// @Autowired +// protected ObjectMapper objectMapper; +// +// protected MockMvc mockMvc; +// +// @Autowired +// private WebApplicationContext webApplicationContext; +// +// void setUp() { +// this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); +// } +// +// void FLUSH_AND_CLEAR_CONTEXT() { +// entityManager.flush(); +// entityManager.clear(); +// } +// +// protected void setMockMvc() { +// this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java index 451e4f06..dc2ebd83 100644 --- a/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java @@ -1,140 +1,140 @@ -package harustudy.backend.integration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.dto.GuestRegisterRequest; -import harustudy.backend.member.dto.MemberResponseV2; -import harustudy.backend.member.dto.MembersResponseV2; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.room.domain.PomodoroRoom; -import jakarta.servlet.http.Cookie; -import java.nio.charset.StandardCharsets; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MvcResult; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -public class MemberIntegrationTest extends IntegrationTest { - - private PomodoroRoom room; - private Member member1; - private Member member2; - - @BeforeEach - void setUp() { - super.setUp(); - - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - room = new PomodoroRoom("roomName", 1, 20, participantCode); - - member1 = new Member("member1"); - member2 = new Member("member2"); - PomodoroProgress pomodoroProgress1 = new PomodoroProgress(room, member1); - PomodoroProgress pomodoroProgress2 = new PomodoroProgress(room, member2); - - entityManager.persist(participantCode); - entityManager.persist(room); - entityManager.persist(member1); - entityManager.persist(member2); - entityManager.persist(pomodoroProgress1); - entityManager.persist(pomodoroProgress2); - } - - @Test - void 멤버를_조회할_수_있다() throws Exception { - // given - MemberResponseV2 expected = new MemberResponseV2(member1.getId(), member1.getNickname()); - - // when - MvcResult result = mockMvc.perform( - get("/api/v2/members/{memberId}", member1.getId())) - .andExpect(status().isOk()) - .andReturn(); - - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - MemberResponseV2 response = objectMapper.readValue(jsonResponse, MemberResponseV2.class); - - // then - assertThat(response).isEqualTo(expected); - } - - @Test - void 스터디에_참여한_멤버들을_조회할_수_있다() throws Exception { - // given - MemberResponseV2 expectedValue1 = new MemberResponseV2(member1.getId(), - member1.getNickname()); - MemberResponseV2 expectedValue2 = new MemberResponseV2(member2.getId(), - member2.getNickname()); - MembersResponseV2 expectedResponses = new MembersResponseV2( - List.of(expectedValue1, expectedValue2)); - - // when - MvcResult result = mockMvc.perform( - get("/api/v2/members") - .param("studyId", String.valueOf(room.getId()))) - .andExpect(status().isOk()) - .andReturn(); - - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - MembersResponseV2 response = objectMapper.readValue(jsonResponse, MembersResponseV2.class); - - // then - assertAll( - () -> assertThat(response.members()).hasSize(2), - () -> assertThat(response.members().get(0).nickname()).isEqualTo( - member1.getNickname()), - () -> assertThat(response.members().get(1).nickname()).isEqualTo( - member2.getNickname()) - ); - - assertSoftly(softly -> { - assertThat(response.members()).hasSize(2); - assertThat(response.members().get(0).nickname()).isEqualTo(member1.getNickname()); - assertThat(response.members().get(1).nickname()).isEqualTo(member2.getNickname()); - }); - } - - @Test - void 스터디에_신규_멤버를_등록한다() throws Exception { - // given - GuestRegisterRequest request = new GuestRegisterRequest(room.getId(), "member3"); - String body = objectMapper.writeValueAsString(request); - - // when - MvcResult result = mockMvc.perform( - post("/api/v2/members/guest") - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isCreated()) - .andReturn(); - - String createdUri = result.getResponse().getHeader("Location"); - Cookie cookie = result.getResponse().getCookie("memberId"); - - // then - assertAll( - () -> assertThat(createdUri).contains("/api/v2/members/"), - () -> assertThat(cookie).isNotNull(), - () -> assertThat(cookie.getValue()).isNotNull() - ); - - assertSoftly(softly -> { - assertThat(createdUri).contains("/api/v2/members/"); - assertThat(cookie).isNotNull(); - assertThat(cookie.getValue()).isNotNull(); - }); - } -} +//package harustudy.backend.integration; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.SoftAssertions.assertSoftly; +//import static org.junit.jupiter.api.Assertions.assertAll; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +//import harustudy.backend.member.domain.Member; +//import harustudy.backend.member.dto.GuestRegisterRequest; +//import harustudy.backend.member.dto.MemberResponseV2; +//import harustudy.backend.member.dto.MembersResponseV2; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.progress.domain.PomodoroProgress; +//import harustudy.backend.room.domain.PomodoroRoom; +//import jakarta.servlet.http.Cookie; +//import java.nio.charset.StandardCharsets; +//import java.util.List; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +//import org.junit.jupiter.api.Test; +//import org.springframework.http.MediaType; +//import org.springframework.test.web.servlet.MvcResult; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(ReplaceUnderscores.class) +//public class MemberIntegrationTest extends IntegrationTest { +// +// private PomodoroRoom room; +// private Member member1; +// private Member member2; +// +// @BeforeEach +// void setUp() { +// super.setUp(); +// +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// room = new PomodoroRoom("roomName", 1, 20, participantCode); +// +// member1 = new Member("member1"); +// member2 = new Member("member2"); +// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(room, member1); +// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(room, member2); +// +// entityManager.persist(participantCode); +// entityManager.persist(room); +// entityManager.persist(member1); +// entityManager.persist(member2); +// entityManager.persist(pomodoroProgress1); +// entityManager.persist(pomodoroProgress2); +// } +// +// @Test +// void 멤버를_조회할_수_있다() throws Exception { +// // given +// MemberResponseV2 expected = new MemberResponseV2(member1.getId(), member1.getNickname()); +// +// // when +// MvcResult result = mockMvc.perform( +// get("/api/v2/members/{memberId}", member1.getId())) +// .andExpect(status().isOk()) +// .andReturn(); +// +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// MemberResponseV2 response = objectMapper.readValue(jsonResponse, MemberResponseV2.class); +// +// // then +// assertThat(response).isEqualTo(expected); +// } +// +// @Test +// void 스터디에_참여한_멤버들을_조회할_수_있다() throws Exception { +// // given +// MemberResponseV2 expectedValue1 = new MemberResponseV2(member1.getId(), +// member1.getNickname()); +// MemberResponseV2 expectedValue2 = new MemberResponseV2(member2.getId(), +// member2.getNickname()); +// MembersResponseV2 expectedResponses = new MembersResponseV2( +// List.of(expectedValue1, expectedValue2)); +// +// // when +// MvcResult result = mockMvc.perform( +// get("/api/v2/members") +// .param("studyId", String.valueOf(room.getId()))) +// .andExpect(status().isOk()) +// .andReturn(); +// +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// MembersResponseV2 response = objectMapper.readValue(jsonResponse, MembersResponseV2.class); +// +// // then +// assertAll( +// () -> assertThat(response.members()).hasSize(2), +// () -> assertThat(response.members().get(0).nickname()).isEqualTo( +// member1.getNickname()), +// () -> assertThat(response.members().get(1).nickname()).isEqualTo( +// member2.getNickname()) +// ); +// +// assertSoftly(softly -> { +// assertThat(response.members()).hasSize(2); +// assertThat(response.members().get(0).nickname()).isEqualTo(member1.getNickname()); +// assertThat(response.members().get(1).nickname()).isEqualTo(member2.getNickname()); +// }); +// } +// +// @Test +// void 스터디에_신규_멤버를_등록한다() throws Exception { +// // given +// GuestRegisterRequest request = new GuestRegisterRequest(room.getId(), "member3"); +// String body = objectMapper.writeValueAsString(request); +// +// // when +// MvcResult result = mockMvc.perform( +// post("/api/v2/members/guest") +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// .andExpect(status().isCreated()) +// .andReturn(); +// +// String createdUri = result.getResponse().getHeader("Location"); +// Cookie cookie = result.getResponse().getCookie("memberId"); +// +// // then +// assertAll( +// () -> assertThat(createdUri).contains("/api/v2/members/"), +// () -> assertThat(cookie).isNotNull(), +// () -> assertThat(cookie.getValue()).isNotNull() +// ); +// +// assertSoftly(softly -> { +// assertThat(createdUri).contains("/api/v2/members/"); +// assertThat(cookie).isNotNull(); +// assertThat(cookie.getValue()).isNotNull(); +// }); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java index ea415be3..25c179da 100644 --- a/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java @@ -1,170 +1,170 @@ -package harustudy.backend.integration; - - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.content.dto.PomodoroContentResponse; -import harustudy.backend.content.dto.PomodoroContentsResponse; -import harustudy.backend.content.dto.WritePlanRequest; -import harustudy.backend.content.dto.WriteRetrospectRequest; -import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.domain.PomodoroStatus; -import harustudy.backend.room.domain.PomodoroRoom; -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MvcResult; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) -public class PomodoroContentIntegrationTest extends IntegrationTest { - - private PomodoroRoom pomodoroRoom; - private Member member; - - @BeforeEach - void setUp() { - super.setUp(); - - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - pomodoroRoom = new PomodoroRoom("roomName", 2, 20, participantCode); - member = new Member("nickname"); - - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - entityManager.persist(member); - } - - @Test - void 계획을_작성할_수_있다() throws Exception { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.PLANNING); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - WritePlanRequest request = new WritePlanRequest(member.getId(), Map.of("plan", "test")); - String body = objectMapper.writeValueAsString(request); - - // when - mockMvc.perform( - post("/api/v2/studies/{studyId}/contents/write-plan", pomodoroRoom.getId()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - - // then - .andExpect(status().isOk()); - } - - @Test - void 회고를_작성할_수_있다() throws Exception { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - pomodoroContent.changePlan(Map.of("plan", "test")); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "test")); - String body = objectMapper.writeValueAsString(request); - - // when - mockMvc.perform( - post("/api/v2/studies/{studyId}/contents/write-retrospect", pomodoroRoom.getId()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - - // then - .andExpect(status().isOk()); - } - - @Test - void 특정_사이클의_계획_및_회고를_조회할_수_있다() throws Exception { - - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.DONE); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - Map plan = Map.of("plan", "test"); - Map retrospect = Map.of("retrospect", "test"); - pomodoroContent.changePlan(plan); - pomodoroContent.changeRetrospect(retrospect); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - // when - MvcResult result = mockMvc.perform( - get("/api/v2/studies/{studyId}/contents", pomodoroRoom.getId()) - .param("memberId", String.valueOf(member.getId())) - .param("cycle", String.valueOf(pomodoroContent.getCycle()))) - - .andExpect(status().isOk()) - .andReturn(); - - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - PomodoroContentsResponse response = objectMapper.readValue(jsonResponse, PomodoroContentsResponse.class); - PomodoroContentsResponse expected = new PomodoroContentsResponse(List.of( - new PomodoroContentResponse(pomodoroContent.getCycle(), plan, retrospect) - )); - - // then - assertThat(response).isEqualTo(expected); - } - - @Test - void 모든_사이클의_계획_및_회고를_조회할_수_있다() throws Exception { - // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 2, PomodoroStatus.DONE); - PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); - Map plan = Map.of("plan", "test"); - Map retrospect = Map.of("retrospect", "test"); - pomodoroContent.changePlan(plan); - pomodoroContent.changeRetrospect(retrospect); - - PomodoroContent anotherPomodoroContent = new PomodoroContent(pomodoroProgress, 2); - Map anotherPlan = Map.of("plan", "test"); - Map anotherRetrospect = Map.of("retrospect", "test"); - anotherPomodoroContent.changePlan(anotherPlan); - anotherPomodoroContent.changeRetrospect(anotherRetrospect); - - entityManager.persist(pomodoroProgress); - entityManager.persist(pomodoroContent); - entityManager.persist(anotherPomodoroContent); - FLUSH_AND_CLEAR_CONTEXT(); - - // when - MvcResult result = mockMvc.perform( - get("/api/v2/studies/{studyId}/contents", pomodoroRoom.getId()) - .param("memberId", String.valueOf(member.getId()))) - - .andExpect(status().isOk()) - .andReturn(); - - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - PomodoroContentsResponse response = objectMapper.readValue(jsonResponse, PomodoroContentsResponse.class); - PomodoroContentsResponse expected = new PomodoroContentsResponse(List.of( - new PomodoroContentResponse(pomodoroContent.getCycle(), plan, retrospect), - new PomodoroContentResponse(anotherPomodoroContent.getCycle(), anotherPlan, anotherRetrospect) - )); - - // then - assertThat(response).isEqualTo(expected); - } -} +//package harustudy.backend.integration; +// +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +//import harustudy.backend.content.domain.PomodoroContent; +//import harustudy.backend.content.dto.PomodoroContentResponse; +//import harustudy.backend.content.dto.PomodoroContentsResponse; +//import harustudy.backend.content.dto.WritePlanRequest; +//import harustudy.backend.content.dto.WriteRetrospectRequest; +//import harustudy.backend.member.domain.Member; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.progress.domain.PomodoroProgress; +//import harustudy.backend.progress.domain.PomodoroStatus; +//import harustudy.backend.room.domain.PomodoroRoom; +//import java.nio.charset.StandardCharsets; +//import java.util.List; +//import java.util.Map; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator; +//import org.junit.jupiter.api.Test; +//import org.springframework.http.MediaType; +//import org.springframework.test.web.servlet.MvcResult; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +//public class PomodoroContentIntegrationTest extends IntegrationTest { +// +// private PomodoroRoom pomodoroRoom; +// private Member member; +// +// @BeforeEach +// void setUp() { +// super.setUp(); +// +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// pomodoroRoom = new PomodoroRoom("roomName", 2, 20, participantCode); +// member = new Member("nickname"); +// +// entityManager.persist(participantCode); +// entityManager.persist(pomodoroRoom); +// entityManager.persist(member); +// } +// +// @Test +// void 계획을_작성할_수_있다() throws Exception { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.PLANNING); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// WritePlanRequest request = new WritePlanRequest(member.getId(), Map.of("plan", "test")); +// String body = objectMapper.writeValueAsString(request); +// +// // when +// mockMvc.perform( +// post("/api/v2/studies/{studyId}/contents/write-plan", pomodoroRoom.getId()) +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// +// // then +// .andExpect(status().isOk()); +// } +// +// @Test +// void 회고를_작성할_수_있다() throws Exception { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.RETROSPECT); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// pomodoroContent.changePlan(Map.of("plan", "test")); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// WriteRetrospectRequest request = new WriteRetrospectRequest(member.getId(), Map.of("retrospect", "test")); +// String body = objectMapper.writeValueAsString(request); +// +// // when +// mockMvc.perform( +// post("/api/v2/studies/{studyId}/contents/write-retrospect", pomodoroRoom.getId()) +// .contentType(MediaType.APPLICATION_JSON) +// .content(body)) +// +// // then +// .andExpect(status().isOk()); +// } +// +// @Test +// void 특정_사이클의_계획_및_회고를_조회할_수_있다() throws Exception { +// +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 1, PomodoroStatus.DONE); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// Map plan = Map.of("plan", "test"); +// Map retrospect = Map.of("retrospect", "test"); +// pomodoroContent.changePlan(plan); +// pomodoroContent.changeRetrospect(retrospect); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// // when +// MvcResult result = mockMvc.perform( +// get("/api/v2/studies/{studyId}/contents", pomodoroRoom.getId()) +// .param("memberId", String.valueOf(member.getId())) +// .param("cycle", String.valueOf(pomodoroContent.getCycle()))) +// +// .andExpect(status().isOk()) +// .andReturn(); +// +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroContentsResponse response = objectMapper.readValue(jsonResponse, PomodoroContentsResponse.class); +// PomodoroContentsResponse expected = new PomodoroContentsResponse(List.of( +// new PomodoroContentResponse(pomodoroContent.getCycle(), plan, retrospect) +// )); +// +// // then +// assertThat(response).isEqualTo(expected); +// } +// +// @Test +// void 모든_사이클의_계획_및_회고를_조회할_수_있다() throws Exception { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, 2, PomodoroStatus.DONE); +// PomodoroContent pomodoroContent = new PomodoroContent(pomodoroProgress, 1); +// Map plan = Map.of("plan", "test"); +// Map retrospect = Map.of("retrospect", "test"); +// pomodoroContent.changePlan(plan); +// pomodoroContent.changeRetrospect(retrospect); +// +// PomodoroContent anotherPomodoroContent = new PomodoroContent(pomodoroProgress, 2); +// Map anotherPlan = Map.of("plan", "test"); +// Map anotherRetrospect = Map.of("retrospect", "test"); +// anotherPomodoroContent.changePlan(anotherPlan); +// anotherPomodoroContent.changeRetrospect(anotherRetrospect); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(pomodoroContent); +// entityManager.persist(anotherPomodoroContent); +// FLUSH_AND_CLEAR_CONTEXT(); +// +// // when +// MvcResult result = mockMvc.perform( +// get("/api/v2/studies/{studyId}/contents", pomodoroRoom.getId()) +// .param("memberId", String.valueOf(member.getId()))) +// +// .andExpect(status().isOk()) +// .andReturn(); +// +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroContentsResponse response = objectMapper.readValue(jsonResponse, PomodoroContentsResponse.class); +// PomodoroContentsResponse expected = new PomodoroContentsResponse(List.of( +// new PomodoroContentResponse(pomodoroContent.getCycle(), plan, retrospect), +// new PomodoroContentResponse(anotherPomodoroContent.getCycle(), anotherPlan, anotherRetrospect) +// )); +// +// // then +// assertThat(response).isEqualTo(expected); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/integration/PomodoroProgressIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/PomodoroProgressIntegrationTest.java index 5cbafe0c..ea4e569a 100644 --- a/backend/src/test/java/harustudy/backend/integration/PomodoroProgressIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/PomodoroProgressIntegrationTest.java @@ -1,84 +1,84 @@ -package harustudy.backend.integration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.domain.PomodoroStatus; -import harustudy.backend.progress.dto.PomodoroProgressResponseV2; -import harustudy.backend.room.domain.PomodoroRoom; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MvcResult; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -public class PomodoroProgressIntegrationTest extends IntegrationTest { - - private PomodoroRoom pomodoroRoom; - private Member member; - private PomodoroProgress pomodoroProgress; - - @BeforeEach - void setUp() { - super.setUp(); - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - pomodoroRoom = new PomodoroRoom("roomName", 3, 20, - participantCode); - member = new Member("nickname"); - pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); - - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - entityManager.persist(member); - entityManager.persist(pomodoroProgress); - - entityManager.flush(); - entityManager.clear(); - } - - @Test - void studyId와_progressId로_진행도를_조회한다() throws Exception { - // when - MvcResult result = mockMvc.perform( - get("/api/v2/studies/{studyId}/progresses", pomodoroRoom.getId()) - .param("memberId", Long.toString(member.getId())) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andReturn(); - - // then - String jsonResponse = result.getResponse().getContentAsString(); - PomodoroProgressResponseV2 response = objectMapper.readValue(jsonResponse, - PomodoroProgressResponseV2.class); - - assertSoftly(softly -> { - softly.assertThat(response.currentCycle()).isEqualTo(1); - softly.assertThat(response.step()) - .isEqualTo(PomodoroStatus.PLANNING.toString().toLowerCase()); - }); - } - - @Test - void studyId와_progressId로_진행도를_진행시킨다() throws Exception { - // when, then - mockMvc.perform( - post("/api/v2/studies/{studyId}/progresses/{progressId}/next-step", - pomodoroRoom.getId(), pomodoroProgress.getId()) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isNoContent()); - - PomodoroProgress foundProgress = entityManager.find(PomodoroProgress.class, - pomodoroProgress.getId()); - assertThat(foundProgress.getPomodoroStatus()).isEqualTo(PomodoroStatus.STUDYING); - } -} +//package harustudy.backend.integration; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.SoftAssertions.assertSoftly; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +//import harustudy.backend.member.domain.Member; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.progress.domain.PomodoroProgress; +//import harustudy.backend.progress.domain.PomodoroStatus; +//import harustudy.backend.progress.dto.PomodoroProgressResponseV2; +//import harustudy.backend.room.domain.PomodoroRoom; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +//import org.junit.jupiter.api.Test; +//import org.springframework.http.MediaType; +//import org.springframework.test.web.servlet.MvcResult; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(ReplaceUnderscores.class) +//public class PomodoroProgressIntegrationTest extends IntegrationTest { +// +// private PomodoroRoom pomodoroRoom; +// private Member member; +// private PomodoroProgress pomodoroProgress; +// +// @BeforeEach +// void setUp() { +// super.setUp(); +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// pomodoroRoom = new PomodoroRoom("roomName", 3, 20, +// participantCode); +// member = new Member("nickname"); +// pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); +// +// entityManager.persist(participantCode); +// entityManager.persist(pomodoroRoom); +// entityManager.persist(member); +// entityManager.persist(pomodoroProgress); +// +// entityManager.flush(); +// entityManager.clear(); +// } +// +// @Test +// void studyId와_progressId로_진행도를_조회한다() throws Exception { +// // when +// MvcResult result = mockMvc.perform( +// get("/api/v2/studies/{studyId}/progresses", pomodoroRoom.getId()) +// .param("memberId", Long.toString(member.getId())) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isOk()) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(); +// PomodoroProgressResponseV2 response = objectMapper.readValue(jsonResponse, +// PomodoroProgressResponseV2.class); +// +// assertSoftly(softly -> { +// softly.assertThat(response.currentCycle()).isEqualTo(1); +// softly.assertThat(response.step()) +// .isEqualTo(PomodoroStatus.PLANNING.toString().toLowerCase()); +// }); +// } +// +// @Test +// void studyId와_progressId로_진행도를_진행시킨다() throws Exception { +// // when, then +// mockMvc.perform( +// post("/api/v2/studies/{studyId}/progresses/{progressId}/next-step", +// pomodoroRoom.getId(), pomodoroProgress.getId()) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isNoContent()); +// +// PomodoroProgress foundProgress = entityManager.find(PomodoroProgress.class, +// pomodoroProgress.getId()); +// assertThat(foundProgress.getPomodoroStatus()).isEqualTo(PomodoroStatus.STUDYING); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/integration/PomodoroRoomIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/PomodoroRoomIntegrationTest.java index 50324f7c..d62a41c0 100644 --- a/backend/src/test/java/harustudy/backend/integration/PomodoroRoomIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/PomodoroRoomIntegrationTest.java @@ -1,117 +1,117 @@ -package harustudy.backend.integration; - -import com.fasterxml.jackson.databind.ObjectMapper; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.dto.CreatePomodoroRoomRequest; -import harustudy.backend.room.dto.CreatePomodoroRoomResponse; -import harustudy.backend.room.dto.PomodoroRoomResponseV2; -import jakarta.persistence.EntityManager; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MvcResult; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -class PomodoroRoomIntegrationTest extends IntegrationTest { - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private EntityManager entityManager; - - private ParticipantCode participantCode; - private PomodoroRoom pomodoroRoom; - - @BeforeEach - void setUp() { - super.setMockMvc(); - participantCode = new ParticipantCode(new CodeGenerationStrategy()); - pomodoroRoom = new PomodoroRoom("room", 3, 20, participantCode); - - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - - entityManager.flush(); - entityManager.clear(); - } - - @Test - void 스터디_아이디로_스터디를_조회한다() throws Exception { - // given, when - MvcResult result = mockMvc.perform(get("/api/v2/studies/{studyId}", pomodoroRoom.getId()) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andReturn(); - - // then - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - PomodoroRoomResponseV2 response = objectMapper.readValue(jsonResponse, - PomodoroRoomResponseV2.class); - - assertAll( - () -> assertThat(response.name()).isEqualTo(pomodoroRoom.getName()), - () -> assertThat(response.totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), - () -> assertThat(response.timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) - ); - } - - @Test - void 참여코드로_스터디를_조회한다() throws Exception { - // given, when - MvcResult result = mockMvc.perform(get("/api/v2/studies") - .param("participantCode", participantCode.getCode()) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andReturn(); - - // then - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - PomodoroRoomResponseV2 response = objectMapper.readValue(jsonResponse, - PomodoroRoomResponseV2.class); - - assertAll( - () -> assertThat(response.name()).isEqualTo(pomodoroRoom.getName()), - () -> assertThat(response.totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), - () -> assertThat(response.timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) - ); - } - - @Test - void 스터디를_개설한다() throws Exception { - // given - CreatePomodoroRoomRequest request = new CreatePomodoroRoomRequest("studyName", 1, 20); - String jsonRequest = objectMapper.writeValueAsString(request); - - // when - MvcResult result = mockMvc.perform(post("/api/v2/studies") - .content(jsonRequest) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andExpect(header().exists("Location")) - .andReturn(); - - // then - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - CreatePomodoroRoomResponse response = objectMapper.readValue(jsonResponse, CreatePomodoroRoomResponse.class); - - assertThat(response.participantCode()) - .isAlphabetic() - .isUpperCase() - .hasSize(6); - } -} +//package harustudy.backend.integration; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.room.domain.PomodoroRoom; +//import harustudy.backend.room.dto.CreatePomodoroRoomRequest; +//import harustudy.backend.room.dto.CreatePomodoroRoomResponse; +//import harustudy.backend.room.dto.PomodoroRoomResponseV2; +//import jakarta.persistence.EntityManager; +//import java.nio.charset.StandardCharsets; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.http.MediaType; +//import org.springframework.test.web.servlet.MvcResult; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.junit.jupiter.api.Assertions.assertAll; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(ReplaceUnderscores.class) +//class PomodoroRoomIntegrationTest extends IntegrationTest { +// +// @Autowired +// private ObjectMapper objectMapper; +// +// @Autowired +// private EntityManager entityManager; +// +// private ParticipantCode participantCode; +// private PomodoroRoom pomodoroRoom; +// +// @BeforeEach +// void setUp() { +// super.setMockMvc(); +// participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// pomodoroRoom = new PomodoroRoom("room", 3, 20, participantCode); +// +// entityManager.persist(participantCode); +// entityManager.persist(pomodoroRoom); +// +// entityManager.flush(); +// entityManager.clear(); +// } +// +// @Test +// void 스터디_아이디로_스터디를_조회한다() throws Exception { +// // given, when +// MvcResult result = mockMvc.perform(get("/api/v2/studies/{studyId}", pomodoroRoom.getId()) +// .accept(MediaType.APPLICATION_JSON)) +// .andExpect(status().isOk()) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroRoomResponseV2 response = objectMapper.readValue(jsonResponse, +// PomodoroRoomResponseV2.class); +// +// assertAll( +// () -> assertThat(response.name()).isEqualTo(pomodoroRoom.getName()), +// () -> assertThat(response.totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), +// () -> assertThat(response.timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) +// ); +// } +// +// @Test +// void 참여코드로_스터디를_조회한다() throws Exception { +// // given, when +// MvcResult result = mockMvc.perform(get("/api/v2/studies") +// .param("participantCode", participantCode.getCode()) +// .accept(MediaType.APPLICATION_JSON)) +// .andExpect(status().isOk()) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroRoomResponseV2 response = objectMapper.readValue(jsonResponse, +// PomodoroRoomResponseV2.class); +// +// assertAll( +// () -> assertThat(response.name()).isEqualTo(pomodoroRoom.getName()), +// () -> assertThat(response.totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), +// () -> assertThat(response.timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) +// ); +// } +// +// @Test +// void 스터디를_개설한다() throws Exception { +// // given +// CreatePomodoroRoomRequest request = new CreatePomodoroRoomRequest("studyName", 1, 20); +// String jsonRequest = objectMapper.writeValueAsString(request); +// +// // when +// MvcResult result = mockMvc.perform(post("/api/v2/studies") +// .content(jsonRequest) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isCreated()) +// .andExpect(header().exists("Location")) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// CreatePomodoroRoomResponse response = objectMapper.readValue(jsonResponse, CreatePomodoroRoomResponse.class); +// +// assertThat(response.participantCode()) +// .isAlphabetic() +// .isUpperCase() +// .hasSize(6); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/member/domain/MemberTest.java b/backend/src/test/java/harustudy/backend/member/domain/MemberTest.java deleted file mode 100644 index 996c79db..00000000 --- a/backend/src/test/java/harustudy/backend/member/domain/MemberTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package harustudy.backend.member.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 harustudy.backend.member.exception.MemberNameLengthException; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -class MemberTest { - - @ParameterizedTest - @ValueSource(strings = {"", "12345678901"}) - void 멤버의_닉네임이_1자_미만_10자_초과이면_예외를_던진다(String nickname) { - assertThatThrownBy(() -> new Member(nickname)) - .isInstanceOf(MemberNameLengthException.class); - } - - @ParameterizedTest - @ValueSource(strings = {"1", "1234567890"}) - void 멤버의_닉네임이_1자_이상_10자_이하이면_정상_케이스이다(String nickname) { - assertThatCode(() -> new Member(nickname)) - .doesNotThrowAnyException(); - } - - @Test - void 멤버간_닉네임이_동일한지_확인할_수_있다() { - // given, when - Member newMember = new Member("sameName"); - Member sameNameMember = new Member("sameName"); - Member otherNameMember = new Member("otherName"); - - // then - assertThat(sameNameMember.hasSameNickname(newMember)).isTrue(); - assertThat(sameNameMember.hasSameNickname(otherNameMember)).isFalse(); - } -} diff --git a/backend/src/test/java/harustudy/backend/member/service/MemberServiceTest.java b/backend/src/test/java/harustudy/backend/member/service/MemberServiceTest.java new file mode 100644 index 00000000..7f68d2d6 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/member/service/MemberServiceTest.java @@ -0,0 +1,65 @@ +package harustudy.backend.member.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.member.domain.LoginType; +import harustudy.backend.member.domain.Member; +import harustudy.backend.member.dto.MemberResponse; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@SpringBootTest +@Transactional +class MemberServiceTest { + + @PersistenceContext + private EntityManager entityManager; + @Autowired + private MemberService memberService; + + private Member member1; + private Member member2; + + @BeforeEach + void setUp() { + member1 = new Member("memberName1", "member1@gmail.com", "member1ProfileImageUrl", + LoginType.GOOGLE); + member2 = new Member("memberName2", "member2@gmail.com", "member2ProfileImageUrl", + LoginType.GOOGLE); + + entityManager.persist(member1); + entityManager.persist(member2); + entityManager.flush(); + entityManager.clear(); + } + + @Test + void 인증된_멤버가_자신의_멤버정보를_조회한다() { + // given + AuthMember authMember = new AuthMember(member1.getId()); + + // when + MemberResponse foundMember = memberService.findOauthProfile(authMember); + + //then + assertSoftly(softly -> { + assertThat(foundMember.memberId()).isEqualTo(member1.getId()); + assertThat(foundMember.name()).isEqualTo(member1.getName()); + assertThat(foundMember.email()).isEqualTo(member1.getEmail()); + assertThat(foundMember.imageUrl()).isEqualTo(member1.getImageUrl()); + assertThat(foundMember.loginType()).isEqualTo( + member1.getLoginType().name().toLowerCase()); + }); + } +} diff --git a/backend/src/test/java/harustudy/backend/member/service/MemberServiceV2Test.java b/backend/src/test/java/harustudy/backend/member/service/MemberServiceV2Test.java deleted file mode 100644 index 39424380..00000000 --- a/backend/src/test/java/harustudy/backend/member/service/MemberServiceV2Test.java +++ /dev/null @@ -1,123 +0,0 @@ -package harustudy.backend.member.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import harustudy.backend.content.domain.PomodoroContent; -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.dto.GuestRegisterRequest; -import harustudy.backend.member.dto.MemberResponseV2; -import harustudy.backend.member.dto.MembersResponseV2; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.room.domain.PomodoroRoom; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import java.util.List; -import org.assertj.core.api.Condition; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@SpringBootTest -@Transactional -class MemberServiceV2Test { - - @PersistenceContext - EntityManager entityManager; - @Autowired - MemberServiceV2 memberServiceV2; - - @Test - void 멤버_아이디로_멤버_엔티티를_조회한다() { - // given - Member member = new Member("member1"); - entityManager.persist(member); - entityManager.flush(); - entityManager.clear(); - - // when - MemberResponseV2 response = memberServiceV2.findMember(member.getId()); - - // then - assertSoftly(softly -> { - assertThat(response.id()).isEqualTo(member.getId()); - assertThat(response.nickname()).isEqualTo(member.getNickname()); - }); - } - - @Test - void 방_아이디로_해당_방에_참여한_멤버들을_모두_조회한다() { - // given - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom room = - new PomodoroRoom("roomName", 1, 20, participantCode); - - Member member1 = new Member("member1"); - Member member2 = new Member("member2"); - PomodoroProgress pomodoroProgress1 = new PomodoroProgress(room, member1); - PomodoroProgress pomodoroProgress2 = new PomodoroProgress(room, member2); - - entityManager.persist(participantCode); - entityManager.persist(room); - entityManager.persist(member1); - entityManager.persist(member2); - entityManager.persist(pomodoroProgress1); - entityManager.persist(pomodoroProgress2); - - entityManager.flush(); - entityManager.clear(); - - // when - MembersResponseV2 response = memberServiceV2.findParticipatedMembers(room.getId()); - - // then - Condition condition = new Condition<>( - s -> s.equals(member1.getNickname()) || s.equals(member2.getNickname()), - "member1의 nickname과 같거나 member2의 nickname과 같다" - ); - - assertSoftly(softly -> { - assertThat(response.members().size()).isEqualTo(2); - assertThat(response.members().get(0).nickname()).is(condition); - assertThat(response.members().get(1).nickname()).is(condition); - }); - } - - @Test - void 스터디_아이디와_닉네임으로_스터디에_멤버를_등록한다() { - // given - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom room = - new PomodoroRoom("roomName", 3, 20, participantCode); - entityManager.persist(participantCode); - entityManager.persist(room); - GuestRegisterRequest request = new GuestRegisterRequest(room.getId(), "nickname"); - - // when - Long memberId = memberServiceV2.register(request); - - // then - entityManager.flush(); - entityManager.clear(); - - Member registeredMember = entityManager.find(Member.class, memberId); - PomodoroRoom registeredPomodoroRoom = entityManager.find(PomodoroRoom.class, room.getId()); - List pomodoroProgresses = registeredPomodoroRoom.getPomodoroProgresses(); - PomodoroProgress registeredPomodoroProgress = pomodoroProgresses.get(0); - List pomodoroContents = registeredPomodoroProgress.getPomodoroContents(); - - assertSoftly(softly -> { - assertThat(registeredMember.getNickname()).isEqualTo(request.nickname()); - assertThat(registeredPomodoroRoom.getName()).isEqualTo(room.getName()); - assertThat(pomodoroProgresses.size()).isEqualTo(1); - assertThat(pomodoroContents.size()).isEqualTo(3); - }); - } -} diff --git a/backend/src/test/java/harustudy/backend/participantcode/domain/CodeGenerationStrategyTest.java b/backend/src/test/java/harustudy/backend/participantcode/domain/CodeGenerationStrategyTest.java index 4ad3e4ee..6aeb2b72 100644 --- a/backend/src/test/java/harustudy/backend/participantcode/domain/CodeGenerationStrategyTest.java +++ b/backend/src/test/java/harustudy/backend/participantcode/domain/CodeGenerationStrategyTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import harustudy.backend.room.domain.CodeGenerationStrategy; +import harustudy.backend.room.domain.GenerationStrategy; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/harustudy/backend/participantcode/domain/ParticipantCodeTest.java b/backend/src/test/java/harustudy/backend/participantcode/domain/ParticipantCodeTest.java index 8452a768..5ae1598d 100644 --- a/backend/src/test/java/harustudy/backend/participantcode/domain/ParticipantCodeTest.java +++ b/backend/src/test/java/harustudy/backend/participantcode/domain/ParticipantCodeTest.java @@ -3,6 +3,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import harustudy.backend.room.domain.CodeGenerationStrategy; +import harustudy.backend.room.domain.GenerationStrategy; +import harustudy.backend.room.domain.ParticipantCode; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/harustudy/backend/participantcode/service/ParticipantCodeServiceTest.java b/backend/src/test/java/harustudy/backend/participantcode/service/ParticipantCodeServiceTest.java deleted file mode 100644 index b419155d..00000000 --- a/backend/src/test/java/harustudy/backend/participantcode/service/ParticipantCodeServiceTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package harustudy.backend.participantcode.service; - -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 harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.participantcode.dto.FindRoomAndNicknameResponse; -import harustudy.backend.participantcode.dto.FindRoomResponse; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.room.domain.PomodoroRoom; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@SpringBootTest -@Transactional -class ParticipantCodeServiceTest { - - @Autowired - private ParticipantCodeService participantCodeService; - @PersistenceContext - private EntityManager entityManager; - - @Test - void 기존_참여멤버의_참여코드를_인증하고_스터디와_멤버_정보를_반환한다() { - // given - ParticipantCode code = new ParticipantCode(new CodeGenerationStrategy()); - entityManager.persist(code); - - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 3, 20, code); - entityManager.persist(pomodoroRoom); - - Member member = new Member("nickname"); - entityManager.persist(member); - - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); - entityManager.persist(pomodoroProgress); - - entityManager.flush(); - entityManager.clear(); - // when - FindRoomAndNicknameResponse response = participantCodeService.findRoomByCodeWithMemberId( - code.getCode(), member.getId()); - - // then - assertAll( - () -> assertThat(response.studyName()).isEqualTo("roomName"), - () -> assertThat(response.nickname()).isEqualTo("nickname") - ); - } - - @Test - void 신규멤버의_참여코드를_인증하고_스터디_정보를_반환한다() { - // given - ParticipantCode code = new ParticipantCode(new CodeGenerationStrategy()); - entityManager.persist(code); - - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 3, 20, code); - entityManager.persist(pomodoroRoom); - - // when - FindRoomResponse response = participantCodeService.findRoomByCode( - code.getCode()); - - // then - assertThat(response.studyName()).isEqualTo("roomName"); - } - - @Test - void 참여코드가_없으면_예외를_던진다() { - // given, when, then - assertThatThrownBy(() -> participantCodeService.findRoomByCodeWithMemberId("FFFFFF", - 1L)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - void 멤버_아이디가_유효하지_않으면_예외를_던진다() { - // given - ParticipantCode code = new ParticipantCode(new CodeGenerationStrategy()); - entityManager.persist(code); - - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 3, 20, code); - entityManager.persist(pomodoroRoom); - - // when & then - assertThatThrownBy(() -> participantCodeService.findRoomByCodeWithMemberId(code.getCode(), - 1L)) - .isInstanceOf(IllegalArgumentException.class); - } -} diff --git a/backend/src/test/java/harustudy/backend/progress/domain/PomodoroProgressTest.java b/backend/src/test/java/harustudy/backend/progress/domain/PomodoroProgressTest.java index bd7178c4..dd858331 100644 --- a/backend/src/test/java/harustudy/backend/progress/domain/PomodoroProgressTest.java +++ b/backend/src/test/java/harustudy/backend/progress/domain/PomodoroProgressTest.java @@ -1,54 +1,97 @@ package harustudy.backend.progress.domain; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.SoftAssertions.assertSoftly; import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.progress.exception.NicknameLengthException; +import harustudy.backend.room.domain.CodeGenerationStrategy; +import harustudy.backend.room.domain.ParticipantCode; import harustudy.backend.room.domain.PomodoroRoom; +import java.util.stream.IntStream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(ReplaceUnderscores.class) class PomodoroProgressTest { + private PomodoroRoom pomodoroRoom; + private Member member; + + @BeforeEach + void setUp() { + ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); + pomodoroRoom = new PomodoroRoom("room", 3, 25, participantCode); + member = Member.guest(); + } + + @ParameterizedTest + @ValueSource(strings = {"", "12345678901"}) + void 멤버의_닉네임이_1자_미만_10자_초과이면_예외를_던진다(String nickname) { + // given, when, then + assertThatThrownBy(() -> new PomodoroProgress(pomodoroRoom, member, nickname)) + .isInstanceOf(NicknameLengthException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"1", "1234567890"}) + void 멤버의_닉네임이_1자_이상_10자_이하이면_정상_케이스이다(String nickname) { + // given, when, then + assertThatCode(() -> new PomodoroProgress(pomodoroRoom, member, nickname)) + .doesNotThrowAnyException(); + } + @Test - void 학습_이후_동일_사이클에_회고_단계로_넘어간다() { + void 닉네임이_동일한지_확인할_수_있다() { // given - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom room = new PomodoroRoom("room", 3, 25, participantCode); - Member member = new Member("nickname"); - PomodoroProgress pomodoroProgress = new PomodoroProgress(room, member); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + PomodoroProgress otherProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); - // when - int notDoneStatusCount = 3; - for (int i = 0; i < notDoneStatusCount - 1; i++) { - pomodoroProgress.proceedV2(); - } + // when, then + assertThat(pomodoroProgress.hasSameNicknameWith(otherProgress)).isTrue(); + } + + @Test + void 닉네임이_다른지_확인할_수_있다() { + // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + PomodoroProgress otherProgress = new PomodoroProgress(pomodoroRoom, member, "another"); + + // when, then + assertThat(pomodoroProgress.hasSameNicknameWith(otherProgress)).isFalse(); + } + + @Test + void 다음_스터디_단계로_넘어갈_수_있다() { + // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + + // given + pomodoroProgress.proceed(); // then assertSoftly(softly -> { softly.assertThat(pomodoroProgress.getCurrentCycle()).isEqualTo(1); softly.assertThat(pomodoroProgress.getPomodoroStatus()) - .isEqualTo(PomodoroStatus.RETROSPECT); + .isEqualTo(PomodoroStatus.STUDYING); }); } @Test void 마지막_사이클이_아니라면_회고_종료_후_사이클_수가_증가한다() { // given - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom room = new PomodoroRoom("room", 3, 25, participantCode); - Member member = new Member("nickname"); - PomodoroProgress pomodoroProgress = new PomodoroProgress(room, member); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); // when - int notDoneStatusCount = 3; - for (int i = 0; i < notDoneStatusCount; i++) { - pomodoroProgress.proceedV2(); - } + int statusCountPerCycle = 3; + + IntStream.range(0, statusCountPerCycle) + .forEach(i -> pomodoroProgress.proceed()); // then assertSoftly(softly -> { @@ -58,23 +101,20 @@ class PomodoroProgressTest { }); } + @Test void 마지막_사이클이라면_회고_이후_사이클은_그대로이며_종료_상태로_넘어간다() { // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + + int statusCountPerCycle = 3; int totalCycle = 3; - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom room = new PomodoroRoom("room", 3, 25, participantCode); - Member member = new Member("nickname"); - PomodoroProgress pomodoroProgress = new PomodoroProgress(room, member); + + IntStream.range(0, statusCountPerCycle * totalCycle) + .forEach(i -> pomodoroProgress.proceed()); // when - // 마지막 사이클의 회고 상태까지 진행시킨다 - int notDoneStatusCount = 3; - for (int i = 0; i < totalCycle * notDoneStatusCount; i++) { - pomodoroProgress.proceedV2(); - } - // 완료 상태로 넘어간다 - pomodoroProgress.proceedV2(); + pomodoroProgress.proceed(); // then assertSoftly(softly -> { diff --git a/backend/src/test/java/harustudy/backend/progress/repository/PomodoroProgressRepositoryTest.java b/backend/src/test/java/harustudy/backend/progress/repository/PomodoroProgressRepositoryTest.java index af25b0b0..85488d62 100644 --- a/backend/src/test/java/harustudy/backend/progress/repository/PomodoroProgressRepositoryTest.java +++ b/backend/src/test/java/harustudy/backend/progress/repository/PomodoroProgressRepositoryTest.java @@ -1,125 +1,125 @@ -package harustudy.backend.progress.repository; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.participantcode.repository.ParticipantCodeRepository; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.repository.PomodoroRoomRepository; -import jakarta.persistence.PersistenceUtil; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@DataJpaTest -class PomodoroProgressRepositoryTest { - - @Autowired - private PomodoroProgressRepository pomodoroProgressRepository; - @Autowired - private PomodoroRoomRepository pomodoroRoomRepository; - @Autowired - private MemberRepository memberRepository; - @Autowired - private ParticipantCodeRepository participantCodeRepository; - - @Autowired - private TestEntityManager testEntityManager; - - @Test - void room과_member를_통해_pomodoroProgress를_조회한다() { - // given - Member member = new Member("member"); - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); - memberRepository.save(member); - participantCodeRepository.save(participantCode); - pomodoroRoomRepository.save(pomodoroRoom); - - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); - pomodoroProgressRepository.save(pomodoroProgress); - - testEntityManager.flush(); - testEntityManager.clear(); - - // when - Optional found = pomodoroProgressRepository.findByPomodoroRoomAndMember( - pomodoroRoom, member); - - // then - assertThat(found).isPresent(); - assertThat(found.get().getId()).isEqualTo(pomodoroProgress.getId()); - } - - @Test - void room으로_pomodoroProgress_리스트를_조회한다() { - // given - Member member1 = new Member("member1"); - Member member2 = new Member("member2"); - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); - memberRepository.save(member1); - memberRepository.save(member2); - participantCodeRepository.save(participantCode); - pomodoroRoomRepository.save(pomodoroRoom); - - PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroRoom, member1); - PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroRoom, member2); - pomodoroProgressRepository.save(pomodoroProgress1); - pomodoroProgressRepository.save(pomodoroProgress2); - - // when - List pomodoroProgresses = pomodoroProgressRepository.findAllByPomodoroRoom( - pomodoroRoom); - - // then - assertThat(pomodoroProgresses.size()).isEqualTo(2); - } - - @Test - void room으로_pomodoroProgress와_member를_함께_조회한다() { - // given - Member member1 = new Member("member1"); - Member member2 = new Member("member2"); - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); - memberRepository.save(member1); - memberRepository.save(member2); - participantCodeRepository.save(participantCode); - pomodoroRoomRepository.save(pomodoroRoom); - - PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroRoom, member1); - PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroRoom, member2); - pomodoroProgressRepository.save(pomodoroProgress1); - pomodoroProgressRepository.save(pomodoroProgress2); - - testEntityManager.flush(); - testEntityManager.clear(); - - PersistenceUtil persistenceUtil = testEntityManager.getEntityManager() - .getEntityManagerFactory().getPersistenceUnitUtil(); - - // when - List progresses = pomodoroProgressRepository.findAllByPomodoroRoomFetchMember( - pomodoroRoom); - - // then - assertSoftly(softly -> { - softly.assertThat(progresses).hasSize(2); - softly.assertThat(persistenceUtil.isLoaded(member1)).isTrue(); - softly.assertThat(persistenceUtil.isLoaded(member2)).isTrue(); - }); - } -} +//package harustudy.backend.progress.repository; +// +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.SoftAssertions.assertSoftly; +// +//import harustudy.backend.member.domain.Member; +//import harustudy.backend.member.repository.MemberRepository; +//import harustudy.backend.room.domain.CodeGenerationStrategy; +//import harustudy.backend.room.domain.ParticipantCode; +//import harustudy.backend.room.repository.ParticipantCodeRepository; +//import harustudy.backend.progress.domain.PomodoroProgress; +//import harustudy.backend.room.domain.PomodoroRoom; +//import harustudy.backend.room.repository.PomodoroRoomRepository; +//import jakarta.persistence.PersistenceUtil; +//import java.util.List; +//import java.util.Optional; +//import org.junit.jupiter.api.DisplayNameGeneration; +//import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +//import org.junit.jupiter.api.Test; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +//import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +// +//@SuppressWarnings("NonAsciiCharacters") +//@DisplayNameGeneration(ReplaceUnderscores.class) +//@DataJpaTest +//class PomodoroProgressRepositoryTest { +// +// @Autowired +// private PomodoroProgressRepository pomodoroProgressRepository; +// @Autowired +// private PomodoroRoomRepository pomodoroRoomRepository; +// @Autowired +// private MemberRepository memberRepository; +// @Autowired +// private ParticipantCodeRepository participantCodeRepository; +// +// @Autowired +// private TestEntityManager testEntityManager; +// +// @Test +// void room과_member를_통해_pomodoroProgress를_조회한다() { +// // given +// Member member = new Member("member"); +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); +// memberRepository.save(member); +// participantCodeRepository.save(participantCode); +// pomodoroRoomRepository.save(pomodoroRoom); +// +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); +// pomodoroProgressRepository.save(pomodoroProgress); +// +// testEntityManager.flush(); +// testEntityManager.clear(); +// +// // when +// Optional found = pomodoroProgressRepository.findByPomodoroRoomAndMember( +// pomodoroRoom, member); +// +// // then +// assertThat(found).isPresent(); +// assertThat(found.get().getId()).isEqualTo(pomodoroProgress.getId()); +// } +// +// @Test +// void room으로_pomodoroProgress_리스트를_조회한다() { +// // given +// Member member1 = new Member("member1"); +// Member member2 = new Member("member2"); +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); +// memberRepository.save(member1); +// memberRepository.save(member2); +// participantCodeRepository.save(participantCode); +// pomodoroRoomRepository.save(pomodoroRoom); +// +// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroRoom, member1); +// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroRoom, member2); +// pomodoroProgressRepository.save(pomodoroProgress1); +// pomodoroProgressRepository.save(pomodoroProgress2); +// +// // when +// List pomodoroProgresses = pomodoroProgressRepository.findAllByPomodoroRoom( +// pomodoroRoom); +// +// // then +// assertThat(pomodoroProgresses.size()).isEqualTo(2); +// } +// +// @Test +// void room으로_pomodoroProgress와_member를_함께_조회한다() { +// // given +// Member member1 = new Member("member1"); +// Member member2 = new Member("member2"); +// ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); +// PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); +// memberRepository.save(member1); +// memberRepository.save(member2); +// participantCodeRepository.save(participantCode); +// pomodoroRoomRepository.save(pomodoroRoom); +// +// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroRoom, member1); +// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroRoom, member2); +// pomodoroProgressRepository.save(pomodoroProgress1); +// pomodoroProgressRepository.save(pomodoroProgress2); +// +// testEntityManager.flush(); +// testEntityManager.clear(); +// +// PersistenceUtil persistenceUtil = testEntityManager.getEntityManager() +// .getEntityManagerFactory().getPersistenceUnitUtil(); +// +// // when +// List progresses = pomodoroProgressRepository.findAllByPomodoroRoomFetchMember( +// pomodoroRoom); +// +// // then +// assertSoftly(softly -> { +// softly.assertThat(progresses).hasSize(2); +// softly.assertThat(persistenceUtil.isLoaded(member1)).isTrue(); +// softly.assertThat(persistenceUtil.isLoaded(member2)).isTrue(); +// }); +// } +//} diff --git a/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceTest.java b/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceTest.java new file mode 100644 index 00000000..1f0f906c --- /dev/null +++ b/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceTest.java @@ -0,0 +1,207 @@ +package harustudy.backend.progress.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; + +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.exception.AuthorizationException; +import harustudy.backend.member.domain.Member; +import harustudy.backend.progress.domain.PomodoroProgress; +import harustudy.backend.progress.domain.PomodoroStatus; +import harustudy.backend.progress.dto.ParticipateStudyRequest; +import harustudy.backend.progress.dto.PomodoroProgressResponse; +import harustudy.backend.progress.dto.PomodoroProgressesResponse; +import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; +import harustudy.backend.room.domain.CodeGenerationStrategy; +import harustudy.backend.room.domain.ParticipantCode; +import harustudy.backend.room.domain.PomodoroRoom; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@SpringBootTest +@Transactional +public class PomodoroProgressServiceTest { + + @Autowired + private PomodoroProgressService pomodoroProgressService; + + @PersistenceContext + private EntityManager entityManager; + + private PomodoroRoom pomodoroRoom1; + private PomodoroRoom pomodoroRoom2; + private Member member1; + private Member member2; + + @BeforeEach + void setUp() { + ParticipantCode participantCode1 = new ParticipantCode(new CodeGenerationStrategy()); + ParticipantCode participantCode2 = new ParticipantCode(new CodeGenerationStrategy()); + pomodoroRoom1 = new PomodoroRoom("roomName1", 3, 20, participantCode1); + pomodoroRoom2 = new PomodoroRoom("roomName2", 3, 20, participantCode2); + member1 = Member.guest(); + member2 = Member.guest(); + + entityManager.persist(participantCode1); + entityManager.persist(participantCode2); + entityManager.persist(pomodoroRoom1); + entityManager.persist(pomodoroRoom2); + entityManager.persist(member1); + entityManager.persist(member2); + + entityManager.flush(); + entityManager.clear(); + } + + @Test + void 진행도를_조회할_수_있다() { + // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom2, member1, "nickname1"); + AuthMember authMember = new AuthMember(member1.getId()); + + entityManager.persist(pomodoroProgress); + + // when + PomodoroProgressResponse response = + pomodoroProgressService.findPomodoroProgress(authMember, pomodoroRoom2.getId(), pomodoroProgress.getId()); + PomodoroProgressResponse expected = PomodoroProgressResponse.from(pomodoroProgress); + + // then + assertThat(response).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + + @Test + void 스터디의_모든_진행도를_조회할_수_있다() { + // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom2, member1, "nickname1"); + PomodoroProgress anotherPomodoroProgress = new PomodoroProgress(pomodoroRoom2, member2, "nickname2"); + AuthMember authMember1 = new AuthMember(member1.getId()); + + entityManager.persist(pomodoroProgress); + entityManager.persist(anotherPomodoroProgress); + + // when + PomodoroProgressesResponse response = + pomodoroProgressService.findPomodoroProgressWithFilter(authMember1, pomodoroRoom2.getId(), null); + PomodoroProgressesResponse expected = PomodoroProgressesResponse.from(List.of( + PomodoroProgressResponse.from(pomodoroProgress), + PomodoroProgressResponse.from(anotherPomodoroProgress) + )); + + // then + assertThat(response).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + + @Test + void 참여하지_않은_스터디에_대해서는_모든_진행도를_조회할_수_없다() { + // given + AuthMember authMember = new AuthMember(member1.getId()); + + // when, then + assertThatThrownBy(() -> + pomodoroProgressService.findPomodoroProgressWithFilter(authMember, pomodoroRoom2.getId(), null)) + .isInstanceOf(AuthorizationException.class); + } + + @Test + void 특정_멤버의_진행도를_조회할_수_있다() { + // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom2, member1, "nickname1"); + PomodoroProgress anotherPomodoroProgress = new PomodoroProgress(pomodoroRoom2, member2, "nickname2"); + AuthMember authMember1 = new AuthMember(member1.getId()); + + entityManager.persist(pomodoroProgress); + entityManager.persist(anotherPomodoroProgress); + + // when + PomodoroProgressesResponse response = + pomodoroProgressService.findPomodoroProgressWithFilter(authMember1, pomodoroRoom2.getId(), member1.getId()); + PomodoroProgressesResponse expected = PomodoroProgressesResponse.from(List.of( + PomodoroProgressResponse.from(pomodoroProgress) + )); + + // then + assertThat(response).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + + @Test + void 자신의_소유가_아닌_진행도는_조회할_수_없다() { + // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom2, member1, "nickname1"); + AuthMember authMember = new AuthMember(member2.getId()); + + entityManager.persist(pomodoroProgress); + + // when, then + assertThatThrownBy(() -> + pomodoroProgressService.findPomodoroProgress(authMember, pomodoroRoom2.getId(), pomodoroProgress.getId())) + .isInstanceOf(AuthorizationException.class); + } + + @Test + void 해당_스터디의_진행도가_아니라면_조회할_수_없다() { + // given + PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroRoom1, member1, "nickname1"); + PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroRoom2, member1, "nickname1"); + AuthMember authMember = new AuthMember(member1.getId()); + + entityManager.persist(pomodoroProgress1); + entityManager.persist(pomodoroProgress2); + + // when, then + assertThatThrownBy(() -> + pomodoroProgressService.findPomodoroProgress(authMember, pomodoroRoom1.getId(), pomodoroProgress2.getId())) + .isInstanceOf(ProgressNotBelongToRoomException.class); + } + + @Test + void 다음_스터디_단계로_이동할_수_있다() { + // given + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom2, member1, "nickname1"); + AuthMember authMember1 = new AuthMember(member1.getId()); + + entityManager.persist(pomodoroProgress); + + // when + pomodoroProgressService.proceed(authMember1, pomodoroRoom2.getId(), pomodoroProgress.getId()); + + // then + assertThat(pomodoroProgress.getPomodoroStatus()).isEqualTo(PomodoroStatus.STUDYING); + } + + @Test + void 스터디에_참여하면_진행도가_생긴다() { + // given + AuthMember authMember1 = new AuthMember(member1.getId()); + + // when + ParticipateStudyRequest request = new ParticipateStudyRequest(member1.getId(), "nick"); + Long progressId = pomodoroProgressService.participateStudy(authMember1, pomodoroRoom2.getId(), request); + + // then + PomodoroProgress pomodoroProgress = entityManager.find(PomodoroProgress.class, progressId); + assertSoftly(softly -> { + assertThat(pomodoroProgress.getNickname()).isEqualTo(request.nickname()); + assertThat(pomodoroProgress.getMember().getId()).isEqualTo(request.memberId()); + assertThat(pomodoroProgress.getCurrentCycle()).isEqualTo(1); + assertThat(pomodoroProgress.getPomodoroStatus()).isEqualTo(PomodoroStatus.PLANNING); + }); + } +} diff --git a/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceV2Test.java b/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceV2Test.java deleted file mode 100644 index ecc05007..00000000 --- a/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceV2Test.java +++ /dev/null @@ -1,78 +0,0 @@ -package harustudy.backend.progress.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import harustudy.backend.member.domain.Member; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.progress.domain.PomodoroStatus; -import harustudy.backend.progress.dto.PomodoroProgressResponseV2; -import harustudy.backend.room.domain.PomodoroRoom; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@SpringBootTest -@Transactional -class PomodoroProgressServiceV2Test { - - @Autowired - private PomodoroProgressServiceV2 pomodoroProgressService; - @PersistenceContext - private EntityManager entityManager; - - private PomodoroRoom pomodoroRoom; - private Member member; - private PomodoroProgress pomodoroProgress; - - @BeforeEach - void setUp() { - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - pomodoroRoom = new PomodoroRoom("roomName", 3, 20, participantCode); - member = new Member("name"); - pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); - - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - entityManager.persist(member); - entityManager.persist(pomodoroProgress); - - entityManager.flush(); - entityManager.clear(); - } - - @Test - void 특정_방에_속하는_멤버의_진행도를_조회한다() { - // when - PomodoroProgressResponseV2 progressResponse = pomodoroProgressService.findProgress( - pomodoroRoom.getId(), member.getId()); - - // then - SoftAssertions.assertSoftly(softly -> { - softly.assertThat(progressResponse.currentCycle()).isEqualTo(1); - softly.assertThat(progressResponse.step()).isEqualTo("planning"); - }); - } - - @Test - void 특정_방에_속하는_진행도를_진행시킨다() { - // when - pomodoroProgressService.proceed(pomodoroRoom.getId(), pomodoroProgress.getId()); - PomodoroProgressResponseV2 foundProgress = pomodoroProgressService.findProgress( - pomodoroRoom.getId(), member.getId()); - - // then - assertThat(foundProgress.step()).isEqualTo( - PomodoroStatus.STUDYING.toString().toLowerCase()); - } -} diff --git a/backend/src/test/java/harustudy/backend/room/domain/PomodoroRoomTest.java b/backend/src/test/java/harustudy/backend/room/domain/PomodoroRoomTest.java index be9e20b6..9f5bfde6 100644 --- a/backend/src/test/java/harustudy/backend/room/domain/PomodoroRoomTest.java +++ b/backend/src/test/java/harustudy/backend/room/domain/PomodoroRoomTest.java @@ -3,8 +3,6 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; import harustudy.backend.room.exception.PomodoroTimePerCycleException; import harustudy.backend.room.exception.PomodoroTotalCycleException; import harustudy.backend.room.exception.PomodoroRoomNameLengthException; diff --git a/backend/src/test/java/harustudy/backend/room/service/CreatePomodoroPomodoroRoomServiceTest.java b/backend/src/test/java/harustudy/backend/room/service/CreatePomodoroPomodoroRoomServiceTest.java deleted file mode 100644 index a84b2809..00000000 --- a/backend/src/test/java/harustudy/backend/room/service/CreatePomodoroPomodoroRoomServiceTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package harustudy.backend.room.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; - -import harustudy.backend.participantcode.domain.GenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.participantcode.repository.ParticipantCodeRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.dto.CreatePomodoroRoomDto; -import harustudy.backend.room.dto.CreatePomodoroRoomRequest; -import harustudy.backend.room.repository.PomodoroRoomRepository; -import java.util.Optional; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@Deprecated -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@ExtendWith(MockitoExtension.class) -class CreatePomodoroPomodoroRoomServiceTest { - - @Mock - private PomodoroRoomRepository pomodoroRoomRepository; - @Mock - private ParticipantCodeRepository participantCodeRepository; - @Mock - private GenerationStrategy generationStrategy; - @InjectMocks - private PomodoroRoomService pomodoroRoomService; - - @Deprecated - @Test - void 스터디_개설시_참여코드가_생성된다() { - // given - CreatePomodoroRoomRequest request = new CreatePomodoroRoomRequest("roomName", 1, 20); - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, - new ParticipantCode(generationStrategy)); - - String code = "ABCDEF"; - - given(pomodoroRoomRepository.save(any(PomodoroRoom.class))).willReturn(pomodoroRoom); - given(participantCodeRepository.findByCode(anyString())).willReturn(Optional.empty()); - given(generationStrategy.generate()).willReturn(code); - - // when - CreatePomodoroRoomDto response = pomodoroRoomService.createPomodoroRoom(request); - - // then - assertThat(response.participantCode()).isEqualTo(code); - } - - @Deprecated - @Test - void 중복된_참여코드로_스터디가_생성되면_참여_코드가_재생성된다() { - // given - CreatePomodoroRoomRequest request = new CreatePomodoroRoomRequest("roomName", 1, 20); - PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, - new ParticipantCode(generationStrategy)); - ParticipantCode participantCode = new ParticipantCode(generationStrategy); - - String originalCode = "ABCDEF"; - String regeneratedCode = "BCDEFG"; - - given(pomodoroRoomRepository.save(any(PomodoroRoom.class))).willReturn(pomodoroRoom); - given(participantCodeRepository.findByCode(anyString())) - .willReturn(Optional.of(participantCode)) - .willReturn(Optional.empty()); - given(generationStrategy.generate()) - .willReturn(originalCode, regeneratedCode); - - // when - CreatePomodoroRoomDto response = pomodoroRoomService.createPomodoroRoom(request); - - // then - assertThat(response.participantCode()).isNotEqualTo(originalCode); - } -} diff --git a/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceTest.java b/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceTest.java index b93272ec..551ab4a2 100644 --- a/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceTest.java +++ b/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceTest.java @@ -1,18 +1,20 @@ package harustudy.backend.room.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.participantcode.domain.CodeGenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.member.exception.MemberNotFoundException; +import harustudy.backend.room.domain.GenerationStrategy; +import harustudy.backend.room.domain.ParticipantCode; import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.exception.DuplicatedNicknameException; -import harustudy.backend.room.repository.PomodoroRoomRepository; +import harustudy.backend.room.dto.CreatePomodoroRoomRequest; +import harustudy.backend.room.dto.CreatePomodoroRoomResponse; +import harustudy.backend.room.dto.PomodoroRoomResponse; +import harustudy.backend.room.dto.PomodoroRoomsResponse; +import harustudy.backend.room.exception.ParticipantCodeNotFoundException; +import harustudy.backend.room.exception.RoomNotFoundException; import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -20,68 +22,135 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; -@Deprecated @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(ReplaceUnderscores.class) -@SpringBootTest @Transactional +@SpringBootTest class PomodoroRoomServiceTest { - @PersistenceContext - private EntityManager entityManager; @Autowired private PomodoroRoomService pomodoroRoomService; + @Autowired - private MemberRepository memberRepository; + private EntityManager entityManager; + @Autowired - private PomodoroRoomRepository pomodoroRoomRepository; + private GenerationStrategy generationStrategy; - private ParticipantCode participantCode; - private PomodoroRoom pomodoroRoom; - private Member member; + @Test + void 룸_아이디로_룸을_조회한다() { + // given + ParticipantCode participantCode = new ParticipantCode(generationStrategy); + PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 20, participantCode); + entityManager.persist(participantCode); + entityManager.persist(pomodoroRoom); - @BeforeEach - void setUp() { - participantCode = new ParticipantCode(new CodeGenerationStrategy()); - pomodoroRoom = new PomodoroRoom("roomName", 3, 20, participantCode); - member = new Member("name"); + entityManager.flush(); + entityManager.clear(); + // when + PomodoroRoomResponse result = pomodoroRoomService.findPomodoroRoom(pomodoroRoom.getId()); + + // then + assertAll( + () -> assertThat(result.studyId()).isEqualTo(pomodoroRoom.getId()), + () -> assertThat(result.name()).isEqualTo(pomodoroRoom.getName()), + () -> assertThat(result.totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), + () -> assertThat(result.timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) + ); + } + + @Test + void 룸_아이디로_룸_조회시_없으면_예외를_던진다() { + // given + ParticipantCode participantCode = new ParticipantCode(generationStrategy); + PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 20, participantCode); entityManager.persist(participantCode); entityManager.persist(pomodoroRoom); - entityManager.persist(member); entityManager.flush(); entityManager.clear(); + + // when, then + assertThatThrownBy(() -> pomodoroRoomService.findPomodoroRoomWithFilter(99999L, null), null) + .isInstanceOf(MemberNotFoundException.class); } - @Deprecated @Test - void 닉네임을_받아_신규_멤버를_생성하고_스터디에_등록한다() { + void 룸을_생성한다() { + // given + CreatePomodoroRoomRequest request = new CreatePomodoroRoomRequest("room", 8, 40); + // when - Long participatedMemberId = pomodoroRoomService.participate(pomodoroRoom.getId(), - member.getNickname()); + CreatePomodoroRoomResponse result = pomodoroRoomService.createPomodoroRoom(request); + + // then + assertAll( + () -> assertThat(result.studyId()).isNotNull(), + () -> assertThat(result.participantCode()).isNotNull() + ); + } + + @Test + void 참여코드에_해당하는_룸을_조회한다() { + // given + ParticipantCode participantCode = new ParticipantCode(generationStrategy); + PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 40, participantCode); + entityManager.persist(participantCode); + entityManager.persist(pomodoroRoom); entityManager.flush(); entityManager.clear(); + // when + PomodoroRoomsResponse result = pomodoroRoomService.findPomodoroRoomWithFilter(null, + participantCode.getCode()); + // then - Member member = memberRepository.findById(participatedMemberId).get(); - PomodoroRoom foundRoom = pomodoroRoomRepository.findById(pomodoroRoom.getId()).get(); + assertAll( + () -> assertThat(result.studies()).hasSize(1), + () -> assertThat(result.studies().get(0).name()).isEqualTo(pomodoroRoom.getName()), + () -> assertThat(result.studies().get(0).totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), + () -> assertThat(result.studies().get(0).timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) + ); + } - assertThat(foundRoom.isParticipatedMember(member)).isTrue(); + @Test + void 참여코드에_해당하는_룸_조회시_참여코드가_없으면_예외를_던진다() { + // given + ParticipantCode participantCode = new ParticipantCode(generationStrategy); + PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 40, participantCode); + entityManager.persist(participantCode); + entityManager.persist(pomodoroRoom); + + entityManager.flush(); + entityManager.clear(); + + ParticipantCode notPersisted = new ParticipantCode(generationStrategy); + + // when, then + assertThatThrownBy( + () -> pomodoroRoomService.findPomodoroRoomWithFilter(null, notPersisted.getCode())) + .isInstanceOf(ParticipantCodeNotFoundException.class); } - @Deprecated @Test - void 한_스터디_내에서_닉네임이_중복되면_예외를_던진다() { + void 참여코드에_해당하는_룸_조회시_참여코드에_해당하는_룸이_없으면_예외를_던진다() { // given - pomodoroRoomService.participate(pomodoroRoom.getId(), member.getNickname()); + ParticipantCode participantCode = new ParticipantCode(generationStrategy); + PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 40, participantCode); + entityManager.persist(participantCode); + entityManager.persist(pomodoroRoom); + + ParticipantCode notRoomsCode = new ParticipantCode(generationStrategy); + entityManager.persist(notRoomsCode); entityManager.flush(); entityManager.clear(); - // when - assertThatThrownBy(() -> pomodoroRoomService.participate(pomodoroRoom.getId(), member.getNickname())) - .isInstanceOf(DuplicatedNicknameException.class); + // when, then + assertThatThrownBy( + () -> pomodoroRoomService.findPomodoroRoomWithFilter(null, notRoomsCode.getCode())) + .isInstanceOf(RoomNotFoundException.class); } } diff --git a/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceV2Test.java b/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceV2Test.java deleted file mode 100644 index b6f1254c..00000000 --- a/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceV2Test.java +++ /dev/null @@ -1,149 +0,0 @@ -package harustudy.backend.room.service; - -import harustudy.backend.common.EntityNotFoundException.ParticipantCodeNotFound; -import harustudy.backend.common.EntityNotFoundException.RoomNotFound; -import harustudy.backend.participantcode.domain.GenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.dto.CreatePomodoroRoomDto; -import harustudy.backend.room.dto.CreatePomodoroRoomRequest; -import harustudy.backend.room.dto.PomodoroRoomResponseV2; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@Transactional -@SpringBootTest -class PomodoroRoomServiceV2Test { - - @Autowired - private PomodoroRoomServiceV2 pomodoroRoomServiceV2; - - @Autowired - private EntityManager entityManager; - - @Autowired - private GenerationStrategy generationStrategy; - - @Test - void 룸_아이디로_룸을_조회한다() { - // given - ParticipantCode participantCode = new ParticipantCode(generationStrategy); - PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 20, participantCode); - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - - entityManager.flush(); - entityManager.clear(); - - // when - PomodoroRoomResponseV2 result = pomodoroRoomServiceV2.findPomodoroRoom(pomodoroRoom.getId()); - - // then - assertAll( - () -> assertThat(result.name()).isEqualTo(pomodoroRoom.getName()), - () -> assertThat(result.totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), - () -> assertThat(result.timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) - ); - } - - @Test - void 룸_아이디로_룸_조회시_없으면_예외를_던진다() { - // given - ParticipantCode participantCode = new ParticipantCode(generationStrategy); - PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 20, participantCode); - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - - entityManager.flush(); - entityManager.clear(); - - // when, then - assertThatThrownBy(() -> pomodoroRoomServiceV2.findPomodoroRoom(99999L)) - .isInstanceOf(RoomNotFound.class); - } - - @Test - void 룸을_생성한다() { - // given - CreatePomodoroRoomRequest request = new CreatePomodoroRoomRequest("room", 8, 40); - - // when - CreatePomodoroRoomDto result = pomodoroRoomServiceV2.createPomodoroRoom(request); - - // then - assertAll( - () -> assertThat(result.studyId()).isNotNull(), - () -> assertThat(result.participantCode()).isNotNull() - ); - } - - @Test - void 참여코드에_해당하는_룸을_조회한다() { - // given - ParticipantCode participantCode = new ParticipantCode(generationStrategy); - PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 40, participantCode); - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - - entityManager.flush(); - entityManager.clear(); - - // when - PomodoroRoomResponseV2 result = pomodoroRoomServiceV2.findPomodoroRoomByParticipantCode(participantCode.getCode()); - - // then - assertAll( - () -> assertThat(result.name()).isEqualTo(pomodoroRoom.getName()), - () -> assertThat(result.totalCycle()).isEqualTo(pomodoroRoom.getTotalCycle()), - () -> assertThat(result.timePerCycle()).isEqualTo(pomodoroRoom.getTimePerCycle()) - ); - } - - @Test - void 참여코드에_해당하는_룸_조회시_참여코드가_없으면_예외를_던진다() { - // given - ParticipantCode participantCode = new ParticipantCode(generationStrategy); - PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 40, participantCode); - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - - entityManager.flush(); - entityManager.clear(); - - ParticipantCode notPersisted = new ParticipantCode(generationStrategy); - - // when, then - assertThatThrownBy(() -> pomodoroRoomServiceV2.findPomodoroRoomByParticipantCode(notPersisted.getCode())) - .isInstanceOf(ParticipantCodeNotFound.class); - } - - @Test - void 참여코드에_해당하는_룸_조회시_참여코드에_해당하는_룸이_없으면_예외를_던진다() { - // given - ParticipantCode participantCode = new ParticipantCode(generationStrategy); - PomodoroRoom pomodoroRoom = new PomodoroRoom("room", 8, 40, participantCode); - entityManager.persist(participantCode); - entityManager.persist(pomodoroRoom); - - ParticipantCode notRoomsCode = new ParticipantCode(generationStrategy); - entityManager.persist(notRoomsCode); - - entityManager.flush(); - entityManager.clear(); - - // when, then - assertThatThrownBy(() -> pomodoroRoomServiceV2.findPomodoroRoomByParticipantCode(notRoomsCode.getCode())) - .isInstanceOf(RoomNotFound.class); - } -} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 99fac108..7afc8dad 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -11,10 +11,24 @@ spring: username: sa password: driver-class-name: org.h2.Driver + flyway: + enabled: false +oauth2: + oauth-properties: + google: + client-id: test-client-id + client-secret: test-client-secret + redirect-uri: http://www.test.com + token-uri: http://www.test.com + user-info-uri: http://www.test.com +jwt: + expire-length: 1 + guest-expire-length: 1 + secret-key: test-secret-key +refresh-token: + expire-length: 1 - - - +cors-allow-origin: test diff --git a/frontend/.eslintrc b/frontend/.eslintrc index eedfe9d3..7ec1456a 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -98,6 +98,11 @@ "pattern": "@Types/*", "group": "internal", "position": "after" + }, + { + "pattern": "@Errors/*", + "group": "internal", + "position": "after" } ], "newlines-between": "always", diff --git a/frontend/.gitignore b/frontend/.gitignore index 2e512f06..1042f00c 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -8,9 +8,9 @@ .DS_Store .env .env.local -.env.development.local +.env.development .env.test.local -.env.production.local +.env.production npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/frontend/__test__/StudyMakingPage.test.tsx b/frontend/__test__/StudyMakingPage.test.tsx index b589ec94..2ab008fc 100644 --- a/frontend/__test__/StudyMakingPage.test.tsx +++ b/frontend/__test__/StudyMakingPage.test.tsx @@ -4,10 +4,14 @@ import { MemoryRouter } from 'react-router-dom'; import CreateStudy from '@Pages/CreateStudy'; +import ModalProvider from '@Contexts/ModalProvider'; + test('스터디 개설 페이지가 잘 렌더링 되었는지 확인한다.', async () => { render( - + + + , ); diff --git a/frontend/env-submodule b/frontend/env-submodule new file mode 160000 index 00000000..fc016bd9 --- /dev/null +++ b/frontend/env-submodule @@ -0,0 +1 @@ +Subproject commit fc016bd93cda55f018d3b9efb6be67aa24b13980 diff --git a/frontend/jest.config.json b/frontend/jest.config.json index 674b574d..0c717483 100644 --- a/frontend/jest.config.json +++ b/frontend/jest.config.json @@ -16,6 +16,7 @@ "^\\@Contexts/(.*)$": "/src/contexts/$1", "^\\@Assets/(.*)$": "/src/assets/$1", "^\\@Utils/(.*)$": "/src/utils/$1", - "^\\@Apis/(.*)$": "/src/api/$1" + "^\\@Apis/(.*)$": "/src/api/$1", + "^\\@Errors/(.*)$": "/src/errors/$1" } } diff --git a/frontend/package.json b/frontend/package.json index 5946657c..ad51635e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,20 +4,20 @@ "main": "index.js", "license": "MIT", "scripts": { - "prod": "NODE_ENV=production webpack serve", - "dev": "NODE_ENV=development webpack serve", + "prod": "NODE_ENV=production webpack server --open --config webpack.prod.js", + "dev": "NODE_ENV=development webpack server --open --config webpack.dev.js", "test": "jest", - "build": "yarn test && NODE_ENV=production webpack", + "build": "yarn test && NODE_ENV=production webpack --config webpack.prod.js", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "ci": "yarn install --immutable --immutable-cache --check-cache" }, "dependencies": { + "msw": "^1.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.1", - "styled-components": "^6.0.2", - "msw": "^1.2.2" + "styled-components": "^6.0.2" }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.22.5", @@ -41,6 +41,7 @@ "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", + "dotenv-webpack": "^8.0.1", "eslint": "^8.44.0", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.8.0", @@ -60,7 +61,8 @@ "typescript": "^5.1.6", "webpack": "^5.88.1", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.15.1" + "webpack-dev-server": "^4.15.1", + "webpack-merge": "^5.9.0" }, "msw": { "workerDirectory": "public" diff --git a/frontend/public/index.html b/frontend/public/index.html index 661e7b2c..afd8f924 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -8,5 +8,6 @@
+ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 543cf5aa..72a589c2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,12 +4,19 @@ import { ThemeProvider } from 'styled-components'; import GlobalStyles from '@Styles/globalStyle'; import { lightTheme } from '@Styles/theme'; +import MemberInfoProvider from '@Contexts/MemberInfoProvider'; +import ModalProvider from '@Contexts/ModalProvider'; + const App = () => { return ( - - - - + + + + + + + + ); }; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index e9cbe6a8..6a860b24 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,88 +1,159 @@ import http from '@Utils/http'; import type { + ResponseMemberProgress, + ResponseAuthToken, ResponseCreateStudy, - ResponseIsCheckMember, + ResponseMemberInfo, ResponseMemberRecordContents, - ResponseMemberStudyMetadata, - ResponsePlanList, - ResponseStudyInfo, - ResponseStudyMetadata, + ResponseOneStudyInfo, + ResponseMemberContents, + ResponseProgresses, + ResponseStudies, + ResponseStudyData, + ResponseStudyDataList, + ResponseStudyMembers, } from '@Types/api'; +import type { OAuthProvider } from '@Types/auth'; import type { PlanList, RetrospectList, StudyTimePerCycleOptions, TotalCycleOptions } from '@Types/study'; -export const requestCreateStudy = async ( - studyName: string, - totalCycle: TotalCycleOptions, - timePerCycle: StudyTimePerCycleOptions, -) => { - const response = await fetch(`/api/studies`, { - body: JSON.stringify({ name: studyName, totalCycle, timePerCycle }), - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, +const BASE_URL = ''; + +// 옛날거 + +export const requestRegisterMember = async (nickname: string, studyId: string) => { + const response = await http.post(`${BASE_URL}/api/studies/${studyId}/members`, { + body: JSON.stringify({ nickname }), }); const locationHeader = response.headers.get('Location'); - const studyId = locationHeader?.split('/').pop() as string; - - const result = (await response.json()) as ResponseCreateStudy; + const memberId = locationHeader?.split('/').pop() as string; - return { studyId, result }; + return { memberId }; }; -export const requestRegisterMember = async (nickname: string | null, studyId: string | null) => { - const response = await fetch(`/api/studies/${studyId ?? ''}/members`, { - body: JSON.stringify({ nickname }), - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, +export const requestSubmitPlanningForm = (studyId: string, memberId: string, plans: PlanList) => + http.post(`/api/studies/${studyId}/members/${memberId}/content/plans`, { + body: JSON.stringify(plans), }); - if (!response.ok) { - throw Error('사용할 수 없는 닉네임입니다.'); - } +export const requestGetStudyData = (studyId: string, accessToken: string) => + http.get(`/api/studies/${studyId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); - const locationHeader = response.headers.get('Location'); - const memberId = locationHeader?.split('/').pop() as string; +export const requestGetMemberStudyListData = (memberId: string, accessToken: string) => + http.get(`/api/studies?memberId=${memberId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); - return { memberId }; +export const requestGetStudyMembers = (studyId: string, accessToken: string) => + http.get(`/api/studies/${studyId}/progresses`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + +export const requestGetMemberRecordContents = (studyId: string, progressId: string, accessToken: string) => + http.get(`/api/studies/${studyId}/contents?progressId=${progressId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + +// 새로 적용되는 api +export const requestGuestLogin = async () => { + const response = await http.post(`${BASE_URL}/api/auth/guest`); + + return (await response.json()) as ResponseAuthToken; +}; + +export const requestOAuthLogin = async (provider: OAuthProvider, code: string) => { + const response = await http.post(`${BASE_URL}/api/auth/login`, { + body: JSON.stringify({ oauthProvider: provider, code }), + }); + + return (await response.json()) as ResponseAuthToken; }; -export const requestAuthenticateParticipationCode = async (participantCode: string) => { - const response = await http.post(`/api/studies/authenticate`, { - body: JSON.stringify({ participantCode }), +export const requestMemberInfo = (accessToken: string) => + http.get('/api/me', { + headers: { Authorization: `Bearer ${accessToken}` }, }); - return (await response.json()) as ResponseStudyInfo; +export const requestAccessTokenRefresh = async () => { + const response = await http.post(`${BASE_URL}/api/auth/refresh`); + + return (await response.json()) as ResponseAuthToken; }; -export const requestCheckIsMember = (studyId: string, memberId: string) => - http.get(`/api/studies/${studyId}/members/${memberId}`); +export const requestGetOneStudyData = (accessToken: string, studyId: string) => + http.get(`${BASE_URL}/api/studies/${studyId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); -export const requestSubmitPlanningForm = (studyId: string, memberId: string, plans: PlanList) => - http.post(`/api/studies/${studyId}/members/${memberId}/content/plans`, { - body: JSON.stringify(plans), +export const requestGetMemberProgress = (accessToken: string, studyId: string, memberId: string) => + http.get(`${BASE_URL}/api/studies/${studyId}/progresses?memberId=${memberId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + +export const requestGetMemberContents = (accessToken: string, studyId: string, progressId: string, cycle: number) => + http.get( + `${BASE_URL}/api/studies/${studyId}/contents?progressId=${progressId}&cycle=${cycle}`, + { + headers: { Authorization: `Bearer ${accessToken}` }, + }, + ); + +export const requestWritePlan = (accessToken: string, studyId: string, progressId: string, plan: PlanList) => + http.post(`${BASE_URL}/api/studies/${studyId}/contents/write-plan`, { + headers: { Authorization: `Bearer ${accessToken}` }, + body: JSON.stringify({ progressId, plan: plan }), + }); + +export const requestWriteRetrospect = ( + accessToken: string, + studyId: string, + progressId: string, + retrospect: RetrospectList, +) => + http.post(`${BASE_URL}/api/studies/${studyId}/contents/write-retrospect`, { + headers: { Authorization: `Bearer ${accessToken}` }, + body: JSON.stringify({ progressId, retrospect: retrospect }), + }); + +export const requestNextStep = (accessToken: string, studyId: string, progressId: string) => + http.post(`${BASE_URL}/api/studies/${studyId}/progresses/${progressId}/next-step`, { + headers: { Authorization: `Bearer ${accessToken}` }, }); -export const requestSubmitRetrospectForm = (studyId: string, memberId: string, retrospects: RetrospectList) => - http.post(`/api/studies/${studyId}/members/${memberId}/content/retrospects`, { body: JSON.stringify(retrospects) }); +export const requestCreateStudy = async ( + studyName: string, + totalCycle: TotalCycleOptions, + timePerCycle: StudyTimePerCycleOptions, + accessToken: string, +) => { + const response = await http.post(`/api/studies`, { + headers: { Authorization: `Bearer ${accessToken}` }, + body: JSON.stringify({ name: studyName, totalCycle, timePerCycle }), + }); + + const locationHeader = response.headers.get('Location'); + const studyId = locationHeader?.split('/').pop() as string; -export const requestGetMemberStudyMetadata = (studyId: string, memberId: string) => - http.get(`/api/studies/${studyId}/members/${memberId}/metadata`); + const result = (await response.json()) as ResponseCreateStudy; -export const requestGetStudyingContent = (studyId: string, memberId: string, cycle: number) => - http.get(`/api/studies/${studyId}/members/${memberId}/content/plans?cycle=${cycle}`); + return { studyId, result }; +}; -export const requestSubmitStudyingForm = (studyId: string, memberId: string) => - http.post(`/api/studies/${studyId}/members/${memberId}/next-step`); +export const requestAuthenticateParticipationCode = (participantCode: string, accessToken: string) => + http.get(`/api/studies?participantCode=${participantCode}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); -// RecordContents -export const requestGetStudyMetadata = (studyId: string) => - http.get(`/api/studies/${studyId}/metadata`); +export const requestCheckProgresses = async (studyId: string, memberId: string, accessToken: string) => + http.get(`/api/studies/${studyId}/progresses?memberId=${memberId}`, { + headers: { Authorization: `Bearer ${accessToken}` }, + }); -// MemberRecord -export const requestGetMemberRecordContents = (studyId: string, memberId: string) => - http.get(`/api/studies/${studyId}/members/${memberId}/content`); +export const requestRegisterProgress = (nickname: string, studyId: string, memberId: string, accessToken: string) => + http.post(`/api/studies/${studyId}/progresses`, { + headers: { Authorization: `Bearer ${accessToken}` }, + body: JSON.stringify({ memberId, nickname }), + }); diff --git a/frontend/src/assets/icons/CalenderIcon.tsx b/frontend/src/assets/icons/CalenderIcon.tsx new file mode 100644 index 00000000..9aaf1aee --- /dev/null +++ b/frontend/src/assets/icons/CalenderIcon.tsx @@ -0,0 +1,16 @@ +type Props = { + color: string; +}; + +const CalenderIcon = ({ color }: Props) => { + return ( + + + + ); +}; + +export default CalenderIcon; diff --git a/frontend/src/assets/icons/PagesIcon.tsx b/frontend/src/assets/icons/PagesIcon.tsx new file mode 100644 index 00000000..9a654801 --- /dev/null +++ b/frontend/src/assets/icons/PagesIcon.tsx @@ -0,0 +1,33 @@ +type Props = { + color: string; +}; + +const PagesIcon = ({ color }: Props) => { + return ( + + + + + + ); +}; + +export default PagesIcon; diff --git a/frontend/src/assets/icons/ResetIcon.tsx b/frontend/src/assets/icons/ResetIcon.tsx new file mode 100644 index 00000000..81382adb --- /dev/null +++ b/frontend/src/assets/icons/ResetIcon.tsx @@ -0,0 +1,19 @@ +type Props = { + color: string; +}; + +const ResetIcon = ({ color }: Props) => { + return ( + + + + + ); +}; + +export default ResetIcon; diff --git a/frontend/src/assets/icons/UserProfileIcon.tsx b/frontend/src/assets/icons/UserProfileIcon.tsx new file mode 100644 index 00000000..03afd433 --- /dev/null +++ b/frontend/src/assets/icons/UserProfileIcon.tsx @@ -0,0 +1,16 @@ +type Props = { + color: string; +}; + +const UserProfileIcon = ({ color }: Props) => { + return ( + + + + ); +}; + +export default UserProfileIcon; diff --git a/frontend/src/components/board/GuideContents/GuideContents.tsx b/frontend/src/components/board/GuideContents/GuideContents.tsx index c9681b21..dd785e65 100644 --- a/frontend/src/components/board/GuideContents/GuideContents.tsx +++ b/frontend/src/components/board/GuideContents/GuideContents.tsx @@ -1,29 +1,34 @@ -import type { StudyData } from '@Types/study'; +import type { ProgressInfo, StudyInfo } from '@Types/study'; import PlanningForm from '../PlanningForm/PlanningForm'; import RetrospectForm from '../RetrospectForm/RetrospectForm'; import StudyingForm from '../StudyingForm/StudyingForm'; type Props = { - studyData: StudyData; - changeNextStep: () => void; + studyInfo: StudyInfo; + progressInfo: ProgressInfo; + changeNextStep: () => Promise; }; -const GuideContents = ({ studyData, changeNextStep }: Props) => { - const isLastCycle = studyData.currentCycle === studyData.totalCycle; +const GuideContents = ({ studyInfo, progressInfo, changeNextStep }: Props) => { + const isLastCycle = progressInfo.currentCycle === studyInfo.totalCycle; - switch (studyData.step) { + switch (progressInfo.step) { case 'planning': return ( - + ); case 'studying': return ( ); case 'retrospect': @@ -31,8 +36,8 @@ const GuideContents = ({ studyData, changeNextStep }: Props) => { ); } diff --git a/frontend/src/components/board/PlanningForm/PlanningForm.tsx b/frontend/src/components/board/PlanningForm/PlanningForm.tsx index 71ed6876..171dfe72 100644 --- a/frontend/src/components/board/PlanningForm/PlanningForm.tsx +++ b/frontend/src/components/board/PlanningForm/PlanningForm.tsx @@ -12,18 +12,21 @@ import { getKeys } from '@Utils/getKeys'; import type { Plan } from '@Types/study'; type Props = { - onClickSubmitButton: () => void; + onClickSubmitButton: () => Promise; studyId: string; - memberId: string; + progressId: string; }; -const PlanningForm = ({ onClickSubmitButton, studyId, memberId }: Props) => { - const { questionTextareaProps, isInvalidForm, isSubmitLoading, submitForm } = usePlanningForm(studyId, memberId); +const PlanningForm = ({ onClickSubmitButton, studyId, progressId }: Props) => { + const { questionTextareaProps, isInvalidForm, isSubmitLoading, submitForm } = usePlanningForm( + studyId, + progressId, + onClickSubmitButton, + ); const handleClickButton = async () => { try { await submitForm(); - onClickSubmitButton(); } catch (error) { if (!(error instanceof Error)) return; alert(error.message); diff --git a/frontend/src/components/board/RetrospectForm/RetrospectForm.tsx b/frontend/src/components/board/RetrospectForm/RetrospectForm.tsx index 2571a07c..a7939b00 100644 --- a/frontend/src/components/board/RetrospectForm/RetrospectForm.tsx +++ b/frontend/src/components/board/RetrospectForm/RetrospectForm.tsx @@ -15,14 +15,18 @@ import type { Retrospect } from '@Types/study'; type Props = { isLastCycle: boolean; - onClickSubmitButton: () => void; + onClickSubmitButton: () => Promise; studyId: string; - memberId: string; + progressId: string; }; -const RetrospectForm = ({ isLastCycle, onClickSubmitButton, studyId, memberId }: Props) => { +const RetrospectForm = ({ isLastCycle, onClickSubmitButton, studyId, progressId }: Props) => { const navigate = useNavigate(); - const { questionTextareaProps, isInvalidForm, isSubmitLoading, submitForm } = useRetrospectForm(studyId, memberId); + const { questionTextareaProps, isInvalidForm, isSubmitLoading, submitForm } = useRetrospectForm( + studyId, + progressId, + onClickSubmitButton, + ); const handleClickButton = async () => { try { @@ -32,7 +36,6 @@ const RetrospectForm = ({ isLastCycle, onClickSubmitButton, studyId, memberId }: navigate(`${ROUTES_PATH.record}/${studyId}`); return; } - onClickSubmitButton(); } catch (error) { if (!(error instanceof Error)) return; alert(error.message); diff --git a/frontend/src/components/board/StudyingForm/StudyingForm.tsx b/frontend/src/components/board/StudyingForm/StudyingForm.tsx index ad0e2966..da17631a 100644 --- a/frontend/src/components/board/StudyingForm/StudyingForm.tsx +++ b/frontend/src/components/board/StudyingForm/StudyingForm.tsx @@ -17,20 +17,24 @@ import { getKeys } from '@Utils/getKeys'; import type { Plan } from '@Types/study'; type Props = { - onClickSubmitButton: () => void; + onClickSubmitButton: () => Promise; studyId: string; - memberId: string; + progressId: string; cycle: number; }; -const StudyingForm = ({ onClickSubmitButton, studyId, memberId, cycle }: Props) => { +const StudyingForm = ({ onClickSubmitButton, studyId, progressId, cycle }: Props) => { const navigate = useNavigate(); - const { planList, isSubmitLoading, error, submitForm } = useStudyingForm(studyId, memberId, cycle); + const { planList, isSubmitLoading, error, submitForm } = useStudyingForm( + studyId, + progressId, + cycle, + onClickSubmitButton, + ); const handleClickButton = async () => { try { await submitForm(); - onClickSubmitButton(); } catch (error) { if (!(error instanceof Error)) return; alert(error.message); diff --git a/frontend/src/components/board/Timer/Timer.tsx b/frontend/src/components/board/Timer/Timer.tsx index 913ccd62..767088d3 100644 --- a/frontend/src/components/board/Timer/Timer.tsx +++ b/frontend/src/components/board/Timer/Timer.tsx @@ -41,7 +41,7 @@ const Timer = ({ studyMinutes, step }: Props) => { { export default AccordionSkeleton; -const AccordionSkeletonLayout = styled.div` +const AccordionSkeletonLayout = styled.ul` display: grid; row-gap: 40px; max-width: 1200px; `; -const SkeletonItem = styled.div` +const SkeletonItem = styled.li` height: 80px; ${TextSkeletonStyle} `; diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx index e9c64162..454af08e 100644 --- a/frontend/src/components/common/Button/Button.tsx +++ b/frontend/src/components/common/Button/Button.tsx @@ -18,11 +18,13 @@ const SIZE_TYPE: Record> = { 'x-small': css` padding: 8px 20px; font-size: ${SIZE['x-small']}; + border-radius: 7px; `, small: css` padding: 12px 24px; font-size: ${SIZE.small}; + border-radius: 7px; `, medium: css` diff --git a/frontend/src/components/common/Input/Input.tsx b/frontend/src/components/common/Input/Input.tsx index 96bf5af5..115e2ea9 100644 --- a/frontend/src/components/common/Input/Input.tsx +++ b/frontend/src/components/common/Input/Input.tsx @@ -101,7 +101,7 @@ const StyledLabel = styled.label` const StyledBottomText = styled.p<{ $error?: boolean }>` position: absolute; margin-top: 10px; - font-size: 16px; + font-size: 1.6rem; font-weight: 200; ${({ $error }) => css` diff --git a/frontend/src/components/common/Menu/Menu.stories.tsx b/frontend/src/components/common/Menu/Menu.stories.tsx index 736c1bd2..f60dee48 100644 --- a/frontend/src/components/common/Menu/Menu.stories.tsx +++ b/frontend/src/components/common/Menu/Menu.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { css } from 'styled-components'; +import HamburgerIcon from '@Assets/icons/HamburgerIcon'; + import Menu from './Menu'; type Story = StoryObj; @@ -20,6 +22,7 @@ export default meta; */ export const DefaultMenu: Story = { args: { + trigger: , children: ( <> alert('아이템1을 클릭했습니다.')}>아이템1 @@ -36,6 +39,7 @@ export const DefaultMenu: Story = { */ export const RightMenu: Story = { args: { + trigger: , children: ( <> alert('아이템1을 클릭했습니다.')}>아이템1 @@ -50,3 +54,22 @@ export const RightMenu: Story = { `, }, }; + +/** + * 구분선이 있는 메뉴 스토입니다. + */ +export const SeparatorMenu: Story = { + args: { + trigger: , + children: ( + <> + alert('아이템1을 클릭했습니다.')}>아이템1 + alert('아이템2을 클릭했습니다.')} bottomSeparator> + 아이템2 + + alert('아이템3을 클릭했습니다.')}>아이템3 + alert('아이템4을 클릭했습니다.')}>아이템4 + + ), + }, +}; diff --git a/frontend/src/components/common/Menu/Menu.tsx b/frontend/src/components/common/Menu/Menu.tsx index d9c9467f..84d6b378 100644 --- a/frontend/src/components/common/Menu/Menu.tsx +++ b/frontend/src/components/common/Menu/Menu.tsx @@ -1,4 +1,4 @@ -import type { PropsWithChildren, ReactElement } from 'react'; +import type { PropsWithChildren, ReactElement, ReactNode } from 'react'; import { Children, cloneElement } from 'react'; import { css, styled } from 'styled-components'; import type { CSSProp } from 'styled-components'; @@ -6,12 +6,15 @@ import type { CSSProp } from 'styled-components'; import useDisplay from '@Hooks/common/useDisplay'; import useOutsideClick from '@Hooks/common/useOutsideClick'; -import color from '@Styles/color'; - -import HamburgerIcon from '@Assets/icons/HamburgerIcon'; - import MenuItem from './MenuItem'; +export type MenuItem = { + key: number; + text: string; + onClick: () => void; + bottomSeparator?: boolean; +}; + const MENU_LIST_POSITION = { left: css` right: 0; @@ -24,23 +27,22 @@ const MENU_LIST_POSITION = { } as const; type Props = { - $iconColor?: string; + trigger: ReactNode; $style?: CSSProp; $menuListPosition?: keyof typeof MENU_LIST_POSITION; + $menuListStyle?: CSSProp; }; -const Menu = ({ $menuListPosition = 'right', $style, children, $iconColor }: PropsWithChildren) => { +const Menu = ({ $menuListPosition = 'right', $style, children, trigger, $menuListStyle }: PropsWithChildren) => { const { isShow, toggleShow, hide } = useDisplay(); const ref = useOutsideClick(hide); return ( - - - + {trigger} {isShow && ( - + {Children.map(children, (child) => { const item = child as ReactElement; return cloneElement(item, { hide }); @@ -73,18 +75,11 @@ const MenuLayout = styled.div` const MenuIconWrapper = styled.div` padding: 4px; - border-radius: 50%; - - transition: background-color 0.2s ease; - - &:hover { - background-color: ${color.neutral[100]}; - } `; -type MenuListProp = Required>; +type MenuListProps = Required> & { $menuListStyle?: CSSProp }; -const MenuList = styled.ul` +const MenuList = styled.ul` position: absolute; top: 34px; @@ -106,4 +101,8 @@ const MenuList = styled.ul` ${MENU_LIST_POSITION[$menuListPosition]} background-color: ${theme.background}; `} + + ${({ $menuListStyle }) => css` + ${$menuListStyle && $menuListStyle} + `} `; diff --git a/frontend/src/components/common/Menu/MenuItem.tsx b/frontend/src/components/common/Menu/MenuItem.tsx index 696b170c..f0a33f53 100644 --- a/frontend/src/components/common/Menu/MenuItem.tsx +++ b/frontend/src/components/common/Menu/MenuItem.tsx @@ -4,11 +4,13 @@ import { styled } from 'styled-components'; import color from '@Styles/color'; type Props = { + bottomSeparator?: boolean; hide?: () => void; onClick: () => void; }; const MenuItem = ({ + bottomSeparator, children, onClick, hide, @@ -20,9 +22,12 @@ const MenuItem = ({ }; return ( - - {children} - + <> + + {children} + + {bottomSeparator && } + ); }; @@ -39,3 +44,8 @@ const MenuItemLayout = styled.li` background-color: ${color.neutral[100]}; } `; + +const Separator = styled.div` + height: 1.5px; + background-color: ${color.neutral[100]}; +`; diff --git a/frontend/src/components/common/Modal/Modal.tsx b/frontend/src/components/common/Modal/Modal.tsx new file mode 100644 index 00000000..07ef9be4 --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.tsx @@ -0,0 +1,59 @@ +import type { MouseEventHandler, PropsWithChildren } from 'react'; +import { createPortal } from 'react-dom'; +import { styled } from 'styled-components'; + +import usePreventScroll from '@Hooks/common/usePreventScroll'; + +import color from '@Styles/color'; + +type Props = { + closeModal: () => void; +}; + +const Modal = ({ children, closeModal }: PropsWithChildren) => { + usePreventScroll(); + + const onClickBackdrop = () => { + closeModal(); + }; + + const preventCloseModal: MouseEventHandler = (event) => { + event.stopPropagation(); + }; + + return createPortal( + + {children} + , + document.getElementById('modal-root') as HTMLElement, + ); +}; + +export default Modal; + +const Backdrop = styled.div` + position: fixed; + min-width: 100vw; + min-height: 100vh; + top: 0; + left: 0; + + display: flex; + align-items: center; + justify-content: center; + + background-color: rgba(0, 0, 0, 0.4); + + z-index: 5; +`; + +const ModalContainer = styled.div` + padding: 20px; + border-radius: 8px; + + background-color: ${color.white}; + + width: 500px; + max-height: 600px; + overflow-y: auto; +`; diff --git a/frontend/src/components/common/Modal/Template/Alert.tsx b/frontend/src/components/common/Modal/Template/Alert.tsx new file mode 100644 index 00000000..e328b531 --- /dev/null +++ b/frontend/src/components/common/Modal/Template/Alert.tsx @@ -0,0 +1,38 @@ +import { styled } from 'styled-components'; + +import Button from '@Components/common/Button/Button'; +import Typography from '@Components/common/Typography/Typography'; + +type Props = { + message: string; + closeModal: () => void; + onClose?: () => void; +}; + +const Alert = ({ message, closeModal, onClose }: Props) => { + const handleClose = () => { + closeModal(); + onClose?.(); + }; + + return ( + + {message} + + + ); +}; + +export default Alert; + +const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + + button { + align-self: flex-end; + } +`; diff --git a/frontend/src/components/common/QuestionAnswer/QuestionAnswer.tsx b/frontend/src/components/common/QuestionAnswer/QuestionAnswer.tsx index 1afaae97..b627e457 100644 --- a/frontend/src/components/common/QuestionAnswer/QuestionAnswer.tsx +++ b/frontend/src/components/common/QuestionAnswer/QuestionAnswer.tsx @@ -53,4 +53,8 @@ const TypographyContainer = styled.div` p { word-break: break-all; } + + h5 { + line-height: 100%; + } `; diff --git a/frontend/src/components/common/QuestionTextarea/QuestionTextarea.tsx b/frontend/src/components/common/QuestionTextarea/QuestionTextarea.tsx index b28cab8f..54395d4d 100644 --- a/frontend/src/components/common/QuestionTextarea/QuestionTextarea.tsx +++ b/frontend/src/components/common/QuestionTextarea/QuestionTextarea.tsx @@ -14,7 +14,7 @@ const QuestionTextarea = ({ question, errorMessage, ...props }: Props) => { return ( {question} - +