diff --git a/README.md b/README.md new file mode 100644 index 00000000..54108844 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# πŸ“– ν•˜λ£¨μŠ€ν„°λ””([haru-study.com](haru-study.com)) + +### μ‹œμŠ€ν…œ ꡬ성도 +![image](https://github.com/woowacourse-teams/2023-haru-study/assets/77962265/15963d75-ca21-4662-8a60-8381cab0523b) + + +### νŒ€ ꡬ성 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Backend

+
+

Backend

+
+

Backend

+
+

Backend

+
+

Frontend

+
+

Frontend

+
+

Frontend

+
+ ν…Œμ˜€(μ΅œμš°μ„±) ν”„λ‘œν•„ + + λ§ˆμ½”(μ΄κ·œμ„±) ν”„λ‘œν•„ + + λͺ¨λ””(μ „μ œν¬) ν”„λ‘œν•„ + + 히이둜(λ¬Έμ œμ›…) ν”„λ‘œν•„ + + μ—½ν† (김건엽) ν”„λ‘œν•„ + + λ…Έμ•„(김홍동) ν”„λ‘œν•„ + + λ£©μ†Œ(μš°μ •κ· ) ν”„λ‘œν•„ +
+ + ν…Œμ˜€(μ΅œμš°μ„±) + + + + λ§ˆμ½”(μ΄κ·œμ„±) + + + + λͺ¨λ””(μ „μ œν¬) + + + + 히이둜(λ¬Έμ œμ›…) + + + + μ—½ν† (김건엽) + + + + λ…Έμ•„(김홍동) + + + + λ£©μ†Œ(μš°μ •κ· ) + +
diff --git a/backend/build.gradle b/backend/build.gradle index 3bb619ab..210b3ea7 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -26,10 +26,10 @@ dependencies { 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.springframework.boot:spring-boot-starter-thymeleaf' 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' diff --git a/backend/src/main/java/harustudy/backend/HaruStudyApplication.java b/backend/src/main/java/harustudy/backend/HaruStudyApplication.java index f23d3627..a2e7da38 100644 --- a/backend/src/main/java/harustudy/backend/HaruStudyApplication.java +++ b/backend/src/main/java/harustudy/backend/HaruStudyApplication.java @@ -3,7 +3,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @EnableJpaAuditing @SpringBootApplication public class HaruStudyApplication { diff --git a/backend/src/main/java/harustudy/backend/auth/AuthArgumentResolver.java b/backend/src/main/java/harustudy/backend/auth/AuthArgumentResolver.java index 0294c100..3207f66c 100644 --- a/backend/src/main/java/harustudy/backend/auth/AuthArgumentResolver.java +++ b/backend/src/main/java/harustudy/backend/auth/AuthArgumentResolver.java @@ -2,7 +2,7 @@ import harustudy.backend.auth.dto.AuthMember; import harustudy.backend.auth.service.AuthService; -import java.util.Objects; +import harustudy.backend.auth.util.BearerAuthorizationParser; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; @@ -16,10 +16,10 @@ @Component public class AuthArgumentResolver implements HandlerMethodArgumentResolver { - private static final int ACCESS_TOKEN_LOCATION = 1; - private final AuthService authService; + private final BearerAuthorizationParser bearerAuthorizationParser; + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(Authenticated.class); @@ -29,8 +29,7 @@ public boolean supportsParameter(MethodParameter parameter) { 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]; + String accessToken = bearerAuthorizationParser.parse(authorizationHeader); 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 index b862ff21..e3d5e7cc 100644 --- a/backend/src/main/java/harustudy/backend/auth/AuthInterceptor.java +++ b/backend/src/main/java/harustudy/backend/auth/AuthInterceptor.java @@ -1,7 +1,7 @@ package harustudy.backend.auth; -import harustudy.backend.auth.exception.InvalidAuthorizationHeaderException; import harustudy.backend.auth.service.AuthService; +import harustudy.backend.auth.util.BearerAuthorizationParser; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -15,6 +15,7 @@ public class AuthInterceptor implements HandlerInterceptor { private final AuthService authService; + private final BearerAuthorizationParser bearerAuthorizationParser; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, @@ -23,12 +24,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons 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]; + String accessToken = bearerAuthorizationParser.parse(authorizationHeader); authService.validateAccessToken(accessToken); return HandlerInterceptor.super.preHandle(request, response, handler); } diff --git a/backend/src/main/java/harustudy/backend/auth/controller/AuthController.java b/backend/src/main/java/harustudy/backend/auth/controller/AuthController.java index f3f2c863..07173edd 100644 --- a/backend/src/main/java/harustudy/backend/auth/controller/AuthController.java +++ b/backend/src/main/java/harustudy/backend/auth/controller/AuthController.java @@ -2,8 +2,9 @@ import harustudy.backend.auth.dto.OauthLoginRequest; import harustudy.backend.auth.dto.TokenResponse; -import harustudy.backend.auth.exception.RefreshTokenCookieNotExistsException; +import harustudy.backend.auth.exception.RefreshTokenNotExistsException; import harustudy.backend.auth.service.AuthService; +import harustudy.backend.auth.service.OauthLoginFacade; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.Cookie; @@ -15,61 +16,63 @@ 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 OauthLoginFacade oauthLoginFacade; private final AuthService authService; + @Operation(summary = "λΉ„νšŒμ› 둜그인 μš”μ²­") + @PostMapping("/api/auth/guest") + public ResponseEntity guestLogin() { + TokenResponse tokenResponse = authService.guestLogin(); + return ResponseEntity.ok(tokenResponse); + } + @Operation(summary = "μ†Œμ…œ 둜그인 μš”μ²­") - @PostMapping("/login") + @PostMapping("/api/auth/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("/"); + TokenResponse tokenResponse = oauthLoginFacade.oauthLogin(request); + Cookie cookie = setUpRefreshTokenCookie(tokenResponse); 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") + @PostMapping("/api/auth/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("/"); + Cookie cookie = setUpRefreshTokenCookie(tokenResponse); httpServletResponse.addCookie(cookie); return ResponseEntity.ok(tokenResponse); } + private Cookie setUpRefreshTokenCookie(TokenResponse tokenResponse) { + Cookie cookie = new Cookie("refreshToken", tokenResponse.refreshToken().toString()); + cookie.setMaxAge(refreshTokenExpireLength.intValue() / 1000); + cookie.setPath("/"); + return cookie; + } + private String extractRefreshTokenFromCookie(HttpServletRequest httpServletRequest) { return Arrays.stream(httpServletRequest.getCookies()) .filter(cookie -> cookie.getName().equals("refreshToken")) .map(Cookie::getValue) .findAny() - .orElseThrow(RefreshTokenCookieNotExistsException::new); + .orElseThrow(RefreshTokenNotExistsException::new); } } diff --git a/backend/src/main/java/harustudy/backend/auth/exception/AuthorizationException.java b/backend/src/main/java/harustudy/backend/auth/exception/AuthorizationException.java index d3013a52..46adac69 100644 --- a/backend/src/main/java/harustudy/backend/auth/exception/AuthorizationException.java +++ b/backend/src/main/java/harustudy/backend/auth/exception/AuthorizationException.java @@ -1,6 +1,6 @@ package harustudy.backend.auth.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 index 82f2bc93..a9e45543 100644 --- a/backend/src/main/java/harustudy/backend/auth/exception/InvalidAccessTokenException.java +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidAccessTokenException.java @@ -1,6 +1,6 @@ package harustudy.backend.auth.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 index 036d9ca4..ab2b8ba1 100644 --- a/backend/src/main/java/harustudy/backend/auth/exception/InvalidAuthorizationHeaderException.java +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidAuthorizationHeaderException.java @@ -1,6 +1,6 @@ package harustudy.backend.auth.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 index 0408caf8..617aa00b 100644 --- a/backend/src/main/java/harustudy/backend/auth/exception/InvalidProviderNameException.java +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidProviderNameException.java @@ -1,6 +1,6 @@ package harustudy.backend.auth.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 index cee73292..0c4a18c0 100644 --- a/backend/src/main/java/harustudy/backend/auth/exception/InvalidRefreshTokenException.java +++ b/backend/src/main/java/harustudy/backend/auth/exception/InvalidRefreshTokenException.java @@ -1,6 +1,6 @@ package harustudy.backend.auth.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 deleted file mode 100644 index 5d2e1183..00000000 --- a/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenCookieNotExistsException.java +++ /dev/null @@ -1,7 +0,0 @@ -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 index bdf594e1..1eff6eae 100644 --- a/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenExpiredException.java +++ b/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenExpiredException.java @@ -1,6 +1,6 @@ package harustudy.backend.auth.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.HaruStudyException; public class RefreshTokenExpiredException extends HaruStudyException { diff --git a/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenNotExistsException.java b/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenNotExistsException.java new file mode 100644 index 00000000..d7bffb9e --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/exception/RefreshTokenNotExistsException.java @@ -0,0 +1,7 @@ +package harustudy.backend.auth.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class RefreshTokenNotExistsException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/auth/service/AuthService.java b/backend/src/main/java/harustudy/backend/auth/service/AuthService.java index 408b6107..502edf1e 100644 --- a/backend/src/main/java/harustudy/backend/auth/service/AuthService.java +++ b/backend/src/main/java/harustudy/backend/auth/service/AuthService.java @@ -1,24 +1,18 @@ 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; @@ -29,29 +23,18 @@ @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: νŠΈλžœμž­μ…˜ 뢄리 + public TokenResponse userLogin(OauthLoginRequest request, UserInfo userInfo) { + Member member = saveOrUpdateMember(request.oauthProvider(), userInfo); 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())) diff --git a/backend/src/main/java/harustudy/backend/auth/service/OauthLoginFacade.java b/backend/src/main/java/harustudy/backend/auth/service/OauthLoginFacade.java new file mode 100644 index 00000000..5a1607e0 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/service/OauthLoginFacade.java @@ -0,0 +1,35 @@ +package harustudy.backend.auth.service; + +import harustudy.backend.auth.config.OauthProperties; +import harustudy.backend.auth.config.OauthProperty; +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.infrastructure.GoogleOauthClient; +import harustudy.backend.auth.util.OauthUserInfoExtractor; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class OauthLoginFacade { + + private final OauthProperties oauthProperties; + private final GoogleOauthClient googleOauthClient; + private final AuthService authService; + + public TokenResponse oauthLogin(OauthLoginRequest request) { + UserInfo userInfo = requestUserInfo(request.oauthProvider(), request.code()); + return authService.userLogin(request, userInfo); + } + + 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); + } +} diff --git a/backend/src/main/java/harustudy/backend/auth/util/BearerAuthorizationParser.java b/backend/src/main/java/harustudy/backend/auth/util/BearerAuthorizationParser.java new file mode 100644 index 00000000..97825ddf --- /dev/null +++ b/backend/src/main/java/harustudy/backend/auth/util/BearerAuthorizationParser.java @@ -0,0 +1,30 @@ +package harustudy.backend.auth.util; + +import harustudy.backend.auth.exception.InvalidAuthorizationHeaderException; +import java.util.Objects; +import org.springframework.stereotype.Component; + +@Component +public class BearerAuthorizationParser { + + private static final String TOKEN_TYPE = "Bearer"; + private static final int TOKEN_TYPE_LOCATION = 0; + private static final int ACCESS_TOKEN_LOCATION = 1; + private static final int HEADER_SIZE = 2; + + public String parse(String authorizationHeader) { + validateIsNonNull(authorizationHeader); + String[] split = authorizationHeader.split(" "); + if (split.length != HEADER_SIZE || !split[TOKEN_TYPE_LOCATION].equals(TOKEN_TYPE)) { + throw new InvalidAuthorizationHeaderException(); + } + return split[ACCESS_TOKEN_LOCATION]; + } + + private void validateIsNonNull(String authorizationHeader) { + if (Objects.isNull(authorizationHeader)) { + throw new InvalidAuthorizationHeaderException(); + } + } +} + diff --git a/backend/src/main/java/harustudy/backend/common/CachingFilter.java b/backend/src/main/java/harustudy/backend/common/CachingFilter.java index 77833a66..b0f0befe 100644 --- a/backend/src/main/java/harustudy/backend/common/CachingFilter.java +++ b/backend/src/main/java/harustudy/backend/common/CachingFilter.java @@ -5,12 +5,10 @@ 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 diff --git a/backend/src/main/java/harustudy/backend/common/SwaggerExceptionResponse.java b/backend/src/main/java/harustudy/backend/common/SwaggerExceptionResponse.java deleted file mode 100644 index 973f095a..00000000 --- a/backend/src/main/java/harustudy/backend/common/SwaggerExceptionResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -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/common/exception/ErrorCodeView.java b/backend/src/main/java/harustudy/backend/common/exception/ErrorCodeView.java new file mode 100644 index 00000000..0d06f0ab --- /dev/null +++ b/backend/src/main/java/harustudy/backend/common/exception/ErrorCodeView.java @@ -0,0 +1,17 @@ +package harustudy.backend.common.exception; + +import java.util.List; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class ErrorCodeView { + + @GetMapping("/api/error-code") + public String errorCodeView(Model model) { + List exceptionSituations = ExceptionMapper.getExceptionSituations(); + model.addAttribute("exceptionSituations", exceptionSituations); + return "error-code"; + } +} diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionAdvice.java b/backend/src/main/java/harustudy/backend/common/exception/ExceptionAdvice.java similarity index 96% rename from backend/src/main/java/harustudy/backend/common/ExceptionAdvice.java rename to backend/src/main/java/harustudy/backend/common/exception/ExceptionAdvice.java index 7e50c7d6..c0d80105 100644 --- a/backend/src/main/java/harustudy/backend/common/ExceptionAdvice.java +++ b/backend/src/main/java/harustudy/backend/common/exception/ExceptionAdvice.java @@ -1,4 +1,4 @@ -package harustudy.backend.common; +package harustudy.backend.common.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionMapper.java b/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java similarity index 74% rename from backend/src/main/java/harustudy/backend/common/ExceptionMapper.java rename to backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java index fc2173a1..89196f77 100644 --- a/backend/src/main/java/harustudy/backend/common/ExceptionMapper.java +++ b/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java @@ -1,40 +1,36 @@ -package harustudy.backend.common; +package harustudy.backend.common.exception; 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.auth.exception.*; 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 harustudy.backend.progress.exception.ProgressNotBelongToStudyException; +import harustudy.backend.study.exception.ParticipantCodeExpiredException; +import harustudy.backend.study.exception.ParticipantCodeNotFoundException; +import harustudy.backend.study.exception.PomodoroStudyNameLengthException; +import harustudy.backend.study.exception.PomodoroTimePerCycleException; +import harustudy.backend.study.exception.PomodoroTotalCycleException; +import harustudy.backend.study.exception.StudyNotFoundException; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; public class ExceptionMapper { - private static final Map, ExceptionSituation> mapper = new HashMap<>(); + private static final Map, ExceptionSituation> mapper = new LinkedHashMap<>(); static { setUpMemberException(); setUpPomodoroContentException(); setUpPomodoroProgressException(); - setUpRoomException(); + setUpStudyException(); setUpAuthenticationException(); setUpAuthorizationException(); } @@ -54,20 +50,20 @@ private static void setUpPomodoroProgressException() { ExceptionSituation.of("ν•΄λ‹Ή μŠ€ν„°λ””μ— μ°Έμ—¬ν•œ μƒνƒœκ°€ μ•„λ‹™λ‹ˆλ‹€.", NOT_FOUND, 1201)); mapper.put(PomodoroProgressStatusException.class, ExceptionSituation.of("μŠ€ν„°λ”” 진행 μƒνƒœκ°€ μ μ ˆν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", BAD_REQUEST, 1202)); - mapper.put(ProgressNotBelongToRoomException.class, + mapper.put(ProgressNotBelongToStudyException.class, ExceptionSituation.of("ν•΄λ‹Ή μŠ€ν„°λ””μ— μ°Έμ—¬ν•œ 기둝이 μ—†μŠ΅λ‹ˆλ‹€.", BAD_REQUEST, 1203)); mapper.put(NicknameLengthException.class, ExceptionSituation.of("λ‹‰λ„€μž„ 길이가 μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", BAD_REQUEST, 1204)); } - private static void setUpRoomException() { + private static void setUpStudyException() { mapper.put(ParticipantCodeNotFoundException.class, - ExceptionSituation.of("ν•΄λ‹Ήν•˜λŠ” μ°Έμ—¬μ½”λ“œκ°€ μ—†μŠ΅λ‹ˆλ‹€.", NOT_FOUND, 1300)); + ExceptionSituation.of("μ°Έμ—¬ μ½”λ“œκ°€ λ§Œλ£Œλ˜μ—ˆκ±°λ‚˜ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", NOT_FOUND, 1300)); mapper.put(ParticipantCodeExpiredException.class, ExceptionSituation.of("만료된 μ°Έμ—¬μ½”λ“œμž…λ‹ˆλ‹€.", BAD_REQUEST, 1301)); - mapper.put(RoomNotFoundException.class, + mapper.put(StudyNotFoundException.class, ExceptionSituation.of("ν•΄λ‹Ήν•˜λŠ” μŠ€ν„°λ””κ°€ μ—†μŠ΅λ‹ˆλ‹€.", NOT_FOUND, 1302)); - mapper.put(PomodoroRoomNameLengthException.class, + mapper.put(PomodoroStudyNameLengthException.class, ExceptionSituation.of("μŠ€ν„°λ”” μ΄λ¦„μ˜ 길이가 μ μ ˆν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", BAD_REQUEST, 1304)); mapper.put(PomodoroTimePerCycleException.class, ExceptionSituation.of("μ‹œκ°„ λ‹Ή 사이클 νšŸμˆ˜κ°€ μ μ ˆν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", BAD_REQUEST, 1305)); @@ -86,6 +82,8 @@ private static void setUpAuthenticationException() { ExceptionSituation.of("μœ νš¨ν•˜μ§€ μ•Šμ€ μ•‘μ„ΈμŠ€ ν† ν°μž…λ‹ˆλ‹€", UNAUTHORIZED, 1403)); mapper.put(InvalidAuthorizationHeaderException.class, ExceptionSituation.of("μœ νš¨ν•˜μ§€ μ•Šμ€ 인증 헀더 ν˜•μ‹μž…λ‹ˆλ‹€.", BAD_REQUEST, 1404)); + mapper.put(RefreshTokenNotExistsException.class, + ExceptionSituation.of("κ°±μ‹  토큰이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", BAD_REQUEST, 1405)); } private static void setUpAuthorizationException() { @@ -96,4 +94,10 @@ private static void setUpAuthorizationException() { public static ExceptionSituation getSituationOf(Exception exception) { return mapper.get(exception.getClass()); } + + public static List getExceptionSituations() { + return mapper.values() + .stream() + .toList(); + } } diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionResponse.java b/backend/src/main/java/harustudy/backend/common/exception/ExceptionResponse.java similarity index 89% rename from backend/src/main/java/harustudy/backend/common/ExceptionResponse.java rename to backend/src/main/java/harustudy/backend/common/exception/ExceptionResponse.java index e0d6125a..8e1457f0 100644 --- a/backend/src/main/java/harustudy/backend/common/ExceptionResponse.java +++ b/backend/src/main/java/harustudy/backend/common/exception/ExceptionResponse.java @@ -1,4 +1,4 @@ -package harustudy.backend.common; +package harustudy.backend.common.exception; import com.fasterxml.jackson.annotation.JsonInclude; diff --git a/backend/src/main/java/harustudy/backend/common/ExceptionSituation.java b/backend/src/main/java/harustudy/backend/common/exception/ExceptionSituation.java similarity index 78% rename from backend/src/main/java/harustudy/backend/common/ExceptionSituation.java rename to backend/src/main/java/harustudy/backend/common/exception/ExceptionSituation.java index 77c07014..033dbe3c 100644 --- a/backend/src/main/java/harustudy/backend/common/ExceptionSituation.java +++ b/backend/src/main/java/harustudy/backend/common/exception/ExceptionSituation.java @@ -1,4 +1,4 @@ -package harustudy.backend.common; +package harustudy.backend.common.exception; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -16,10 +16,6 @@ private ExceptionSituation(String message, HttpStatus statusCode, Integer errorC 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/exception/HaruStudyException.java similarity index 58% rename from backend/src/main/java/harustudy/backend/common/HaruStudyException.java rename to backend/src/main/java/harustudy/backend/common/exception/HaruStudyException.java index cadf5f33..cb745a33 100644 --- a/backend/src/main/java/harustudy/backend/common/HaruStudyException.java +++ b/backend/src/main/java/harustudy/backend/common/exception/HaruStudyException.java @@ -1,4 +1,4 @@ -package harustudy.backend.common; +package harustudy.backend.common.exception; public class HaruStudyException extends RuntimeException { diff --git a/backend/src/main/java/harustudy/backend/config/BeanConfig.java b/backend/src/main/java/harustudy/backend/config/BeanConfig.java index 05f849f7..d1ec9568 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.room.domain.CodeGenerationStrategy; -import harustudy.backend.room.domain.GenerationStrategy; +import harustudy.backend.participantcode.domain.CodeGenerationStrategy; +import harustudy.backend.participantcode.domain.GenerationStrategy; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/harustudy/backend/config/FilterConfig.java b/backend/src/main/java/harustudy/backend/config/FilterConfig.java new file mode 100644 index 00000000..453fcf26 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/config/FilterConfig.java @@ -0,0 +1,20 @@ +package harustudy.backend.config; + +import harustudy.backend.common.CachingFilter; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean contentCachingFilter(){ + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new CachingFilter()); + registrationBean.addUrlPatterns("/api/studies/*", "/api/temp/*", "/api/auth/*", "/api/me/*"); + registrationBean.setOrder(1); + registrationBean.setName("CachingFilter"); + return registrationBean; + } +} diff --git a/backend/src/main/java/harustudy/backend/config/WebMvcConfig.java b/backend/src/main/java/harustudy/backend/config/WebMvcConfig.java index 17db8334..1d322b72 100644 --- a/backend/src/main/java/harustudy/backend/config/WebMvcConfig.java +++ b/backend/src/main/java/harustudy/backend/config/WebMvcConfig.java @@ -3,15 +3,19 @@ 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.http.CacheControl; 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.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.time.Duration; +import java.util.List; + @Configuration @RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { @@ -26,11 +30,15 @@ public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loggingInterceptor) - .addPathPatterns("/api/**"); + .addPathPatterns("/api/**") + .excludePathPatterns("/api/error-code") + .excludePathPatterns("/api/resources/**"); registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") - .excludePathPatterns("/api/auth/**"); + .excludePathPatterns("/api/auth/**") + .excludePathPatterns("/api/error-code") + .excludePathPatterns("/api/resources/**"); } @Override @@ -41,6 +49,14 @@ public void addCorsMappings(CorsRegistry registry) { .allowCredentials(true); } + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/api/resources/**") + .addResourceLocations("classpath:/static/") + .setUseLastModified(true) + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic()); + } + @Override public void addArgumentResolvers(List resolvers) { resolvers.add(authArgumentResolver); diff --git a/backend/src/main/java/harustudy/backend/config/swagger/SwaggerConfig.java b/backend/src/main/java/harustudy/backend/config/swagger/SwaggerConfig.java deleted file mode 100644 index 5159f177..00000000 --- a/backend/src/main/java/harustudy/backend/config/swagger/SwaggerConfig.java +++ /dev/null @@ -1,91 +0,0 @@ -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 1560c4a6..c62c9b85 100644 --- a/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentController.java +++ b/backend/src/main/java/harustudy/backend/content/controller/PomodoroContentController.java @@ -1,24 +1,21 @@ 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.auth.dto.AuthMember; 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 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.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; @Tag(name = "컨텐츠 κ΄€λ ¨ κΈ°λŠ₯") @RequiredArgsConstructor @@ -27,13 +24,6 @@ public class PomodoroContentController { private final PomodoroContentService pomodoroContentService; - @SwaggerExceptionResponse({ - RoomNotFoundException.class, - MemberNotFoundException.class, - AuthorizationException.class, - PomodoroProgressNotFoundException.class, - PomodoroContentNotFoundException.class - }) @Operation(summary = "필터링 쑰건으둜 멀버 컨텐츠 쑰회") @GetMapping("/api/studies/{studyId}/contents") public ResponseEntity findMemberContentsWithFilter( @@ -46,13 +36,6 @@ public ResponseEntity findMemberContentsWithFilter( return ResponseEntity.ok(response); } - @SwaggerExceptionResponse({ - RoomNotFoundException.class, - MemberNotFoundException.class, - AuthorizationException.class, - PomodoroProgressNotFoundException.class, - ProgressNotBelongToRoomException.class - }) @Operation(summary = "μŠ€ν„°λ”” κ³„νš μž‘μ„±") @PostMapping("/api/studies/{studyId}/contents/write-plan") public ResponseEntity writePlan( @@ -64,14 +47,6 @@ public ResponseEntity writePlan( return ResponseEntity.ok().build(); } - @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( diff --git a/backend/src/main/java/harustudy/backend/content/exception/PomodoroContentNotFoundException.java b/backend/src/main/java/harustudy/backend/content/exception/PomodoroContentNotFoundException.java index 1df8f5a0..54edb13b 100644 --- a/backend/src/main/java/harustudy/backend/content/exception/PomodoroContentNotFoundException.java +++ b/backend/src/main/java/harustudy/backend/content/exception/PomodoroContentNotFoundException.java @@ -1,6 +1,6 @@ package harustudy.backend.content.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 bf60d5ff..f2659e6f 100644 --- a/backend/src/main/java/harustudy/backend/content/service/PomodoroContentService.java +++ b/backend/src/main/java/harustudy/backend/content/service/PomodoroContentService.java @@ -14,11 +14,11 @@ import harustudy.backend.progress.domain.PomodoroProgress; import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; import harustudy.backend.progress.exception.PomodoroProgressStatusException; -import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; +import harustudy.backend.progress.exception.ProgressNotBelongToStudyException; 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 harustudy.backend.study.domain.PomodoroStudy; +import harustudy.backend.study.exception.StudyNotFoundException; +import harustudy.backend.study.repository.PomodoroStudyRepository; import jakarta.annotation.Nullable; import java.util.List; import java.util.Objects; @@ -31,16 +31,17 @@ @Service public class PomodoroContentService { - private final PomodoroRoomRepository pomodoroRoomRepository; + private final PomodoroStudyRepository pomodoroStudyRepository; private final MemberRepository memberRepository; private final PomodoroProgressRepository pomodoroProgressRepository; private final PomodoroContentRepository pomodoroContentRepository; + @Transactional(readOnly = true) public PomodoroContentsResponse findContentsWithFilter( - AuthMember authMember, Long roomId, Long progressId, @Nullable Integer cycle + AuthMember authMember, Long studyId, Long progressId, @Nullable Integer cycle ) { List pomodoroProgresses = getProgressesIfAuthorized( - authMember, roomId); + authMember, studyId); PomodoroProgress pomodoroProgress = filterSingleProgressById( pomodoroProgresses, progressId); @@ -51,11 +52,11 @@ public PomodoroContentsResponse findContentsWithFilter( return getPomodoroContentsResponseWithCycleFilter(pomodoroContents, cycle); } - private List getProgressesIfAuthorized(AuthMember authMember, Long roomId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFoundException::new); - List pomodoroProgresses = pomodoroProgressRepository.findAllByPomodoroRoomFetchMember( - pomodoroRoom); + private List getProgressesIfAuthorized(AuthMember authMember, Long studyId) { + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); + List pomodoroProgresses = pomodoroProgressRepository.findAllByPomodoroStudyFetchMember( + pomodoroStudy); Member member = memberRepository.findByIdIfExists(authMember.id()); if (isProgressNotRelatedToMember(pomodoroProgresses, member)) { throw new AuthorizationException(); @@ -93,27 +94,27 @@ private PomodoroContentsResponse getPomodoroContentsResponseWithCycleFilter( return PomodoroContentsResponse.from(pomodoroContentResponses); } - public void writePlan(AuthMember authMember, Long roomId, WritePlanRequest request) { + public void writePlan(AuthMember authMember, Long studyId, WritePlanRequest request) { Member member = memberRepository.findByIdIfExists(authMember.id()); - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, request.progressId()); + PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(studyId, request.progressId()); validateMemberOwnsProgress(member, pomodoroProgress); validateProgressIsPlanning(pomodoroProgress); PomodoroContent recentContent = findContentWithSameCycle(pomodoroProgress); recentContent.changePlan(request.plan()); } - private PomodoroProgress findPomodoroProgressFrom(Long roomId, Long progressId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findById(roomId) - .orElseThrow(RoomNotFoundException::new); + private PomodoroProgress findPomodoroProgressFrom(Long studyId, Long progressId) { + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new); PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findById(progressId) .orElseThrow(PomodoroProgressNotFoundException::new); - validateProgressBelongsToRoom(pomodoroRoom, pomodoroProgress); + validateProgressBelongsToStudy(pomodoroStudy, pomodoroProgress); return pomodoroProgress; } - private void validateProgressBelongsToRoom(PomodoroRoom pomodoroRoom, PomodoroProgress pomodoroProgress) { - if (!pomodoroProgress.isProgressOf(pomodoroRoom)) { - throw new ProgressNotBelongToRoomException(); + private void validateProgressBelongsToStudy(PomodoroStudy pomodoroStudy, PomodoroProgress pomodoroProgress) { + if (!pomodoroProgress.isProgressOf(pomodoroStudy)) { + throw new ProgressNotBelongToStudyException(); } } @@ -133,9 +134,9 @@ private PomodoroContent findContentWithSameCycle(PomodoroProgress pomodoroProgre .orElseThrow(PomodoroContentNotFoundException::new); } - public void writeRetrospect(AuthMember authMember, Long roomId, WriteRetrospectRequest request) { + public void writeRetrospect(AuthMember authMember, Long studyId, WriteRetrospectRequest request) { Member member = memberRepository.findByIdIfExists(authMember.id()); - PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(roomId, request.progressId()); + PomodoroProgress pomodoroProgress = findPomodoroProgressFrom(studyId, request.progressId()); validateMemberOwnsProgress(member, pomodoroProgress); validateProgressIsRetrospect(pomodoroProgress); PomodoroContent recentContent = findContentWithSameCycle(pomodoroProgress); 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 836c3876..8cef13ca 100644 --- a/backend/src/main/java/harustudy/backend/member/controller/MemberController.java +++ b/backend/src/main/java/harustudy/backend/member/controller/MemberController.java @@ -2,9 +2,7 @@ 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; @@ -20,15 +18,12 @@ public class MemberController { private final MemberService memberService; - @SwaggerExceptionResponse({ - MemberNotFoundException.class - }) @Operation(summary = "멀버 Oauth ν”„λ‘œν•„ 정보 쑰회") @GetMapping("/api/me") public ResponseEntity findOauthProfile( @Authenticated AuthMember authMember ) { - MemberResponse response = memberService.findOauthProfile(authMember); + MemberResponse response = memberService.findMemberProfile(authMember); return ResponseEntity.ok(response); } } diff --git a/backend/src/main/java/harustudy/backend/member/exception/MemberNotFoundException.java b/backend/src/main/java/harustudy/backend/member/exception/MemberNotFoundException.java index 6d772c13..7778b08d 100644 --- a/backend/src/main/java/harustudy/backend/member/exception/MemberNotFoundException.java +++ b/backend/src/main/java/harustudy/backend/member/exception/MemberNotFoundException.java @@ -1,6 +1,6 @@ package harustudy.backend.member.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.HaruStudyException; public class MemberNotFoundException extends HaruStudyException { 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 fbd8a9fb..9e861c83 100644 --- a/backend/src/main/java/harustudy/backend/member/service/MemberService.java +++ b/backend/src/main/java/harustudy/backend/member/service/MemberService.java @@ -10,13 +10,13 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor -@Transactional +@Transactional(readOnly = true) @Service public class MemberService { private final MemberRepository memberRepository; - public MemberResponse findOauthProfile(AuthMember authMember) { + public MemberResponse findMemberProfile(AuthMember authMember) { Member authorizedMember = memberRepository.findById(authMember.id()) .orElseThrow(MemberNotFoundException::new); diff --git a/backend/src/main/java/harustudy/backend/room/domain/CodeGenerationStrategy.java b/backend/src/main/java/harustudy/backend/participantcode/domain/CodeGenerationStrategy.java similarity index 66% rename from backend/src/main/java/harustudy/backend/room/domain/CodeGenerationStrategy.java rename to backend/src/main/java/harustudy/backend/participantcode/domain/CodeGenerationStrategy.java index 894b8441..ad73be1c 100644 --- a/backend/src/main/java/harustudy/backend/room/domain/CodeGenerationStrategy.java +++ b/backend/src/main/java/harustudy/backend/participantcode/domain/CodeGenerationStrategy.java @@ -1,4 +1,6 @@ -package harustudy.backend.room.domain; +package harustudy.backend.participantcode.domain; + +import java.time.LocalDateTime; public class CodeGenerationStrategy implements GenerationStrategy { @@ -12,4 +14,9 @@ public String generate() { } return sb.toString(); } + + @Override + public LocalDateTime getCreatedDate() { + return LocalDateTime.now(); + } } diff --git a/backend/src/main/java/harustudy/backend/participantcode/domain/GenerationStrategy.java b/backend/src/main/java/harustudy/backend/participantcode/domain/GenerationStrategy.java new file mode 100644 index 00000000..6005a731 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/participantcode/domain/GenerationStrategy.java @@ -0,0 +1,10 @@ +package harustudy.backend.participantcode.domain; + +import java.time.LocalDateTime; + +public interface GenerationStrategy { + + String generate(); + + LocalDateTime getCreatedDate(); +} diff --git a/backend/src/main/java/harustudy/backend/room/domain/ParticipantCode.java b/backend/src/main/java/harustudy/backend/participantcode/domain/ParticipantCode.java similarity index 53% rename from backend/src/main/java/harustudy/backend/room/domain/ParticipantCode.java rename to backend/src/main/java/harustudy/backend/participantcode/domain/ParticipantCode.java index 80f6ef9c..90ee32e2 100644 --- a/backend/src/main/java/harustudy/backend/room/domain/ParticipantCode.java +++ b/backend/src/main/java/harustudy/backend/participantcode/domain/ParticipantCode.java @@ -1,7 +1,16 @@ -package harustudy.backend.room.domain; +package harustudy.backend.participantcode.domain; import harustudy.backend.common.BaseTimeEntity; -import jakarta.persistence.*; +import harustudy.backend.study.domain.PomodoroStudy; +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 jakarta.persistence.Transient; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -15,18 +24,22 @@ public class ParticipantCode extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "study_id") + private PomodoroStudy pomodoroStudy; + @Column(unique = true, length = 6) private String code; @Transient private GenerationStrategy generationStrategy; - public ParticipantCode(GenerationStrategy generationStrategy) { + public ParticipantCode(PomodoroStudy pomodoroStudy, GenerationStrategy generationStrategy) { + this.pomodoroStudy = pomodoroStudy; this.generationStrategy = generationStrategy; this.code = generationStrategy.generate(); } - // TODO: μ°Έμ—¬μ½”λ“œ 생성에 λŒ€ν•œ 좩돌 문제 κ°œμ„  public void regenerate() { String generated = code; while (code.equals(generated)) { diff --git a/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java b/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java new file mode 100644 index 00000000..3277a791 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java @@ -0,0 +1,10 @@ +package harustudy.backend.participantcode.repository; + +import harustudy.backend.participantcode.domain.ParticipantCode; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ParticipantCodeRepository extends JpaRepository { + + Optional findByCode(String code); +} 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 40f6c861..40300aca 100644 --- a/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressController.java +++ b/backend/src/main/java/harustudy/backend/progress/controller/PomodoroProgressController.java @@ -2,21 +2,13 @@ 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.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 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.DeleteMapping; @@ -27,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.net.URI; + @Tag(name = "진행 κ΄€λ ¨ κΈ°λŠ₯") @RequiredArgsConstructor @RestController @@ -34,13 +28,6 @@ public class PomodoroProgressController { private final PomodoroProgressService pomodoroProgressService; - @SwaggerExceptionResponse({ - MemberNotFoundException.class, - RoomNotFoundException.class, - PomodoroProgressNotFoundException.class, - AuthorizationException.class, - ProgressNotBelongToRoomException.class - }) @Operation(summary = "단일 μŠ€ν„°λ”” 진행도 쑰회") @GetMapping("/api/studies/{studyId}/progresses/{progressId}") public ResponseEntity findPomodoroProgress( @@ -53,12 +40,6 @@ public ResponseEntity findPomodoroProgress( return ResponseEntity.ok(response); } - @SwaggerExceptionResponse({ - RoomNotFoundException.class, - MemberNotFoundException.class, - PomodoroProgressNotFoundException.class, - AuthorizationException.class - }) @Operation(summary = "필터링 쑰건으둜 μŠ€ν„°λ”” 진행도 쑰회") @GetMapping("/api/studies/{studyId}/progresses") public ResponseEntity findPomodoroProgressesWithFilter( @@ -71,13 +52,19 @@ public ResponseEntity findPomodoroProgressesWithFilt return ResponseEntity.ok(response); } - @SwaggerExceptionResponse({ - MemberNotFoundException.class, - PomodoroProgressNotFoundException.class, - RoomNotFoundException.class, - AuthorizationException.class, - ProgressNotBelongToRoomException.class - }) + @Operation(summary = "필터링 쑰건으둜 μŠ€ν„°λ”” 진행도 쑰회(μž„μ‹œ)") + @GetMapping("/api/temp/studies/{studyId}/progresses") + public ResponseEntity findPomodoroProgressesWithFilterTemp( + @Authenticated AuthMember authMember, + @PathVariable Long studyId, + @RequestParam(required = false) Long memberId + ) { + PomodoroProgressesResponse response = + pomodoroProgressService.tempFindPomodoroProgressWithFilter(authMember, studyId, memberId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "λ‹€μŒ μŠ€ν„°λ”” λ‹¨κ³„λ‘œ 이동") @ApiResponse(responseCode = "204") @PostMapping("/api/studies/{studyId}/progresses/{progressId}/next-step") @@ -90,12 +77,6 @@ public ResponseEntity proceed( return ResponseEntity.noContent().build(); } - @SwaggerExceptionResponse({ - MemberNotFoundException.class, - RoomNotFoundException.class, - AuthorizationException.class, - NicknameLengthException.class - }) @Operation(summary = "μŠ€ν„°λ”” μ°Έμ—¬") @ApiResponse(responseCode = "201") @PostMapping("/api/studies/{studyId}/progresses") @@ -109,13 +90,6 @@ public ResponseEntity participate( URI.create("/api/studies/" + studyId + "/progresses/" + progressId)).build(); } - @SwaggerExceptionResponse({ - RoomNotFoundException.class, - MemberNotFoundException.class, - AuthorizationException.class, - PomodoroProgressNotFoundException.class, - ProgressNotBelongToRoomException.class - }) @Operation(summary = "μŠ€ν„°λ”” 진행도 μ‚­μ œ") @ApiResponse(responseCode = "204") @DeleteMapping("/api/studies/{studyId}/progresses/{progressId}") 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 73e71bd6..bb25342c 100644 --- a/backend/src/main/java/harustudy/backend/progress/domain/PomodoroProgress.java +++ b/backend/src/main/java/harustudy/backend/progress/domain/PomodoroProgress.java @@ -4,7 +4,7 @@ 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 harustudy.backend.study.domain.PomodoroStudy; import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -34,8 +34,8 @@ public class PomodoroProgress extends BaseTimeEntity { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "pomodoro_room_id") - private PomodoroRoom pomodoroRoom; + @JoinColumn(name = "pomodoro_study_id") + private PomodoroStudy pomodoroStudy; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") @@ -52,8 +52,8 @@ public class PomodoroProgress extends BaseTimeEntity { @Enumerated(value = EnumType.STRING) private PomodoroStatus pomodoroStatus; - public PomodoroProgress(PomodoroRoom pomodoroRoom, Member member, String nickname) { - this.pomodoroRoom = pomodoroRoom; + public PomodoroProgress(PomodoroStudy pomodoroStudy, Member member, String nickname) { + this.pomodoroStudy = pomodoroStudy; this.member = member; this.nickname = nickname; this.currentCycle = 1; @@ -78,7 +78,7 @@ public void generateContents(int totalCycle) { public void proceed() { // TODO: μ„œλΉ„μŠ€λ‘œ 뺄지 말지(일관성을 μœ„ν•΄) if (pomodoroStatus.equals(PomodoroStatus.RETROSPECT)) { - if (currentCycle.equals(pomodoroRoom.getTotalCycle())) { + if (currentCycle.equals(pomodoroStudy.getTotalCycle())) { pomodoroStatus = PomodoroStatus.DONE; return; } @@ -87,8 +87,8 @@ public void proceed() { pomodoroStatus = pomodoroStatus.getNext(); } - public boolean isProgressOf(PomodoroRoom pomodoroRoom) { - return this.pomodoroRoom.getId().equals(pomodoroRoom.getId()); + public boolean isProgressOf(PomodoroStudy pomodoroStudy) { + return this.pomodoroStudy.getId().equals(pomodoroStudy.getId()); } public boolean isOwnedBy(Member member) { @@ -111,7 +111,7 @@ public boolean isNotRetrospect() { return pomodoroStatus != PomodoroStatus.RETROSPECT; } - public boolean isNotIncludedIn(PomodoroRoom other) { - return !pomodoroRoom.getId().equals(other.getId()); + public boolean isNotIncludedIn(PomodoroStudy other) { + return !pomodoroStudy.getId().equals(other.getId()); } } diff --git a/backend/src/main/java/harustudy/backend/progress/exception/NicknameLengthException.java b/backend/src/main/java/harustudy/backend/progress/exception/NicknameLengthException.java index 67a2bef7..814720a7 100644 --- a/backend/src/main/java/harustudy/backend/progress/exception/NicknameLengthException.java +++ b/backend/src/main/java/harustudy/backend/progress/exception/NicknameLengthException.java @@ -1,6 +1,6 @@ package harustudy.backend.progress.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 index 5136e6cf..b8f3a9c4 100644 --- a/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressNotFoundException.java +++ b/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressNotFoundException.java @@ -1,6 +1,6 @@ package harustudy.backend.progress.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 9f238756..748ba56c 100644 --- a/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressStatusException.java +++ b/backend/src/main/java/harustudy/backend/progress/exception/PomodoroProgressStatusException.java @@ -1,6 +1,6 @@ package harustudy.backend.progress.exception; -import harustudy.backend.common.HaruStudyException; +import harustudy.backend.common.exception.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 deleted file mode 100644 index 64e8772d..00000000 --- a/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToRoomException.java +++ /dev/null @@ -1,6 +0,0 @@ -package harustudy.backend.progress.exception; - -import harustudy.backend.common.HaruStudyException; - -public class ProgressNotBelongToRoomException extends HaruStudyException { -} diff --git a/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToStudyException.java b/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToStudyException.java new file mode 100644 index 00000000..5a39261e --- /dev/null +++ b/backend/src/main/java/harustudy/backend/progress/exception/ProgressNotBelongToStudyException.java @@ -0,0 +1,6 @@ +package harustudy.backend.progress.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class ProgressNotBelongToStudyException 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 32bfec27..a20e89e9 100644 --- a/backend/src/main/java/harustudy/backend/progress/repository/PomodoroProgressRepository.java +++ b/backend/src/main/java/harustudy/backend/progress/repository/PomodoroProgressRepository.java @@ -3,7 +3,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 harustudy.backend.study.domain.PomodoroStudy; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -12,17 +12,17 @@ public interface PomodoroProgressRepository extends JpaRepository { - Optional findByPomodoroRoomAndMember(PomodoroRoom pomodoroRoom, + Optional findByPomodoroStudyAndMember(PomodoroStudy pomodoroStudy, Member member); - @Query("select p from PomodoroProgress p join fetch p.member where p.pomodoroRoom = :pomodoroRoom") - List findAllByPomodoroRoomFetchMember( - @Param("pomodoroRoom") PomodoroRoom pomodoroRoom); + @Query("select p from PomodoroProgress p join fetch p.member where p.pomodoroStudy = :pomodoroStudy") + List findAllByPomodoroStudyFetchMember( + @Param("pomodoroStudy") PomodoroStudy pomodoroStudy); List findByMember(Member member); - List findByPomodoroRoom(PomodoroRoom pomodoroRoom); + List findByPomodoroStudy(PomodoroStudy pomodoroStudy); default PomodoroProgress findByIdIfExists(Long id) { return findById(id) 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 052ca228..6143385a 100644 --- a/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressService.java +++ b/backend/src/main/java/harustudy/backend/progress/service/PomodoroProgressService.java @@ -9,72 +9,96 @@ import harustudy.backend.progress.dto.PomodoroProgressResponse; import harustudy.backend.progress.dto.PomodoroProgressesResponse; import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; -import harustudy.backend.progress.exception.ProgressNotBelongToRoomException; +import harustudy.backend.progress.exception.ProgressNotBelongToStudyException; import harustudy.backend.progress.repository.PomodoroProgressRepository; -import harustudy.backend.room.domain.PomodoroRoom; -import harustudy.backend.room.repository.PomodoroRoomRepository; +import harustudy.backend.study.domain.PomodoroStudy; +import harustudy.backend.study.repository.PomodoroStudyRepository; 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 @RequiredArgsConstructor @Transactional +@Service public class PomodoroProgressService { private final MemberRepository memberRepository; private final PomodoroProgressRepository pomodoroProgressRepository; - private final PomodoroRoomRepository pomodoroRoomRepository; + private final PomodoroStudyRepository pomodoroStudyRepository; + @Transactional(readOnly = true) public PomodoroProgressResponse findPomodoroProgress( AuthMember authMember, Long studyId, Long progressId ) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findByIdIfExists(studyId); PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId); - validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroRoom); + validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroStudy); return PomodoroProgressResponse.from(pomodoroProgress); } + // TODO: μž„μ‹œμš©μ΄λ―€λ‘œ 이후에 제거 + public PomodoroProgressesResponse tempFindPomodoroProgressWithFilter( + AuthMember authMember, Long studyId, Long memberId + ) { + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findByIdIfExists(studyId); + if (Objects.isNull(memberId)) { + validateEverParticipated(authMember, pomodoroStudy); + return getPomodoroProgressesResponseWithoutMemberFilter(pomodoroStudy); + } + Member member = memberRepository.findByIdIfExists(memberId); + validateIsSameMemberId(authMember, memberId); + return tempGetPomodoroProgressesResponseWithMemberFilter(pomodoroStudy, member); + } + + // TODO: μž„μ‹œμš©μ΄λ―€λ‘œ 이후에 제거 + private PomodoroProgressesResponse tempGetPomodoroProgressesResponseWithMemberFilter( + PomodoroStudy pomodoroStudy, Member member) { + return pomodoroProgressRepository.findByPomodoroStudyAndMember(pomodoroStudy, member) + .map(PomodoroProgressResponse::from) + .map(response -> PomodoroProgressesResponse.from(List.of(response))) + .orElseGet(() -> PomodoroProgressesResponse.from(null)); + } + // TODO: λ™μ μΏΌλ¦¬λ‘œ λ³€κ²½(memberId μœ λ¬΄μ— λ”°λ₯Έ λΆ„κΈ°μ²˜λ¦¬) + @Transactional(readOnly = true) public PomodoroProgressesResponse findPomodoroProgressWithFilter( AuthMember authMember, Long studyId, @Nullable Long memberId ) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findByIdIfExists(studyId); if (Objects.isNull(memberId)) { - validateEverParticipated(authMember, pomodoroRoom); - return getPomodoroProgressesResponseWithoutMemberFilter(pomodoroRoom); + validateEverParticipated(authMember, pomodoroStudy); + return getPomodoroProgressesResponseWithoutMemberFilter(pomodoroStudy); } Member member = memberRepository.findByIdIfExists(memberId); validateIsSameMemberId(authMember, memberId); - return getPomodoroProgressesResponseWithMemberFilter(pomodoroRoom, member); + return getPomodoroProgressesResponseWithMemberFilter(pomodoroStudy, member); } - private void validateEverParticipated(AuthMember authMember, PomodoroRoom pomodoroRoom) { + private void validateEverParticipated(AuthMember authMember, PomodoroStudy pomodoroStudy) { Member member = memberRepository.findByIdIfExists(authMember.id()); - pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) + pomodoroProgressRepository.findByPomodoroStudyAndMember(pomodoroStudy, member) .orElseThrow(AuthorizationException::new); } private PomodoroProgressesResponse getPomodoroProgressesResponseWithoutMemberFilter( - PomodoroRoom pomodoroRoom + PomodoroStudy pomodoroStudy ) { List responses = - pomodoroProgressRepository.findByPomodoroRoom(pomodoroRoom) + pomodoroProgressRepository.findByPomodoroStudy(pomodoroStudy) .stream() .map(PomodoroProgressResponse::from) - .collect(Collectors.toList()); + .toList(); return PomodoroProgressesResponse.from(responses); } private PomodoroProgressesResponse getPomodoroProgressesResponseWithMemberFilter( - PomodoroRoom pomodoroRoom, Member member + PomodoroStudy pomodoroStudy, Member member ) { PomodoroProgressResponse response = - pomodoroProgressRepository.findByPomodoroRoomAndMember(pomodoroRoom, member) + pomodoroProgressRepository.findByPomodoroStudyAndMember(pomodoroStudy, member) .map(PomodoroProgressResponse::from) .orElseThrow(PomodoroProgressNotFoundException::new); return PomodoroProgressesResponse.from(List.of(response)); @@ -82,18 +106,19 @@ private PomodoroProgressesResponse getPomodoroProgressesResponseWithMemberFilter public void proceed(AuthMember authMember, Long studyId, Long progressId) { PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId); - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findByIdIfExists(studyId); - validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroRoom); + validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroStudy); pomodoroProgress.proceed(); } - public Long participateStudy(AuthMember authMember, Long studyId, ParticipateStudyRequest request) { + 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()); + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findByIdIfExists(studyId); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, request.nickname()); + pomodoroProgress.generateContents(pomodoroStudy.getTotalCycle()); PomodoroProgress saved = pomodoroProgressRepository.save(pomodoroProgress); return saved.getId(); } @@ -105,10 +130,10 @@ private void validateIsSameMemberId(AuthMember authMember, Long memberId) { } private void validateProgressIsRelatedWith( - PomodoroProgress pomodoroProgress, AuthMember authMember, PomodoroRoom pomodoroRoom + PomodoroProgress pomodoroProgress, AuthMember authMember, PomodoroStudy pomodoroStudy ) { validateMemberOwns(pomodoroProgress, authMember); - validateProgressIsIncludedIn(pomodoroRoom, pomodoroProgress); + validateProgressIsIncludedIn(pomodoroStudy, pomodoroProgress); } private void validateMemberOwns(PomodoroProgress pomodoroProgress, AuthMember authMember) { @@ -118,18 +143,18 @@ private void validateMemberOwns(PomodoroProgress pomodoroProgress, AuthMember au } } - private void validateProgressIsIncludedIn(PomodoroRoom pomodoroRoom, + private void validateProgressIsIncludedIn(PomodoroStudy pomodoroStudy, PomodoroProgress pomodoroProgress) { - if (pomodoroProgress.isNotIncludedIn(pomodoroRoom)) { - throw new ProgressNotBelongToRoomException(); + if (pomodoroProgress.isNotIncludedIn(pomodoroStudy)) { + throw new ProgressNotBelongToStudyException(); } } public void deleteProgress(AuthMember authMember, Long studyId, Long progressId) { - PomodoroRoom pomodoroRoom = pomodoroRoomRepository.findByIdIfExists(studyId); - validateEverParticipated(authMember, pomodoroRoom); + PomodoroStudy pomodoroStudy = pomodoroStudyRepository.findByIdIfExists(studyId); + validateEverParticipated(authMember, pomodoroStudy); PomodoroProgress pomodoroProgress = pomodoroProgressRepository.findByIdIfExists(progressId); - validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroRoom); + validateProgressIsRelatedWith(pomodoroProgress, authMember, pomodoroStudy); pomodoroProgressRepository.delete(pomodoroProgress); } } diff --git a/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomController.java b/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomController.java deleted file mode 100644 index 96b53de4..00000000 --- a/backend/src/main/java/harustudy/backend/room/controller/PomodoroRoomController.java +++ /dev/null @@ -1,83 +0,0 @@ -package harustudy.backend.room.controller; - -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 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.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@Tag(name = "μŠ€ν„°λ”” κ΄€λ ¨ κΈ°λŠ₯") -@RequiredArgsConstructor -@RestController -public class PomodoroRoomController { - - private final PomodoroRoomService pomodoroRoomService; - - @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); - } - - @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 - ) { - PomodoroRoomsResponse response = pomodoroRoomService.findPomodoroRoomWithFilter( - memberId, participantCode); - return ResponseEntity.ok(response); - } - - @SwaggerExceptionResponse({ - PomodoroRoomNameLengthException.class, - PomodoroTotalCycleException.class, - PomodoroTimePerCycleException.class - }) - @Operation(summary = "μŠ€ν„°λ”” 생성") - @ApiResponse(responseCode = "201") - @PostMapping("/api/studies") - public ResponseEntity createStudy( - @Authenticated AuthMember authMember, - @RequestBody CreatePomodoroRoomRequest request - ) { - 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/domain/GenerationStrategy.java b/backend/src/main/java/harustudy/backend/room/domain/GenerationStrategy.java deleted file mode 100644 index cf4182ed..00000000 --- a/backend/src/main/java/harustudy/backend/room/domain/GenerationStrategy.java +++ /dev/null @@ -1,6 +0,0 @@ -package harustudy.backend.room.domain; - -public interface GenerationStrategy { - - String generate(); -} diff --git a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomResponse.java b/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomResponse.java deleted file mode 100644 index df86d163..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package harustudy.backend.room.dto; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import harustudy.backend.room.domain.ParticipantCode; -import harustudy.backend.room.domain.PomodoroRoom; - -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/PomodoroRoomResponse.java b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponse.java deleted file mode 100644 index 552a3e2c..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -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/PomodoroRoomsResponse.java b/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomsResponse.java deleted file mode 100644 index fbc93625..00000000 --- a/backend/src/main/java/harustudy/backend/room/dto/PomodoroRoomsResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -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/ParticipantCodeExpiredException.java b/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeExpiredException.java deleted file mode 100644 index 090c85c6..00000000 --- a/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeExpiredException.java +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 1533baab..00000000 --- a/backend/src/main/java/harustudy/backend/room/exception/ParticipantCodeNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 4e5f80d0..00000000 --- a/backend/src/main/java/harustudy/backend/room/exception/PomodoroRoomNameLengthException.java +++ /dev/null @@ -1,7 +0,0 @@ -package harustudy.backend.room.exception; - -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 deleted file mode 100644 index 07fc82c3..00000000 --- a/backend/src/main/java/harustudy/backend/room/exception/PomodoroTimePerCycleException.java +++ /dev/null @@ -1,7 +0,0 @@ -package harustudy.backend.room.exception; - -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 deleted file mode 100644 index 549d2187..00000000 --- a/backend/src/main/java/harustudy/backend/room/exception/PomodoroTotalCycleException.java +++ /dev/null @@ -1,7 +0,0 @@ -package harustudy.backend.room.exception; - -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 deleted file mode 100644 index ea3b3d6c..00000000 --- a/backend/src/main/java/harustudy/backend/room/exception/RoomNotFoundException.java +++ /dev/null @@ -1,7 +0,0 @@ -package harustudy.backend.room.exception; - -import harustudy.backend.common.HaruStudyException; - -public class RoomNotFoundException extends HaruStudyException { - -} diff --git a/backend/src/main/java/harustudy/backend/room/repository/ParticipantCodeRepository.java b/backend/src/main/java/harustudy/backend/room/repository/ParticipantCodeRepository.java deleted file mode 100644 index 094c1f4e..00000000 --- a/backend/src/main/java/harustudy/backend/room/repository/ParticipantCodeRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package harustudy.backend.room.repository; - -import harustudy.backend.room.domain.ParticipantCode; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.query.Param; - -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 deleted file mode 100644 index d6b88fdb..00000000 --- a/backend/src/main/java/harustudy/backend/room/repository/PomodoroRoomRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package harustudy.backend.room.repository; - -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 { - - // 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 deleted file mode 100644 index 1691cf70..00000000 --- a/backend/src/main/java/harustudy/backend/room/service/PomodoroRoomService.java +++ /dev/null @@ -1,103 +0,0 @@ -package harustudy.backend.room.service; - -import harustudy.backend.member.domain.Member; -import harustudy.backend.member.exception.MemberNotFoundException; -import harustudy.backend.member.repository.MemberRepository; -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.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 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; - -@RequiredArgsConstructor -@Transactional -@Service -public class PomodoroRoomService { - - private final PomodoroRoomRepository pomodoroRoomRepository; - private final PomodoroProgressRepository pomodoroProgressRepository; - private final ParticipantCodeRepository participantCodeRepository; - private final MemberRepository memberRepository; - private final GenerationStrategy generationStrategy; - - 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); - - PomodoroRoom pomodoroRoom = new PomodoroRoom(request.name(), request.totalCycle(), - request.timePerCycle(), participantCode); - PomodoroRoom savedRoom = pomodoroRoomRepository.save(pomodoroRoom); - - return CreatePomodoroRoomResponse.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(); - } -} diff --git a/backend/src/main/java/harustudy/backend/study/controller/PomodoroStudyController.java b/backend/src/main/java/harustudy/backend/study/controller/PomodoroStudyController.java new file mode 100644 index 00000000..64f6d122 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/controller/PomodoroStudyController.java @@ -0,0 +1,58 @@ +package harustudy.backend.study.controller; + +import harustudy.backend.auth.Authenticated; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.study.dto.CreatePomodoroStudyRequest; +import harustudy.backend.study.dto.CreatePomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudiesResponse; +import harustudy.backend.study.service.PomodoroStudyService; +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.*; + +@Tag(name = "μŠ€ν„°λ”” κ΄€λ ¨ κΈ°λŠ₯") +@RequiredArgsConstructor +@RestController +public class PomodoroStudyController { + + private final PomodoroStudyService pomodoroStudyService; + + @Operation(summary = "단일 μŠ€ν„°λ”” 정보 쑰회") + @GetMapping("/api/studies/{studyId}") + public ResponseEntity findStudy( + @Authenticated AuthMember authMember, + @PathVariable Long studyId + ) { + PomodoroStudyResponse response = pomodoroStudyService.findPomodoroStudy(studyId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "필터링 쑰건으둜 μŠ€ν„°λ”” 쑰회") + @GetMapping("/api/studies") + public ResponseEntity findStudiesWithFilter( + @Authenticated AuthMember authMember, + @RequestParam(required = false) Long memberId, + @RequestParam(required = false) String participantCode + ) { + PomodoroStudiesResponse response = pomodoroStudyService.findPomodoroStudyWithFilter( + memberId, participantCode); + return ResponseEntity.ok(response); + } + + @Operation(summary = "μŠ€ν„°λ”” 생성") + @ApiResponse(responseCode = "201") + @PostMapping("/api/studies") + public ResponseEntity createStudy( + @Authenticated AuthMember authMember, + @RequestBody CreatePomodoroStudyRequest request + ) { + CreatePomodoroStudyResponse response = pomodoroStudyService.createPomodoroStudy(request); + return ResponseEntity.created(URI.create("/api/studies/" + response.studyId())) + .body(response); + } +} diff --git a/backend/src/main/java/harustudy/backend/room/domain/PomodoroRoom.java b/backend/src/main/java/harustudy/backend/study/domain/PomodoroStudy.java similarity index 71% rename from backend/src/main/java/harustudy/backend/room/domain/PomodoroRoom.java rename to backend/src/main/java/harustudy/backend/study/domain/PomodoroStudy.java index 323e899d..f4eb826b 100644 --- a/backend/src/main/java/harustudy/backend/room/domain/PomodoroRoom.java +++ b/backend/src/main/java/harustudy/backend/study/domain/PomodoroStudy.java @@ -1,19 +1,16 @@ -package harustudy.backend.room.domain; +package harustudy.backend.study.domain; import harustudy.backend.common.BaseTimeEntity; import harustudy.backend.progress.domain.PomodoroProgress; -import harustudy.backend.room.exception.PomodoroRoomNameLengthException; -import harustudy.backend.room.exception.PomodoroTimePerCycleException; -import harustudy.backend.room.exception.PomodoroTotalCycleException; +import harustudy.backend.study.exception.PomodoroStudyNameLengthException; +import harustudy.backend.study.exception.PomodoroTimePerCycleException; +import harustudy.backend.study.exception.PomodoroTotalCycleException; 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,7 +21,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity -public class PomodoroRoom extends BaseTimeEntity { +public class PomodoroStudy extends BaseTimeEntity { private static final int MIN_NAME_LENGTH = 1; private static final int MAX_NAME_LENGTH = 10; @@ -39,10 +36,6 @@ public class PomodoroRoom extends BaseTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "participant_code_id") - private ParticipantCode participantCode; - @NotNull @Column(length = 10) private String name; @@ -53,16 +46,15 @@ public class PomodoroRoom extends BaseTimeEntity { @NotNull private Integer timePerCycle; - @OneToMany(mappedBy = "pomodoroRoom") + @OneToMany(mappedBy = "pomodoroStudy") private List pomodoroProgresses = new ArrayList<>(); - public PomodoroRoom(@NotNull String name, @NotNull Integer totalCycle, - @NotNull Integer timePerCycle, @NotNull ParticipantCode participantCode) { + public PomodoroStudy(@NotNull String name, @NotNull Integer totalCycle, + @NotNull Integer timePerCycle) { validate(name, totalCycle, timePerCycle); this.totalCycle = totalCycle; this.timePerCycle = timePerCycle; this.name = name; - this.participantCode = participantCode; } private void validate(String name, Integer totalCycle, Integer timePerCycle) { @@ -73,7 +65,7 @@ private void validate(String name, Integer totalCycle, Integer timePerCycle) { private void validateName(String name) { if (name.length() < MIN_NAME_LENGTH || name.length() > MAX_NAME_LENGTH) { - throw new PomodoroRoomNameLengthException(); + throw new PomodoroStudyNameLengthException(); } } diff --git a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomRequest.java b/backend/src/main/java/harustudy/backend/study/dto/CreatePomodoroStudyRequest.java similarity index 72% rename from backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomRequest.java rename to backend/src/main/java/harustudy/backend/study/dto/CreatePomodoroStudyRequest.java index ce1da4a1..228d822e 100644 --- a/backend/src/main/java/harustudy/backend/room/dto/CreatePomodoroRoomRequest.java +++ b/backend/src/main/java/harustudy/backend/study/dto/CreatePomodoroStudyRequest.java @@ -1,9 +1,9 @@ -package harustudy.backend.room.dto; +package harustudy.backend.study.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -public record CreatePomodoroRoomRequest( +public record CreatePomodoroStudyRequest( @NotBlank String name, @NotNull Integer totalCycle, @NotNull Integer timePerCycle diff --git a/backend/src/main/java/harustudy/backend/study/dto/CreatePomodoroStudyResponse.java b/backend/src/main/java/harustudy/backend/study/dto/CreatePomodoroStudyResponse.java new file mode 100644 index 00000000..d3ae4a57 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/dto/CreatePomodoroStudyResponse.java @@ -0,0 +1,13 @@ +package harustudy.backend.study.dto; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.study.domain.PomodoroStudy; + +public record CreatePomodoroStudyResponse(@JsonIgnore Long studyId, String participantCode) { + + public static CreatePomodoroStudyResponse from(PomodoroStudy savedStudy, + ParticipantCode participantCode) { + return new CreatePomodoroStudyResponse(savedStudy.getId(), participantCode.getCode()); + } +} diff --git a/backend/src/main/java/harustudy/backend/study/dto/PomodoroStudiesResponse.java b/backend/src/main/java/harustudy/backend/study/dto/PomodoroStudiesResponse.java new file mode 100644 index 00000000..a8c13ba0 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/dto/PomodoroStudiesResponse.java @@ -0,0 +1,13 @@ +package harustudy.backend.study.dto; + +import harustudy.backend.study.domain.PomodoroStudy; +import java.util.List; + +public record PomodoroStudiesResponse(List studies) { + + public static PomodoroStudiesResponse from(List pomodoroStudies) { + return new PomodoroStudiesResponse(pomodoroStudies.stream() + .map(PomodoroStudyResponse::from) + .toList()); + } +} diff --git a/backend/src/main/java/harustudy/backend/study/dto/PomodoroStudyResponse.java b/backend/src/main/java/harustudy/backend/study/dto/PomodoroStudyResponse.java new file mode 100644 index 00000000..9f7de432 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/dto/PomodoroStudyResponse.java @@ -0,0 +1,14 @@ +package harustudy.backend.study.dto; + +import harustudy.backend.study.domain.PomodoroStudy; +import java.time.LocalDateTime; + +public record PomodoroStudyResponse(Long studyId, String name, Integer totalCycle, + Integer timePerCycle, LocalDateTime createdDateTime) { + + public static PomodoroStudyResponse from(PomodoroStudy pomodoroStudy) { + return new PomodoroStudyResponse(pomodoroStudy.getId(), pomodoroStudy.getName(), + pomodoroStudy.getTotalCycle(), pomodoroStudy.getTimePerCycle(), + pomodoroStudy.getCreatedDate()); + } +} diff --git a/backend/src/main/java/harustudy/backend/study/exception/ParticipantCodeExpiredException.java b/backend/src/main/java/harustudy/backend/study/exception/ParticipantCodeExpiredException.java new file mode 100644 index 00000000..482f635d --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/exception/ParticipantCodeExpiredException.java @@ -0,0 +1,7 @@ +package harustudy.backend.study.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class ParticipantCodeExpiredException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/study/exception/ParticipantCodeNotFoundException.java b/backend/src/main/java/harustudy/backend/study/exception/ParticipantCodeNotFoundException.java new file mode 100644 index 00000000..e92ca89a --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/exception/ParticipantCodeNotFoundException.java @@ -0,0 +1,7 @@ +package harustudy.backend.study.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class ParticipantCodeNotFoundException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/study/exception/PomodoroStudyNameLengthException.java b/backend/src/main/java/harustudy/backend/study/exception/PomodoroStudyNameLengthException.java new file mode 100644 index 00000000..8475feb8 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/exception/PomodoroStudyNameLengthException.java @@ -0,0 +1,7 @@ +package harustudy.backend.study.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class PomodoroStudyNameLengthException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/study/exception/PomodoroTimePerCycleException.java b/backend/src/main/java/harustudy/backend/study/exception/PomodoroTimePerCycleException.java new file mode 100644 index 00000000..50286723 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/exception/PomodoroTimePerCycleException.java @@ -0,0 +1,7 @@ +package harustudy.backend.study.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class PomodoroTimePerCycleException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/study/exception/PomodoroTotalCycleException.java b/backend/src/main/java/harustudy/backend/study/exception/PomodoroTotalCycleException.java new file mode 100644 index 00000000..3107245c --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/exception/PomodoroTotalCycleException.java @@ -0,0 +1,7 @@ +package harustudy.backend.study.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class PomodoroTotalCycleException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/study/exception/StudyNotFoundException.java b/backend/src/main/java/harustudy/backend/study/exception/StudyNotFoundException.java new file mode 100644 index 00000000..078b9a1b --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/exception/StudyNotFoundException.java @@ -0,0 +1,7 @@ +package harustudy.backend.study.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class StudyNotFoundException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/study/repository/PomodoroStudyRepository.java b/backend/src/main/java/harustudy/backend/study/repository/PomodoroStudyRepository.java new file mode 100644 index 00000000..19fc24c1 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/repository/PomodoroStudyRepository.java @@ -0,0 +1,13 @@ +package harustudy.backend.study.repository; + +import harustudy.backend.study.domain.PomodoroStudy; +import harustudy.backend.study.exception.StudyNotFoundException; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PomodoroStudyRepository extends JpaRepository { + + default PomodoroStudy findByIdIfExists(Long id) { + return findById(id) + .orElseThrow(StudyNotFoundException::new); + } +} diff --git a/backend/src/main/java/harustudy/backend/study/service/PomodoroStudyService.java b/backend/src/main/java/harustudy/backend/study/service/PomodoroStudyService.java new file mode 100644 index 00000000..f3910fd0 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/service/PomodoroStudyService.java @@ -0,0 +1,96 @@ +package harustudy.backend.study.service; + +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.study.domain.PomodoroStudy; +import harustudy.backend.study.dto.CreatePomodoroStudyRequest; +import harustudy.backend.study.dto.CreatePomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudiesResponse; +import harustudy.backend.study.exception.ParticipantCodeNotFoundException; +import harustudy.backend.study.exception.StudyNotFoundException; +import harustudy.backend.study.repository.PomodoroStudyRepository; +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 PomodoroStudyService { + + private final PomodoroStudyRepository pomodoroStudyRepository; + private final PomodoroProgressRepository pomodoroProgressRepository; + private final MemberRepository memberRepository; + private final GenerationStrategy generationStrategy; + private final ParticipantCodeRepository participantCodeRepository; + + @Transactional(readOnly = true) + public PomodoroStudyResponse findPomodoroStudy(Long studyId) { + return PomodoroStudyResponse.from(pomodoroStudyRepository.findById(studyId) + .orElseThrow(StudyNotFoundException::new)); + } + + @Transactional(readOnly = true) + public PomodoroStudiesResponse findPomodoroStudyWithFilter(Long memberId, String code) { + if (Objects.nonNull(code)) { + ParticipantCode participantCode = participantCodeRepository.findByCode(code) + .orElseThrow(ParticipantCodeNotFoundException::new); + PomodoroStudy pomodoroStudy = participantCode.getPomodoroStudy(); + return PomodoroStudiesResponse.from(List.of(pomodoroStudy)); + } + if (Objects.nonNull(memberId)) { + return findPomodoroStudyByMemberId(memberId); + } + + return PomodoroStudiesResponse.from(pomodoroStudyRepository.findAll()); + } + + private PomodoroStudiesResponse findPomodoroStudyByMemberId(Long memberId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(MemberNotFoundException::new); + List pomodoroProgresses = pomodoroProgressRepository.findByMember(member); + + List pomodoroStudies = mapToPomodoroStudies(pomodoroProgresses); + + return PomodoroStudiesResponse.from(pomodoroStudies); + } + + private List mapToPomodoroStudies(List pomodoroProgresses) { + return pomodoroProgresses.stream() + .map(PomodoroProgress::getPomodoroStudy) + .toList(); + } + + public CreatePomodoroStudyResponse createPomodoroStudy(CreatePomodoroStudyRequest request) { + PomodoroStudy pomodoroStudy = new PomodoroStudy(request.name(), request.totalCycle(), + request.timePerCycle()); + PomodoroStudy savedStudy = pomodoroStudyRepository.save(pomodoroStudy); + + ParticipantCode participantCode = generateUniqueCode(pomodoroStudy); + participantCodeRepository.save(participantCode); + + return CreatePomodoroStudyResponse.from(savedStudy, participantCode); + } + + private ParticipantCode generateUniqueCode(PomodoroStudy pomodoroStudy) { + ParticipantCode participantCode = new ParticipantCode(pomodoroStudy, generationStrategy); + while (isParticipantCodePresent(participantCode)) { + participantCode.regenerate(); + } + return participantCode; + } + + private boolean isParticipantCodePresent(ParticipantCode participantCode) { + return participantCodeRepository.findByCode(participantCode.getCode()) + .isPresent(); + } +} diff --git a/backend/src/main/resources/application-develop.yml b/backend/src/main/resources/application-develop.yml index ef4884ad..45fc0389 100644 --- a/backend/src/main/resources/application-develop.yml +++ b/backend/src/main/resources/application-develop.yml @@ -5,6 +5,7 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect + open-in-view: false flyway: enabled: false diff --git a/backend/src/main/resources/application-production.yml b/backend/src/main/resources/application-production.yml index b8f7aa42..460ccf7a 100644 --- a/backend/src/main/resources/application-production.yml +++ b/backend/src/main/resources/application-production.yml @@ -5,6 +5,7 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect + open-in-view: false flyway: enabled: false diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e529fca4..581ca020 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,11 +1,11 @@ spring: jpa: - hibernate: - ddl-auto: create-drop properties: hibernate: + ddl-auto: create-drop format_sql: true show-sql: true + open-in-view: false datasource: url: jdbc:h2:mem:testdb;MODE=MySQL @@ -17,5 +17,8 @@ spring: console: enabled: true + flyway: + enabled: false + config: import: classpath:/submodule/application-auth-develop.yml diff --git a/backend/src/main/resources/static/error-code.css b/backend/src/main/resources/static/error-code.css new file mode 100644 index 00000000..9f71e096 --- /dev/null +++ b/backend/src/main/resources/static/error-code.css @@ -0,0 +1,82 @@ +body { + padding:1.5em; + background: #f5f5f5 +} + +table { + border: 1px #a39485 solid; + font-size: .9em; + box-shadow: 0 2px 5px rgba(0,0,0,.25); + width: 100%; + border-collapse: collapse; + border-radius: 5px; + overflow: hidden; +} + +th { + text-align: left; +} + +thead { + font-weight: bold; + color: #fff; + background: rgb(59, 130, 246); +} + +td, th { + padding: 1em .5em; + vertical-align: middle; + text-align: center; +} + +td { + border-bottom: 1px solid rgba(0,0,0,.1); + background: #fff; +} + +a { + color: #73685d; +} + +@media all and (max-width: 768px) { + + table, thead, tbody, th, td, tr { + display: block; + } + + th { + text-align: right; + } + + table { + position: relative; + padding-bottom: 0; + border: none; + box-shadow: 0 0 10px rgba(0,0,0,.2); + } + + thead { + float: left; + white-space: nowrap; + } + + tbody { + overflow-x: auto; + overflow-y: hidden; + position: relative; + white-space: nowrap; + } + + tr { + display: inline-block; + vertical-align: top; + } + + th { + border-bottom: 1px solid #a39485; + } + + td { + border-bottom: 1px solid #e5e5e5; + } +} diff --git a/backend/src/main/resources/templates/error-code.html b/backend/src/main/resources/templates/error-code.html new file mode 100644 index 00000000..33762b8e --- /dev/null +++ b/backend/src/main/resources/templates/error-code.html @@ -0,0 +1,27 @@ + + + + + μ—λŸ¬ μ½”λ“œ λͺ…μ„Έ + + + + + + + + + + + + + + + + + + + +
μ—λŸ¬μ½”λ“œμƒνƒœμ½”λ“œμ˜ˆμ™Έ 메세지
+ + diff --git a/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java b/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java index f665e810..92632a50 100644 --- a/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java @@ -1,124 +1,268 @@ -//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")); -// } -// ); -// } -//} +package harustudy.backend.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +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 com.fasterxml.jackson.databind.ObjectMapper; +import harustudy.backend.auth.config.OauthProperty; +import harustudy.backend.auth.config.TokenConfig; +import harustudy.backend.auth.dto.OauthLoginRequest; +import harustudy.backend.auth.dto.OauthTokenResponse; +import harustudy.backend.auth.dto.TokenResponse; +import harustudy.backend.auth.infrastructure.GoogleOauthClient; +import harustudy.backend.auth.util.JwtTokenProvider; +import harustudy.backend.content.dto.WritePlanRequest; +import harustudy.backend.content.dto.WriteRetrospectRequest; +import harustudy.backend.integration.LoginResponse; +import harustudy.backend.progress.dto.ParticipateStudyRequest; +import harustudy.backend.progress.dto.PomodoroProgressesResponse; +import harustudy.backend.study.dto.CreatePomodoroStudyRequest; +import harustudy.backend.study.dto.CreatePomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudiesResponse; +import jakarta.servlet.http.Cookie; +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.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +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.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +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) +@ExtendWith(MockitoExtension.class) +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class AcceptanceTest { + + @MockBean + GoogleOauthClient googleOauthClient; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private TokenConfig tokenConfig; + + @Autowired + private WebApplicationContext webApplicationContext; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); + } + + @Test + void νšŒμ›μœΌλ‘œ_μŠ€ν„°λ””λ₯Ό_μ§„ν–‰ν•œλ‹€() throws Exception { + LoginResponse 둜그인_정보 = ꡬ글_λ‘œκ·ΈμΈμ„_μ§„ν–‰ν•œλ‹€(); + String μ°Έμ—¬_μ½”λ“œ = μŠ€ν„°λ””λ₯Ό_κ°œμ„€ν•œλ‹€(둜그인_정보); + Long μŠ€ν„°λ””_아이디 = μŠ€ν„°λ””λ₯Ό_μ‘°νšŒν•œλ‹€(둜그인_정보, μ°Έμ—¬_μ½”λ“œ); + Long 진행도_아이디 = μŠ€ν„°λ””μ—_μ°Έμ—¬ν•œλ‹€(둜그인_정보, μŠ€ν„°λ””_아이디); + μŠ€ν„°λ””_κ³„νšμ„_μž‘μ„±ν•œλ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μƒνƒœλ₯Ό_λ‹€μŒ_λ‹¨κ³„λ‘œ_λ„˜κΈ΄λ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μƒνƒœλ₯Ό_λ‹€μŒ_λ‹¨κ³„λ‘œ_λ„˜κΈ΄λ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_회고λ₯Ό_μž‘μ„±ν•œλ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μƒνƒœλ₯Ό_λ‹€μŒ_λ‹¨κ³„λ‘œ_λ„˜κΈ΄λ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μ’…λ£Œ_ν›„_κ²°κ³Ό_쑰회(둜그인_정보, μŠ€ν„°λ””_아이디); + } + + @Test + void νšŒμ›μœΌλ‘œ_μ§„ν–‰ν–ˆμ—ˆλ˜_μŠ€ν„°λ””_λͺ©λ‘μ„_μ‘°νšŒν•œλ‹€() throws Exception { + νšŒμ›μœΌλ‘œ_μŠ€ν„°λ””λ₯Ό_μ§„ν–‰ν•œλ‹€(); + νšŒμ›μœΌλ‘œ_μŠ€ν„°λ””λ₯Ό_μ§„ν–‰ν•œλ‹€(); + LoginResponse 둜그인_정보 = ꡬ글_λ‘œκ·ΈμΈμ„_μ§„ν–‰ν•œλ‹€(); + List νšŒμ›μœΌλ‘œ_μ™„λ£Œν•œ_μŠ€ν„°λ””_λͺ©λ‘ = νšŒμ›μœΌλ‘œ_μ§„ν–‰ν–ˆλ˜_λͺ¨λ“ _μŠ€ν„°λ””_λͺ©λ‘μ„_μ‘°νšŒν•œλ‹€(둜그인_정보); + for (PomodoroStudyResponse μŠ€ν„°λ””_정보 : νšŒμ›μœΌλ‘œ_μ™„λ£Œν•œ_μŠ€ν„°λ””_λͺ©λ‘) { + μŠ€ν„°λ””_μ’…λ£Œ_ν›„_κ²°κ³Ό_쑰회(둜그인_정보, μŠ€ν„°λ””_정보.studyId()); + } + } + + @Test + void λΉ„νšŒμ›μœΌλ‘œ_μŠ€ν„°λ””λ₯Ό_μ§„ν–‰ν•œλ‹€() throws Exception { + LoginResponse 둜그인_정보 = λΉ„νšŒμ›_λ‘œκ·ΈμΈμ„_μ§„ν–‰ν•œλ‹€(); + String μ°Έμ—¬_μ½”λ“œ = μŠ€ν„°λ””λ₯Ό_κ°œμ„€ν•œλ‹€(둜그인_정보); + Long μŠ€ν„°λ””_아이디 = μŠ€ν„°λ””λ₯Ό_μ‘°νšŒν•œλ‹€(둜그인_정보, μ°Έμ—¬_μ½”λ“œ); + Long 진행도_아이디 = μŠ€ν„°λ””μ—_μ°Έμ—¬ν•œλ‹€(둜그인_정보, μŠ€ν„°λ””_아이디); + μŠ€ν„°λ””_κ³„νšμ„_μž‘μ„±ν•œλ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μƒνƒœλ₯Ό_λ‹€μŒ_λ‹¨κ³„λ‘œ_λ„˜κΈ΄λ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μƒνƒœλ₯Ό_λ‹€μŒ_λ‹¨κ³„λ‘œ_λ„˜κΈ΄λ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_회고λ₯Ό_μž‘μ„±ν•œλ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μƒνƒœλ₯Ό_λ‹€μŒ_λ‹¨κ³„λ‘œ_λ„˜κΈ΄λ‹€(둜그인_정보, μŠ€ν„°λ””_아이디, 진행도_아이디); + μŠ€ν„°λ””_μ’…λ£Œ_ν›„_κ²°κ³Ό_쑰회(둜그인_정보, μŠ€ν„°λ””_아이디); + } + + private List νšŒμ›μœΌλ‘œ_μ§„ν–‰ν–ˆλ˜_λͺ¨λ“ _μŠ€ν„°λ””_λͺ©λ‘μ„_μ‘°νšŒν•œλ‹€(LoginResponse 둜그인_정보) + throws Exception { + long memberId = Long.parseLong(jwtTokenProvider + .parseSubject(둜그인_정보.tokenResponse().accessToken(), tokenConfig.secretKey())); + + MvcResult result = mockMvc.perform( + get("/api/studies") + .param("memberId", String.valueOf(memberId)) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + PomodoroStudiesResponse pomodoroStudiesResponse = objectMapper.readValue(jsonResponse, + PomodoroStudiesResponse.class); + + return pomodoroStudiesResponse.studies(); + } + + private LoginResponse λΉ„νšŒμ›_λ‘œκ·ΈμΈμ„_μ§„ν–‰ν•œλ‹€() throws Exception { + MvcResult result = mockMvc.perform(post("/api/auth/guest")) + .andExpect(status().isOk()) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + TokenResponse tokenResponse = objectMapper.readValue(jsonResponse, TokenResponse.class); + return new LoginResponse(tokenResponse, null); + } + + public LoginResponse ꡬ글_λ‘œκ·ΈμΈμ„_μ§„ν–‰ν•œλ‹€() throws Exception { + OauthLoginRequest request = new OauthLoginRequest("google", "oauthLoginCode"); + String jsonRequest = objectMapper.writeValueAsString(request); + + given(googleOauthClient.requestOauthToken(any(String.class), any(OauthProperty.class))) + .willReturn(new OauthTokenResponse("mock-token-type", "mock-access-token", + "mock-scope")); + given(googleOauthClient.requestOauthUserInfo(any(OauthProperty.class), any(String.class))) + .willReturn(Map.of("name", "mock-name", "email", "mock-email", "picture", + "mock-picture")); + + MvcResult result = mockMvc.perform( + post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + Cookie refreshToken = result.getResponse().getCookie("refreshToken"); + TokenResponse tokenResponse = objectMapper.readValue(jsonResponse, TokenResponse.class); + return new LoginResponse(tokenResponse, refreshToken); + } + + private String μŠ€ν„°λ””λ₯Ό_κ°œμ„€ν•œλ‹€(LoginResponse 둜그인_정보) throws Exception { + CreatePomodoroStudyRequest request = new CreatePomodoroStudyRequest("studyName", 1, 20); + String jsonRequest = objectMapper.writeValueAsString(request); + MvcResult result = mockMvc.perform( + post("/api/studies") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isCreated()) + .andReturn(); + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + CreatePomodoroStudyResponse response = objectMapper.readValue(jsonResponse, + CreatePomodoroStudyResponse.class); + return response.participantCode(); + } + + private Long μŠ€ν„°λ””λ₯Ό_μ‘°νšŒν•œλ‹€(LoginResponse 둜그인_정보, String μ°Έμ—¬_μ½”λ“œ) throws Exception { + MvcResult result = mockMvc.perform( + get("/api/studies") + .param("participantCode", μ°Έμ—¬_μ½”λ“œ) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + PomodoroStudiesResponse responses = objectMapper.readValue(jsonResponse, + PomodoroStudiesResponse.class); + PomodoroStudyResponse response = responses.studies().get(0); + return response.studyId(); + } + + private Long μŠ€ν„°λ””μ—_μ°Έμ—¬ν•œλ‹€(LoginResponse 둜그인_정보, Long μŠ€ν„°λ””_아이디) throws Exception { + Long memberId = Long.valueOf(jwtTokenProvider + .parseSubject(둜그인_정보.tokenResponse().accessToken(), tokenConfig.secretKey())); + ParticipateStudyRequest request = new ParticipateStudyRequest(memberId, "nickname"); + String jsonRequest = objectMapper.writeValueAsString(request); + + MvcResult result = mockMvc.perform( + post("/api/studies/{studyId}/progresses", μŠ€ν„°λ””_아이디) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isCreated()) + .andReturn(); + + String location = result.getResponse().getHeader("Location"); + String[] split = location.split("/"); + String progressId = split[split.length - 1]; + + return Long.valueOf(progressId); + } + + private void μŠ€ν„°λ””_κ³„νšμ„_μž‘μ„±ν•œλ‹€(LoginResponse 둜그인_정보, Long μŠ€ν„°λ””_아이디, Long 진행도_아이디) throws Exception { + WritePlanRequest request = new WritePlanRequest(진행도_아이디, Map.of("plan", "test")); + String jsonRequest = objectMapper.writeValueAsString(request); + + mockMvc.perform(post("/api/studies/{studyId}/contents/write-plan", + μŠ€ν„°λ””_아이디) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isOk()); + } + + private void μŠ€ν„°λ””_μƒνƒœλ₯Ό_λ‹€μŒ_λ‹¨κ³„λ‘œ_λ„˜κΈ΄λ‹€(LoginResponse 둜그인_정보, Long μŠ€ν„°λ””_아이디, Long 진행도_아이디) + throws Exception { + mockMvc.perform(post("/api/studies/{studyId}/progresses/{progressId}/next-step", + μŠ€ν„°λ””_아이디, 진행도_아이디) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isNoContent()); + } + + private void μŠ€ν„°λ””_회고λ₯Ό_μž‘μ„±ν•œλ‹€(LoginResponse 둜그인_정보, Long μŠ€ν„°λ””_아이디, Long 진행도_아이디) throws Exception { + WriteRetrospectRequest request = new WriteRetrospectRequest(진행도_아이디, + Map.of("retrospect", "test")); + String jsonRequest = objectMapper.writeValueAsString(request); + + mockMvc.perform(post("/api/studies/{studyId}/contents/write-retrospect", + μŠ€ν„°λ””_아이디) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isOk()); + } + + private void μŠ€ν„°λ””_μ’…λ£Œ_ν›„_κ²°κ³Ό_쑰회(LoginResponse 둜그인_정보, Long μŠ€ν„°λ””_아이디) throws Exception { + MvcResult result = mockMvc.perform(get("/api/studies/{studyId}/progresses", μŠ€ν„°λ””_아이디) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, 둜그인_정보.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + String response = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + PomodoroProgressesResponse jsonResponse = objectMapper.readValue(response, + PomodoroProgressesResponse.class); + + assertThat(jsonResponse.progresses()).hasSize(1); + } +} diff --git a/backend/src/test/java/harustudy/backend/auth/AuthArgumentResolverTest.java b/backend/src/test/java/harustudy/backend/auth/AuthArgumentResolverTest.java new file mode 100644 index 00000000..bcc70182 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/auth/AuthArgumentResolverTest.java @@ -0,0 +1,61 @@ +package harustudy.backend.auth; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.service.AuthService; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.method.support.ModelAndViewContainer; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@SpringBootTest +class AuthArgumentResolverTest { + + @Autowired + private AuthArgumentResolver authArgumentResolver; + + @MockBean + private AuthService authService; + + @Mock + private MethodParameter methodParameter; + @Mock + private ModelAndViewContainer modelAndViewContainer; + @Mock + private WebDataBinderFactory webDataBinderFactory; + + @Test + void μ•‘μ„ΈμŠ€_ν† ν°μ˜_νŒŒμ‹±λœ_아이디에_ν•΄λ‹Ήν•˜λŠ”_인증_멀버가_λ°˜ν™˜λœλ‹€() { + // given + String accessToken = "access-token"; + String mockedAuthMemberId = "1"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + NativeWebRequest nativeWebRequest = new ServletWebRequest(request); + + given(authService.parseMemberId(any(String.class))) + .willReturn(mockedAuthMemberId); + + // when + AuthMember authMember = authArgumentResolver.resolveArgument(methodParameter, + modelAndViewContainer, nativeWebRequest, webDataBinderFactory); + + // then + assertThat(authMember.id()).isEqualTo(Long.valueOf(mockedAuthMemberId)); + } +} diff --git a/backend/src/test/java/harustudy/backend/auth/AuthInterceptorTest.java b/backend/src/test/java/harustudy/backend/auth/AuthInterceptorTest.java new file mode 100644 index 00000000..9f1b428f --- /dev/null +++ b/backend/src/test/java/harustudy/backend/auth/AuthInterceptorTest.java @@ -0,0 +1,64 @@ +package harustudy.backend.auth; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.times; + +import harustudy.backend.auth.service.AuthService; +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.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@SpringBootTest +class AuthInterceptorTest { + + @Autowired + private AuthInterceptor authInterceptor; + + @MockBean + private AuthService authService; + + @Test + void preflight_μš”μ²­μ‹œ_μ˜ˆμ™Έλ₯Ό_λ˜μ§€μ§€_μ•ŠλŠ”λ‹€() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.setMethod(HttpMethod.OPTIONS.name()); + Object handler = new Object(); + + // when, then + assertThatCode(() -> authInterceptor.preHandle(request, response, handler)) + .doesNotThrowAnyException(); + } + + @Test + void μ•‘μ„ΈμŠ€_토큰_검증을_μˆ˜ν–‰ν•œλ‹€() throws Exception { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + Object handler = new Object(); + request.addHeader("Authorization", "Bearer access-token"); + + willDoNothing() + .given(authService) + .validateAccessToken(any(String.class)); + + // when + authInterceptor.preHandle(request, response, handler); + + // then + then(authService) + .should(times(1)) + .validateAccessToken(any(String.class)); + } +} diff --git a/backend/src/test/java/harustudy/backend/auth/service/AuthServiceTest.java b/backend/src/test/java/harustudy/backend/auth/service/AuthServiceTest.java new file mode 100644 index 00000000..d27e556a --- /dev/null +++ b/backend/src/test/java/harustudy/backend/auth/service/AuthServiceTest.java @@ -0,0 +1,112 @@ +package harustudy.backend.auth.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +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.infrastructure.GoogleOauthClient; +import harustudy.backend.member.domain.LoginType; +import harustudy.backend.member.domain.Member; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.Map; +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.boot.test.mock.mockito.MockBean; +import org.springframework.transaction.annotation.Transactional; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@Transactional +@SpringBootTest +class AuthServiceTest { + + @Autowired + private OauthLoginFacade oauthLoginFacade; + + @Autowired + private AuthService authService; + + @Autowired + private TokenConfig tokenConfig; + + @PersistenceContext + private EntityManager entityManager; + + @MockBean + private GoogleOauthClient googleOauthClient; + + @Test + void ꡬ글_λ‘œκ·ΈμΈμ‹œ_멀버가_μ €μž₯되고_멀버_μ•„μ΄λ””μ˜_μ•‘μ„ΈμŠ€_토큰과_κ°±μ‹ _토큰을_λ°˜ν™˜ν•œλ‹€() { + // given + OauthLoginRequest request = new OauthLoginRequest("google", "google-code"); + UserInfo userInfo = new UserInfo("test", "test@test.com", "test.png"); + + given(googleOauthClient.requestOauthToken(any(String.class), any(OauthProperty.class))) + .willReturn(new OauthTokenResponse("Bearer", "google-access-token", "scope")); + given(googleOauthClient.requestOauthUserInfo(any(OauthProperty.class), any(String.class))) + .willReturn(Map.of("name", userInfo.name(), "email", userInfo.email(), "picture", + userInfo.imageUrl())); + + // when + TokenResponse response = oauthLoginFacade.oauthLogin(request); + + // then + String memberId = authService.parseMemberId(response.accessToken()); + Member foundMember = entityManager.find(Member.class, memberId); + + assertSoftly(softly -> { + softly.assertThat(response.accessToken()).isNotNull(); + softly.assertThat(response.refreshToken()).isNotNull(); + softly.assertThat(foundMember.getName()).isEqualTo(userInfo.name()); + softly.assertThat(foundMember.getEmail()).isEqualTo(userInfo.email()); + softly.assertThat(foundMember.getImageUrl()).isEqualTo(userInfo.imageUrl()); + softly.assertThat(foundMember.getLoginType()).isEqualTo(LoginType.GOOGLE); + }); + } + + @Test + void 게슀트_λ‘œκ·ΈμΈμ‹œ_멀버가_μ €μž₯되고_멀버_μ•„μ΄λ””μ˜_μ•‘μ„ΈμŠ€_토큰을_λ°˜ν™˜ν•œλ‹€() { + // when + TokenResponse response = authService.guestLogin(); + + // then + String memberId = authService.parseMemberId(response.accessToken()); + Member foundMember = entityManager.find(Member.class, memberId); + + assertSoftly(softly -> { + softly.assertThat(response.accessToken()).isNotNull(); + softly.assertThat(foundMember).isNotNull(); + }); + } + + @Test + void κ°±μ‹ _토큰을_κ°±μ‹ ν•œλ‹€() { + // given + Member member = new Member("test", "test@test.com", "test.png", LoginType.GOOGLE); + RefreshToken refreshToken = new RefreshToken(member, + tokenConfig.refreshTokenExpireLength()); + + entityManager.persist(member); + entityManager.persist(refreshToken); + entityManager.flush(); + entityManager.clear(); + + // when + TokenResponse response = authService.refresh(refreshToken.getUuid().toString()); + + // then + assertThat(response.refreshToken()).isNotEqualTo(refreshToken.getUuid()); + } +} diff --git a/backend/src/test/java/harustudy/backend/auth/util/BearerAuthorizationParserTest.java b/backend/src/test/java/harustudy/backend/auth/util/BearerAuthorizationParserTest.java new file mode 100644 index 00000000..2c3a48f7 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/auth/util/BearerAuthorizationParserTest.java @@ -0,0 +1,53 @@ +package harustudy.backend.auth.util; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import harustudy.backend.auth.exception.InvalidAuthorizationHeaderException; +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; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@SpringBootTest +class BearerAuthorizationParserTest { + + @Autowired + private BearerAuthorizationParser bearerAuthorizationParser; + + @Test + void 인증_ν—€λ”μ—μ„œ_μ•‘μ„ΈμŠ€_토큰을_νŒŒμ‹±ν•œλ‹€() { + // given + String tokenType = "Bearer"; + String accessToken = "access-token"; + String authorizationHeader = tokenType + " " + accessToken; + + // when + String parsed = bearerAuthorizationParser.parse(authorizationHeader); + + // then + assertThat(parsed).isEqualTo(accessToken); + } + + @Test + void 인증_헀더가_μ—†μœΌλ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given, when, then + assertThatThrownBy(() -> bearerAuthorizationParser.parse(null)) + .isInstanceOf(InvalidAuthorizationHeaderException.class); + } + + @Test + void 인증_ν—€λ”μ˜_ν˜•μ‹μ΄_μ˜¬λ°”λ₯΄μ§€_μ•ŠμœΌλ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given + String tokenType = "Basic"; + String email = "haru-study@harustudy.com:harustudy"; + String authorizationHeader = tokenType + " " + email; + + // when, then + assertThatThrownBy(() -> bearerAuthorizationParser.parse(authorizationHeader)) + .isInstanceOf(InvalidAuthorizationHeaderException.class); + } +} diff --git a/backend/src/test/java/harustudy/backend/auth/util/JwtTokenProviderTest.java b/backend/src/test/java/harustudy/backend/auth/util/JwtTokenProviderTest.java new file mode 100644 index 00000000..7e965f18 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/auth/util/JwtTokenProviderTest.java @@ -0,0 +1,50 @@ +package harustudy.backend.auth.util; + + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import harustudy.backend.auth.config.TokenConfig; +import io.jsonwebtoken.JwtException; +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; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@SpringBootTest +class JwtTokenProviderTest { + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private TokenConfig tokenConfig; + + @Test + void μ•‘μ„ΈμŠ€_토큰을_μƒμ„±ν•œλ‹€() { + // given + String subject = "1L"; + + // when + String accessToken = jwtTokenProvider.createAccessToken(subject, + tokenConfig.accessTokenExpireLength(), tokenConfig.secretKey()); + + // then + assertThat(jwtTokenProvider.parseSubject(accessToken, tokenConfig.secretKey())).isEqualTo( + subject); + } + + @Test + void μ˜¬λ°”λ₯΄μ§€_μ•Šμ€_μ•‘μ„ΈμŠ€_토큰을_κ²€μ¦ν•˜λ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given + String invalidAccessToken = "invalid-access-token"; + + // when, then + assertThatThrownBy(() -> jwtTokenProvider.validateAccessToken(invalidAccessToken, + tokenConfig.secretKey())) + .isInstanceOf(JwtException.class); + } +} diff --git a/backend/src/test/java/harustudy/backend/auth/util/OauthUserInfoExtractorTest.java b/backend/src/test/java/harustudy/backend/auth/util/OauthUserInfoExtractorTest.java new file mode 100644 index 00000000..8bd158dd --- /dev/null +++ b/backend/src/test/java/harustudy/backend/auth/util/OauthUserInfoExtractorTest.java @@ -0,0 +1,54 @@ +package harustudy.backend.auth.util; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import harustudy.backend.auth.dto.UserInfo; +import harustudy.backend.auth.exception.InvalidProviderNameException; +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +class OauthUserInfoExtractorTest { + + @Test + void ꡬ글_ν”„λ‘œλ°”μ΄λ”μ˜_μ‚¬μš©μž_정보λ₯Ό_μΆ”μΆœν•œλ‹€() { + // given + String providerName = "google"; + UserInfo userInfo = new UserInfo("test", "test@test.com", "test.png"); + Map attributes = Map.of( + "name", userInfo.name(), + "email", userInfo.email(), + "picture", userInfo.imageUrl() + ); + + // when + UserInfo result = OauthUserInfoExtractor.extract(providerName, attributes); + + // then + assertSoftly(softly -> { + softly.assertThat(result.name()).isEqualTo(userInfo.name()); + softly.assertThat(result.email()).isEqualTo(userInfo.email()); + softly.assertThat(result.imageUrl()).isEqualTo(userInfo.imageUrl()); + }); + } + + @Test + void μ˜¬λ°”λ₯΄μ§€_μ•Šμ€_ν”„λ‘œλ°”μ΄λ”_이름은_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given + String providerName = "invalidProviderName"; + UserInfo userInfo = new UserInfo("test", "test@test.com", "test.png"); + Map attributes = Map.of( + "name", userInfo.name(), + "email", userInfo.email(), + "picture", userInfo.imageUrl() + ); + + // when, then + assertThatThrownBy(() -> OauthUserInfoExtractor.extract(providerName, attributes)) + .isInstanceOf(InvalidProviderNameException.class); + } +} 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 53d9cdd5..a3afad58 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,88 @@ -//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); -// } -//} +package harustudy.backend.content.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import harustudy.backend.content.domain.PomodoroContent; +import harustudy.backend.member.domain.LoginType; +import harustudy.backend.member.domain.Member; +import harustudy.backend.progress.domain.PomodoroProgress; +import harustudy.backend.study.domain.PomodoroStudy; +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() { + PomodoroStudy pomodoroStudy = new PomodoroStudy("studyName", 3, 20); + Member member = new Member("member", "email", "imageUrl", LoginType.GUEST); + pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); + + testEntityManager.persist(pomodoroStudy); + 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); + testEntityManager.persist(pomodoroContent); + + testEntityManager.flush(); + testEntityManager.clear(); + + // when + 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); + testEntityManager.persist(pomodoroContent); + + testEntityManager.flush(); + testEntityManager.clear(); + + // when + 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 70b52f97..45d17a60 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,236 @@ -//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(); -// } -//} +package harustudy.backend.content.service; + +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.auth.dto.AuthMember; +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.LoginType; +import harustudy.backend.member.domain.Member; +import harustudy.backend.progress.domain.PomodoroProgress; +import harustudy.backend.progress.exception.PomodoroProgressNotFoundException; +import harustudy.backend.progress.exception.PomodoroProgressStatusException; +import harustudy.backend.study.domain.PomodoroStudy; +import harustudy.backend.study.exception.StudyNotFoundException; +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 PomodoroContentService pomodoroContentService; + + private PomodoroStudy pomodoroStudy; + private Member member; + private PomodoroProgress pomodoroProgress; + private PomodoroContent pomodoroContent; + + @BeforeEach + void setUp() { + pomodoroStudy = new PomodoroStudy("studyName", 1, 20); + member = new Member("nickname", "email", "imageUrl", LoginType.GUEST); + pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); + pomodoroContent = new PomodoroContent(pomodoroProgress, 1); + + entityManager.persist(pomodoroStudy); + entityManager.persist(member); + entityManager.persist(pomodoroProgress); + entityManager.persist(pomodoroContent); + FLUSH_AND_CLEAR_CONTEXT(); + } + + void FLUSH_AND_CLEAR_CONTEXT() { + entityManager.flush(); + entityManager.clear(); + } + + @Test + void κ³„νš_단계가_아닐_λ•Œ_κ³„νšμ„_μž‘μ„±ν•˜λ €_ν•˜λ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + + pomodoroProgress.proceed(); + entityManager.merge(pomodoroProgress); + + WritePlanRequest request = new WritePlanRequest(pomodoroProgress.getId(), + Map.of("plan", "abc")); + + // when, then + assertThatThrownBy( + () -> pomodoroContentService.writePlan(authMember, pomodoroStudy.getId(), request)) + .isInstanceOf(PomodoroProgressStatusException.class); + } + + @Test + void κ³„νš_λ‹¨κ³„μ—μ„œλŠ”_κ³„νšμ„_μž‘μ„±ν• _수_μžˆλ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + WritePlanRequest request = new WritePlanRequest(pomodoroProgress.getId(), + Map.of("plan", "abc")); + + // when, then + assertThatCode( + () -> pomodoroContentService.writePlan(authMember, pomodoroStudy.getId(), request)) + .doesNotThrowAnyException(); + } + + @Test + void κ³„νšμ΄_μž‘μ„±λ˜μ–΄_μžˆμ§€_μ•Šμ€_경우_회고λ₯Ό_μž‘μ„±ν•˜λ €_ν•˜λ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + WriteRetrospectRequest request = new WriteRetrospectRequest(pomodoroProgress.getId(), + Map.of("retrospect", "abc")); + + // when, then + assertThatThrownBy( + () -> pomodoroContentService.writeRetrospect(authMember, pomodoroStudy.getId(), + request)) + .isInstanceOf(PomodoroProgressStatusException.class); + } + + @Test + void 회고_μž‘μ„±_단계가_아닐_λ•Œ_회고λ₯Ό_μž‘μ„±ν•˜λ €_ν•˜λ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + WriteRetrospectRequest request = new WriteRetrospectRequest(pomodoroProgress.getId(), + Map.of("retrospect", "abc")); + + // when, then + assertThatThrownBy( + () -> pomodoroContentService.writeRetrospect(authMember, pomodoroStudy.getId(), + request)) + .isInstanceOf(PomodoroProgressStatusException.class); + } + + @Test + void 회고_λ‹¨κ³„μ—μ„œλŠ”_회고λ₯Ό_μž‘μ„±ν• _수_μžˆλ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + pomodoroContent.changePlan(Map.of("plan", "abc")); + + pomodoroProgress.proceed(); + pomodoroProgress.proceed(); + + entityManager.merge(pomodoroProgress); + entityManager.merge(pomodoroContent); + FLUSH_AND_CLEAR_CONTEXT(); + + WriteRetrospectRequest request = new WriteRetrospectRequest(pomodoroProgress.getId(), + Map.of("retrospect", "abc")); + + // when, then + assertThatCode( + () -> pomodoroContentService.writeRetrospect(authMember, pomodoroStudy.getId(), + request)) + .doesNotThrowAnyException(); + } + + @Test + void μŠ€ν„°λ””μ—_μ°Έμ—¬ν•œ_μŠ€ν„°λ””μ›μ˜_νŠΉμ •_μ‚¬μ΄ν΄μ˜_μ½˜ν…μΈ λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + Map plan = Map.of("plan", "abc"); + Map retrospect = Map.of("retrospect", "abc"); + pomodoroContent.changePlan(plan); + pomodoroContent.changeRetrospect(retrospect); + + entityManager.merge(pomodoroContent); + FLUSH_AND_CLEAR_CONTEXT(); + + // when + PomodoroContentsResponse pomodoroContentsResponse = + pomodoroContentService.findContentsWithFilter(authMember, pomodoroStudy.getId(), + pomodoroProgress.getId(), 1); + PomodoroContentResponse expectedPomodoroContentResponse = + new PomodoroContentResponse(1, plan, retrospect); + List content = pomodoroContentsResponse.content(); + + // then + assertThat(content).containsExactly(expectedPomodoroContentResponse); + } + + @Test + void μŠ€ν„°λ””μ—_μ°Έμ—¬ν•œ_μŠ€ν„°λ””μ›μ˜_λͺ¨λ“ _μ‚¬μ΄ν΄μ˜_μ½˜ν…μΈ λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + + pomodoroProgress.proceed(); + pomodoroProgress.proceed(); + pomodoroProgress.proceed(); + pomodoroProgress.proceed(); + + 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.merge(pomodoroProgress); + entityManager.merge(pomodoroContent); + entityManager.persist(anotherPomodoroContent); + FLUSH_AND_CLEAR_CONTEXT(); + + // when + PomodoroContentsResponse pomodoroContentsResponse = pomodoroContentService.findContentsWithFilter(authMember, + pomodoroStudy.getId(), pomodoroProgress.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 + AuthMember authMember = new AuthMember(member.getId()); + + // when, then + assertThatThrownBy( + () -> pomodoroContentService.findContentsWithFilter(authMember, 999L, + pomodoroProgress.getId(), null)) + .isInstanceOf(StudyNotFoundException.class); + } + + @Test + void μŠ€ν„°λ””μ—_μ°Έμ—¬ν•œ_νŠΉμ •_μŠ€ν„°λ””μ›μ˜_μ½˜ν…μΈ λ₯Ό_쑰회_μ‹œ_진행도가_μ—†μœΌλ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { + // given + AuthMember authMember = new AuthMember(member.getId()); + + // when, then + assertThatThrownBy( + () -> pomodoroContentService.findContentsWithFilter(authMember, + pomodoroStudy.getId(), 999L, null)) + .isInstanceOf(PomodoroProgressNotFoundException.class); + } +} diff --git a/backend/src/test/java/harustudy/backend/integration/AuthIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/AuthIntegrationTest.java new file mode 100644 index 00000000..722971b1 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/integration/AuthIntegrationTest.java @@ -0,0 +1,90 @@ +package harustudy.backend.integration; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import harustudy.backend.auth.dto.TokenResponse; +import jakarta.servlet.http.Cookie; +import java.nio.charset.StandardCharsets; +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.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MvcResult; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class AuthIntegrationTest extends IntegrationTest { + + @BeforeEach + void setUp() { + super.setUp(); + } + + @Test + void ꡬ글_λ‘œκ·ΈμΈμ„_ν•œλ‹€() throws Exception { + // TODO : μ‹€μ œ ꡬ글 id둜 Oauth κΈ°λŠ₯ ν…ŒμŠ€νŠΈν•˜κΈ° + // given, when + LoginResponse response = ꡬ글_둜그인("member1"); + + // then + assertSoftly(softly -> { + softly.assertThat(response.tokenResponse().accessToken()).isNotNull(); + softly.assertThat(response.cookie().getValue()).isNotNull(); + softly.assertThat(response.cookie().getName()).isEqualTo("refreshToken"); + }); + } + + @Test + void λΉ„νšŒμ›_λ‘œκ·ΈμΈμ„_ν•œλ‹€() throws Exception { + // given, when + MockHttpServletResponse response = mockMvc.perform( + post("/api/auth/guest")) + .andExpect(status().isOk()) + .andReturn() + .getResponse(); + + String jsonResponse = response.getContentAsString(StandardCharsets.UTF_8); + TokenResponse tokenResponse = objectMapper.readValue(jsonResponse, TokenResponse.class); + + // then + assertSoftly(softly -> { + softly.assertThat(tokenResponse.accessToken()).isNotNull(); + softly.assertThat(response.getCookie("refreshToken")).isNull(); + }); + } + + @Test + void μ•‘μ„ΈμŠ€_토큰을_κ°±μ‹ ν•œλ‹€() throws Exception { + // given + MemberDto memberDto1 = createMember("member1"); + + // access token을 μž¬λ°œκΈ‰ ν•˜λ”λΌλ„ DateλŠ” 초 λ‹¨μœ„μ˜ μ‹œκ°„ 정보λ₯Ό 담은 μ•‘μ„ΈμŠ€ 토큰을 μƒμ„±ν•˜κΈ° λ•Œλ¬Έμ— + // 같은 access token이 λ§Œλ“€μ–΄μ§€λŠ” λ¬Έμ œκ°€ μžˆμ–΄μ„œ κ°±μ‹ λœλ‹€λŠ” 것을 κ²€μ¦ν•˜κΈ° μœ„ν•΄ μ‚¬μš© + Thread.sleep(1000); + + // when + MvcResult result = mockMvc.perform( + post("/api/auth/refresh") + .contentType(MediaType.APPLICATION_JSON) + .cookie(memberDto1.cookie())) + .andExpect(status().isOk()) + .andReturn(); + + // then + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + Cookie refreshToken = result.getResponse().getCookie("refreshToken"); + TokenResponse response = objectMapper.readValue(jsonResponse, TokenResponse.class); + + assertSoftly(softly -> { + softly.assertThat(response.accessToken()).isNotNull(); + softly.assertThat(response.accessToken()).isNotEqualTo(memberDto1.accessToken()); + softly.assertThat(refreshToken).isNotNull(); + softly.assertThat(refreshToken.getName()).isEqualTo("refreshToken"); + softly.assertThat(refreshToken.getValue()).isNotEqualTo(memberDto1.cookie().getValue()); + }); + } +} diff --git a/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java index 993bb811..626bcb87 100644 --- a/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/IntegrationTest.java @@ -1,42 +1,120 @@ -//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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.infrastructure.GoogleOauthClient; +import harustudy.backend.auth.repository.RefreshTokenRepository; +import harustudy.backend.auth.util.JwtTokenProvider; +import harustudy.backend.member.domain.LoginType; +import harustudy.backend.member.domain.Member; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.servlet.http.Cookie; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +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.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +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) +@AutoConfigureMockMvc +@Transactional +@SpringBootTest +class IntegrationTest { + + @PersistenceContext + protected EntityManager entityManager; + + @Autowired + protected ObjectMapper objectMapper; + + protected MockMvc mockMvc; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private TokenConfig tokenConfig; + + @MockBean + private GoogleOauthClient googleOauthClient; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + 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(); + } + + public LoginResponse ꡬ글_둜그인(String name) throws Exception { + OauthLoginRequest request = new OauthLoginRequest("google", "oauthLoginCode"); + String jsonRequest = objectMapper.writeValueAsString(request); + + given(googleOauthClient.requestOauthToken(any(String.class), any(OauthProperty.class))) + .willReturn(new OauthTokenResponse("mock-token-type", "mock-access-token", + "mock-scope")); + given(googleOauthClient.requestOauthUserInfo(any(OauthProperty.class), any(String.class))) + .willReturn(Map.of("name", name, "email", "mock-email", "picture", "mock-picture")); + + MvcResult result = mockMvc.perform( + post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest)) + .andReturn(); + + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + Cookie refreshToken = result.getResponse().getCookie("refreshToken"); + TokenResponse tokenResponse = objectMapper.readValue(jsonResponse, TokenResponse.class); + return new LoginResponse(tokenResponse, refreshToken); + } + + private RefreshToken saveRefreshTokenOf(Member member) { + RefreshToken refreshToken = new RefreshToken(member, + tokenConfig.refreshTokenExpireLength()); + entityManager.persist(refreshToken); + return refreshToken; + } + + protected MemberDto createMember(String name) { + Member member = new Member(name, "email", "imageUrl", LoginType.GOOGLE); + entityManager.persist(member); + String accessToken = jwtTokenProvider.createAccessToken(String.valueOf(member.getId()), + tokenConfig.accessTokenExpireLength(), tokenConfig.secretKey()); + RefreshToken refreshToken = new RefreshToken(member, + tokenConfig.refreshTokenExpireLength()); + entityManager.persist(refreshToken); + Cookie cookie = new Cookie("refreshToken", refreshToken.getUuid().toString()); + return new MemberDto(member, accessToken, cookie); + } +} diff --git a/backend/src/test/java/harustudy/backend/integration/LoginResponse.java b/backend/src/test/java/harustudy/backend/integration/LoginResponse.java new file mode 100644 index 00000000..d7b603c8 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/integration/LoginResponse.java @@ -0,0 +1,11 @@ +package harustudy.backend.integration; + +import harustudy.backend.auth.dto.TokenResponse; +import jakarta.servlet.http.Cookie; + +public record LoginResponse(TokenResponse tokenResponse, Cookie cookie) { + + public String createAuthorizationHeader() { + return "Bearer " + tokenResponse.accessToken(); + } +} diff --git a/backend/src/test/java/harustudy/backend/integration/MemberDto.java b/backend/src/test/java/harustudy/backend/integration/MemberDto.java new file mode 100644 index 00000000..20878153 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/integration/MemberDto.java @@ -0,0 +1,11 @@ +package harustudy.backend.integration; + +import harustudy.backend.member.domain.Member; +import jakarta.servlet.http.Cookie; + +public record MemberDto(Member member, String accessToken, Cookie cookie) { + + public String createAuthorizationHeader() { + return "Bearer " + accessToken(); + } +} diff --git a/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java index dc2ebd83..90a3fa07 100644 --- a/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java @@ -1,140 +1,60 @@ -//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(); -// }); -// } -//} +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.result.MockMvcResultMatchers.status; + +import harustudy.backend.member.dto.MemberResponse; +import harustudy.backend.progress.domain.PomodoroProgress; +import harustudy.backend.study.domain.PomodoroStudy; +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.http.HttpHeaders; +import org.springframework.test.web.servlet.MvcResult; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +class MemberIntegrationTest extends IntegrationTest { + + private PomodoroStudy study; + private MemberDto memberDto1; + private MemberDto memberDto2; + + @BeforeEach + void setUp() { + super.setUp(); + + study = new PomodoroStudy("studyName", 1, 20); + + memberDto1 = createMember("member1"); + memberDto2 = createMember("member2"); + + PomodoroProgress pomodoroProgress1 = new PomodoroProgress(study, memberDto1.member(), + "name1"); + PomodoroProgress pomodoroProgress2 = new PomodoroProgress(study, memberDto2.member(), + "name2"); + + entityManager.persist(study); + entityManager.persist(pomodoroProgress1); + entityManager.persist(pomodoroProgress2); + } + + @Test + void 멀버λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() throws Exception { + // given, when + MvcResult result = mockMvc.perform( + get("/api/me") + .header(HttpHeaders.AUTHORIZATION, memberDto1.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + // then + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + MemberResponse response = objectMapper.readValue(jsonResponse, MemberResponse.class); + + assertThat(response.memberId()).isEqualTo(memberDto1.member().getId()); + } +} diff --git a/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java index 25c179da..721337c3 100644 --- a/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/PomodoroContentIntegrationTest.java @@ -1,170 +1,167 @@ -//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); -// } -//} +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.progress.domain.PomodoroProgress; +import harustudy.backend.study.domain.PomodoroStudy; +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.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +public class PomodoroContentIntegrationTest extends IntegrationTest { + + private PomodoroStudy pomodoroStudy; + private MemberDto memberDto; + + private PomodoroProgress pomodoroProgress; + private PomodoroContent pomodoroContent; + + @BeforeEach + void setUp() { + super.setUp(); + + pomodoroStudy = new PomodoroStudy("studyName", 2, 20); + memberDto = createMember("member1"); + pomodoroProgress = new PomodoroProgress(pomodoroStudy, memberDto.member(), "nickname"); + pomodoroContent = new PomodoroContent(pomodoroProgress, 1); + + entityManager.persist(pomodoroStudy); + entityManager.persist(pomodoroProgress); + entityManager.persist(pomodoroContent); + + FLUSH_AND_CLEAR_CONTEXT(); + } + + @Test + void κ³„νšμ„_μž‘μ„±ν• _수_μžˆλ‹€() throws Exception { + // given + WritePlanRequest request = new WritePlanRequest(pomodoroProgress.getId(), + Map.of("plan", "test")); + String body = objectMapper.writeValueAsString(request); + + // when, then + mockMvc.perform( + post("/api/studies/{studyId}/contents/write-plan", pomodoroStudy.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .header(HttpHeaders.AUTHORIZATION, memberDto.createAuthorizationHeader())) + .andExpect(status().isOk()); + } + + @Test + void 회고λ₯Ό_μž‘μ„±ν• _수_μžˆλ‹€() throws Exception { + // given + pomodoroContent.changePlan(Map.of("plan", "test")); + pomodoroProgress.proceed(); + pomodoroProgress.proceed(); + + entityManager.merge(pomodoroProgress); + entityManager.merge(pomodoroContent); + FLUSH_AND_CLEAR_CONTEXT(); + + WriteRetrospectRequest request = new WriteRetrospectRequest(pomodoroProgress.getId(), + Map.of("retrospect", "test")); + String body = objectMapper.writeValueAsString(request); + + // when, then + mockMvc.perform( + post("/api/studies/{studyId}/contents/write-retrospect", pomodoroStudy.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(body) + .header(HttpHeaders.AUTHORIZATION, memberDto.createAuthorizationHeader())) + .andExpect(status().isOk()); + } + + @Test + void νŠΉμ •_μ‚¬μ΄ν΄μ˜_κ³„νš_및_회고λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() throws Exception { + // given + Map plan = Map.of("plan", "test"); + Map retrospect = Map.of("retrospect", "test"); + pomodoroContent.changePlan(plan); + pomodoroProgress.proceed(); + pomodoroProgress.proceed(); + pomodoroContent.changeRetrospect(retrospect); + pomodoroProgress.proceed(); + + entityManager.merge(pomodoroContent); + FLUSH_AND_CLEAR_CONTEXT(); + + // when + MvcResult result = mockMvc.perform( + get("/api/studies/{studyId}/contents", pomodoroStudy.getId()) + .param("progressId", String.valueOf(pomodoroProgress.getId())) + .param("memberId", String.valueOf(memberDto.member().getId())) + .param("cycle", String.valueOf(pomodoroContent.getCycle())) + .header(HttpHeaders.AUTHORIZATION, memberDto.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + // then + 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) + )); + + assertThat(response).isEqualTo(expected); + } + + @Test + void λͺ¨λ“ _μ‚¬μ΄ν΄μ˜_κ³„νš_및_회고λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() throws Exception { + // given + 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.merge(pomodoroContent); + entityManager.merge(pomodoroProgress); + entityManager.persist(anotherPomodoroContent); + FLUSH_AND_CLEAR_CONTEXT(); + + // when + MvcResult result = mockMvc.perform( + get("/api/studies/{studyId}/contents", pomodoroStudy.getId()) + .param("progressId", String.valueOf(pomodoroProgress.getId())) + .header(HttpHeaders.AUTHORIZATION, memberDto.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + // then + 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) + )); + + 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 ea4e569a..2ed2fb29 100644 --- a/backend/src/test/java/harustudy/backend/integration/PomodoroProgressIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/PomodoroProgressIntegrationTest.java @@ -1,84 +1,80 @@ -//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); -// } -//} +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.progress.domain.PomodoroProgress; +import harustudy.backend.progress.domain.PomodoroStatus; +import harustudy.backend.progress.dto.PomodoroProgressResponse; +import harustudy.backend.study.domain.PomodoroStudy; +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.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +class PomodoroProgressIntegrationTest extends IntegrationTest { + + private PomodoroStudy pomodoroStudy; + private PomodoroProgress pomodoroProgress; + private MemberDto memberDto; + + @BeforeEach + void setUp() { + super.setUp(); + pomodoroStudy = new PomodoroStudy("studyName", 3, 20); + memberDto = createMember("member1"); + pomodoroProgress = new PomodoroProgress(pomodoroStudy, memberDto.member(), "nickname"); + + entityManager.persist(pomodoroStudy); + entityManager.persist(pomodoroProgress); + + entityManager.flush(); + entityManager.clear(); + } + + @Test + void progressId둜_진행도λ₯Ό_μ‘°νšŒν•œλ‹€() throws Exception { + // given, when + MvcResult result = mockMvc.perform( + get("/api/studies/{studyId}/progresses/{progressId}", pomodoroStudy.getId(), + pomodoroProgress.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, memberDto.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + // then + String jsonResponse = result.getResponse().getContentAsString(); + PomodoroProgressResponse response = objectMapper.readValue(jsonResponse, + PomodoroProgressResponse.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/studies/{studyId}/progresses/{progressId}/next-step", + pomodoroStudy.getId(), pomodoroProgress.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, memberDto.createAuthorizationHeader())) + .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 deleted file mode 100644 index d62a41c0..00000000 --- a/backend/src/test/java/harustudy/backend/integration/PomodoroRoomIntegrationTest.java +++ /dev/null @@ -1,117 +0,0 @@ -//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/integration/PomodoroStudyIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/PomodoroStudyIntegrationTest.java new file mode 100644 index 00000000..e75d59ab --- /dev/null +++ b/backend/src/test/java/harustudy/backend/integration/PomodoroStudyIntegrationTest.java @@ -0,0 +1,177 @@ +package harustudy.backend.integration; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +class PomodoroStudyIntegrationTest extends IntegrationTest { + +// @Autowired +// private ObjectMapper objectMapper; +// +// @Autowired +// private EntityManager entityManager; +// +// private ParticipantCode participantCode1; +// private ParticipantCode participantCode2; +// private PomodoroStudy pomodoroStudy1; +// private PomodoroStudy pomodoroStudy2; +// private MemberDto memberDto1; +// +// @BeforeEach +// void setUp() { +// super.setMockMvc(); +// participantCode1 = new ParticipantCode(new CodeGenerationStrategy()); +// participantCode2 = new ParticipantCode(new CodeGenerationStrategy()); +// pomodoroStudy1 = new PomodoroStudy("study1", 3, 20, participantCode1); +// pomodoroStudy2 = new PomodoroStudy("study2", 4, 30, participantCode2); +// memberDto1 = createMember("member1"); +// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroStudy1, +// memberDto1.member(), "nickname"); +// +// entityManager.persist(participantCode1); +// entityManager.persist(participantCode2); +// entityManager.persist(pomodoroStudy1); +// entityManager.persist(pomodoroStudy2); +// entityManager.persist(pomodoroProgress1); +// +// entityManager.flush(); +// entityManager.clear(); +// } +// +// @Test +// void μŠ€ν„°λ””_μ•„μ΄λ””λ‘œ_μŠ€ν„°λ””λ₯Ό_μ‘°νšŒν•œλ‹€() throws Exception { +// // given +// Long studyId = pomodoroStudy1.getId(); +// +// // when +// MvcResult result = mockMvc.perform(get("/api/studies/{studyId}", studyId) +// .accept(MediaType.APPLICATION_JSON) +// .header(HttpHeaders.AUTHORIZATION, memberDto1.createAuthorizationHeader())) +// .andExpect(status().isOk()) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroStudyResponse response = objectMapper.readValue(jsonResponse, +// PomodoroStudyResponse.class); +// +// assertSoftly(softly -> { +// softly.assertThat(response.studyId()).isEqualTo(pomodoroStudy1.getId()); +// softly.assertThat(response.name()).isEqualTo(pomodoroStudy1.getName()); +// softly.assertThat(response.totalCycle()).isEqualTo(pomodoroStudy1.getTotalCycle()); +// softly.assertThat(response.timePerCycle()).isEqualTo(pomodoroStudy1.getTimePerCycle()); +// }); +// } +// +// @Test +// void μ°Έμ—¬μ½”λ“œλ‘œ_μŠ€ν„°λ””λ₯Ό_μ‘°νšŒν•œλ‹€() throws Exception { +// // given +// +// // when +// MvcResult result = mockMvc.perform(get("/api/studies") +// .param("participantCode", participantCode1.getCode()) +// .accept(MediaType.APPLICATION_JSON) +// .header(HttpHeaders.AUTHORIZATION, memberDto1.createAuthorizationHeader())) +// .andExpect(status().isOk()) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroStudyResponse response = objectMapper.readValue(jsonResponse, +// PomodoroStudyResponse.class); +// List studies = response.studies(); +// +// assertSoftly(softly -> { +// softly.assertThat(studies).hasSize(1); +// softly.assertThat(studies.get(0).studyId()).isEqualTo(pomodoroStudy1.getId()); +// softly.assertThat(studies.get(0).name()).isEqualTo(pomodoroStudy1.getName()); +// softly.assertThat(studies.get(0).totalCycle()).isEqualTo(pomodoroStudy1.getTotalCycle()); +// softly.assertThat(studies.get(0).timePerCycle()) +// .isEqualTo(pomodoroStudy1.getTimePerCycle()); +// }); +// } +// +// @Test +// void 멀버_μ•„μ΄λ””λ‘œ_μŠ€ν„°λ””λ₯Ό_μ‘°νšŒν•œλ‹€() throws Exception { +// // given, when +// MvcResult result = mockMvc.perform(get("/api/studies") +// .param("memberId", String.valueOf(memberDto1.member().getId())) +// .accept(MediaType.APPLICATION_JSON) +// .header(HttpHeaders.AUTHORIZATION, memberDto1.createAuthorizationHeader())) +// .andExpect(status().isOk()) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroStudyResponse response = objectMapper.readValue(jsonResponse, +// PomodoroStudyResponse.class); +// List studies = response.studies(); +// +// assertSoftly(softly -> { +// softly.assertThat(studies).hasSize(1); +// softly.assertThat(studies.get(0).studyId()).isEqualTo(pomodoroStudy1.getId()); +// softly.assertThat(studies.get(0).name()).isEqualTo(pomodoroStudy1.getName()); +// softly.assertThat(studies.get(0).totalCycle()).isEqualTo(pomodoroStudy1.getTotalCycle()); +// softly.assertThat(studies.get(0).timePerCycle()) +// .isEqualTo(pomodoroStudy1.getTimePerCycle()); +// }); +// } +// +// @Test +// void λͺ¨λ“ _μŠ€ν„°λ””λ₯Ό_μ‘°νšŒν•œλ‹€() throws Exception { +// // given, when +// MvcResult result = mockMvc.perform(get("/api/studies") +// .accept(MediaType.APPLICATION_JSON) +// .header(HttpHeaders.AUTHORIZATION, memberDto1.createAuthorizationHeader())) +// .andExpect(status().isOk()) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// PomodoroStudyResponse response = objectMapper.readValue(jsonResponse, +// PomodoroStudyResponse.class); +// List studies = response.studies(); +// +// assertSoftly(softly -> { +// softly.assertThat(studies).hasSize(2); +// softly.assertThat(studies.get(0).studyId()).isEqualTo(pomodoroStudy1.getId()); +// softly.assertThat(studies.get(0).name()).isEqualTo(pomodoroStudy1.getName()); +// softly.assertThat(studies.get(0).totalCycle()).isEqualTo(pomodoroStudy1.getTotalCycle()); +// softly.assertThat(studies.get(0).timePerCycle()) +// .isEqualTo(pomodoroStudy1.getTimePerCycle()); +// softly.assertThat(studies.get(1).studyId()).isEqualTo(pomodoroStudy2.getId()); +// softly.assertThat(studies.get(1).name()).isEqualTo(pomodoroStudy2.getName()); +// softly.assertThat(studies.get(1).totalCycle()).isEqualTo(pomodoroStudy2.getTotalCycle()); +// softly.assertThat(studies.get(1).timePerCycle()) +// .isEqualTo(pomodoroStudy2.getTimePerCycle()); +// }); +// } +// +// @Test +// void μŠ€ν„°λ””λ₯Ό_κ°œμ„€ν•œλ‹€() throws Exception { +// // given +// CreatePomodoroStudyRequest request = new CreatePomodoroStudyRequest("studyName", 1, 20); +// String jsonRequest = objectMapper.writeValueAsString(request); +// +// // when +// MvcResult result = mockMvc.perform(post("/api/studies") +// .content(jsonRequest) +// .contentType(MediaType.APPLICATION_JSON) +// .header(HttpHeaders.AUTHORIZATION, memberDto1.createAuthorizationHeader())) +// .andExpect(status().isCreated()) +// .andExpect(header().exists("Location")) +// .andReturn(); +// +// // then +// String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); +// CreatePomodoroStudyResponse response = objectMapper.readValue(jsonResponse, +// CreatePomodoroStudyResponse.class); +// +// assertThat(response.participantCode()) +// .isAlphabetic() +// .isUpperCase() +// .hasSize(6); +// } +} diff --git a/backend/src/test/java/harustudy/backend/member/service/MemberServiceTest.java b/backend/src/test/java/harustudy/backend/member/service/MemberServiceTest.java index 7f68d2d6..0acaa552 100644 --- a/backend/src/test/java/harustudy/backend/member/service/MemberServiceTest.java +++ b/backend/src/test/java/harustudy/backend/member/service/MemberServiceTest.java @@ -50,7 +50,7 @@ void setUp() { AuthMember authMember = new AuthMember(member1.getId()); // when - MemberResponse foundMember = memberService.findOauthProfile(authMember); + MemberResponse foundMember = memberService.findMemberProfile(authMember); //then assertSoftly(softly -> { 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 5ae1598d..5c6e6c6f 100644 --- a/backend/src/test/java/harustudy/backend/participantcode/domain/ParticipantCodeTest.java +++ b/backend/src/test/java/harustudy/backend/participantcode/domain/ParticipantCodeTest.java @@ -3,9 +3,7 @@ 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 harustudy.backend.study.domain.PomodoroStudy; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -19,7 +17,8 @@ class ParticipantCodeTest { // given & when GenerationStrategy generationStrategy = new CodeGenerationStrategy(); // then - assertThatCode(() -> new ParticipantCode(generationStrategy)) + assertThatCode( + () -> new ParticipantCode(new PomodoroStudy("name", 1, 20), generationStrategy)) .doesNotThrowAnyException(); } @@ -27,7 +26,8 @@ class ParticipantCodeTest { void κΈ°μ‘΄_κ°’κ³Ό_λ‹€λ₯Έ_μ°Έμ—¬μ½”λ“œλ₯Ό_생성할_수_μžˆλ‹€() { // given & when GenerationStrategy generationStrategy = new CodeGenerationStrategy(); - ParticipantCode participantCode = new ParticipantCode(generationStrategy); + ParticipantCode participantCode = new ParticipantCode(new PomodoroStudy("name", 1, 20), + generationStrategy); String oldCode = participantCode.getCode(); participantCode.regenerate(); 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 dd858331..25c8c135 100644 --- a/backend/src/test/java/harustudy/backend/progress/domain/PomodoroProgressTest.java +++ b/backend/src/test/java/harustudy/backend/progress/domain/PomodoroProgressTest.java @@ -1,13 +1,13 @@ package harustudy.backend.progress.domain; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import harustudy.backend.member.domain.Member; 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 harustudy.backend.study.domain.PomodoroStudy; import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -20,13 +20,12 @@ @DisplayNameGeneration(ReplaceUnderscores.class) class PomodoroProgressTest { - private PomodoroRoom pomodoroRoom; + private PomodoroStudy pomodoroStudy; private Member member; @BeforeEach void setUp() { - ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); - pomodoroRoom = new PomodoroRoom("room", 3, 25, participantCode); + pomodoroStudy = new PomodoroStudy("study", 3, 25); member = Member.guest(); } @@ -34,7 +33,7 @@ void setUp() { @ValueSource(strings = {"", "12345678901"}) void λ©€λ²„μ˜_λ‹‰λ„€μž„μ΄_1자_미만_10자_초과이면_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€(String nickname) { // given, when, then - assertThatThrownBy(() -> new PomodoroProgress(pomodoroRoom, member, nickname)) + assertThatThrownBy(() -> new PomodoroProgress(pomodoroStudy, member, nickname)) .isInstanceOf(NicknameLengthException.class); } @@ -42,15 +41,15 @@ void setUp() { @ValueSource(strings = {"1", "1234567890"}) void λ©€λ²„μ˜_λ‹‰λ„€μž„μ΄_1자_이상_10자_μ΄ν•˜μ΄λ©΄_정상_μΌ€μ΄μŠ€μ΄λ‹€(String nickname) { // given, when, then - assertThatCode(() -> new PomodoroProgress(pomodoroRoom, member, nickname)) + assertThatCode(() -> new PomodoroProgress(pomodoroStudy, member, nickname)) .doesNotThrowAnyException(); } @Test void λ‹‰λ„€μž„μ΄_λ™μΌν•œμ§€_확인할_수_μžˆλ‹€() { // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); - PomodoroProgress otherProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); + PomodoroProgress otherProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); // when, then assertThat(pomodoroProgress.hasSameNicknameWith(otherProgress)).isTrue(); @@ -59,8 +58,8 @@ void setUp() { @Test void λ‹‰λ„€μž„μ΄_λ‹€λ₯Έμ§€_확인할_수_μžˆλ‹€() { // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); - PomodoroProgress otherProgress = new PomodoroProgress(pomodoroRoom, member, "another"); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); + PomodoroProgress otherProgress = new PomodoroProgress(pomodoroStudy, member, "another"); // when, then assertThat(pomodoroProgress.hasSameNicknameWith(otherProgress)).isFalse(); @@ -69,7 +68,7 @@ void setUp() { @Test void λ‹€μŒ_μŠ€ν„°λ””_λ‹¨κ³„λ‘œ_λ„˜μ–΄κ°ˆ_수_μžˆλ‹€() { // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); // given pomodoroProgress.proceed(); @@ -85,7 +84,7 @@ void setUp() { @Test void λ§ˆμ§€λ§‰_사이클이_μ•„λ‹ˆλΌλ©΄_회고_μ’…λ£Œ_ν›„_사이클_μˆ˜κ°€_μ¦κ°€ν•œλ‹€() { // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); // when int statusCountPerCycle = 3; @@ -105,7 +104,7 @@ void setUp() { @Test void λ§ˆμ§€λ§‰_사이클이라면_회고_이후_사이클은_κ·ΈλŒ€λ‘œμ΄λ©°_μ’…λ£Œ_μƒνƒœλ‘œ_λ„˜μ–΄κ°„λ‹€() { // given - PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member, "nickname"); + PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); int statusCountPerCycle = 3; int totalCycle = 3; 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 85488d62..dd294763 100644 --- a/backend/src/test/java/harustudy/backend/progress/repository/PomodoroProgressRepositoryTest.java +++ b/backend/src/test/java/harustudy/backend/progress/repository/PomodoroProgressRepositoryTest.java @@ -1,62 +1,45 @@ -//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 { +package harustudy.backend.progress.repository; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@DataJpaTest +class PomodoroProgressRepositoryTest { // // @Autowired // private PomodoroProgressRepository pomodoroProgressRepository; +// // @Autowired -// private PomodoroRoomRepository pomodoroRoomRepository; +// private PomodoroStudyRepository pomodoroStudyRepository; +// // @Autowired // private MemberRepository memberRepository; -// @Autowired -// private ParticipantCodeRepository participantCodeRepository; // // @Autowired // private TestEntityManager testEntityManager; // // @Test -// void roomκ³Ό_memberλ₯Ό_톡해_pomodoroProgressλ₯Ό_μ‘°νšŒν•œλ‹€() { +// void study와_memberλ₯Ό_톡해_pomodoroProgressλ₯Ό_μ‘°νšŒν•œλ‹€() { // // given -// Member member = new Member("member"); +// Member member = new Member("member", "email", "imageUrl", LoginType.GUEST); // ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); -// PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); +// PomodoroStudy pomodoroStudy = new PomodoroStudy("studyName", 1, 20, participantCode); // memberRepository.save(member); // participantCodeRepository.save(participantCode); -// pomodoroRoomRepository.save(pomodoroRoom); +// pomodoroStudyRepository.save(pomodoroStudy); // -// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroRoom, member); +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy, member, "nickname"); // pomodoroProgressRepository.save(pomodoroProgress); // // testEntityManager.flush(); // testEntityManager.clear(); // // // when -// Optional found = pomodoroProgressRepository.findByPomodoroRoomAndMember( -// pomodoroRoom, member); +// Optional found = pomodoroProgressRepository.findByPomodoroStudyAndMember( +// pomodoroStudy, member); // // // then // assertThat(found).isPresent(); @@ -64,44 +47,44 @@ // } // // @Test -// void room으둜_pomodoroProgress_리슀트λ₯Ό_μ‘°νšŒν•œλ‹€() { +// void study둜_pomodoroProgress_리슀트λ₯Ό_μ‘°νšŒν•œλ‹€() { // // given -// Member member1 = new Member("member1"); -// Member member2 = new Member("member2"); +// Member member1 = new Member("member1", "email", "imageUrl", LoginType.GUEST); +// Member member2 = new Member("member2", "email", "imageUrl", LoginType.GUEST); // ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); -// PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); +// PomodoroStudy pomodoroStudy = new PomodoroStudy("studyName", 1, 20, participantCode); // memberRepository.save(member1); // memberRepository.save(member2); // participantCodeRepository.save(participantCode); -// pomodoroRoomRepository.save(pomodoroRoom); +// pomodoroStudyRepository.save(pomodoroStudy); // -// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroRoom, member1); -// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroRoom, member2); +// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroStudy, member1, "nickname1"); +// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroStudy, member2, "nickname2"); // pomodoroProgressRepository.save(pomodoroProgress1); // pomodoroProgressRepository.save(pomodoroProgress2); // // // when -// List pomodoroProgresses = pomodoroProgressRepository.findAllByPomodoroRoom( -// pomodoroRoom); +// List pomodoroProgresses = pomodoroProgressRepository.findByPomodoroStudy( +// pomodoroStudy); // // // then -// assertThat(pomodoroProgresses.size()).isEqualTo(2); +// assertThat(pomodoroProgresses).hasSize(2); // } // // @Test -// void room으둜_pomodoroProgress와_memberλ₯Ό_ν•¨κ»˜_μ‘°νšŒν•œλ‹€() { +// void study둜_pomodoroProgress와_memberλ₯Ό_ν•¨κ»˜_μ‘°νšŒν•œλ‹€() { // // given -// Member member1 = new Member("member1"); -// Member member2 = new Member("member2"); +// Member member1 = new Member("member1", "email", "imageUrl", LoginType.GUEST); +// Member member2 = new Member("member2", "email", "imageUrl", LoginType.GUEST); // ParticipantCode participantCode = new ParticipantCode(new CodeGenerationStrategy()); -// PomodoroRoom pomodoroRoom = new PomodoroRoom("roomName", 1, 20, participantCode); +// PomodoroStudy pomodoroStudy = new PomodoroStudy("studyName", 1, 20, participantCode); // memberRepository.save(member1); // memberRepository.save(member2); // participantCodeRepository.save(participantCode); -// pomodoroRoomRepository.save(pomodoroRoom); +// pomodoroStudyRepository.save(pomodoroStudy); // -// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroRoom, member1); -// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroRoom, member2); +// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroStudy, member1, "nickname1"); +// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroStudy, member2, "nickname2"); // pomodoroProgressRepository.save(pomodoroProgress1); // pomodoroProgressRepository.save(pomodoroProgress2); // @@ -112,8 +95,8 @@ // .getEntityManagerFactory().getPersistenceUnitUtil(); // // // when -// List progresses = pomodoroProgressRepository.findAllByPomodoroRoomFetchMember( -// pomodoroRoom); +// List progresses = pomodoroProgressRepository.findAllByPomodoroStudyFetchMember( +// pomodoroStudy); // // // then // assertSoftly(softly -> { @@ -122,4 +105,4 @@ // 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 index 1f0f906c..29ffded4 100644 --- a/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceTest.java +++ b/backend/src/test/java/harustudy/backend/progress/service/PomodoroProgressServiceTest.java @@ -1,28 +1,11 @@ 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 harustudy.backend.study.domain.PomodoroStudy; 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; @@ -33,175 +16,175 @@ @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); - }); - } +// @Autowired +// private PomodoroProgressService pomodoroProgressService; +// +// @PersistenceContext +// private EntityManager entityManager; +// +// private PomodoroStudy pomodoroStudy1; +// private PomodoroStudy pomodoroStudy2; +// private Member member1; +// private Member member2; +// +// @BeforeEach +// void setUp() { +// ParticipantCode participantCode1 = new ParticipantCode(new CodeGenerationStrategy()); +// ParticipantCode participantCode2 = new ParticipantCode(new CodeGenerationStrategy()); +// pomodoroStudy1 = new PomodoroStudy("studyName1", 3, 20, participantCode1); +// pomodoroStudy2 = new PomodoroStudy("studyName2", 3, 20, participantCode2); +// member1 = Member.guest(); +// member2 = Member.guest(); +// +// entityManager.persist(participantCode1); +// entityManager.persist(participantCode2); +// entityManager.persist(pomodoroStudy1); +// entityManager.persist(pomodoroStudy2); +// entityManager.persist(member1); +// entityManager.persist(member2); +// +// entityManager.flush(); +// entityManager.clear(); +// } +// +// @Test +// void 진행도λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy2, member1, "nickname1"); +// AuthMember authMember = new AuthMember(member1.getId()); +// +// entityManager.persist(pomodoroProgress); +// +// // when +// PomodoroProgressResponse response = +// pomodoroProgressService.findPomodoroProgress(authMember, pomodoroStudy2.getId(), pomodoroProgress.getId()); +// PomodoroProgressResponse expected = PomodoroProgressResponse.from(pomodoroProgress); +// +// // then +// assertThat(response).usingRecursiveComparison() +// .ignoringExpectedNullFields() +// .isEqualTo(expected); +// } +// +// @Test +// void μŠ€ν„°λ””μ˜_λͺ¨λ“ _진행도λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy2, member1, "nickname1"); +// PomodoroProgress anotherPomodoroProgress = new PomodoroProgress(pomodoroStudy2, member2, "nickname2"); +// AuthMember authMember1 = new AuthMember(member1.getId()); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(anotherPomodoroProgress); +// +// // when +// PomodoroProgressesResponse response = +// pomodoroProgressService.findPomodoroProgressWithFilter(authMember1, pomodoroStudy2.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, pomodoroStudy2.getId(), null)) +// .isInstanceOf(AuthorizationException.class); +// } +// +// @Test +// void νŠΉμ •_λ©€λ²„μ˜_진행도λ₯Ό_μ‘°νšŒν• _수_μžˆλ‹€() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy2, member1, "nickname1"); +// PomodoroProgress anotherPomodoroProgress = new PomodoroProgress(pomodoroStudy2, member2, "nickname2"); +// AuthMember authMember1 = new AuthMember(member1.getId()); +// +// entityManager.persist(pomodoroProgress); +// entityManager.persist(anotherPomodoroProgress); +// +// // when +// PomodoroProgressesResponse response = +// pomodoroProgressService.findPomodoroProgressWithFilter(authMember1, pomodoroStudy2.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(pomodoroStudy2, member1, "nickname1"); +// AuthMember authMember = new AuthMember(member2.getId()); +// +// entityManager.persist(pomodoroProgress); +// +// // when, then +// assertThatThrownBy(() -> +// pomodoroProgressService.findPomodoroProgress(authMember, pomodoroStudy2.getId(), pomodoroProgress.getId())) +// .isInstanceOf(AuthorizationException.class); +// } +// +// @Test +// void ν•΄λ‹Ή_μŠ€ν„°λ””μ˜_진행도가_μ•„λ‹ˆλΌλ©΄_μ‘°νšŒν• _수_μ—†λ‹€() { +// // given +// PomodoroProgress pomodoroProgress1 = new PomodoroProgress(pomodoroStudy1, member1, "nickname1"); +// PomodoroProgress pomodoroProgress2 = new PomodoroProgress(pomodoroStudy2, member1, "nickname1"); +// AuthMember authMember = new AuthMember(member1.getId()); +// +// entityManager.persist(pomodoroProgress1); +// entityManager.persist(pomodoroProgress2); +// +// // when, then +// assertThatThrownBy(() -> +// pomodoroProgressService.findPomodoroProgress(authMember, pomodoroStudy1.getId(), pomodoroProgress2.getId())) +// .isInstanceOf(ProgressNotBelongToStudyException.class); +// } +// +// @Test +// void λ‹€μŒ_μŠ€ν„°λ””_λ‹¨κ³„λ‘œ_이동할_수_μžˆλ‹€() { +// // given +// PomodoroProgress pomodoroProgress = new PomodoroProgress(pomodoroStudy2, member1, "nickname1"); +// AuthMember authMember1 = new AuthMember(member1.getId()); +// +// entityManager.persist(pomodoroProgress); +// +// // when +// pomodoroProgressService.proceed(authMember1, pomodoroStudy2.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, pomodoroStudy2.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/room/service/PomodoroRoomServiceTest.java b/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceTest.java deleted file mode 100644 index 551ab4a2..00000000 --- a/backend/src/test/java/harustudy/backend/room/service/PomodoroRoomServiceTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package harustudy.backend.room.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -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.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 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 PomodoroRoomServiceTest { - - @Autowired - private PomodoroRoomService pomodoroRoomService; - - @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 - 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.flush(); - entityManager.clear(); - - // when, then - assertThatThrownBy(() -> pomodoroRoomService.findPomodoroRoomWithFilter(99999L, null), null) - .isInstanceOf(MemberNotFoundException.class); - } - - @Test - void 룸을_μƒμ„±ν•œλ‹€() { - // given - CreatePomodoroRoomRequest request = new CreatePomodoroRoomRequest("room", 8, 40); - - // when - 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 - 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()) - ); - } - - @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); - } - - @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( - () -> pomodoroRoomService.findPomodoroRoomWithFilter(null, notRoomsCode.getCode())) - .isInstanceOf(RoomNotFoundException.class); - } -} diff --git a/backend/src/test/java/harustudy/backend/participantcode/domain/CodeGenerationStrategyTest.java b/backend/src/test/java/harustudy/backend/study/domain/CodeGenerationStrategyTest.java similarity index 84% rename from backend/src/test/java/harustudy/backend/participantcode/domain/CodeGenerationStrategyTest.java rename to backend/src/test/java/harustudy/backend/study/domain/CodeGenerationStrategyTest.java index 6aeb2b72..76bea775 100644 --- a/backend/src/test/java/harustudy/backend/participantcode/domain/CodeGenerationStrategyTest.java +++ b/backend/src/test/java/harustudy/backend/study/domain/CodeGenerationStrategyTest.java @@ -1,9 +1,9 @@ -package harustudy.backend.participantcode.domain; +package harustudy.backend.study.domain; import static org.assertj.core.api.Assertions.assertThat; -import harustudy.backend.room.domain.CodeGenerationStrategy; -import harustudy.backend.room.domain.GenerationStrategy; +import harustudy.backend.participantcode.domain.CodeGenerationStrategy; +import harustudy.backend.participantcode.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/room/domain/PomodoroRoomTest.java b/backend/src/test/java/harustudy/backend/study/domain/PomodoroStudyTest.java similarity index 64% rename from backend/src/test/java/harustudy/backend/room/domain/PomodoroRoomTest.java rename to backend/src/test/java/harustudy/backend/study/domain/PomodoroStudyTest.java index 9f5bfde6..ca9a5517 100644 --- a/backend/src/test/java/harustudy/backend/room/domain/PomodoroRoomTest.java +++ b/backend/src/test/java/harustudy/backend/study/domain/PomodoroStudyTest.java @@ -1,12 +1,11 @@ -package harustudy.backend.room.domain; +package harustudy.backend.study.domain; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import harustudy.backend.room.exception.PomodoroTimePerCycleException; -import harustudy.backend.room.exception.PomodoroTotalCycleException; -import harustudy.backend.room.exception.PomodoroRoomNameLengthException; -import org.junit.jupiter.api.BeforeEach; +import harustudy.backend.study.exception.PomodoroStudyNameLengthException; +import harustudy.backend.study.exception.PomodoroTimePerCycleException; +import harustudy.backend.study.exception.PomodoroTotalCycleException; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -15,19 +14,12 @@ @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(ReplaceUnderscores.class) -class PomodoroRoomTest { - - private ParticipantCode participantCode; - - @BeforeEach - void setUp() { - participantCode = new ParticipantCode(new CodeGenerationStrategy()); - } +class PomodoroStudyTest { @Test void μŠ€ν„°λ””λ°©μ€_μŠ€ν„°λ””λͺ…_사이클_수_사이클_λ‹Ή_μŠ€ν„°λ””_μ‹œκ°„μ΄_ν•„μš”ν•˜λ‹€() { // given, when, then - assertThatCode(() -> new PomodoroRoom("teo", 3, 20, participantCode)) + assertThatCode(() -> new PomodoroStudy("teo", 3, 20)) .doesNotThrowAnyException(); } @@ -37,7 +29,7 @@ void setUp() { String name = "12345"; // when, then - assertThatCode(() -> new PomodoroRoom(name, 3, 20, participantCode)) + assertThatCode(() -> new PomodoroStudy(name, 3, 20)) .doesNotThrowAnyException(); } @@ -45,15 +37,15 @@ void setUp() { @ValueSource(strings = {"", "01234567890"}) void μŠ€ν„°λ””λ°©_이름이_1자_λ―Έλ§Œμ΄κ±°λ‚˜_10자_초과라면_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€(String name) { // given, when, then - assertThatThrownBy(() -> new PomodoroRoom(name, 3, 20, participantCode)) - .isInstanceOf(PomodoroRoomNameLengthException.class); + assertThatThrownBy(() -> new PomodoroStudy(name, 3, 20)) + .isInstanceOf(PomodoroStudyNameLengthException.class); } @ParameterizedTest @ValueSource(ints = {1, 8}) void 사이클은_μ΅œμ†Œ_1번_μ΅œλŒ€_8번이_정상_μΌ€μ΄μŠ€μ΄λ‹€(int cycle) { // given, when, then - assertThatCode(() -> new PomodoroRoom("teo", cycle, 20, participantCode)) + assertThatCode(() -> new PomodoroStudy("teo", cycle, 20)) .doesNotThrowAnyException(); } @@ -61,7 +53,7 @@ void setUp() { @ValueSource(ints = {0, 9}) void 사이클은_1번_λ―Έλ§Œμ΄κ±°λ‚˜_8번_초과라면_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€(int cycle) { // given, when, then - assertThatThrownBy(() -> new PomodoroRoom("teo", cycle, 20, participantCode)) + assertThatThrownBy(() -> new PomodoroStudy("teo", cycle, 20)) .isInstanceOf(PomodoroTotalCycleException.class); } @@ -69,7 +61,7 @@ void setUp() { @ValueSource(ints = {20, 60}) void μŠ€ν„°λ””_μ‹œκ°„μ€_μ΅œμ†Œ_20λΆ„_μ΅œλŒ€_60뢄이_정상_μΌ€μ΄μŠ€μ΄λ‹€(int timePerCycle) { // given, when, then - assertThatCode(() -> new PomodoroRoom("teo", 5, timePerCycle, participantCode)) + assertThatCode(() -> new PomodoroStudy("teo", 5, timePerCycle)) .doesNotThrowAnyException(); } @@ -77,7 +69,7 @@ void setUp() { @ValueSource(ints = {19, 61}) void μŠ€ν„°λ””_μ‹œκ°„μ€_20λΆ„_λ―Έλ§Œμ΄κ±°λ‚˜_60λΆ„_초과라면_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€(int timePerCycle) { // given, when, then - assertThatThrownBy(() -> new PomodoroRoom("teo", 5, timePerCycle, participantCode)) + assertThatThrownBy(() -> new PomodoroStudy("teo", 5, timePerCycle)) .isInstanceOf(PomodoroTimePerCycleException.class); } } diff --git a/backend/src/test/java/harustudy/backend/study/service/PomodoroStudyServiceTest.java b/backend/src/test/java/harustudy/backend/study/service/PomodoroStudyServiceTest.java new file mode 100644 index 00000000..6eb9a5c7 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/study/service/PomodoroStudyServiceTest.java @@ -0,0 +1,146 @@ +package harustudy.backend.study.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import harustudy.backend.member.exception.MemberNotFoundException; +import harustudy.backend.participantcode.domain.GenerationStrategy; +import harustudy.backend.study.domain.PomodoroStudy; +import harustudy.backend.study.dto.CreatePomodoroStudyRequest; +import harustudy.backend.study.dto.CreatePomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudyResponse; +import harustudy.backend.study.dto.PomodoroStudiesResponse; +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) +@Transactional +@SpringBootTest +class PomodoroStudyServiceTest { +// +// @Autowired +// private PomodoroStudyService pomodoroStudyService; +// +// @PersistenceContext +// private EntityManager entityManager; +// +// @Autowired +// private GenerationStrategy generationStrategy; +// +// @Test +// void λ£Έ_μ•„μ΄λ””λ‘œ_룸을_μ‘°νšŒν•œλ‹€() { +// // given +// PomodoroStudy pomodoroStudy = new PomodoroStudy("study", 8, 20); +// entityManager.persist(pomodoroStudy); +// entityManager.flush(); +// entityManager.clear(); +// +// // when +// PomodoroStudyResponse result = pomodoroStudyService.findPomodoroStudy(pomodoroStudy.getId()); +// +// // then +// assertAll( +// () -> assertThat(result.studyId()).isEqualTo(pomodoroStudy.getId()), +// () -> assertThat(result.name()).isEqualTo(pomodoroStudy.getName()), +// () -> assertThat(result.totalCycle()).isEqualTo(pomodoroStudy.getTotalCycle()), +// () -> assertThat(result.timePerCycle()).isEqualTo(pomodoroStudy.getTimePerCycle()) +// ); +// } +// +// @Test +// void λ£Έ_μ•„μ΄λ””λ‘œ_λ£Έ_μ‘°νšŒμ‹œ_μ—†μœΌλ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { +// // given +// PomodoroStudy pomodoroStudy = new PomodoroStudy("study", 8, 20); +// entityManager.persist(pomodoroStudy); +// entityManager.flush(); +// entityManager.clear(); +// +// // when, then +// assertThatThrownBy(() -> pomodoroStudyService.findPomodoroStudyWithFilter(99999L, null), null) +// .isInstanceOf(MemberNotFoundException.class); +// } +// +// @Test +// void 룸을_μƒμ„±ν•œλ‹€() { +// // given +// CreatePomodoroStudyRequest request = new CreatePomodoroStudyRequest("study", 8, 40); +// +// // when +// CreatePomodoroStudyResponse result = pomodoroStudyService.createPomodoroStudy(request); +// +// // then +// assertAll( +// () -> assertThat(result.studyId()).isNotNull(), +// () -> assertThat(result.participantCode()).isNotNull() +// ); +// } +// +// @Test +// void μ°Έμ—¬μ½”λ“œμ—_ν•΄λ‹Ήν•˜λŠ”_룸을_μ‘°νšŒν•œλ‹€() { +// // given +// CreatePomodoroStudyRequest request = new CreatePomodoroStudyRequest("study", 8, 40); +// CreatePomodoroStudyResponse response = pomodoroStudyService.createPomodoroStudy(request); +// String participantCode = response.participantCode(); +// +// // when +// PomodoroStudiesResponse result = pomodoroStudyService.findPomodoroStudyWithFilter(null, +// participantCode); +// +// // then +// assertAll( +// () -> assertThat(result.studies()).hasSize(1), +// () -> assertThat(result.studies().get(0).name()).isEqualTo(request.name()), +// () -> assertThat(result.studies().get(0).totalCycle()).isEqualTo( +// request.totalCycle()), +// () -> assertThat(result.studies().get(0).timePerCycle()).isEqualTo( +// request.timePerCycle()) +// ); +// } +// +// @Test +// void μ°Έμ—¬μ½”λ“œμ—_ν•΄λ‹Ήν•˜λŠ”_λ£Έ_μ‘°νšŒμ‹œ_μ°Έμ—¬μ½”λ“œκ°€_μ—†μœΌλ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { +// // given +// ParticipantCode participantCode = new ParticipantCode(generationStrategy); +// PomodoroStudy pomodoroStudy = new PomodoroStudy("study", 8, 40, participantCode); +// entityManager.persist(participantCode); +// entityManager.persist(pomodoroStudy); +// +// entityManager.flush(); +// entityManager.clear(); +// +// ParticipantCode notPersisted = new ParticipantCode(generationStrategy); +// +// // when, then +// assertThatThrownBy( +// () -> pomodoroStudyService.findPomodoroStudyWithFilter(null, notPersisted.getCode())) +// .isInstanceOf(ParticipantCodeNotFoundException.class); +// } +// +// @Test +// void μ°Έμ—¬μ½”λ“œμ—_ν•΄λ‹Ήν•˜λŠ”_λ£Έ_μ‘°νšŒμ‹œ_μ°Έμ—¬μ½”λ“œμ—_ν•΄λ‹Ήν•˜λŠ”_룸이_μ—†μœΌλ©΄_μ˜ˆμ™Έλ₯Ό_λ˜μ§„λ‹€() { +// // given +// ParticipantCode participantCode = new ParticipantCode(generationStrategy); +// PomodoroStudy pomodoroStudy = new PomodoroStudy("study", 8, 40, participantCode); +// entityManager.persist(participantCode); +// entityManager.persist(pomodoroStudy); +// +// ParticipantCode notStudiesCode = new ParticipantCode(generationStrategy); +// entityManager.persist(notStudiesCode); +// +// entityManager.flush(); +// entityManager.clear(); +// +// // when, then +// assertThatThrownBy( +// () -> pomodoroStudyService.findPomodoroStudyWithFilter(null, notStudiesCode.getCode())) +// .isInstanceOf(StudyNotFoundException.class); +// } +} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 7afc8dad..831109e0 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -24,11 +24,11 @@ oauth2: user-info-uri: http://www.test.com jwt: - expire-length: 1 - guest-expire-length: 1 - secret-key: test-secret-key + expire-length: 12345 + guest-expire-length: 12345 + secret-key: test-secret-key-test-secret-key-test-secret-key-test-secret-key-test-secret-key-test-secret-key refresh-token: - expire-length: 1 + expire-length: 12345 cors-allow-origin: test diff --git a/frontend/.eslintrc b/frontend/.eslintrc index 7ec1456a..8c7ccb89 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -38,6 +38,7 @@ "@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-throw-literal": "off", "import/no-unresolved": "off", "import/order": [ "error", diff --git a/frontend/.gitignore b/frontend/.gitignore index 1042f00c..e955120d 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -15,5 +15,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* .gitmessage +.DS_Store /storybook-static diff --git a/frontend/__test__/CreateStudyPage.test.tsx b/frontend/__test__/CreateStudyPage.test.tsx new file mode 100644 index 00000000..2f20b8e7 --- /dev/null +++ b/frontend/__test__/CreateStudyPage.test.tsx @@ -0,0 +1,42 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import { MemoryRouter } from 'react-router-dom'; + +import CreateStudy from '@Pages/CreateStudy'; + +import MemberInfoProvider from '@Contexts/MemberInfoProvider'; +import ModalProvider from '@Contexts/ModalProvider'; + +describe('μŠ€ν„°λ”” κ°œμ„€ νŽ˜μ΄μ§€ ν…ŒμŠ€νŠΈ', () => { + test('폼 μž…λ ₯ ν›„ μ μ ˆν•œ μ˜ˆμƒμ‹œκ°„μ΄ λ‚˜μ˜€λŠ”μ§€ ν™•μΈν•œλ‹€.', async () => { + render( + + + + + + + , + ); + + const studyNameInput = screen.getByLabelText('μŠ€ν„°λ””μ˜ 이름은 λ¬΄μ—‡μΈκ°€μš”?'); + await userEvent.type(studyNameInput, 'ν•˜λ£¨μŠ€ν„°λ””'); + + const cycleSelectBox = screen.getByTestId('cycle'); + await userEvent.click(cycleSelectBox); + + const onceElement = screen.getByText('1회'); + await userEvent.click(onceElement); + + const timePerCycleSelectBox = screen.getByTestId('timepercycle'); + await userEvent.click(timePerCycleSelectBox); + + const twentyMinuteElement = screen.getByText('20λΆ„'); + await userEvent.click(twentyMinuteElement); + + const estimatedTimeElement = screen.getByText('0μ‹œκ°„ 40λΆ„'); + + expect(estimatedTimeElement).toBeInTheDocument(); + }); +}); diff --git a/frontend/__test__/LandingPage.test.tsx b/frontend/__test__/LandingPage.test.tsx new file mode 100644 index 00000000..f1ec5ce4 --- /dev/null +++ b/frontend/__test__/LandingPage.test.tsx @@ -0,0 +1,72 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { MemoryRouter } from 'react-router-dom'; + +import Landing from '@Pages/Landing'; + +import MemberInfoProvider from '@Contexts/MemberInfoProvider'; +import ModalProvider from '@Contexts/ModalProvider'; + +const USER_MOCK = { + memberId: '1', + name: '맘λͺ¨μŠ€', + email: 'haru12@gmail.com', + imageUrl: 'https://lh3.google.com/u/0/ogw/AGvuzYZKngVdZecWrpGTnHj7hQRtO5p9tjelI5lvCcsk=s64-c-mo', + loginType: 'google', +}; + +const server = setupServer( + rest.get('/api/me', (_, res, ctx) => { + return res(ctx.json(null)); + }), +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('λžœλ”© νŽ˜μ΄μ§€ ν…ŒμŠ€νŠΈ', () => { + test('λ‘œκ·ΈμΈμ„ ν•˜μ§€ μ•Šμ•˜λ‹€λ©΄ ν•˜λ£¨μŠ€ν„°λ”” μ‹œμž‘ν•˜κΈ° λ²„νŠΌμ΄ 보여진닀.', async () => { + render( + + + + + + + , + ); + + await waitFor(() => { + expect(screen.getAllByRole('button')[0].textContent).toBe('ν•˜λ£¨μŠ€ν„°λ”” μ‹œμž‘ν•˜κΈ°'); + }); + }); + + test('λ‘œκ·ΈμΈμ„ ν–ˆλ‹€λ©΄ ν•˜λ£¨μŠ€ν„°λ”” μŠ€ν„°λ”” κ°œμ„€ν•˜κΈ°, μŠ€ν„°λ”” μ°Έμ—¬ν•˜κΈ° λ²„νŠΌμ΄ 보여진닀.', async () => { + server.use( + rest.get('/api/me', (_, res, ctx) => { + return res(ctx.json(USER_MOCK)); + }), + ); + + render( + + + + + + + , + ); + + await waitFor(() => { + screen.getAllByRole('button').forEach((button, index) => { + if (index > 1) return; + const buttonText = index === 0 ? 'μŠ€ν„°λ”” κ°œμ„€ν•˜κΈ°' : 'μŠ€ν„°λ”” μ°Έμ—¬ν•˜κΈ°'; + expect(button.textContent).toBe(buttonText); + }); + }); + }); +}); diff --git a/frontend/__test__/MemberRecord.test.tsx b/frontend/__test__/MemberRecord.test.tsx new file mode 100644 index 00000000..55a82c94 --- /dev/null +++ b/frontend/__test__/MemberRecord.test.tsx @@ -0,0 +1,73 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { MemoryRouter } from 'react-router-dom'; + +import MemberRecord from '@Pages/MemberRecord'; + +import MemberInfoProvider from '@Contexts/MemberInfoProvider'; +import ModalProvider from '@Contexts/ModalProvider'; + +const STUDY_LIST = { + studies: [ + { + studyId: '1', + name: 'μ•ˆμ˜€λ©΄ 지상렬', + totalCycle: 3, + timePerCycle: 60, + createdDateTime: '2023-08-16T13:33:02.810Z', + }, + ], +}; + +const USER_MOCK = { + memberId: '1', + name: '맘λͺ¨μŠ€', + email: 'haru12@gmail.com', + imageUrl: 'https://lh3.google.com/u/0/ogw/AGvuzYZKngVdZecWrpGTnHj7hQRtO5p9tjelI5lvCcsk=s64-c-mo', + loginType: 'google', +}; + +const server = setupServer( + rest.get('/api/me', (_, res, ctx) => { + return res(ctx.json(null)); + }), + + rest.get('/api/studies', (_, res, ctx) => { + return res(ctx.json(null)); + }), +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('λ‚˜μ˜ μŠ€ν„°λ”” 기둝 νŽ˜μ΄μ§€ ν…ŒμŠ€νŠΈ', () => { + test('λ‚˜μ˜ μŠ€ν„°λ””λ‘œ μ΄λ™ν•˜λ©΄ μ°Έμ—¬ν•œ μŠ€ν„°λ””μ˜ 이름과 μ§„ν–‰ν•œ λ‚ μ§œ, 정보가 보여진닀.', async () => { + server.use( + rest.get('/api/me', (_, res, ctx) => { + return res(ctx.json(USER_MOCK)); + }), + + rest.get('/api/studies', (_, res, ctx) => { + return res(ctx.json(STUDY_LIST)); + }), + ); + + render( + + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('heading', { level: 6 }).textContent).toBe('μ•ˆμ˜€λ©΄ 지상렬 μŠ€ν„°λ””'); + expect(screen.getByTestId('progress-date').textContent).toBe('2023λ…„ 8μ›” 16일'); + }); + }); +}); diff --git a/frontend/__test__/StudyMakingPage.test.tsx b/frontend/__test__/StudyMakingPage.test.tsx deleted file mode 100644 index 2ab008fc..00000000 --- a/frontend/__test__/StudyMakingPage.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import { MemoryRouter } from 'react-router-dom'; - -import CreateStudy from '@Pages/CreateStudy'; - -import ModalProvider from '@Contexts/ModalProvider'; - -test('μŠ€ν„°λ”” κ°œμ„€ νŽ˜μ΄μ§€κ°€ 잘 λ Œλ”λ§ λ˜μ—ˆλŠ”μ§€ ν™•μΈν•œλ‹€.', async () => { - render( - - - - - , - ); - - const title = await screen.findAllByRole('heading'); - - expect(title[1]).toHaveTextContent('μŠ€ν„°λ”” κ°œμ„€ν•˜κΈ°'); -}); diff --git a/frontend/__test__/StudyParticipationPage.test.tsx b/frontend/__test__/StudyParticipationPage.test.tsx new file mode 100644 index 00000000..ff866fb8 --- /dev/null +++ b/frontend/__test__/StudyParticipationPage.test.tsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { MemoryRouter } from 'react-router-dom'; + +import StudyParticipation from '@Pages/StudyParticipation'; + +import MemberInfoProvider from '@Contexts/MemberInfoProvider'; +import ModalProvider from '@Contexts/ModalProvider'; + +describe('μŠ€ν„°λ”” μ°Έμ—¬ νŽ˜μ΄μ§€ ν…ŒμŠ€νŠΈ', () => { + test('μ°Έμ—¬μ½”λ“œ μž…λ ₯ 폼이 보여진닀.', async () => { + render( + + + + + + + , + ); + + expect(screen.getByText('μŠ€ν„°λ””μž₯μ—κ²Œ 받은 μ°Έμ—¬μ½”λ“œλ₯Ό μž…λ ₯ν•˜μ„Έμš”.')).toBeInTheDocument(); + }); +}); diff --git a/frontend/__test__/StudyPreparationPage.test.tsx b/frontend/__test__/StudyPreparationPage.test.tsx new file mode 100644 index 00000000..4f6de2af --- /dev/null +++ b/frontend/__test__/StudyPreparationPage.test.tsx @@ -0,0 +1,77 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { MemoryRouter } from 'react-router-dom'; + +import StudyPreparation from '@Pages/StudyPreparation'; + +import MemberInfoProvider from '@Contexts/MemberInfoProvider'; +import ModalProvider from '@Contexts/ModalProvider'; + +jest.mock('react-router-dom', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...jest.requireActual('react-router-dom'), + useLocation: () => { + return { + state: { participantCode: '123456', studyName: 'ν•˜λ£¨μŠ€ν„°λ””', isHost: true }, + }; + }, + useParams: () => ({ studyId: '1' }), + }; +}); + +const server = setupServer( + rest.get('/api/temp/studies/:studyId/progresses', (_, res, ctx) => { + return res(ctx.status(200), ctx.json({ progresses: null })); + }), +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('μŠ€ν„°λ”” μ€€λΉ„ νŽ˜μ΄μ§€ ν…ŒμŠ€νŠΈ', () => { + test('μš”μ²­ν•œ Progresses 데이터가 μ—†μœΌλ©΄ λ‹‰λ„€μž„ μž…λ ₯ 폼이 보여진닀.', async () => { + render( + + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText('μŠ€ν„°λ””μ—μ„œ μ‚¬μš©ν•  λ‹‰λ„€μž„')).toBeInTheDocument(); + }); + }); + + test('μš”μ²­ν•œ Progresses 데이터가 μžˆλ‹€λ©΄ 이미 μŠ€ν„°λ”” 정보가 μžˆλ‹€λŠ” 폼이 보여진닀.', async () => { + server.use( + rest.get('/api/temp/studies/:studyId/progresses', (_, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ progresses: [{ progressId: 1, nickname: 'ν•˜λ£¨', currentCycle: 1, step: 'planning' }] }), + ); + }), + ); + + render( + + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText('ν•˜λ£¨')).toBeInTheDocument(); + expect(screen.getByText('ν•™μŠ΅μ„ μ΄μ–΄μ„œ 진행 ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/__test__/StudyRecord.test.tsx b/frontend/__test__/StudyRecord.test.tsx new file mode 100644 index 00000000..7ed5d9d8 --- /dev/null +++ b/frontend/__test__/StudyRecord.test.tsx @@ -0,0 +1,182 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { MemoryRouter } from 'react-router-dom'; + +import StudyRecord from '@Pages/StudyRecord'; + +import MemberInfoProvider from '@Contexts/MemberInfoProvider'; +import ModalProvider from '@Contexts/ModalProvider'; + +jest.mock('react-router-dom', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...jest.requireActual('react-router-dom'), + useParams: () => ({ studyId: '1' }), + }; +}); + +const STUDY_CONTENT = { + content: [ + { + cycle: 1, + plan: { + toDo: 'λͺ¨λ˜ μžλ°”μŠ€ν¬λ¦½νŠΈ 15μž₯ 정독(let, const ν‚€μ›Œλ“œμ™€ 블둝 레벨 μŠ€μ½”ν”„)', + completionCondition: + '블둝 레벨 μŠ€μ½”ν”„κ°€ 무엇인지 ν•œ μ€„λ‘œ μ„€λͺ…ν•  수 μžˆλ‹€. -> μ˜ˆμ‹œλ₯Ό 톡해 1λΆ„ μ΄λ‚΄λ‘œ μ„€λͺ…ν•  수 μžˆλ‹€.', + expectedProbability: '80% 이미 ν•™μŠ΅ν•œ λ‚΄μš©μ΄κΈ° λ•Œλ¬Έμ΄λ‹€.', + expectedDifficulty: 'κ°œλ…μ„ ν•™μŠ΅ν•œ ν›„, λ‚˜λ§Œμ˜ μ–Έμ–΄λ‘œ μ •λ¦¬ν•˜λŠ” 것', + whatCanYouDo: '핡심적인 λ‚΄μš©μ„ λ¨Όμ € μ •λ¦¬ν•œλ‹€.', + }, + retrospect: { + doneAsExpected: + '이전에 ν•™μŠ΅ν•œ λ‚΄μš©μ΄μ—¬μ„œ μ΄ν•΄λŠ” 어렡지 μ•Šμ•˜μ§€λ§Œ 짧게 정리λ₯Ό ν•˜κ³  이λ₯Ό μŠ΅λ“ν•˜λŠ”λ° 어렀움이 μžˆμ—ˆλ‹€. μ‹€μ œλ‘œ λˆ„κ΅°κ°€μ—κ²Œ μ„€λͺ…을 ν•  수 μžˆλŠ”μ§€ 확인해봐야겠닀.', + experiencedDifficulty: 'κΉ”λ”ν•œ λ¬Έμž₯으둜 μ •λ¦¬ν•˜λŠ” 것, λˆ„κ΅°κ°€μ—κ²Œ κ³Όμ—° λ§€λ„λŸ½κ²Œ μ„€λͺ…을 ν•  수 μžˆμ„μ§€', + lesson: 'varν‚€μ›Œλ“œλŠ” ν˜Όλž€λ§Œ μ΄ˆλž˜ν•  뿐이닀. var ν‚€μ›Œλ“œλŠ” λΈŒλΌμš°μ €μ—κ²Œ 맑기자', + }, + }, + { + cycle: 2, + plan: { + toDo: 'μ•Œκ³ λ¦¬μ¦˜ ν•œ 문제λ₯Ό ν‘Όλ‹€', + completionCondition: 'μ•Œκ³ λ¦¬μ¦˜ ν•œ λ¬Έμ œμ— λŒ€ν•΄ λͺ¨λ“  ν…ŒμŠ€νŠΈμΌ€μ΄μŠ€κ°€ 톡과해야 ν•œλ‹€.', + expectedProbability: '80% λ„μ „ν•΄λ³Όλ§Œν•œ κ°€μΉ˜κ°€ 있기 λ•Œλ¬Έ', + expectedDifficulty: '아직 잘 λͺ¨λ₯΄λŠ” μ•Œκ³ λ¦¬μ¦˜ κ°œλ…μ΄ λ‚˜μ˜€λ©΄ νž˜λ“€ 것 κ°™λ‹€.', + whatCanYouDo: '문제 μ„€λͺ… λΆ€ν„° 잘 보자.', + }, + retrospect: { + doneAsExpected: 'λ‘œμ§μ€ λ§žλŠ”κ±°κ°™μ€λ° μ‹œκ°„μ΄ˆκ³Ό λ•Œλ¬Έμ— ν†΅κ³Όν•˜μ§€λŠ” λͺ»ν–ˆμŠ΅λ‹ˆλ‹€', + experiencedDifficulty: + '볡병은 μ‹œκ°„μ΄ˆκ³Όλ₯Ό μ˜ˆμƒν•˜μ§€ μ•Šκ³  μ½”λ“œλ₯Ό μ§°λ‹€λŠ” 것..? μ•Œκ³ λ¦¬μ¦˜μ— μ΅μˆ™ν•˜μ§€ μ•Šμ•„ λ‘œμ§μ—λ§Œ μ§‘μ€‘ν•˜λ‹€λ³΄λ‹ˆ 그런 것 κ°™μŠ΅λ‹ˆλ‹€', + lesson: ' 둜직이 λ§žλ‹€κ³  λ§žλŠ”κ±΄ μ•„λ‹ˆλ‹€. 풀기전에 잘 μƒκ°ν•΄λ³΄μž..', + }, + }, + { + cycle: 3, + plan: { + toDo: 'λ―Έμ…˜ 회고 κΈ€ ν¬μŠ€νŒ…', + completionCondition: 'ν•œ μ±•ν„°μ˜ λ‚΄μš©μ„ μ™„μ„±', + expectedProbability: '60%', + expectedDifficulty: '쒋은 글을 μ“°κΈ° μœ„ν•΄ 고민이 λ§Žμ•„μ§ˆ 수 μžˆλ‹€.', + whatCanYouDo: '글을 μ™„λ²½ν•˜κ²Œ μ“°λ €κ³  ν•˜κΈ°λ³΄λ‹¨ λŸ¬ν”„ν•˜κ²Œ λ¨Όμ € μ“΄λ‹€.', + }, + retrospect: { + doneAsExpected: 'ν•œ μ±•ν„°μ˜ 절반 정도 μž‘μ„±ν•œ 것 κ°™μŠ΅λ‹ˆλ‹€.', + experiencedDifficulty: + 'μΊ‘μ²˜ν•˜κ³  λΆ™μ—¬ λ„£λŠλΌ μ‹œκ°„μ΄ μ’€ κ±Έλ ΈμŠ΅λ‹ˆλ‹€. 그리고 μ±•ν„°μ˜ λ°©ν–₯성이 쑰금 μˆ˜μ •λ˜μ–΄μ„œ μ›ν• ν•˜κ²Œ μž‘μ„±ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + lesson: '글을 ν•œ λ²ˆμ— μ“°λ €κ³  ν•˜μ§€λ§κ³  쀑간쀑간 ν‹ˆν‹ˆμ΄ 기둝을 해놔야 ν•  것 κ°™μ•„μš”.', + }, + }, + ], +}; + +const STUDY_MEMBERS = { + progresses: [ + { + progressId: '1', + nickname: 'λ…Έμ•„', + currentCycle: 3, + step: 'done', + }, + { + progressId: '2', + nickname: 'λ£©μ†Œ', + currentCycle: 2, + step: 'planning', + }, + { + progressId: '3', + nickname: 'μ—½ν† ', + currentCycle: 3, + step: 'retrospect', + }, + ], +}; + +const STUDY_METADATA = { + name: 'μ•ˆμ˜€λ©΄ 지상렬', + totalCycle: 3, + timePerCycle: 25, + createdDateTime: '2023-08-15T06:25:39.093Z', +}; + +const server = setupServer( + rest.get('/api/studies/1/progresses', (_, res, ctx) => { + return res(ctx.json(STUDY_MEMBERS)); + }), + + rest.get('/api/studies/:studyId', (_, res, ctx) => { + return res(ctx.json(STUDY_METADATA)); + }), + + rest.get('/api/studies/:studyId/contents?progressId=1', (_, res, ctx) => { + return res(ctx.json(STUDY_CONTENT)); + }), +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('μŠ€ν„°λ”” 기둝 νŽ˜μ΄μ§€ ν…ŒμŠ€νŠΈ', () => { + test('μŠ€ν„°λ”” 기둝 νŽ˜μ΄μ§€μ—μ„œ μŠ€ν„°λ””μ›μ„ 확인할 수 μžˆλ‹€.', async () => { + render( + + + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText('λ…Έμ•„μ˜ 기둝')).toBeInTheDocument(); + expect(screen.getByText('λ£©μ†Œμ˜ 기둝')).toBeInTheDocument(); + expect(screen.getByText('μ—½ν† μ˜ 기둝')).toBeInTheDocument(); + }); + }); + + test('μŠ€ν„°λ”” 기둝 νŽ˜μ΄μ§€μ—μ„œ μŠ€ν„°λ””μ—μ„œ μ§„ν–‰ν•œ 사이클 횟수λ₯Ό 확인할 수 μžˆλ‹€.', async () => { + render( + + + + + + + , + ); + + await waitFor(() => { + const button = screen.getAllByText('펼쳐보기')[0]; + fireEvent.click(button); + }); + + await waitFor(() => { + expect(screen.getAllByTestId('tab').length).toBe(3); + }); + }); + + test('μŠ€ν„°λ”” 기둝 νŽ˜μ΄μ§€μ—μ„œ μŠ€ν„°λ””μ›μ˜ 기둝을 확인할 수 μžˆλ‹€.', async () => { + render( + + + + + + + , + ); + + await waitFor(() => { + const button = screen.getAllByText('펼쳐보기')[0]; + fireEvent.click(button); + }); + + await waitFor(() => { + expect(screen.getByText('λͺ¨λ˜ μžλ°”μŠ€ν¬λ¦½νŠΈ 15μž₯ 정독(let, const ν‚€μ›Œλ“œμ™€ 블둝 레벨 μŠ€μ½”ν”„)')).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/__test__/example.test.tsx b/frontend/__test__/example.test.tsx deleted file mode 100644 index 48d21d3e..00000000 --- a/frontend/__test__/example.test.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import '@testing-library/jest-dom'; - -import Hero from '@Components/landing/Hero/Hero'; - -test('test', async () => { - render(); - - const text = await screen.findByRole('heading'); - - expect(text).toHaveTextContent('μŠ€ν„°λ””'); -}); diff --git a/frontend/env-submodule b/frontend/env-submodule index fc016bd9..e4a67a35 160000 --- a/frontend/env-submodule +++ b/frontend/env-submodule @@ -1 +1 @@ -Subproject commit fc016bd93cda55f018d3b9efb6be67aa24b13980 +Subproject commit e4a67a3507c66486bfb746f6ed4b822c19f2836c diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index ebf79ef7..a17b20be 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -1,14 +1,3 @@ -import { server } from './src/mocks/server'; import mockFetch from 'jest-fetch-mock'; mockFetch.enableMocks(); - -// Establish API mocking before all tests. -beforeAll(() => server.listen()); - -// Reset any request handlers that we may add during the tests, -// so they don't affect other tests. -afterEach(() => server.resetHandlers()); - -// Clean up after the tests are finished. -afterAll(() => server.close()); diff --git a/frontend/package.json b/frontend/package.json index ad51635e..a771dd25 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,6 @@ "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", @@ -35,12 +34,14 @@ "@storybook/testing-library": "^0.0.14-next.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.2", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/parser": "^5.61.0", + "copy-webpack-plugin": "^11.0.0", "dotenv-webpack": "^8.0.1", "eslint": "^8.44.0", "eslint-config-airbnb-typescript": "^17.0.0", @@ -54,6 +55,7 @@ "jest-environment-jsdom": "^29.6.0", "jest-fetch-mock": "^3.0.3", "jest-transform-css": "^6.0.1", + "msw": "^1.2.3", "prettier": "^2.8.8", "storybook": "^7.0.25", "ts-loader": "^9.4.4", @@ -66,5 +68,8 @@ }, "msw": { "workerDirectory": "public" - } + }, + "sideEffects": [ + "./src/fonts/font.css" + ] } diff --git a/frontend/public/assets/favicon-blue.ico b/frontend/public/assets/favicon-blue.ico new file mode 100644 index 00000000..184107e2 Binary files /dev/null and b/frontend/public/assets/favicon-blue.ico differ diff --git a/frontend/public/assets/favicon-green.ico b/frontend/public/assets/favicon-green.ico new file mode 100644 index 00000000..423d54b7 Binary files /dev/null and b/frontend/public/assets/favicon-green.ico differ diff --git a/frontend/public/assets/favicon-red.ico b/frontend/public/assets/favicon-red.ico new file mode 100644 index 00000000..e664a4a9 Binary files /dev/null and b/frontend/public/assets/favicon-red.ico differ diff --git a/frontend/public/assets/favicon.ico b/frontend/public/assets/favicon.ico new file mode 100644 index 00000000..eebf4590 Binary files /dev/null and b/frontend/public/assets/favicon.ico differ diff --git a/frontend/public/assets/og-image.png b/frontend/public/assets/og-image.png new file mode 100644 index 00000000..95c96641 Binary files /dev/null and b/frontend/public/assets/og-image.png differ diff --git a/frontend/public/fonts/Pretendard-Bold.woff b/frontend/public/fonts/Pretendard-Bold.woff new file mode 100644 index 00000000..53470bae Binary files /dev/null and b/frontend/public/fonts/Pretendard-Bold.woff differ diff --git a/frontend/public/fonts/Pretendard-Bold.woff2 b/frontend/public/fonts/Pretendard-Bold.woff2 new file mode 100644 index 00000000..8975b802 Binary files /dev/null and b/frontend/public/fonts/Pretendard-Bold.woff2 differ diff --git a/frontend/public/fonts/Pretendard-Light.woff b/frontend/public/fonts/Pretendard-Light.woff new file mode 100644 index 00000000..bc0ad69f Binary files /dev/null and b/frontend/public/fonts/Pretendard-Light.woff differ diff --git a/frontend/public/fonts/Pretendard-Light.woff2 b/frontend/public/fonts/Pretendard-Light.woff2 new file mode 100644 index 00000000..a86436a3 Binary files /dev/null and b/frontend/public/fonts/Pretendard-Light.woff2 differ diff --git a/frontend/public/fonts/Pretendard-Medium.woff b/frontend/public/fonts/Pretendard-Medium.woff new file mode 100644 index 00000000..92ca0c39 Binary files /dev/null and b/frontend/public/fonts/Pretendard-Medium.woff differ diff --git a/frontend/public/fonts/Pretendard-Medium.woff2 b/frontend/public/fonts/Pretendard-Medium.woff2 new file mode 100644 index 00000000..153fd556 Binary files /dev/null and b/frontend/public/fonts/Pretendard-Medium.woff2 differ diff --git a/frontend/public/fonts/font.css b/frontend/public/fonts/font.css new file mode 100644 index 00000000..9945d17c --- /dev/null +++ b/frontend/public/fonts/font.css @@ -0,0 +1,21 @@ +@font-face { + font-family: 'Pretendard'; + font-weight: 700; + font-display: swap; + src: local('Pretendard Bold'), url('./Pretendard-Bold.woff2') format('woff2'), + url('./Pretendard-Bold.woff') format('woff'); +} +@font-face { + font-family: 'Pretendard'; + font-weight: 500; + font-display: swap; + src: local('Pretendard Medium'), url('./Pretendard-Medium.woff2') format('woff2'), + url('./Pretendard-Medium.woff') format('woff'); +} +@font-face { + font-family: 'Pretendard'; + font-weight: 300; + font-display: swap; + src: local('Pretendard Light'), url('./Pretendard-Light.woff2') format('woff2'), + url('./Pretendard-Light.woff') format('woff'); +} diff --git a/frontend/public/index.html b/frontend/public/index.html index afd8f924..09a2a281 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -2,10 +2,35 @@ + + + + + + + + + + + + + ν•˜λ£¨μŠ€ν„°λ”” + + + +
diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js index 8ee70b3e..0f24d515 100644 --- a/frontend/public/mockServiceWorker.js +++ b/frontend/public/mockServiceWorker.js @@ -2,7 +2,7 @@ /* tslint:disable */ /** - * Mock Service Worker (1.2.2). + * Mock Service Worker (1.3.1). * @see https://github.com/mswjs/msw * - Please do NOT modify this file. * - Please do NOT serve this file on production. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 72a589c2..4e6fc8d7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,10 @@ +import { Suspense } from 'react'; import { Outlet } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; +import ErrorBoundary from '@Components/common/ErrorBoundary/ErrorBoundary'; +import ErrorFallback from '@Components/common/ErrorFallback/ErrorFallback'; + import GlobalStyles from '@Styles/globalStyle'; import { lightTheme } from '@Styles/theme'; @@ -9,14 +13,18 @@ import ModalProvider from '@Contexts/ModalProvider'; const App = () => { return ( - - - - - - - - + + + + + + + + + + + + ); }; diff --git a/frontend/src/api/httpInstance.ts b/frontend/src/api/httpInstance.ts new file mode 100644 index 00000000..9bf3bc60 --- /dev/null +++ b/frontend/src/api/httpInstance.ts @@ -0,0 +1,69 @@ +import { ROUTES_PATH } from '@Constants/routes'; + +import type { HttpResponse } from '@Utils/Http'; +import Http from '@Utils/Http'; +import tokenStorage from '@Utils/tokenStorage'; +import url from '@Utils/url'; + +import type { ResponseAPIError } from '@Types/api'; + +import { ApiError, UnknownApiError } from '@Errors/index'; + +const http = new Http(process.env.REACT_APP_BASE_URL, { headers: { 'Content-Type': 'application/json' } }); + +const refreshAndRefetch = async (response: HttpResponse) => { + const { + data: { accessToken }, + } = await http.post<{ accessToken: string }>('/api/auth/refresh'); + + tokenStorage.setAccessToken(accessToken); + + return http.request(response.url, response.config); +}; + +const logout = () => { + tokenStorage.clear(); + if (url.getPathName() !== ROUTES_PATH.landing) { + alert('토큰이 만료 λ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ 둜그인 ν•΄μ£Όμ„Έμš”.'); + url.changePathName(ROUTES_PATH.landing); + } +}; + +const isApiErrorData = (data: object): data is ResponseAPIError => { + return 'code' in data && 'message' in data; +}; + +http.registerInterceptor({ + onRequest: (config) => { + if (!tokenStorage.accessToken) return config; + + config.headers = { + ...config.headers, + Authorization: `Bearer ${tokenStorage.accessToken}`, + }; + + return config; + }, + + onResponse: async (response: HttpResponse) => { + if (response.ok) return response; + + if (isApiErrorData(response.data)) { + const errorCode = response.data.code; + + if (errorCode === 1403 || errorCode === 1404) { + return refreshAndRefetch(response); + } + + if (errorCode === 1402 || errorCode === 1405) { + logout(); + } + + throw new ApiError(response.data.message, response.data.code, response.config); + } + + throw new UnknownApiError(response.config); + }, +}); + +export default http; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f7465fe7..940ecb8f 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,5 +1,3 @@ -import http from '@Utils/http'; - import type { ResponseMemberProgress, ResponseAuthToken, @@ -8,157 +6,103 @@ import type { ResponseMemberRecordContents, ResponseOneStudyInfo, ResponseMemberContents, - ResponseProgresses, ResponseStudies, ResponseStudyData, ResponseStudyDataList, ResponseStudyMembers, + ResponseCheckProgresses, } from '@Types/api'; import type { OAuthProvider } from '@Types/auth'; import type { PlanList, RetrospectList, StudyTimePerCycleOptions, TotalCycleOptions } from '@Types/study'; -const BASE_URL = ''; - -// μ˜›λ‚ κ±° +import http from './httpInstance'; -export const requestRegisterMember = async (nickname: string, studyId: string) => { - const response = await http.post(`${BASE_URL}/api/studies/${studyId}/members`, { - body: JSON.stringify({ nickname }), - }); +export const requestGetStudyData = (studyId: string) => http.get(`/api/studies/${studyId}`); - const locationHeader = response.headers.get('Location'); - const memberId = locationHeader?.split('/').pop() as string; +export const requestGetMemberStudyListData = (memberId: string) => + http.get(`/api/studies?memberId=${memberId}`); - return { memberId }; -}; +export const requestGetStudyMembers = (studyId: string) => + http.get(`/api/studies/${studyId}/progresses`); -export const requestSubmitPlanningForm = (studyId: string, memberId: string, plans: PlanList) => - http.post(`/api/studies/${studyId}/members/${memberId}/content/plans`, { - body: JSON.stringify(plans), - }); +export const requestGetMemberRecordContents = (studyId: string, progressId: string) => + http.get(`/api/studies/${studyId}/contents?progressId=${progressId}`); -export const requestGetStudyData = (studyId: string, accessToken: string) => - http.get(`/api/studies/${studyId}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); +export const requestPostGuestLogin = () => http.post(`/api/auth/guest`); -export const requestGetMemberStudyListData = (memberId: string, accessToken: string) => - http.get(`/api/studies?memberId=${memberId}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - -export const requestGetStudyMembers = (studyId: string, accessToken: string) => - http.get(`/api/studies/${studyId}/progresses`, { - headers: { Authorization: `Bearer ${accessToken}` }, +export const requestPostOAuthLogin = (provider: OAuthProvider, code: string) => + http.post(`/api/auth/login`, { + body: JSON.stringify({ oauthProvider: provider, code }), }); -export const requestGetMemberRecordContents = (studyId: string, progressId: string, accessToken: string) => - http.get(`/api/studies/${studyId}/contents?progressId=${progressId}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); +export const requestGetMemberInfo = () => http.get('/api/me'); -// μƒˆλ‘œ μ μš©λ˜λŠ” api -export const requestGuestLogin = async () => { - const response = await http.post(`${BASE_URL}/api/auth/guest`); +export const requestGetOneStudyData = async (studyId: string) => { + const { data } = await http.get(`/api/studies/${studyId}`); - return (await response.json()) as ResponseAuthToken; + return data; }; -export const requestOAuthLogin = async (provider: OAuthProvider, code: string) => { - const response = await http.post(`${BASE_URL}/api/auth/login`, { - body: JSON.stringify({ oauthProvider: provider, code }), - }); +export const requestGetMemberProgress = async (studyId: string, memberId: string) => { + const { data } = await http.get(`/api/studies/${studyId}/progresses?memberId=${memberId}`); - return (await response.json()) as ResponseAuthToken; + return data.progresses[0]; }; -export const requestMemberInfo = (accessToken: string) => - http.get('/api/me', { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - -export const requestAccessTokenRefresh = async () => { - const response = await http.post(`${BASE_URL}/api/auth/refresh`); +export const requestGetMemberContents = async (studyId: string, progressId: string, cycle: number) => { + const { data } = await http.get( + `/api/studies/${studyId}/contents?progressId=${progressId}&cycle=${cycle}`, + ); - return (await response.json()) as ResponseAuthToken; + return data.content[0].plan; }; -export const requestGetOneStudyData = (accessToken: string, studyId: string) => - http.get(`${BASE_URL}/api/studies/${studyId}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - -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}` }, +export const requestWritePlan = (studyId: string, progressId: string, plan: PlanList) => + http.post(`/api/studies/${studyId}/contents/write-plan`, { 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}` }, +export const requestWriteRetrospect = (studyId: string, progressId: string, retrospect: RetrospectList) => + http.post(`/api/studies/${studyId}/contents/write-retrospect`, { 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 requestNextStep = (studyId: string, progressId: string) => + http.post(`/api/studies/${studyId}/progresses/${progressId}/next-step`); -export const requestCreateStudy = async ( +export const requestPostCreateStudy = async ( studyName: string, - totalCycle: TotalCycleOptions, - timePerCycle: StudyTimePerCycleOptions, - accessToken: string, + totalCycle: TotalCycleOptions | null, + timePerCycle: StudyTimePerCycleOptions | null, ) => { - const response = await http.post(`/api/studies`, { - headers: { Authorization: `Bearer ${accessToken}` }, + const response = await http.post(`/api/studies`, { body: JSON.stringify({ name: studyName, totalCycle, timePerCycle }), }); const locationHeader = response.headers.get('Location'); const studyId = locationHeader?.split('/').pop() as string; - const result = (await response.json()) as ResponseCreateStudy; + const data = response.data; - return { studyId, result }; + return { studyId, data }; }; -export const requestAuthenticateParticipationCode = (participantCode: string, accessToken: string) => - http.get(`/api/studies?participantCode=${participantCode}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); +export const requestGetAuthenticateParticipationCode = async (participantCode: string) => { + const response = http.get(`/api/studies?participantCode=${participantCode}`); -export const requestCheckProgresses = async (studyId: string, memberId: string, accessToken: string) => - http.get(`/api/studies/${studyId}/progresses?memberId=${memberId}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); + return (await response).data; +}; + +export const requestGetCheckProgresses = async (studyId: string, memberId: string) => { + const response = http.get(`/api/temp/studies/${studyId}/progresses?memberId=${memberId}`); -export const requestRegisterProgress = (nickname: string, studyId: string, memberId: string, accessToken: string) => + return (await response).data; +}; + +export const requestPostRegisterProgress = (nickname: string, studyId: string, memberId: string) => http.post(`/api/studies/${studyId}/progresses`, { - headers: { Authorization: `Bearer ${accessToken}` }, body: JSON.stringify({ memberId, nickname }), }); -export const requestDeleteProgress = (studyId: string, progressId: number, accessToken: string) => - http.delete(`/api/studies/${studyId}/progresses/${progressId}`, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); +export const requestDeleteProgress = (studyId: string, progressId: number) => + http.delete(`/api/studies/${studyId}/progresses/${progressId}`); diff --git a/frontend/src/assets/icons/ChatIcon.tsx b/frontend/src/assets/icons/ChatIcon.tsx new file mode 100644 index 00000000..3b981c85 --- /dev/null +++ b/frontend/src/assets/icons/ChatIcon.tsx @@ -0,0 +1,16 @@ +type Props = { + color: string; +}; + +const ChatIcon = ({ color }: Props) => { + return ( + + + + ); +}; + +export default ChatIcon; diff --git a/frontend/src/assets/icons/CycleIcon.tsx b/frontend/src/assets/icons/CycleIcon.tsx index 7104aae4..7cf4cf3f 100644 --- a/frontend/src/assets/icons/CycleIcon.tsx +++ b/frontend/src/assets/icons/CycleIcon.tsx @@ -1,8 +1,8 @@ type Props = { - color: string; + color?: string; }; -const CycleIcon = ({ color }: Props) => { +const CycleIcon = ({ color = '#000000' }: Props) => { return ( diff --git a/frontend/src/assets/icons/GoogleIcon.tsx b/frontend/src/assets/icons/GoogleIcon.tsx new file mode 100644 index 00000000..2407c085 --- /dev/null +++ b/frontend/src/assets/icons/GoogleIcon.tsx @@ -0,0 +1,24 @@ +const GoogleIcon = () => { + return ( + + + + + + + ); +}; + +export default GoogleIcon; diff --git a/frontend/src/assets/icons/ReportIon.tsx b/frontend/src/assets/icons/ReportIon.tsx new file mode 100644 index 00000000..19c3fa94 --- /dev/null +++ b/frontend/src/assets/icons/ReportIon.tsx @@ -0,0 +1,31 @@ +type Props = { + color?: string; +}; + +const ReportIcon = ({ color = '#000000' }: Props) => { + return ( + + + + + + + + + + ); +}; + +export default ReportIcon; diff --git a/frontend/src/assets/icons/ResetIcon.tsx b/frontend/src/assets/icons/ResetIcon.tsx index 81382adb..43e445ef 100644 --- a/frontend/src/assets/icons/ResetIcon.tsx +++ b/frontend/src/assets/icons/ResetIcon.tsx @@ -1,8 +1,8 @@ type Props = { - color: string; + color?: string; }; -const ResetIcon = ({ color }: Props) => { +const ResetIcon = ({ color = '#000000' }: Props) => { return ( Promise; -}; - -const StepContents = ({ studyInfo, progressInfo, changeNextStep }: Props) => { - const isLastCycle = progressInfo.currentCycle === studyInfo.totalCycle; - - switch (progressInfo.step) { - case 'planning': - return ( - - ); - case 'studying': - return ( - - ); - case 'retrospect': - return ( - - ); - } -}; - -export default StepContents; diff --git a/frontend/src/components/board/StudyingForm/StudyingForm.tsx b/frontend/src/components/board/StudyingForm/StudyingForm.tsx deleted file mode 100644 index ec0455ca..00000000 --- a/frontend/src/components/board/StudyingForm/StudyingForm.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { css, styled } from 'styled-components'; - -import Button from '@Components/common/Button/Button'; -import CircularProgress from '@Components/common/CircularProgress/CircularProgress'; -import QuestionAnswer from '@Components/common/QuestionAnswer/QuestionAnswer'; - -import useStudyingForm from '@Hooks/board/useStudyingForm'; - -import color from '@Styles/color'; - -import { ROUTES_PATH } from '@Constants/routes'; -import { PLAN_KEYWORDS } from '@Constants/study'; - -import { getKeys } from '@Utils/getKeys'; - -import type { Plan } from '@Types/study'; - -type Props = { - onClickSubmitButton: () => Promise; - studyId: string; - progressId: string; - cycle: number; -}; - -const StudyingForm = ({ onClickSubmitButton, studyId, progressId, cycle }: Props) => { - const navigate = useNavigate(); - const { planList, isSubmitLoading, error, submitForm } = useStudyingForm( - studyId, - progressId, - cycle, - onClickSubmitButton, - ); - - const handleClickButton = async () => { - try { - await submitForm(); - } catch (error) { - if (!(error instanceof Error)) return; - alert(error.message); - } - }; - - if (error) { - alert(error.message); - if (confirm('메인 νŽ˜μ΄μ§€λ‘œ λŒμ•„κΈ°μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) { - navigate(ROUTES_PATH.landing); - } - } - - if (planList === null) { - return ( - - - - ); - } - - return ( - - - {getKeys(PLAN_KEYWORDS).map((planKey) => ( - - ))} - - - - ); -}; - -export default StudyingForm; - -const Layout = styled.section` - width: 100%; - height: 100%; - - display: flex; - flex-direction: column; - gap: 30px; - - padding: 60px 85px; -`; - -const PlanResultList = styled.ul` - width: 100%; - height: 90%; - - display: flex; - flex-direction: column; - gap: 60px; - - padding: 50px; - background-color: #fff; - border-radius: 14px; - - overflow-y: auto; -`; - -const LoadingLayout = styled.div` - width: 100%; - height: 100%; - - display: flex; - align-items: center; - justify-content: center; -`; diff --git a/frontend/src/components/common/Accordion/AccordionHeader.tsx b/frontend/src/components/common/Accordion/AccordionHeader.tsx index a1cbef85..fa3a7f89 100644 --- a/frontend/src/components/common/Accordion/AccordionHeader.tsx +++ b/frontend/src/components/common/Accordion/AccordionHeader.tsx @@ -28,4 +28,5 @@ const AccordionHeaderLayout = styled.div` display: grid; grid-template-columns: 1fr auto; align-items: center; + gap: 20px; `; diff --git a/frontend/src/components/common/AlertErrorBoundary/AlertErrorBoundary.tsx b/frontend/src/components/common/AlertErrorBoundary/AlertErrorBoundary.tsx new file mode 100644 index 00000000..8ab5ef40 --- /dev/null +++ b/frontend/src/components/common/AlertErrorBoundary/AlertErrorBoundary.tsx @@ -0,0 +1,41 @@ +import type { PropsWithChildren } from 'react'; +import { Component } from 'react'; + +import type { ModalContextType } from '@Contexts/ModalProvider'; +import { ModalContext } from '@Contexts/ModalProvider'; +import { ApiError } from '@Errors/index'; + +type AlertErrorBoundaryState = { + error: Error | null; +}; + +class AlertErrorBoundary extends Component { + state: AlertErrorBoundaryState = { + error: null, + }; + + static getDerivedStateFromError(error: Error) { + if (error instanceof ApiError) { + if (error.code === 1402 || error.code === 1405) { + throw error; + } + } + return { error: error }; + } + + static contextType = ModalContext; + + componentDidCatch(error: Error): void { + const { openAlert } = this.context as ModalContextType; + + openAlert(error.message); + } + + render() { + const { children } = this.props; + + return children; + } +} + +export default AlertErrorBoundary; diff --git a/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000..708e967e --- /dev/null +++ b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,40 @@ +import type { ComponentType, PropsWithChildren } from 'react'; +import { Component } from 'react'; + +import type { ErrorFallbackProps } from '../ErrorFallback/ErrorFallback'; + +type ErrorBoundaryProps = { + fallback: ComponentType; +}; + +type ErrorBoundaryState = { + error: Error | null; +}; + +class ErrorBoundary extends Component, ErrorBoundaryState> { + state: ErrorBoundaryState = { + error: null, + }; + + resetErrorBoundary = () => { + this.setState({ error: null }); + }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + render() { + const { children, fallback: Fallback } = this.props; + + const { error } = this.state; + + if (error) { + return ; + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/common/ErrorFallback/ErrorFallback.tsx b/frontend/src/components/common/ErrorFallback/ErrorFallback.tsx new file mode 100644 index 00000000..8012229e --- /dev/null +++ b/frontend/src/components/common/ErrorFallback/ErrorFallback.tsx @@ -0,0 +1,55 @@ +import { Link } from 'react-router-dom'; +import { styled } from 'styled-components'; + +import color from '@Styles/color'; + +import ResetIcon from '@Assets/icons/ResetIcon'; + +import Button from '../Button/Button'; +import Typography from '../Typography/Typography'; + +export type ErrorFallbackProps = { + error: Error; + resetErrorBoundary?: () => void; + layoutHeight?: string; +}; + +const ErrorFallback = ({ error, resetErrorBoundary, layoutHeight = '100vh' }: ErrorFallbackProps) => { + const errorMessage = error.message || 'μ•Œ 수 μ—†λŠ” μ—λŸ¬μž…λ‹ˆλ‹€.'; + return ( + + λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. + {errorMessage} + + + ν™ˆμœΌλ‘œ μ΄λ™ν•˜κΈ° + + + ); +}; + +export default ErrorFallback; + +const Layout = styled.section<{ height: string }>` + width: 100%; + height: ${({ height }) => height}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + + p { + display: flex; + align-items: center; + + gap: 10px; + } + + a { + color: ${color.blue[500]}; + text-decoration: underline; + } +`; diff --git a/frontend/src/components/common/Footer/Footer.tsx b/frontend/src/components/common/Footer/Footer.tsx new file mode 100644 index 00000000..e2c2775d --- /dev/null +++ b/frontend/src/components/common/Footer/Footer.tsx @@ -0,0 +1,56 @@ +import color from '@Styles/color'; +import { styled } from 'styled-components'; +import Typography from '../Typography/Typography'; + +const URL = { + github: 'https://github.com/woowacourse-teams/2023-haru-study', + feedback: 'https://docs.google.com/forms/d/e/1FAIpQLSdwvz3y9xYc9PHCLw1LiaLB8TGfGao91cVs_NwERHSV9c5Mfg/viewform', +}; + +const Footer = () => { + return ( + + + μš°μ•„ν•œν…Œν¬μ½”μŠ€ 5κΈ° ν•˜λ£¨μŠ€ν„°λ”” + + + Copyright Β© 2023 ν•˜λ£¨μŠ€ν„°λ”” - All rights reserved. + + + + + Github + + + + + μ‚¬μš©μž ν”Όλ“œλ°± + + + + + ); +}; + +export default Footer; + +const Layout = styled.footer` + width: 100%; + height: 100px; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +const LinkContainer = styled.div` + display: flex; + gap: 20px; + + margin-top: 10px; + + p { + text-decoration: underline; + } +`; diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx index e47f9d01..a2110a27 100644 --- a/frontend/src/components/common/Header/Header.tsx +++ b/frontend/src/components/common/Header/Header.tsx @@ -31,6 +31,8 @@ export default Header; const Layout = styled.header` padding: 40px; + + display: flex; `; const Emphasis = styled.span` diff --git a/frontend/src/components/common/Image/Image.tsx b/frontend/src/components/common/Image/Image.tsx new file mode 100644 index 00000000..7dbbf4b7 --- /dev/null +++ b/frontend/src/components/common/Image/Image.tsx @@ -0,0 +1,22 @@ +type Props = { + webpUrl: string[] | string; + originUrl: string; + alt?: string; +}; + +const Image = ({ webpUrl, originUrl, alt }: Props) => { + if (typeof webpUrl === 'string') { + webpUrl = [webpUrl]; + } + + return ( + + {webpUrl.map((item, index) => ( + + ))} + {alt} + + ); +}; + +export default Image; diff --git a/frontend/src/components/common/Input/Input.tsx b/frontend/src/components/common/Input/Input.tsx index 115e2ea9..df3a8a99 100644 --- a/frontend/src/components/common/Input/Input.tsx +++ b/frontend/src/components/common/Input/Input.tsx @@ -90,12 +90,17 @@ const Layout = styled.div` `; const StyledLabel = styled.label` - font-weight: 200; + font-size: 2.4rem; + font-weight: 300; ${({ $labelSize = 'medium', $style, theme }) => css` color: ${theme.text}; ${SIZE_TYPE[$labelSize]} ${$style}; `} + + @media screen and (max-width: 768px) { + font-size: 2rem; + } `; const StyledBottomText = styled.p<{ $error?: boolean }>` @@ -104,6 +109,10 @@ const StyledBottomText = styled.p<{ $error?: boolean }>` font-size: 1.6rem; font-weight: 200; + @media screen and (max-width: 768px) { + font-size: 1.4rem; + } + ${({ $error }) => css` color: ${$error ? color.red[600] : color.neutral[400]}; `}; diff --git a/frontend/src/components/common/LodingFallback/LoadingFallback.tsx b/frontend/src/components/common/LodingFallback/LoadingFallback.tsx new file mode 100644 index 00000000..36ecdb8f --- /dev/null +++ b/frontend/src/components/common/LodingFallback/LoadingFallback.tsx @@ -0,0 +1,35 @@ +import { css, styled } from 'styled-components'; + +import CircularProgress from '@Components/common/CircularProgress/CircularProgress'; + +import color from '@Styles/color'; + +type Props = { + height?: string; + circleColor?: string; +}; + +const LoadingFallback = ({ height = '100%', circleColor = color.blue[500] }: Props) => { + return ( + + + + ); +}; + +const Layout = styled.div<{ height: string }>` + width: 100%; + height: ${({ height }) => height}; + + display: flex; + align-items: center; + justify-content: center; +`; + +export default LoadingFallback; diff --git a/frontend/src/components/common/MemberInfoGuard/MemberInfoGuard.tsx b/frontend/src/components/common/MemberInfoGuard/MemberInfoGuard.tsx new file mode 100644 index 00000000..559e81b6 --- /dev/null +++ b/frontend/src/components/common/MemberInfoGuard/MemberInfoGuard.tsx @@ -0,0 +1,15 @@ +import type { PropsWithChildren } from 'react'; + +import LoadingFallback from '@Components/common/LodingFallback/LoadingFallback'; + +import { useMemberInfo } from '@Contexts/MemberInfoProvider'; + +const MemberInfoGuard = ({ children }: PropsWithChildren) => { + const memberInfo = useMemberInfo(); + + if (memberInfo === null) return ; + + return children; +}; + +export default MemberInfoGuard; diff --git a/frontend/src/components/common/Modal/Modal.tsx b/frontend/src/components/common/Modal/Modal.tsx index 07ef9be4..82ea1e11 100644 --- a/frontend/src/components/common/Modal/Modal.tsx +++ b/frontend/src/components/common/Modal/Modal.tsx @@ -56,4 +56,8 @@ const ModalContainer = styled.div` width: 500px; max-height: 600px; overflow-y: auto; + + @media screen and (max-width: 768px) { + width: 90%; + } `; diff --git a/frontend/src/components/common/Select/Select.tsx b/frontend/src/components/common/Select/Select.tsx index e62576e9..f2b4ac2c 100644 --- a/frontend/src/components/common/Select/Select.tsx +++ b/frontend/src/components/common/Select/Select.tsx @@ -49,7 +49,7 @@ const Select = ({ children, label, $style }: Props) => { value={{ selectedItem, isOpen, changeSelectedItem, toggleOpen, triggerSuffixText, changeTriggerSuffixText }} > - + {label} {children} @@ -66,6 +66,15 @@ const Layout = styled.div>` `} `; +const StyledLabel = styled.label` + font-size: 2.4rem; + font-weight: 300; + + @media screen and (max-width: 768px) { + font-size: 2rem; + } +`; + Select.Trigger = SelectTrigger; Select.List = SelectList; Select.Item = SelectItem; diff --git a/frontend/src/components/common/Select/SelectItem.tsx b/frontend/src/components/common/Select/SelectItem.tsx index 4c0b055f..704acd4b 100644 --- a/frontend/src/components/common/Select/SelectItem.tsx +++ b/frontend/src/components/common/Select/SelectItem.tsx @@ -49,4 +49,9 @@ const Layout = styled.div>` background-color: ${theme.background}; ${$style} `}; + + @media screen and (max-width: 768px) { + font-size: 1.8rem; + padding: 14px; + } `; diff --git a/frontend/src/components/common/Select/SelectList.tsx b/frontend/src/components/common/Select/SelectList.tsx index 92888393..f56b215b 100644 --- a/frontend/src/components/common/Select/SelectList.tsx +++ b/frontend/src/components/common/Select/SelectList.tsx @@ -1,15 +1,25 @@ -import type { HTMLAttributes, MouseEvent, ReactNode } from 'react'; +import type { EventHandler, HTMLAttributes, MouseEvent, ReactNode, SyntheticEvent } from 'react'; import { Children, cloneElement, isValidElement } from 'react'; import type { CSSProp } from 'styled-components'; import { css, styled } from 'styled-components'; import color from '@Styles/color'; -import { composeEventHandlers } from '@Utils/domEventHandler'; - import { useSelectContext } from './SelectContext'; import type { ItemProps } from './SelectItem'; +export type ComposeEventHandlers = >( + externalEventHandler?: EventHandler, + innerEventHandler?: EventHandler, +) => EventHandler; + +const composeEventHandlers: ComposeEventHandlers = (externalEventHandler, innerEventHandler) => { + return (event) => { + externalEventHandler?.(event); + innerEventHandler?.(event); + }; +}; + type Props = { children: ReactNode; diff --git a/frontend/src/components/common/Select/SelectTrigger.tsx b/frontend/src/components/common/Select/SelectTrigger.tsx index 62440377..8673f771 100644 --- a/frontend/src/components/common/Select/SelectTrigger.tsx +++ b/frontend/src/components/common/Select/SelectTrigger.tsx @@ -8,15 +8,16 @@ import { useSelectContext } from './SelectContext'; type Props = { triggerText?: string; + testId?: string; $style?: CSSProp; } & ButtonHTMLAttributes; -const SelectTrigger = ({ triggerText = '선택', ...props }: Props) => { +const SelectTrigger = ({ triggerText = '선택', testId, ...props }: Props) => { const { isOpen, selectedItem, toggleOpen, triggerSuffixText } = useSelectContext(); return ( - + {selectedItem === null ? triggerText : selectedItem.toString() + triggerSuffixText} {isOpen ? : } @@ -43,6 +44,11 @@ const Layout = styled.button` border-bottom-left-radius: ${$isOpen ? 'none' : '7px'}; ${$style} `}; + + @media screen and (max-width: 768px) { + font-size: 1.8rem; + padding: 14px; + } `; const Mark = styled.p` diff --git a/frontend/src/components/common/Tabs/TabList.tsx b/frontend/src/components/common/Tabs/TabList.tsx index a57861b5..998f1ab3 100644 --- a/frontend/src/components/common/Tabs/TabList.tsx +++ b/frontend/src/components/common/Tabs/TabList.tsx @@ -11,7 +11,7 @@ const TabList = () => { const isSelected = tab === selectedTab; return ( - changeTab(tab)} key={`${tab}${index}`} $isSelected={isSelected}> + changeTab(tab)} key={`${tab}${index}`} $isSelected={isSelected}> {tab} ); @@ -23,9 +23,11 @@ const TabList = () => { export default TabList; const TabListLayout = styled.ul` - display: grid; - grid-template-columns: repeat(8, 1fr); + width: 100%; + display: flex; row-gap: 20px; + + overflow-x: scroll; `; type TabProps = { @@ -37,7 +39,7 @@ const Tab = styled.li` align-items: center; justify-content: center; - padding-bottom: 5px; + padding: 0px 10px 5px; font-size: 1.8rem; text-align: center; diff --git a/frontend/src/components/common/Tabs/Tabs.tsx b/frontend/src/components/common/Tabs/Tabs.tsx index 52686452..2811ec72 100644 --- a/frontend/src/components/common/Tabs/Tabs.tsx +++ b/frontend/src/components/common/Tabs/Tabs.tsx @@ -23,4 +23,10 @@ Tabs.Item = TabItem; const TabsLayout = styled.div` display: grid; row-gap: 40px; + + @media screen and (max-width: 768px) { + row-gap: 20px; + + font-size: 1.4rem; + } `; diff --git a/frontend/src/components/create/CreateStudyForm/CreateStudyForm.tsx b/frontend/src/components/create/CreateStudyForm/CreateStudyForm.tsx index 8ccfb13b..cf00f0e4 100644 --- a/frontend/src/components/create/CreateStudyForm/CreateStudyForm.tsx +++ b/frontend/src/components/create/CreateStudyForm/CreateStudyForm.tsx @@ -4,10 +4,8 @@ import { css, styled } from 'styled-components'; import Button from '@Components/common/Button/Button'; import Input from '@Components/common/Input/Input'; import Select from '@Components/common/Select/Select'; -import Typography from '@Components/common/Typography/Typography'; - -import useCreateStudy from '@Hooks/create/useCreateStudy'; -import useCreateStudyForm from '@Hooks/create/useCreateStudyForm'; +import useCreateStudy from '@Components/create/hooks/useCreateStudy'; +import useCreateStudyForm from '@Components/create/hooks/useCreateStudyForm'; import color from '@Styles/color'; @@ -36,23 +34,16 @@ const CreateStudyForm = () => { isSelectedOptions, } = useCreateStudyForm(); - const errorHandler = (error: Error) => { - alert(error.message); - }; - - const { isLoading, createStudy } = useCreateStudy(errorHandler); + const { createStudy, isLoading } = useCreateStudy(studyName, totalCycle, timePerCycle); const handleClickCreateStudyButton = async () => { - if (!studyName || !totalCycle || !timePerCycle) { - alert('μ΄λ¦„μ˜ 길이와, 사이클 수, 사이클 λ‹Ή μ‹œκ°„μ„ λ‹€μ‹œ ν•œλ²ˆ ν™•μΈν•΄μ£Όμ„Έμš”'); - return; - } + const result = await createStudy(); - const data = await createStudy(studyName, totalCycle, timePerCycle); + if (result) { + const { studyId, data } = result; - if (data) { - navigate(`${ROUTES_PATH.preparation}/${data.studyId}`, { - state: { participantCode: data.result.participantCode, studyName, isHost: true }, + navigate(`${ROUTES_PATH.preparation}/${studyId}`, { + state: { participantCode: data.participantCode, studyName, isHost: true }, }); } }; @@ -64,10 +55,7 @@ const CreateStudyForm = () => { return ( - μŠ€ν„°λ””μ˜ 이름은 λ¬΄μ—‡μΈκ°€μš”?} - errorMessage={ERROR_MESSAGE.studyName} - > + { /> - + {isSelectedOptions ? ( <> μ˜ˆμƒ μŠ€ν„°λ”” μ‹œκ°„μ€{' '} @@ -140,7 +123,7 @@ const CreateStudyForm = () => { ) : ( '사이클 νšŸμˆ˜μ™€ 사이클 λ‹Ή ν•™μŠ΅ μ‹œκ°„μ„ μ„ νƒν•˜μ„Έμš”.' )} - + @@ -162,6 +145,16 @@ const Container = styled.div` gap: 70px; `; +const TimeDescription = styled.p` + font-size: 2rem; + font-weight: 300; + text-align: center; + + @media screen and (max-width: 768px) { + font-size: 1.6rem; + } +`; + const TimeText = styled.span` color: ${color.blue[500]}; text-decoration: underline; diff --git a/frontend/src/components/create/hooks/useCreateStudy.ts b/frontend/src/components/create/hooks/useCreateStudy.ts new file mode 100644 index 00000000..1c0ffb25 --- /dev/null +++ b/frontend/src/components/create/hooks/useCreateStudy.ts @@ -0,0 +1,25 @@ +import useMutation from '@Hooks/api/useMutation'; + +import { requestPostCreateStudy } from '@Apis/index'; + +import type { ResponseCreateStudy } from '@Types/api'; +import type { StudyTimePerCycleOptions, TotalCycleOptions } from '@Types/study'; + +type CreateStudyResult = { + studyId: string; + data: ResponseCreateStudy; +}; + +const useCreateStudy = ( + studyName: string, + totalCycle: TotalCycleOptions | null, + timePerCycle: StudyTimePerCycleOptions | null, +) => { + const { mutate: createStudy, isLoading } = useMutation(() => + requestPostCreateStudy(studyName, totalCycle, timePerCycle), + ); + + return { createStudy, isLoading }; +}; + +export default useCreateStudy; diff --git a/frontend/src/hooks/create/useCreateStudyForm.ts b/frontend/src/components/create/hooks/useCreateStudyForm.ts similarity index 97% rename from frontend/src/hooks/create/useCreateStudyForm.ts rename to frontend/src/components/create/hooks/useCreateStudyForm.ts index 0f9032ff..639575fa 100644 --- a/frontend/src/hooks/create/useCreateStudyForm.ts +++ b/frontend/src/components/create/hooks/useCreateStudyForm.ts @@ -26,7 +26,7 @@ const useCreateStudyForm = () => { }; return { - studyName: studyNameInput.state, + studyName: studyNameInput.state ?? '', changeStudyName: studyNameInput.onChangeInput, isStudyNameError: studyNameInput.isInputError, timePerCycle: timePerCycleSelect.state, diff --git a/frontend/src/components/landing/BugReportingLink/BugReportingLink.tsx b/frontend/src/components/landing/BugReportingLink/BugReportingLink.tsx new file mode 100644 index 00000000..fa4acbf3 --- /dev/null +++ b/frontend/src/components/landing/BugReportingLink/BugReportingLink.tsx @@ -0,0 +1,69 @@ +import { useState } from 'react'; +import { css, styled } from 'styled-components'; + +import color from '@Styles/color'; + +import ReportIcon from '@Assets/icons/ReportIon'; + +const BUG_REPORTING_LINK = + 'https://docs.google.com/forms/d/e/1FAIpQLSdwvz3y9xYc9PHCLw1LiaLB8TGfGao91cVs_NwERHSV9c5Mfg/viewform'; + +const BugReportingLink = () => { + const [isHoverIcon, setIsHoverIcon] = useState(false); + + return ( + + λΆˆνŽΈμ‚¬ν•­ ν”Όλ“œλ°±ν•˜κΈ° + setIsHoverIcon(true)} + onMouseLeave={() => setIsHoverIcon(false)} + > + + + + ); +}; + +export default BugReportingLink; + +const Layout = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + +type GuideMessageProps = { + $isHoverIcon: boolean; +}; + +const GuideMessage = styled.p` + font-size: 1.4rem; + + color: ${color.neutral[600]}; + + ${({ $isHoverIcon }) => css` + opacity: ${$isHoverIcon ? '1' : '0'}; + `} + + transition: opacity 0.15s ease; + + @media screen and (max-width: 768px) { + display: none; + } +`; + +const Link = styled.a` + cursor: pointer; + + background-color: ${color.neutral[100]}; + + padding: 12px; + border-radius: 14px; + + svg { + width: 24px; + height: 24px; + } +`; diff --git a/frontend/src/components/landing/ChattingLink/ChattingLink.tsx b/frontend/src/components/landing/ChattingLink/ChattingLink.tsx new file mode 100644 index 00000000..ae46278c --- /dev/null +++ b/frontend/src/components/landing/ChattingLink/ChattingLink.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { css, styled } from 'styled-components'; + +import color from '@Styles/color'; + +import ChatIcon from '@Assets/icons/ChatIcon'; + +const OPEN_CHATTING_LINK = 'https://open.kakao.com/o/gDt2u0Hf'; + +const ChattingLink = () => { + const [isHoverIcon, setIsHoverIcon] = useState(false); + + return ( + + λ‹€λ₯Έ μ‚¬λžŒκ³Ό ν•¨κ»˜ ν•™μŠ΅ν•˜κ³  μ‹Άλ‹€λ©΄? + setIsHoverIcon(true)} + onMouseLeave={() => setIsHoverIcon(false)} + > + + + + ); +}; + +export default ChattingLink; + +const Layout = styled.div` + display: flex; + align-items: center; + gap: 10px; +`; + +type GuideMessageProps = { + $isHoverIcon: boolean; +}; + +const GuideMessage = styled.p` + font-size: 1.4rem; + + color: ${color.neutral[600]}; + + ${({ $isHoverIcon }) => css` + opacity: ${$isHoverIcon ? '1' : '0'}; + `} + + transition: opacity 0.15s ease; + + @media screen and (max-width: 768px) { + display: none; + } +`; + +const Link = styled.a` + cursor: pointer; + + background-color: ${color.brand.kakao}; + + padding: 12px; + border-radius: 14px; + + svg { + width: 24px; + height: 24px; + } +`; diff --git a/frontend/src/components/landing/GuideSection/GuideSection.tsx b/frontend/src/components/landing/GuideSection/GuideSection.tsx new file mode 100644 index 00000000..c70bad06 --- /dev/null +++ b/frontend/src/components/landing/GuideSection/GuideSection.tsx @@ -0,0 +1,70 @@ +import { css, styled } from 'styled-components'; + +import Typography from '@Components/common/Typography/Typography'; + +import color from '@Styles/color'; + +import StudyEffectGuide from '../StudyEffectGuide/StudyEffectGuide'; +import StudyStepGuide from '../StudyStepGuide/StudyStepGuide'; + +const GuideSection = () => { + return ( + + + ν•˜λ£¨μŠ€ν„°λ”” ν•™μŠ΅ 사이클 + + ν•œ μ‚¬μ΄ν΄λ§ˆλ‹€ λͺ©ν‘œ μ„€μ • 단계, ν•™μŠ΅ 단계, 회고 λ‹¨κ³„λ‘œ κ΅¬μ„±λ˜μ–΄ 있으며, +
μ„Έ 가지 단계λ₯Ό 짧은 주기둜 μ—¬λŸ¬λ²ˆ λ°˜λ³΅ν•˜μ—¬ ν•™μŠ΅ν•©λ‹ˆλ‹€. +
+
+ + +
+ ); +}; + +export default GuideSection; + +const Layout = styled.section` + display: flex; + flex-direction: column; + gap: 180px; + + padding: 135px 0; +`; + +const Introduce = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + + text-align: center; + + span:nth-child(1) { + color: ${color.blue[500]}; + } + + span:nth-child(2) { + color: ${color.red[500]}; + } + + span:nth-child(3) { + color: ${color.teal[500]}; + } + + @media screen and (max-width: 768px) { + h2 { + font-size: 28px; + } + + p { + font-size: 20px; + } + } +`; diff --git a/frontend/src/components/landing/Hero/Hero.tsx b/frontend/src/components/landing/Hero/Hero.tsx deleted file mode 100644 index 77967a0b..00000000 --- a/frontend/src/components/landing/Hero/Hero.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { styled } from 'styled-components'; - -import Typography from '@Components/common/Typography/Typography'; - -import color from '@Styles/color'; - -import HeroImage from '@Assets/image/heroImage.png'; - -const Hero = () => { - return ( - - - - μŠ€ν„°λ””μ— -
- ν•„μš”ν•œ -
- μ‹œκ°„ -
- 단, ν•˜λ£¨ -
-
- - - μž‘κ°€ jcomp 좜처 Freepik - -
- ); -}; - -export default Hero; - -const HeroContainer = styled.div` - position: relative; - - background-color: ${color.blue[500]}; -`; - -const SloganContainer = styled.div` - position: absolute; - - top: 12%; - left: 15%; - - h3 { - font-size: 64px; - font-weight: 600; - color: ${color.white}; - line-height: 110%; - } -`; - -const ImageBox = styled.div` - position: absolute; - - left: 50%; - bottom: 10px; - - transform: translate(-50%, 0); - - width: 75%; - - p { - float: right; - opacity: 0.2; - } -`; - -const Emphasis = styled.span` - color: ${color.yellow}; -`; diff --git a/frontend/src/components/landing/LandingButton/LandingButton.tsx b/frontend/src/components/landing/LandingButton/LandingButton.tsx new file mode 100644 index 00000000..2d4e9564 --- /dev/null +++ b/frontend/src/components/landing/LandingButton/LandingButton.tsx @@ -0,0 +1,65 @@ +import { Link } from 'react-router-dom'; +import { styled } from 'styled-components'; + +import Button from '@Components/common/Button/Button'; + +import { ROUTES_PATH } from '@Constants/routes'; + +import { useMemberInfo } from '@Contexts/MemberInfoProvider'; +import { useModal } from '@Contexts/ModalProvider'; + +import LoginModalContents from '../LoginModalContents/LoginModalContents'; + +const LandingButton = () => { + const { openModal } = useModal(); + const memberInfo = useMemberInfo(); + const isLogin = !!memberInfo; + + if (isLogin) { + return ( + + + + + + + + + ); + } + + return ( + + + + ); +}; + +export default LandingButton; + +type ButtonContainerProps = { + $isLogin: boolean; +}; + +const ButtonContainer = styled.div` + display: flex; + gap: 10px; + + @media screen and (max-width: 768px) { + flex-direction: column; + align-items: center; + } +`; diff --git a/frontend/src/components/landing/LandingContents/LandingContents.tsx b/frontend/src/components/landing/LandingContents/LandingContents.tsx deleted file mode 100644 index 54ffc00f..00000000 --- a/frontend/src/components/landing/LandingContents/LandingContents.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Link } from 'react-router-dom'; -import { css, styled } from 'styled-components'; - -import Button from '@Components/common/Button/Button'; -import Typography from '@Components/common/Typography/Typography'; - -import color from '@Styles/color'; - -import { ROUTES_PATH } from '@Constants/routes'; - -import MemberProfile from '../MemberProfile/MemberProfile'; - -const LandingContents = () => { - return ( - - - - - ν•˜λ£¨μŠ€ν„°λ”” - - - ν•˜λ£¨μŠ€ν„°λ””λ§Œμ˜ ν•™μŠ΅ 사이클을 톡해 -
- μ—¬λŸ¬λΆ„μ˜ μŠ€ν„°λ”” νš¨μœ¨μ„±μ„ -
- λŒμ–΄μ˜¬λ¦½λ‹ˆλ‹€. -
-
- - - - - - - - -
- ); -}; - -export default LandingContents; - -const ContentsContainer = styled.div` - display: grid; - grid-template-rows: auto 1fr auto; - row-gap: 100px; - - padding: 40px 60px; - - ${({ theme }) => css` - background-color: ${theme.background}; - `} -`; - -const TopicSummaryContainer = styled.div` - align-self: flex-end; - justify-self: flex-end; - - text-align: end; - - p { - font-size: 2rem; - font-weight: 200; - line-height: 150%; - } - - svg { - font-size: 4rem; - } -`; - -const ButtonContainer = styled.div` - display: grid; - row-gap: 20px; -`; - -const Emphasis = styled.span` - color: ${color.blue[500]}; -`; diff --git a/frontend/src/components/landing/LandingMainSection/LandingMainSection.tsx b/frontend/src/components/landing/LandingMainSection/LandingMainSection.tsx new file mode 100644 index 00000000..0c315835 --- /dev/null +++ b/frontend/src/components/landing/LandingMainSection/LandingMainSection.tsx @@ -0,0 +1,92 @@ +import { styled } from 'styled-components'; + +import Image from '@Components/common/Image/Image'; +import Typography from '@Components/common/Typography/Typography'; + +import color from '@Styles/color'; + +import ArrowIcon from '@Assets/icons/ArrowIcon'; +import heroImageJpg from '@Assets/images/heroImage.jpg'; +import heroImageWebp from '@Assets/images/heroImage.webp'; + +import LandingButton from '../LandingButton/LandingButton'; + +const LandingMainSection = () => { + return ( +
+ + + + ν•˜λ£¨μŠ€ν„°λ””λ§Œμ˜ +
+ ν•™μŠ΅ 사이클을 톡해 +
+ 효율적으둜 ν•™μŠ΅ν•΄λ³΄μ„Έμš”. +
+ +
+ + λͺ©ν‘œ, ν•™μŠ΅, 회고 μŠ€νƒ­ + +
+ + ν•˜λ£¨μŠ€ν„°λ””λ§Œμ˜ ν•™μŠ΅ μ‚¬μ΄ν΄μ΄λž€? + + +
+ ); +}; + +export default LandingMainSection; + +const LandingHeader = styled.div` + height: calc(100vh - 240px); + + display: flex; + justify-content: space-between; + align-items: center; +`; + +const LandingContents = styled.div` + width: 40%; + + display: flex; + flex-direction: column; + gap: 40px; + align-self: center; + + h2 { + font-weight: 500; + } + + @media screen and (max-width: 768px) { + width: 100%; + + justify-self: center; + text-align: center; + + h2 { + font-size: 28px; + } + } +`; + +const HeroImage = styled.div` + width: 50%; + + @media screen and (max-width: 768px) { + display: none; + width: 0; + } +`; + +const LoadMoreContents = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +`; + +const Emphasis = styled.span` + color: ${color.blue[500]}; +`; diff --git a/frontend/src/components/landing/LoginModalContents/LoginModalContents.tsx b/frontend/src/components/landing/LoginModalContents/LoginModalContents.tsx new file mode 100644 index 00000000..38a875af --- /dev/null +++ b/frontend/src/components/landing/LoginModalContents/LoginModalContents.tsx @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +import { Link } from 'react-router-dom'; +import { css, styled } from 'styled-components'; + +import Button from '@Components/common/Button/Button'; +import Typography from '@Components/common/Typography/Typography'; + +import color from '@Styles/color'; + +import { ROUTES_PATH } from '@Constants/routes'; + +import GoogleIcon from '@Assets/icons/GoogleIcon'; + +const REDIRECT_URI_PARAMETER = '/auth?provider=google'; + +const LoginModalContents = () => { + const baseUri = `${window.location.protocol}//${window.location.host}`; + + const googleOAuthUrl = `https://accounts.google.com/o/oauth2/v2/auth?scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email&client_id=${process.env.REACT_APP_GOOGLE_CLIENT_ID}&response_type=code&redirect_uri=${baseUri}${REDIRECT_URI_PARAMETER}`; + + return ( + + + ν•˜λ£¨μŠ€ν„°λ”” 둜그인 + + + + + + κ΅¬κΈ€λ‘œ 둜그인 + + + + + λ˜λŠ” + + + + λΉ„νšŒμ›μœΌλ‘œ 둜그인 + + + + ); +}; + +export default LoginModalContents; + +const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 40px; + justify-content: center; + align-items: center; + + padding: 40px 0px; +`; + +const ButtonContainer = styled.div` + display: flex; + flex-direction: column; + gap: 20px; + + width: 360px; + + @media screen and (max-width: 768px) { + width: 95%; + } +`; + +const OAutLoginButton = styled(Button)` + position: relative; + + border-radius: 8px; + border: 1px solid ${color.neutral[300]}; + + color: ${color.black}; + font-size: 1.8rem; + + svg { + position: absolute; + top: 0; + bottom: 0; + left: 32px; + + display: flex; + margin: auto 0; + } +`; + +const Emphasis = styled.span` + color: ${color.blue[500]}; +`; + +const DividedContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + color: ${color.neutral[600]}; +`; + +const DividedLine = styled.div` + width: 40%; + height: 1px; + + background-color: ${color.neutral[300]}; +`; + +const NonMemberLoginButton = styled.button` + margin: 0 auto; + + a { + text-decoration: underline; + color: ${color.neutral[500]}; + } +`; diff --git a/frontend/src/components/landing/MemberProfile/MemberProfile.tsx b/frontend/src/components/landing/MemberProfile/MemberProfile.tsx index a9aa85b4..d4936e33 100644 --- a/frontend/src/components/landing/MemberProfile/MemberProfile.tsx +++ b/frontend/src/components/landing/MemberProfile/MemberProfile.tsx @@ -1,11 +1,9 @@ import { useNavigate } from 'react-router-dom'; -import { css, styled } from 'styled-components'; +import { css } from 'styled-components'; import type { MenuItem } from '@Components/common/Menu/Menu'; import Menu from '@Components/common/Menu/Menu'; -import { DefaultSkeletonStyle } from '@Styles/common'; - import { ROUTES_PATH } from '@Constants/routes'; import { useMemberInfo, useMemberInfoAction } from '@Contexts/MemberInfoProvider'; @@ -32,19 +30,14 @@ const DEFAULT_MENU_ITEMS: MenuItem[] = [ const MemberProfile = () => { const navigate = useNavigate(); - const { data } = useMemberInfo(); + const memberInfo = useMemberInfo(); const { clearMemberInfo } = useMemberInfoAction(); - const isLoading = !data; - - if (isLoading) - return ( - - - - ); + if (!memberInfo) { + return <>; + } - const { name, imageUrl, loginType } = data; + const { name, imageUrl, loginType } = memberInfo; const guestMenu = ( @@ -84,17 +77,3 @@ const MemberProfile = () => { }; export default MemberProfile; - -const LoadingImageWrapper = styled.div` - padding: 4px; - margin: 0 0 0 auto; -`; - -const LoadingImage = styled.div` - width: 36px; - height: 36px; - - border-radius: 50%; - - ${DefaultSkeletonStyle} -`; diff --git a/frontend/src/components/landing/MenuTrigger/MenuTrigger.tsx b/frontend/src/components/landing/MenuTrigger/MenuTrigger.tsx index ae281e82..64bdff72 100644 --- a/frontend/src/components/landing/MenuTrigger/MenuTrigger.tsx +++ b/frontend/src/components/landing/MenuTrigger/MenuTrigger.tsx @@ -54,7 +54,7 @@ const Layout = styled.div` p { font-size: 2rem; - font-weight: 400; + font-weight: 500; } svg { diff --git a/frontend/src/components/landing/SideLink/SideLik.tsx b/frontend/src/components/landing/SideLink/SideLik.tsx new file mode 100644 index 00000000..5188c316 --- /dev/null +++ b/frontend/src/components/landing/SideLink/SideLik.tsx @@ -0,0 +1,31 @@ +import { styled } from 'styled-components'; + +import BugReportingLink from '../BugReportingLink/BugReportingLink'; +import ChattingLink from '../ChattingLink/ChattingLink'; + +const SideLink = () => { + return ( + + + + + ); +}; + +export default SideLink; + +const Layout = styled.div` + position: fixed; + bottom: 36px; + right: 36px; + + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; + + @media screen and (max-width: 768px) { + bottom: 18px; + right: 18px; + } +`; diff --git a/frontend/src/components/landing/StartSection/StartSection.tsx b/frontend/src/components/landing/StartSection/StartSection.tsx new file mode 100644 index 00000000..50070bff --- /dev/null +++ b/frontend/src/components/landing/StartSection/StartSection.tsx @@ -0,0 +1,37 @@ +import { styled } from 'styled-components'; + +import Typography from '@Components/common/Typography/Typography'; + +import LandingButton from '../LandingButton/LandingButton'; + +const StartSection = () => { + return ( + + ν•˜λ£¨μŠ€ν„°λ””λ₯Ό μ‹œμž‘ν•΄λ³ΌκΉŒμš”? + + + ); +}; + +export default StartSection; + +const Layout = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 40px; + + padding: 50px 0px 160px; + + @media screen and (max-width: 768px) { + padding: 60px 0px 120px; + } + + h4 { + font-weight: 500; + + @media screen and (max-width: 768px) { + font-size: 2.4rem; + } + } +`; diff --git a/frontend/src/components/landing/StudyEffectGuide/StudyEffectGuide.tsx b/frontend/src/components/landing/StudyEffectGuide/StudyEffectGuide.tsx new file mode 100644 index 00000000..5bede910 --- /dev/null +++ b/frontend/src/components/landing/StudyEffectGuide/StudyEffectGuide.tsx @@ -0,0 +1,106 @@ +import { styled } from 'styled-components'; + +import Typography from '@Components/common/Typography/Typography'; + +import color from '@Styles/color'; + +import GoalIcon from '@Assets/icons/GoalIcon'; +import PencilIcon from '@Assets/icons/PencilIcon'; +import TimeLineIcon from '@Assets/icons/TimeLineIcon'; + +const STUDY_EFFECT = [ + { + icon: , + title: 'λͺ©ν‘œ μ„€μ •', + description: `ν•œ μ‚¬μ΄ν΄μ˜ ν•™μŠ΅μ„ μ‹œμž‘ν•˜κΈ° μ „, ν•™μŠ΅λͺ©ν‘œμ™€ μ™„λ£Œ 쑰건을 μ„€μ •ν•΄ ν•™μŠ΅ λ°©ν–₯μ„±κ³Ό 동기 λΆ€μ—¬λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.`, + }, + { + icon: , + title: '회고', + description: `ν•œ μ‚¬μ΄ν΄μ˜ ν•™μŠ΅μ΄ λλ‚œ ν›„, ν•™μŠ΅μ— λŒ€ν•΄ 슀슀둜 ν”Όλ“œλ°±ν•©λ‹ˆλ‹€. ν”Όλ“œλ°±μ„ 톡해 전보닀 더 κ°œμ„ λœ ν•™μŠ΅μ΄ λ˜λ„λ‘ ν•©λ‹ˆλ‹€.`, + }, + { + icon: , + title: '단계별 타이머', + description: `λ‹¨κ³„λ³„λ‘œ μ‹œκ°„μ„ μ œν•œν•˜μ—¬ ν•™μŠ΅μ„ ν•˜λŠ” λ™μ•ˆ 집쀑λ ₯을 μžƒμ§€ μ•Šλ„λ‘ λ„μ™€μ€λ‹ˆλ‹€.`, + }, +]; + +const StudyEffectGuide = () => { + return ( + + 사이클 μš”μ†Œ 별 ν•™μŠ΅ 효과 + + {STUDY_EFFECT.map(({ icon, title, description }) => ( + + + {icon} + {title} + + {description} + + ))} + + + ); +}; + +export default StudyEffectGuide; + +const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 50px; + + h2 { + text-align: center; + + @media screen and (max-width: 768px) { + font-size: 28px; + } + } +`; + +const StudyEffectList = styled.ul` + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 50px; + + @media screen and (max-width: 768px) { + flex-direction: column; + } +`; + +const StudyEffectItem = styled.li` + width: 400px; + + display: flex; + flex-direction: column; + gap: 10px; + + padding: 20px; + + background-color: ${color.neutral[100]}; + border-radius: 14px; + + white-space: pre-line; + + @media screen and (max-width: 768px) { + width: 100%; + } +`; + +const StudyEffectTitle = styled.h6` + display: flex; + align-items: center; + gap: 10px; + + font-size: 22px; + font-weight: 500; +`; + +const StudyEffectDescription = styled.p` + font-size: 20px; + word-break: keep-all; +`; diff --git a/frontend/src/components/landing/StudyStepGuide/StudyStepGuide.tsx b/frontend/src/components/landing/StudyStepGuide/StudyStepGuide.tsx new file mode 100644 index 00000000..2e019a1c --- /dev/null +++ b/frontend/src/components/landing/StudyStepGuide/StudyStepGuide.tsx @@ -0,0 +1,121 @@ +import { css, styled } from 'styled-components'; + +import Image from '@Components/common/Image/Image'; +import Typography from '@Components/common/Typography/Typography'; + +import color from '@Styles/color'; + +import planningStepJpg from '@Assets/images/planningStep.jpg'; +import planningStepWebp from '@Assets/images/planningStep.webp'; +import retrospectStepJpg from '@Assets/images/retrospectStep.jpg'; +import retrospectStepWebp from '@Assets/images/retrospectStep.webp'; +import studyingStepJpg from '@Assets/images/studyingStep.jpg'; +import studyingStepWebp from '@Assets/images/studyingStep.webp'; + +const GUIDE = [ + { + originUrl: planningStepJpg, + webpUrl: planningStepWebp, + title: 'λͺ©ν‘œ μ„€μ • 단계', + description: `ν•™μŠ΅μ„ μ‹œμž‘ν•˜κΈ° μ „, ν•™μŠ΅ λͺ©ν‘œλ₯Ό μ„€μ •ν•˜λŠ” λ‹¨κ³„μž…λ‹ˆλ‹€. + 무엇을 ν•™μŠ΅ν•  것인지, ν•™μŠ΅μ— λŒ€ν•œ μ™„λ£Œ 쑰건은 무엇인지 생각해 λ΄…λ‹ˆλ‹€. + (사이클 λ‹Ή μ‹œκ°„: 10λΆ„ 이내)`, + }, + { + originUrl: studyingStepJpg, + webpUrl: studyingStepWebp, + title: 'ν•™μŠ΅ 단계', + description: `ν•™μŠ΅ λͺ©ν‘œλ₯Ό λ‹¬μ„±ν•˜κΈ° μœ„ν•΄ μ—΄μ‹¬νžˆ ν•™μŠ΅ν•˜λŠ” λ‹¨κ³„μž…λ‹ˆλ‹€. + (사이클 λ‹Ή μ‹œκ°„: 20~60λΆ„)`, + }, + { + originUrl: retrospectStepJpg, + webpUrl: retrospectStepWebp, + title: '회고 단계', + description: `μ§„ν–‰ν•œ ν•™μŠ΅μ„ λ˜λŒμ•„λ³΄λŠ” λ‹¨κ³„μž…λ‹ˆλ‹€. + 회고λ₯Ό μ™„λ£Œν–ˆλ‹€λ©΄, λ‹€μ‹œ λͺ©ν‘œ μ„€μ • λ‹¨κ³„λ‘œ λŒμ•„κ°‘λ‹ˆλ‹€. + (사이클 λ‹Ή μ‹œκ°„: 10λΆ„ 이내)`, + }, +] as const; + +const StudyStepGuide = () => { + return ( + + {GUIDE.map(({ originUrl, webpUrl, title, description }, index) => ( + + + {title} + + + {title} + + {description} + + + + ))} + + ); +}; + +export default StudyStepGuide; + +const Layout = styled.ul` + display: flex; + flex-direction: column; + gap: 135px; +`; + +const StepGuide = styled.div<{ isEvenIndex: boolean }>` + display: flex; + flex-direction: ${({ isEvenIndex }) => (isEvenIndex ? 'row' : 'row-reverse')}; + justify-content: center; + align-items: center; + gap: 80px; + + @media screen and (max-width: 768px) { + flex-direction: column; + gap: 40px; + } +`; + +const StepGuideImage = styled.div` + width: 55%; + border-radius: 20px; + + box-shadow: rgba(17, 12, 46, 0.15) 0px 48px 100px 0px; + + @media screen and (max-width: 768px) { + width: 100%; + } +`; + +const StepGuideDescription = styled.div` + width: 40%; + + display: flex; + flex-direction: column; + gap: 20px; + + margin-right: auto; + white-space: pre-line; + + @media screen and (max-width: 768px) { + width: 100%; + + h2 { + font-size: 32px; + } + + p { + font-size: 20px; + } + } +`; diff --git a/frontend/src/components/participation/ParticipationCodeInput/ParticipationCodeInput.tsx b/frontend/src/components/participation/ParticipationCodeInput/ParticipationCodeInput.tsx index cdda2ebc..bd493027 100644 --- a/frontend/src/components/participation/ParticipationCodeInput/ParticipationCodeInput.tsx +++ b/frontend/src/components/participation/ParticipationCodeInput/ParticipationCodeInput.tsx @@ -3,10 +3,9 @@ import { css, styled } from 'styled-components'; import Button from '@Components/common/Button/Button'; import Input from '@Components/common/Input/Input'; -import Typography from '@Components/common/Typography/Typography'; +import useParticipationCode from '@Components/participation/hooks/useParticipationCode'; import useInput from '@Hooks/common/useInput'; -import useParticipationCode from '@Hooks/participation/useParticipationCode'; import { ROUTES_PATH } from '@Constants/routes'; @@ -15,33 +14,21 @@ const ParticipationCodeInput = () => { const participantCodeInput = useInput(false); - const errorHandler = (error: Error) => { - alert(error.message); - }; - - const { authenticateParticipationCode, isLoading } = useParticipationCode(errorHandler); + const { authenticateParticipationCode, isLoading } = useParticipationCode(participantCodeInput.state ?? ''); const handleOnClickParticipateButton = async () => { - if (!participantCodeInput.state) { - alert('μ°Έμ—¬μ½”λ“œλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.'); - return; - } + const result = await authenticateParticipationCode(); - const data = await authenticateParticipationCode(participantCodeInput.state); - - if (data) { - navigate(`${ROUTES_PATH.preparation}/${data.studies[0].studyId}`, { - state: { participantCode: participantCodeInput.state, studyName: data.studies[0].name, isHost: false }, + if (result) { + navigate(`${ROUTES_PATH.preparation}/${result.studies[0].studyId}`, { + state: { participantCode: participantCodeInput.state, studyName: result.studies[0].name, isHost: false }, }); } }; return ( - μ°Έμ—¬μ½”λ“œ} - bottomText="μŠ€ν„°λ””μž₯μ—κ²Œ 받은 μ°Έμ—¬μ½”λ“œλ₯Ό μž…λ ₯ν•˜μ„Έμš”." - > + diff --git a/frontend/src/components/participation/MemberRegister/MemberRegister.tsx b/frontend/src/components/participation/PrticipationContents/MemberRegister/MemberRegister.tsx similarity index 77% rename from frontend/src/components/participation/MemberRegister/MemberRegister.tsx rename to frontend/src/components/participation/PrticipationContents/MemberRegister/MemberRegister.tsx index ae11d5cd..0715d679 100644 --- a/frontend/src/components/participation/MemberRegister/MemberRegister.tsx +++ b/frontend/src/components/participation/PrticipationContents/MemberRegister/MemberRegister.tsx @@ -4,9 +4,9 @@ import { styled } from 'styled-components'; import Button from '@Components/common/Button/Button'; import Input from '@Components/common/Input/Input'; import Typography from '@Components/common/Typography/Typography'; +import useRegisterProgress from '@Components/participation/hooks/useRegisterProgress'; import useInput from '@Hooks/common/useInput'; -import useRegisterMember from '@Hooks/participation/useRegisterProgress'; import { ERROR_MESSAGE } from '@Constants/errorMessage'; import { ROUTES_PATH } from '@Constants/routes'; @@ -19,24 +19,15 @@ type Props = { const MemberRegister = ({ studyId, studyName }: Props) => { const navigate = useNavigate(); - const errorHandler = (error: Error) => { - alert(error.message); - }; - - const { isLoading, registerProgress } = useRegisterMember(errorHandler); - const nickNameInput = useInput(true); - const handleOnClickStartButton = async () => { - if (!nickNameInput.state || !studyId) { - alert('잘λͺ»λœ μ ‘κ·Όμž…λ‹ˆλ‹€.'); - return; - } - - await registerProgress(nickNameInput.state, studyId); + const { isLoading, registerProgress } = useRegisterProgress(nickNameInput.state ?? '', studyId); - navigate(`${ROUTES_PATH.board}/${studyId}`); + const handleOnClickStartButton = async () => { + const result = await registerProgress(); + if (result?.ok) return navigate(`${ROUTES_PATH.progress}/${studyId}`); }; + return ( <> void; + progressId: number; + nickname: string; + showMemberRegister: () => void; }; -const MemberRestart = ({ studyName, nickname, studyId, restart }: Props) => { +const MemberRestart = ({ studyName, nickname, studyId, progressId, showMemberRegister }: Props) => { const navigate = useNavigate(); + const { mutate } = useMutation(() => requestDeleteProgress(studyId, progressId)); + const handleOnClickContinueStart = async () => { - navigate(`${ROUTES_PATH.board}/${studyId}`); + navigate(`${ROUTES_PATH.progress}/${studyId}`); + }; + + const restart = async () => { + const result = await mutate(); + + if (result?.ok) return showMemberRegister(); }; return ( - <> +
{ {studyName} μŠ€ν„°λ””μ—μ„œ 이미 ν•™μŠ΅μ„ μ§„ν–‰ν•œ 기둝이 μžˆμŠ΅λ‹ˆλ‹€. λ‹‰λ„€μž„}> - +
@@ -69,15 +82,36 @@ const MemberRestart = ({ studyName, nickname, studyId, restart }: Props) => {
- +
); }; export default MemberRestart; +const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 70px; + + @media screen and (max-width: 768px) { + p { + font-size: 2rem; + } + } +`; + const ButtonContainer = styled.div` display: flex; justify-content: space-between; + + @media screen and (max-width: 768px) { + flex-direction: column; + gap: 10px; + + button { + width: 100%; + } + } `; const StudyNameText = styled.span` diff --git a/frontend/src/components/participation/PrticipationContents/PartcipationContents.tsx b/frontend/src/components/participation/PrticipationContents/PartcipationContents.tsx new file mode 100644 index 00000000..75e5a28b --- /dev/null +++ b/frontend/src/components/participation/PrticipationContents/PartcipationContents.tsx @@ -0,0 +1,54 @@ +import { useState } from 'react'; +import { styled } from 'styled-components'; + +import AlertErrorBoundary from '@Components/common/AlertErrorBoundary/AlertErrorBoundary'; +import useCheckProgresses from '@Components/participation/hooks/useCheckProgresses'; + +import MemberRegister from './MemberRegister/MemberRegister'; +import MemberRestart from './MemberRestart/MemberRestart'; +import ParticipationCodeCopier from './ParticipationCodeCopier/ParticipationCodeCopier'; + +type Props = { + participantCode: string; + studyName: string; + isHost: boolean; +}; + +const ParticipationContents = ({ participantCode, studyName, isHost }: Props) => { + const { result, studyId } = useCheckProgresses(); + + const [isRegisterShow, setRegisterShow] = useState(false); + + const handleShowMemberRegister = () => { + setRegisterShow(true); + }; + + return ( + + {isHost && } + {result && ( + + {result.progresses && !isRegisterShow ? ( + + ) : ( + + )} + + )} + + ); +}; + +const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 70px; +`; + +export default ParticipationContents; diff --git a/frontend/src/components/participation/ParticipationCodeCopier/ParticipationCodeCopier.tsx b/frontend/src/components/participation/PrticipationContents/ParticipationCodeCopier/ParticipationCodeCopier.tsx similarity index 76% rename from frontend/src/components/participation/ParticipationCodeCopier/ParticipationCodeCopier.tsx rename to frontend/src/components/participation/PrticipationContents/ParticipationCodeCopier/ParticipationCodeCopier.tsx index 6b60169c..462a0824 100644 --- a/frontend/src/components/participation/ParticipationCodeCopier/ParticipationCodeCopier.tsx +++ b/frontend/src/components/participation/PrticipationContents/ParticipationCodeCopier/ParticipationCodeCopier.tsx @@ -1,7 +1,6 @@ import { css, styled } from 'styled-components'; import Input from '@Components/common/Input/Input'; -import Typography from '@Components/common/Typography/Typography'; import useClipBoard from '@Hooks/common/useClipBoard'; @@ -30,10 +29,7 @@ const ParticipationCodeCopier = ({ participantCode }: Props) => { return (
- μ°Έμ—¬μ½”λ“œ} - bottomText="μŠ€ν„°λ”” μ°Έμ—¬μ½”λ“œλ₯Ό μŠ€ν„°λ””μ›λ“€μ—κ²Œ κ³΅μœ ν•˜μ„Έμš”." - > + {
); @@ -88,3 +73,19 @@ const ClipBoardButton = styled.button` border-bottom-left-radius: 0px; margin-top: 10px; `; + +const HelperText = styled.p` + position: absolute; + margin-top: 20px; + + font-size: 1.6rem; + font-weight: 300; + text-decoration: underline; + color: ${color.neutral[400]}; + + cursor: pointer; + + @media screen and (max-width: 768px) { + font-size: 1.4rem; + } +`; diff --git a/frontend/src/components/participation/hooks/useCheckProgresses.ts b/frontend/src/components/participation/hooks/useCheckProgresses.ts new file mode 100644 index 00000000..7ae45147 --- /dev/null +++ b/frontend/src/components/participation/hooks/useCheckProgresses.ts @@ -0,0 +1,25 @@ +import { useParams } from 'react-router-dom'; + +import useFetch from '@Hooks/api/useFetch'; + +import { useMemberInfo } from '@Contexts/MemberInfoProvider'; + +import { requestGetCheckProgresses } from '@Apis/index'; + +import type { ResponseCheckProgresses } from '@Types/api'; + +const useCheckProgresses = () => { + const { studyId } = useParams(); + + const memberInfo = useMemberInfo(); + + const { result } = useFetch(() => + requestGetCheckProgresses(studyId ?? '', memberInfo?.memberId ?? ''), + ); + + if (!studyId) throw new Error('잘λͺ»λœ μ ‘κ·Όμž…λ‹ˆλ‹€.'); + + return { studyId, result }; +}; + +export default useCheckProgresses; diff --git a/frontend/src/components/participation/hooks/useParticipationCode.ts b/frontend/src/components/participation/hooks/useParticipationCode.ts new file mode 100644 index 00000000..3ffd2c16 --- /dev/null +++ b/frontend/src/components/participation/hooks/useParticipationCode.ts @@ -0,0 +1,11 @@ +import useMutation from '@Hooks/api/useMutation'; + +import { requestGetAuthenticateParticipationCode } from '@Apis/index'; + +const useParticipationCode = (participantCode: string) => { + const { mutate: authenticateParticipationCode, isLoading } = useMutation(() => requestGetAuthenticateParticipationCode(participantCode)); + + return { authenticateParticipationCode, isLoading }; +}; + +export default useParticipationCode; diff --git a/frontend/src/components/participation/hooks/useRegisterProgress.ts b/frontend/src/components/participation/hooks/useRegisterProgress.ts new file mode 100644 index 00000000..af1ffc8d --- /dev/null +++ b/frontend/src/components/participation/hooks/useRegisterProgress.ts @@ -0,0 +1,17 @@ +import useMutation from '@Hooks/api/useMutation'; + +import { useMemberInfo } from '@Contexts/MemberInfoProvider'; + +import { requestPostRegisterProgress } from '@Apis/index'; + +const useRegisterProgress = (nickname: string, studyId: string) => { + const memberInfo = useMemberInfo(); + + const { isLoading, mutate: registerProgress } = useMutation(() => + requestPostRegisterProgress(nickname, studyId, memberInfo!.memberId), + ); + + return { isLoading, registerProgress }; +}; + +export default useRegisterProgress; diff --git a/frontend/src/components/board/GuideModal/GuideModal.tsx b/frontend/src/components/progress/GuideModal/GuideModal.tsx similarity index 100% rename from frontend/src/components/board/GuideModal/GuideModal.tsx rename to frontend/src/components/progress/GuideModal/GuideModal.tsx diff --git a/frontend/src/components/board/PlanningForm/PlanningForm.tsx b/frontend/src/components/progress/PlanningForm/PlanningForm.tsx similarity index 80% rename from frontend/src/components/board/PlanningForm/PlanningForm.tsx rename to frontend/src/components/progress/PlanningForm/PlanningForm.tsx index 93adfcf5..1853cf59 100644 --- a/frontend/src/components/board/PlanningForm/PlanningForm.tsx +++ b/frontend/src/components/progress/PlanningForm/PlanningForm.tsx @@ -4,7 +4,6 @@ import Button from '@Components/common/Button/Button'; import QuestionTextarea from '@Components/common/QuestionTextarea/QuestionTextarea'; import Typography from '@Components/common/Typography/Typography'; -import usePlanningForm from '@Hooks/board/usePlanningForm'; import useDisplay from '@Hooks/common/useDisplay'; import { PLAN_QUESTIONS } from '@Constants/study'; @@ -14,32 +13,14 @@ import { useModal } from '@Contexts/ModalProvider'; import ArrowIcon from '@Assets/icons/ArrowIcon'; import GuideModal from '../GuideModal/GuideModal'; +import usePlanningForm from '../hooks/usePlanningForm'; -type Props = { - onClickSubmitButton: () => Promise; - studyId: string; - progressId: string; -}; - -const PlanningForm = ({ onClickSubmitButton, studyId, progressId }: Props) => { - const { questionTextareaProps, isInvalidForm, isSubmitLoading, submitForm } = usePlanningForm( - studyId, - progressId, - onClickSubmitButton, - ); +const PlanningForm = () => { + const { questionTextareaProps, isInvalidForm, isSubmitLoading, submitForm } = usePlanningForm(); const { isShow: isOpenOptionalQuestion, toggleShow: toggleOptionalQuestion } = useDisplay(); const { openModal } = useModal(); - const handleClickButton = async () => { - try { - await submitForm(); - } catch (error) { - if (!(error instanceof Error)) return; - alert(error.message); - } - }; - const handleClickGuideButton = (question: 'toDo' | 'completionCondition') => () => { openModal(); }; @@ -82,13 +63,7 @@ const PlanningForm = ({ onClickSubmitButton, studyId, progressId }: Props) => { )} -
diff --git a/frontend/src/components/board/RetrospectForm/RetrospectForm.tsx b/frontend/src/components/progress/RetrospectForm/RetrospectForm.tsx similarity index 78% rename from frontend/src/components/board/RetrospectForm/RetrospectForm.tsx rename to frontend/src/components/progress/RetrospectForm/RetrospectForm.tsx index 992b607c..9f947c26 100644 --- a/frontend/src/components/board/RetrospectForm/RetrospectForm.tsx +++ b/frontend/src/components/progress/RetrospectForm/RetrospectForm.tsx @@ -5,42 +5,29 @@ import Button from '@Components/common/Button/Button'; import QuestionTextarea from '@Components/common/QuestionTextarea/QuestionTextarea'; import Typography from '@Components/common/Typography/Typography'; -import useRetrospectForm from '@Hooks/board/useRetrospectForm'; import useDisplay from '@Hooks/common/useDisplay'; import { ROUTES_PATH } from '@Constants/routes'; import { RETROSPECT_QUESTIONS } from '@Constants/study'; +import { useStudyInfo } from '@Contexts/StudyProgressProvider'; + import ArrowIcon from '@Assets/icons/ArrowIcon'; -type Props = { - isLastCycle: boolean; - onClickSubmitButton: () => Promise; - studyId: string; - progressId: string; -}; +import useRetrospectForm from '../hooks/useRetrospectForm'; -const RetrospectForm = ({ isLastCycle, onClickSubmitButton, studyId, progressId }: Props) => { - const navigate = useNavigate(); - const { questionTextareaProps, isInvalidForm, isSubmitLoading, submitForm } = useRetrospectForm( - studyId, - progressId, - onClickSubmitButton, - ); +const RetrospectForm = () => { + const { questionTextareaProps, isInvalidForm, isSubmitLoading, isLastCycle, submitForm } = useRetrospectForm(); + const { studyId } = useStudyInfo(); + const navigate = useNavigate(); const { isShow: isOpenOptionalQuestion, toggleShow: toggleOptionalQuestion } = useDisplay(); - const handleClickButton = async () => { - try { - await submitForm(); - - if (isLastCycle) { - navigate(`${ROUTES_PATH.record}/${studyId}`); - return; - } - } catch (error) { - if (!(error instanceof Error)) return; - alert(error.message); + const handleSubmitForm = async () => { + await submitForm(); + + if (isLastCycle) { + navigate(`${ROUTES_PATH.record}/${studyId}`); } }; @@ -74,7 +61,7 @@ const RetrospectForm = ({ isLastCycle, onClickSubmitButton, studyId, progressId +
+ ) + ); +}; + +export default StudyingForm; + +const Layout = styled.section` + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + gap: 30px; + + padding: 60px 85px; +`; + +const PlanResultList = styled.ul` + width: 100%; + height: 90%; + + display: flex; + flex-direction: column; + gap: 60px; + + padding: 50px; + background-color: #fff; + border-radius: 14px; + + overflow-y: auto; +`; diff --git a/frontend/src/components/board/Timer/Timer.stories.tsx b/frontend/src/components/progress/Timer/Timer.stories.tsx similarity index 100% rename from frontend/src/components/board/Timer/Timer.stories.tsx rename to frontend/src/components/progress/Timer/Timer.stories.tsx diff --git a/frontend/src/components/board/Timer/Timer.tsx b/frontend/src/components/progress/Timer/Timer.tsx similarity index 70% rename from frontend/src/components/board/Timer/Timer.tsx rename to frontend/src/components/progress/Timer/Timer.tsx index febb622e..73303418 100644 --- a/frontend/src/components/board/Timer/Timer.tsx +++ b/frontend/src/components/progress/Timer/Timer.tsx @@ -1,14 +1,23 @@ +import { useEffect } from 'react'; import { css, styled } from 'styled-components'; import Button from '@Components/common/Button/Button'; import Typography from '@Components/common/Typography/Typography'; -import useTimer from '@Hooks/board/useTimer'; - import color from '@Styles/color'; +import alarm from '@Assets/sounds/alarm.mp3'; + +import audioPlayer from '@Utils/audioPlayer'; +import dom from '@Utils/dom'; +import format from '@Utils/format'; + import type { Step } from '@Types/study'; +import useStepTimer from '../hooks/useStepTimer'; + +const alarmAudio = audioPlayer({ asset: alarm }); + const BUTTON_COLOR: Record = { planning: color.blue[500], studying: color.red[600], @@ -21,14 +30,21 @@ type Props = { }; const Timer = ({ studyMinutes, step }: Props) => { - const { start, stop, leftTime, isTicking } = useTimer(studyMinutes, step); + const { start, stop, leftSeconds, isTicking } = useStepTimer({ + studyMinutes, + step, + onComplete: () => { + alarmAudio.play(); + }, + }); + + const formattedTime = format.time(leftSeconds); + + useEffect(() => { + dom.updateTitle(`${formattedTime} - ν•˜λ£¨μŠ€ν„°λ””`); - const leftMinutes = Math.floor(leftTime / 60) - .toString() - .padStart(2, '0'); - const leftSeconds = Math.floor(leftTime % 60) - .toString() - .padStart(2, '0'); + return () => dom.updateTitle('ν•˜λ£¨μŠ€ν„°λ””'); + }, [formattedTime]); const buttonColor = BUTTON_COLOR[step]; const buttonText = isTicking ? '정지' : 'μ‹œμž‘'; @@ -45,9 +61,9 @@ const Timer = ({ studyMinutes, step }: Props) => { color={color.white} tabIndex={0} role="timer" - aria-label={`남은 μ‹œκ°„ ${leftMinutes}λΆ„ ${leftSeconds}초`} + aria-label={`남은 μ‹œκ°„ ${formattedTime}`} > - {`${leftMinutes}:${leftSeconds}`} + {`${formattedTime}`} - - - -
- ); -}; - -export default Login; - -const Layout = styled.div` - width: 100vw; - height: 100vh; - - display: flex; - flex-direction: column; - gap: 40px; - justify-content: center; - align-items: center; -`; - -const ButtonContainer = styled.div` - display: flex; - flex-direction: column; - gap: 20px; - - width: 360px; -`; - -const Emphasis = styled.span` - color: ${color.blue[500]}; -`; diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFound.tsx similarity index 95% rename from frontend/src/pages/NotFoundPage.tsx rename to frontend/src/pages/NotFound.tsx index e9ac5553..d9a957ea 100644 --- a/frontend/src/pages/NotFoundPage.tsx +++ b/frontend/src/pages/NotFound.tsx @@ -9,7 +9,7 @@ import { lightTheme } from '@Styles/theme'; import { ROUTES_PATH } from '@Constants/routes'; -const NotFoundPage = () => { +const NotFound = () => { return ( @@ -28,7 +28,7 @@ const NotFoundPage = () => { ); }; -export default NotFoundPage; +export default NotFound; const Layout = styled.div` height: 100vh; diff --git a/frontend/src/pages/StudyBoard.tsx b/frontend/src/pages/StudyBoard.tsx deleted file mode 100644 index ed1661fd..00000000 --- a/frontend/src/pages/StudyBoard.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { css, styled } from 'styled-components'; - -import Sidebar from '@Components/board/Sidebar/Sidebar'; -import StepContents from '@Components/board/StepContents/StepContents'; -import CircularProgress from '@Components/common/CircularProgress/CircularProgress'; - -import useStudyBoard from '@Hooks/board/useStudyBoard'; - -import color from '@Styles/color'; - -import { ROUTES_PATH } from '@Constants/routes'; - -const StudyBoard = () => { - const navigate = useNavigate(); - const { studyInfo, progressInfo, error, changeNextStep } = useStudyBoard(); - - if (error) { - alert(error.message); - navigate(ROUTES_PATH.landing); - } - - if (studyInfo === null || progressInfo === null) { - return ( - - - - ); - } - - if (progressInfo.step === 'done') { - alert('이미 λλ‚œ μŠ€ν„°λ””μž…λ‹ˆλ‹€.'); - navigate(`/record/${studyInfo.studyId}`); - return <>; - } - - return ( - - - - - - - ); -}; - -export default StudyBoard; - -const Layout = styled.div` - width: 100%; - display: flex; -`; - -const Contents = styled.section` - width: calc(100% - 590px); - min-width: 670px; - height: 100vh; - - background-color: ${color.neutral[100]}; -`; - -const LoadingLayout = styled.div` - width: 100vw; - height: 100vh; - - display: flex; - align-items: center; - justify-content: center; -`; diff --git a/frontend/src/pages/StudyParticipation.tsx b/frontend/src/pages/StudyParticipation.tsx index 5a745104..dacd1b75 100644 --- a/frontend/src/pages/StudyParticipation.tsx +++ b/frontend/src/pages/StudyParticipation.tsx @@ -1,3 +1,4 @@ +import AlertErrorBoundary from '@Components/common/AlertErrorBoundary/AlertErrorBoundary'; import ParticipationCodeInput from '@Components/participation/ParticipationCodeInput/ParticipationCodeInput'; import StudyParticipationLayout from './layout/StudyParticipationLayout'; @@ -5,7 +6,9 @@ import StudyParticipationLayout from './layout/StudyParticipationLayout'; const StudyParticipation = () => { return ( - + + + ); }; diff --git a/frontend/src/pages/StudyPreparation.tsx b/frontend/src/pages/StudyPreparation.tsx index d037eb92..c68a2b3c 100644 --- a/frontend/src/pages/StudyPreparation.tsx +++ b/frontend/src/pages/StudyPreparation.tsx @@ -1,15 +1,9 @@ -import { useCallback } from 'react'; +import { Suspense } from 'react'; import { useLocation } from 'react-router-dom'; -import { css, styled } from 'styled-components'; -import CircularProgress from '@Components/common/CircularProgress/CircularProgress'; -import MemberRegister from '@Components/participation/MemberRegister/MemberRegister'; -import MemberRestart from '@Components/participation/MemberRestart/MemberRestart'; -import ParticipationCodeCopier from '@Components/participation/ParticipationCodeCopier/ParticipationCodeCopier'; - -import useCheckProgresses from '@Hooks/participation/useCheckProgresses'; - -import color from '@Styles/color'; +import LoadingFallback from '@Components/common/LodingFallback/LoadingFallback'; +import MemberInfoGuard from '@Components/common/MemberInfoGuard/MemberInfoGuard'; +import ParticipationContents from '@Components/participation/PrticipationContents/PartcipationContents'; import StudyParticipationLayout from './layout/StudyParticipationLayout'; @@ -22,46 +16,15 @@ const StudyPreparation = () => { state: { participantCode, studyName, isHost }, } = useLocation() as LocationState; - const errorHandler = useCallback((error: Error) => { - alert(error.message); - }, []); - - const { nickname, restart, studyId } = useCheckProgresses(isHost, errorHandler); - - const isExistMember = Boolean(nickname); - - if (nickname === null) - return ( + return ( + - + }> + + - ); - - return ( - - - {isHost && } - {isExistMember ? ( - - ) : ( - - )} - - + ); }; export default StudyPreparation; - -const Layout = styled.div` - display: flex; - flex-direction: column; - gap: 70px; -`; diff --git a/frontend/src/pages/StudyProgress.tsx b/frontend/src/pages/StudyProgress.tsx new file mode 100644 index 00000000..74c97c60 --- /dev/null +++ b/frontend/src/pages/StudyProgress.tsx @@ -0,0 +1,30 @@ +import { Suspense } from 'react'; +import { styled } from 'styled-components'; + +import MemberInfoGuard from '@Components/common/MemberInfoGuard/MemberInfoGuard'; +import StudyBoard from '@Components/progress/StudyBoard/StudyBoard'; + +import StudyProgressProvider from '@Contexts/StudyProgressProvider'; + +import LoadingFallback from '../components/common/LodingFallback/LoadingFallback'; + +const StudyProgress = () => { + return ( + + }> + + + + + + + + ); +}; + +export default StudyProgress; + +const Layout = styled.div` + width: 100%; + height: 100%; +`; diff --git a/frontend/src/pages/layout/RecordLayout.tsx b/frontend/src/pages/layout/RecordLayout.tsx index 410beaa1..a4a66a45 100644 --- a/frontend/src/pages/layout/RecordLayout.tsx +++ b/frontend/src/pages/layout/RecordLayout.tsx @@ -23,12 +23,20 @@ const Layout = styled.div` `; const ContentsContainer = styled.div` - display: grid; - row-gap: 40px; + display: flex; + flex-direction: column; + gap: 40px; max-width: 1200px; margin: 0 auto; padding: 0px 60px; padding-bottom: 60px; + + @media screen and (max-width: 768px) { + width: 90%; + + padding: 0px; + padding-bottom: 60px; + } `; diff --git a/frontend/src/pages/layout/StudyParticipationLayout.tsx b/frontend/src/pages/layout/StudyParticipationLayout.tsx index 42472738..b20a084c 100644 --- a/frontend/src/pages/layout/StudyParticipationLayout.tsx +++ b/frontend/src/pages/layout/StudyParticipationLayout.tsx @@ -34,4 +34,17 @@ const Layout = styled.div` width: 520px; margin: 0 auto; padding-bottom: 60px; + + @media screen and (max-width: 768px) { + width: 90%; + + h2 { + font-size: 3.2rem; + } + + input { + font-size: 1.8rem; + padding: 14px; + } + } `; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 700577fe..b61fb4b3 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -1,41 +1,38 @@ +import { lazy } from 'react'; import { createBrowserRouter } from 'react-router-dom'; -import Auth from '@Pages/Auth'; -import CreateStudy from '@Pages/CreateStudy'; import Landing from '@Pages/Landing'; -import Login from '@Pages/Login'; -import NotFoundPage from '@Pages/NotFoundPage'; -import MemberRecord from '@Pages/MemberRecord'; -import StudyBoard from '@Pages/StudyBoard'; -import StudyParticipation from '@Pages/StudyParticipation'; -import StudyPreparation from '@Pages/StudyPreparation'; -import StudyRecord from '@Pages/StudyRecord'; +import NotFound from '@Pages/NotFound'; import { ROUTES_PATH } from '@Constants/routes'; import App from '../App'; +const Auth = lazy(() => import(/* webpackChunkName: "Auth" */ '@Pages/Auth')); +const StudyRecord = lazy(() => import('@Pages/StudyRecord')); +const CreateStudy = lazy(() => import('@Pages/CreateStudy')); +const MemberRecord = lazy(() => import('@Pages/MemberRecord')); +const StudyProgress = lazy(() => import('@Pages/StudyProgress')); +const StudyPreparation = lazy(() => import('@Pages/StudyPreparation')); +const StudyParticipation = lazy(() => import('@Pages/StudyParticipation')); + const router = createBrowserRouter([ { path: ROUTES_PATH.landing, element: , - errorElement: , + errorElement: , children: [ { index: true, element: , }, - { - path: ROUTES_PATH.login, - element: , - }, { path: ROUTES_PATH.auth, element: , }, { - path: `${ROUTES_PATH.board}/:studyId`, - element: , + path: `${ROUTES_PATH.progress}/:studyId`, + element: , }, { path: `${ROUTES_PATH.record}/:studyId`, diff --git a/frontend/src/styles/color.ts b/frontend/src/styles/color.ts index f7944ac9..8db31045 100644 --- a/frontend/src/styles/color.ts +++ b/frontend/src/styles/color.ts @@ -2,6 +2,9 @@ const color = { white: '#ffffff', black: '#000000', yellow: '#fcd34d', + brand: { + kakao: '#ffe812', + }, red: { 200: '#cf8080', 300: '#c86d6e', diff --git a/frontend/src/styles/globalStyle.ts b/frontend/src/styles/globalStyle.ts index 6627d5a9..65f11714 100644 --- a/frontend/src/styles/globalStyle.ts +++ b/frontend/src/styles/globalStyle.ts @@ -1,7 +1,6 @@ import { createGlobalStyle, css } from 'styled-components'; import resetStyle from './reset'; -import '../fonts/font.css'; const GlobalStyles = createGlobalStyle` ${resetStyle} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index d61c9625..c728eceb 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -59,8 +59,8 @@ type ResponseProgress = { step: Step; }; -export type ResponseProgresses = { - progresses: ResponseProgress[]; +export type ResponseCheckProgresses = { + progresses: ResponseProgress[] | null; }; export type ResponseAuthToken = { diff --git a/frontend/src/types/dom.ts b/frontend/src/types/dom.ts deleted file mode 100644 index 99706e3e..00000000 --- a/frontend/src/types/dom.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type EventHandler = (e: E) => void; - -export type ComposeEventHandlers = ( - externalEventHandler?: EventHandler, - innerEventHandler?: EventHandler, -) => EventHandler; diff --git a/frontend/src/types/index.d.ts b/frontend/src/types/index.d.ts index 6f0bd3e3..f1c797e1 100644 --- a/frontend/src/types/index.d.ts +++ b/frontend/src/types/index.d.ts @@ -14,3 +14,13 @@ declare module '*.gif' { const value: string; export default value; } + +declare module '*.webp' { + const value: string; + export default value; +} + +declare module '*.mp3' { + const value: string; + export default value; +} diff --git a/frontend/src/utils/Http.ts b/frontend/src/utils/Http.ts new file mode 100644 index 00000000..9fc8f18d --- /dev/null +++ b/frontend/src/utils/Http.ts @@ -0,0 +1,101 @@ +export type HttpResponse = { + data: T; + config: RequestInit; + headers: Headers; + ok: boolean; + redirected: boolean; + status: number; + statusText: string; + type: ResponseType; + url: string; +}; + +type Interceptor = { + onRequest: (config: RequestInit) => RequestInit; + onResponse: (response: HttpResponse) => HttpResponse | PromiseLike>; + onRequestError: (reason: unknown) => Promise; + onResponseError: (reason: unknown) => Promise; +}; + +const processHttpResponse = async (response: Response, config: RequestInit) => { + const data = (await response.json().catch(() => ({}))) as T; + const { headers, ok, redirected, status, statusText, type, url } = response; + return { data, config, headers, ok, redirected, status, statusText, type, url }; +}; + +class Http { + private baseURL; + + private defaultConfig: RequestInit; + + private interceptor: Interceptor; + + constructor(baseURL = '', defaultConfig: RequestInit = {}) { + this.baseURL = baseURL; + this.defaultConfig = defaultConfig; + this.interceptor = { + onRequest: (config) => config, + onResponse: (response) => response, + onRequestError: (reason) => Promise.reject(reason), + onResponseError: (reason) => Promise.reject(reason), + }; + } + + registerInterceptor(interceptor: Partial) { + this.interceptor = { + ...this.interceptor, + ...interceptor, + }; + } + + request(url: string, config: RequestInit) { + config = { ...this.defaultConfig, ...this.interceptor.onRequest(config) }; + config.headers = { ...this.defaultConfig.headers, ...config.headers }; + + try { + return fetch(`${this.baseURL}${url}`, config) + .then((response) => processHttpResponse(response, config)) + .then(this.interceptor.onResponse) + .catch(this.interceptor.onResponseError); + } catch (reason) { + return this.interceptor.onRequestError(reason); + } + } + + get(url: string, config: RequestInit = {}) { + return this.request(url, { + ...config, + method: 'GET', + }); + } + + post(url: string, config: RequestInit = {}) { + return this.request(url, { + ...config, + method: 'POST', + }); + } + + patch(url: string, config: RequestInit = {}) { + return this.request(url, { + ...config, + method: 'PATCH', + }); + } + + put(url: string, config: RequestInit = {}) { + return this.request(url, { + ...config, + method: 'PUT', + }); + } + + delete(url: string, config: RequestInit = {}) { + return this.request(url, { + ...config, + method: 'DELETE', + }); + } +} + +export default Http; diff --git a/frontend/src/utils/audioPlayer.ts b/frontend/src/utils/audioPlayer.ts new file mode 100644 index 00000000..20ee8e3d --- /dev/null +++ b/frontend/src/utils/audioPlayer.ts @@ -0,0 +1,47 @@ +type Params = { + asset: string; + volume?: number; + loop?: boolean; +}; + +const audioPlayer = ({ asset, volume = 0.5, loop = false }: Params) => { + const audio = new Audio(); + audio.src = asset; + audio.volume = volume; + + if (loop) { + audio.addEventListener( + 'ended', + () => { + audio.currentTime = 0; + audio.play(); + }, + false, + ); + } + + const play = () => { + if (audio.paused || !audio.currentTime) { + audio.play().catch(console.error); + } + }; + + const stop = () => { + audio.pause(); + }; + + const setVolume = (volume: number) => (audio.volume = volume / 100); + + const setAudio = (src: string) => { + audio.src = src; + }; + + return { + play, + stop, + setVolume, + setAudio, + }; +}; + +export default audioPlayer; diff --git a/frontend/src/utils/cookie.ts b/frontend/src/utils/cookie.ts index e901a5db..5e7ef546 100644 --- a/frontend/src/utils/cookie.ts +++ b/frontend/src/utils/cookie.ts @@ -10,8 +10,8 @@ export const getCookie = (key: string) => { return match ? decodeURIComponent(match[1]) : null; }; -export const boolCheckCookie = (key: string) => { - return getCookie(key) !== null ? true : false; +export const hasCookie = (key: string) => { + return Boolean(getCookie(key)); }; export const deleteCookie = (name: string) => { diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts deleted file mode 100644 index 6d074a3c..00000000 --- a/frontend/src/utils/date.ts +++ /dev/null @@ -1,5 +0,0 @@ -const date = { - format: (date: Date) => `${date.getFullYear()}λ…„ ${date.getMonth() + 1}μ›” ${date.getDate()}일`, -}; - -export default date; diff --git a/frontend/src/utils/dom.ts b/frontend/src/utils/dom.ts new file mode 100644 index 00000000..8db84526 --- /dev/null +++ b/frontend/src/utils/dom.ts @@ -0,0 +1,12 @@ +const dom = { + updateTitle: (title: string) => { + document.title = title; + }, + + updateFavicon: (path: string) => { + const favicon = document.getElementById('favicon') as HTMLLinkElement; + favicon.href = path; + }, +}; + +export default dom; diff --git a/frontend/src/utils/domEventHandler.ts b/frontend/src/utils/domEventHandler.ts deleted file mode 100644 index 22f6cbb9..00000000 --- a/frontend/src/utils/domEventHandler.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ComposeEventHandlers } from '@Types/dom'; - -export const composeEventHandlers: ComposeEventHandlers = (externalEventHandler, innerEventHandler) => { - return (event) => { - externalEventHandler?.(event); - innerEventHandler?.(event); - }; -}; diff --git a/frontend/src/utils/format.ts b/frontend/src/utils/format.ts new file mode 100644 index 00000000..a3cef8d2 --- /dev/null +++ b/frontend/src/utils/format.ts @@ -0,0 +1,16 @@ +const format = { + date: (date: Date) => `${date.getFullYear()}λ…„ ${date.getMonth() + 1}μ›” ${date.getDate()}일`, + + time: (seconds: number) => { + const minutesFormatted = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const secondsFormatted = Math.floor(seconds % 60) + .toString() + .padStart(2, '0'); + + return `${minutesFormatted}:${secondsFormatted}`; + }, +}; + +export default format; diff --git a/frontend/src/utils/getUrlQuery.ts b/frontend/src/utils/getUrlQuery.ts deleted file mode 100644 index 5d9adc48..00000000 --- a/frontend/src/utils/getUrlQuery.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const getUrlQuery = (name: string) => - new URL(window.location.href).searchParams.get(name) as unknown as T; diff --git a/frontend/src/utils/http.ts b/frontend/src/utils/http.ts deleted file mode 100644 index 1cbfa0b3..00000000 --- a/frontend/src/utils/http.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { ResponseAPIError } from '@Types/api'; - -import { APIError, OfflineError, ResponseError } from '@Errors/index'; - -const isAPIErrorData = (data: ResponseAPIError | undefined): data is ResponseAPIError => { - return (data as ResponseAPIError).code !== undefined; -}; - -const fetchAPI = async (url: string, config: RequestInit) => { - if (!navigator.onLine) throw new OfflineError(); - - try { - const response = await fetch(url, config); - - if (!response.ok) { - const data = (await response.json()) as ResponseAPIError | undefined; - - if (isAPIErrorData(data)) { - throw new APIError(data.message, data.code); - } - - throw new ResponseError(); - } - - return response; - } catch (error) { - if (error instanceof APIError) throw error; - - throw new ResponseError(); - } -}; - -const http = { - get: async (url: string, config: RequestInit = {}) => { - const response = await fetchAPI(url, { - ...config, - method: 'GET', - headers: { - 'Content-Type': 'application/json', - ...config.headers, - }, - }); - - return response.json() as T; - }, - - post: (url: string, config: RequestInit = {}) => { - return fetchAPI(url, { - ...config, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...config.headers, - }, - }); - }, - - delete: (url: string, config: RequestInit = {}) => { - return fetchAPI(url, { - ...config, - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - ...config.headers, - }, - }); - }, -}; - -export default http; diff --git a/frontend/src/utils/params.ts b/frontend/src/utils/params.ts deleted file mode 100644 index 146a5fb8..00000000 --- a/frontend/src/utils/params.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const get = (name: string) => - new URL(window.location.href).searchParams.get(name) as unknown as T; diff --git a/frontend/src/utils/tokenStorage.ts b/frontend/src/utils/tokenStorage.ts new file mode 100644 index 00000000..889f853e --- /dev/null +++ b/frontend/src/utils/tokenStorage.ts @@ -0,0 +1,37 @@ +import { deleteCookie, hasCookie } from './cookie'; + +const tokenStorage = { + ACCESS_TOKEN_KEY: 'accessToken', + REFRESH_TOKEN_KEY: 'refreshToken', + + get accessToken() { + return sessionStorage.getItem(this.ACCESS_TOKEN_KEY); + }, + + setAccessToken(accessToken: string) { + sessionStorage.setItem(this.ACCESS_TOKEN_KEY, accessToken); + }, + + hasAccessToken() { + return Boolean(this.accessToken); + }, + + hasRefreshToken() { + return hasCookie(this.REFRESH_TOKEN_KEY); + }, + + removeAccessToken() { + sessionStorage.removeItem(this.ACCESS_TOKEN_KEY); + }, + + removeRefreshToken() { + deleteCookie(this.REFRESH_TOKEN_KEY); + }, + + clear() { + this.removeAccessToken(); + this.removeRefreshToken(); + }, +}; + +export default tokenStorage; diff --git a/frontend/src/utils/url.ts b/frontend/src/utils/url.ts new file mode 100644 index 00000000..4c8fae2f --- /dev/null +++ b/frontend/src/utils/url.ts @@ -0,0 +1,12 @@ +const url = { + getPathName: () => window.location.pathname, + + changePathName: (pathname: string) => { + window.location.pathname = pathname; + }, + + getQueryString: (name: string) => + new URL(window.location.href).searchParams.get(name) as unknown as T, +}; + +export default url; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index f71ab899..1d093034 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -4,11 +4,12 @@ "target": "esnext", "jsx": "react-jsx", "module": "esnext", - "moduleResolution": "NodeNext", + "moduleResolution": "Node", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true }, - "include": ["src", "__test__"] + "include": ["src", "__test__"], + "exclude": ["node_modules/*"] } diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 43021064..10c25f3b 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -1,15 +1,10 @@ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); +const CopyPlugin = require('copy-webpack-plugin'); module.exports = () => { return { entry: './src/index.tsx', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - clean: true, - publicPath: '/', - }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], alias: { @@ -43,7 +38,7 @@ module.exports = () => { exclude: /node_modules/, }, { - test: /\.(png|svg|jpg|jpeg|gif)$/i, + test: /\.(png|svg|jpg|jpeg|gif|mp3|webp)$/i, type: 'asset/resource', }, ], @@ -52,6 +47,19 @@ module.exports = () => { new HtmlWebpackPlugin({ template: 'public/index.html', }), + + new CopyPlugin({ + patterns: [ + { + from: path.resolve(__dirname, 'public', 'fonts'), + to: path.resolve(__dirname, 'dist', 'fonts'), + }, + { + from: path.resolve(__dirname, 'public', 'assets'), + to: path.resolve(__dirname, 'dist', 'assets'), + }, + ], + }), ], }; }; diff --git a/frontend/webpack.dev.js b/frontend/webpack.dev.js index fa832de8..13618b5d 100644 --- a/frontend/webpack.dev.js +++ b/frontend/webpack.dev.js @@ -1,9 +1,17 @@ const { merge } = require('webpack-merge'); +const path = require('path'); const common = require('./webpack.common.js'); const Dotenv = require('dotenv-webpack'); module.exports = merge(common(), { mode: 'development', + output: { + filename: '[name].js', + chunkFilename: '[id].chunk.js', + path: path.resolve(__dirname, 'dist'), + clean: true, + publicPath: '/', + }, devtool: 'eval-source-map', devServer: { port: 3000, diff --git a/frontend/webpack.prod.js b/frontend/webpack.prod.js index e1e5c776..e9805788 100644 --- a/frontend/webpack.prod.js +++ b/frontend/webpack.prod.js @@ -1,9 +1,17 @@ const { merge } = require('webpack-merge'); +const path = require('path'); const common = require('./webpack.common.js'); const Dotenv = require('dotenv-webpack'); module.exports = merge(common(), { mode: 'production', + output: { + filename: '[name].[hash].js', + chunkFilename: '[id].[hash].chunk.js', + path: path.resolve(__dirname, 'dist'), + clean: true, + publicPath: '/', + }, devtool: 'source-map', plugins: [ new Dotenv({ diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 29ea759b..881f24aa 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1900,16 +1900,16 @@ "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" -"@mswjs/interceptors@^0.17.5": - version "0.17.9" - resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.17.9.tgz#0096fc88fea63ee42e36836acae8f4ae33651c04" - integrity sha512-4LVGt03RobMH/7ZrbHqRxQrS9cc2uh+iNKSj8UWr8M26A2i793ju+csaB5zaqYltqJmA2jUq4VeYfKmVqvsXQg== +"@mswjs/interceptors@^0.17.10": + version "0.17.10" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.17.10.tgz#857b41f30e2b92345ed9a4e2b1d0a08b8b6fcad4" + integrity sha512-N8x7eSLGcmUFNWZRxT1vsHvypzIRgQYdG0rJey/rZCy6zT/30qDt8Joj7FxzGNLSwXbeZqJOMqDurp7ra4hgbw== dependencies: "@open-draft/until" "^1.0.3" "@types/debug" "^4.1.7" "@xmldom/xmldom" "^0.8.3" debug "^4.3.3" - headers-polyfill "^3.1.0" + headers-polyfill "3.2.5" outvariant "^1.2.1" strict-event-emitter "^0.2.4" web-encoding "^1.1.5" @@ -3047,6 +3047,11 @@ dependencies: "@babel/runtime" "^7.12.5" +"@testing-library/user-event@^14.4.3": + version "14.4.3" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" + integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -4486,14 +4491,6 @@ case-sensitive-paths-webpack-plugin@^2.4.0: resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== -chalk@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" - integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -4817,6 +4814,18 @@ cookie@^0.4.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== +copy-webpack-plugin@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" + integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== + dependencies: + fast-glob "^3.2.11" + glob-parent "^6.0.1" + globby "^13.1.1" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + core-js-compat@^3.25.1, core-js-compat@^3.30.1, core-js-compat@^3.31.0: version "3.31.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.31.0.tgz#4030847c0766cc0e803dcdfb30055d7ef2064bf1" @@ -5878,6 +5887,17 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.2.11, fast-glob@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-glob@^3.2.9: version "3.3.0" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" @@ -6281,7 +6301,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.2: +glob-parent@^6.0.1, glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -6354,6 +6374,17 @@ globby@^11.0.1, globby@^11.0.2, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +globby@^13.1.1: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.3.0" + ignore "^5.2.4" + merge2 "^1.4.1" + slash "^4.0.0" + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6461,10 +6492,10 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -headers-polyfill@^3.1.0, headers-polyfill@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.1.2.tgz#9a4dcb545c5b95d9569592ef7ec0708aab763fbe" - integrity sha512-tWCK4biJ6hcLqTviLXVR9DTRfYGQMXEIUj3gwJ2rZ5wO/at3XtkI4g8mCvFdUF9l1KMBNCfmNAdnahm1cgavQA== +headers-polyfill@3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.2.5.tgz#6e67d392c9d113d37448fe45014e0afdd168faed" + integrity sha512-tUCGvt191vNSQgttSyJoibR+VO+I6+iCHIUdhzEMJKE+EAL8BwCN7fUOZlY4ofOelNHsK+gEjxB/B+9N3EWtdA== hoist-non-react-statics@^3.3.0: version "3.3.2" @@ -6654,7 +6685,7 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -8039,21 +8070,21 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msw@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/msw/-/msw-1.2.2.tgz#126c3150c07f651e97b24fbd405821f3aeaf9397" - integrity sha512-GsW3PE/Es/a1tYThXcM8YHOZ1S1MtivcS3He/LQbbTCx3rbWJYCtWD5XXyJ53KlNPT7O1VI9sCW3xMtgFe8XpQ== +msw@^1.2.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/msw/-/msw-1.3.1.tgz#84a12bb17e76c25a7accaf921317044907ccd501" + integrity sha512-GhP5lHSTXNlZb9EaKgPRJ01YAnVXwzkvnTzRn4W8fxU2DXuJrRO+Nb6OHdYqB4fCkwSNpIJH9JkON5Y6rHqJMQ== dependencies: "@mswjs/cookies" "^0.2.2" - "@mswjs/interceptors" "^0.17.5" + "@mswjs/interceptors" "^0.17.10" "@open-draft/until" "^1.0.3" "@types/cookie" "^0.4.1" "@types/js-levenshtein" "^1.1.1" - chalk "4.1.1" + chalk "^4.1.1" chokidar "^3.4.2" cookie "^0.4.2" graphql "^15.0.0 || ^16.0.0" - headers-polyfill "^3.1.2" + headers-polyfill "3.2.5" inquirer "^8.2.0" is-node-process "^1.2.0" js-levenshtein "^1.1.6" @@ -9381,7 +9412,7 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" -serialize-javascript@^6.0.1: +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== @@ -9516,6 +9547,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" diff --git a/jenkins/frontend-develop.jenkinsfile b/jenkins/frontend-develop.jenkinsfile index ad86ceba..6fece9e6 100644 --- a/jenkins/frontend-develop.jenkinsfile +++ b/jenkins/frontend-develop.jenkinsfile @@ -20,7 +20,7 @@ pipeline { stage('deploy') { steps { dir('frontend') { - sshPublisher(publishers: [sshPublisherDesc(configName: 'haru-study-develop', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/2023-haru-study/develop/html', remoteDirectorySDF: false, removePrefix: 'dist', sourceFiles: 'dist/*')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) + sshPublisher(publishers: [sshPublisherDesc(configName: 'haru-study-develop', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/2023-haru-study/develop/html', remoteDirectorySDF: false, removePrefix: 'dist', sourceFiles: 'dist/**/*')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) } } } diff --git a/jenkins/frontend-production.jenkinsfile b/jenkins/frontend-production.jenkinsfile index b3f32384..2d77b58b 100644 --- a/jenkins/frontend-production.jenkinsfile +++ b/jenkins/frontend-production.jenkinsfile @@ -20,7 +20,7 @@ pipeline { stage('deploy') { steps { dir('frontend') { - sshPublisher(publishers: [sshPublisherDesc(configName: 'haru-study-production', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/2023-haru-study/production/html', remoteDirectorySDF: false, removePrefix: 'dist', sourceFiles: 'dist/*')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) + sshPublisher(publishers: [sshPublisherDesc(configName: 'haru-study-production', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: '/2023-haru-study/production/html', remoteDirectorySDF: false, removePrefix: 'dist', sourceFiles: 'dist/**/*')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)]) } } }