diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 73a91626..bacf8490 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -1,38 +1,47 @@ -name: sonar backend +name: sonarqube backend + on: pull_request: branches: [dev] paths: ["backend/**"] types: [opened, synchronize, reopened] + defaults: run: working-directory: backend + jobs: build: - name: sonar backend + name: sonarqube backend runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: checkout source + uses: actions/checkout@v2 with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + fetch-depth: 0 + - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 - - name: Cache SonarCloud packages + + - name: Cache SonarQube packages uses: actions/cache@v1 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages uses: actions/cache@v1 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST_URL }} + run: ./gradlew build sonarqube --info diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 69f106ea..a3068e34 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -1,12 +1,14 @@ -name: sonar frontend +name: frontend on: pull_request: branches: [dev] - paths: ["frontend/**"] + paths: ['frontend/**'] types: [opened, synchronize, reopened] + defaults: run: working-directory: frontend + jobs: sonarcloud: name: sonar frontend @@ -15,3 +17,131 @@ jobs: - uses: actions/checkout@v2 - run: npm ci - run: npm run build + + lhci: + name: Lighthouse CI + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Use Node.js 16.x + uses: actions/setup-node@v1 + with: + node-version: 16.x + + - name: NPM CI + run: | + npm ci + + - name: Build + run: | + npm run build + + - name: Lighthouse Run + env: + LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} + run: | + npm install -g @lhci/cli + lhci autorun || echo "LHCI failed!" + + - name: Format lighthouse score + id: format_lighthouse_score + uses: actions/github-script@v3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const results = JSON.parse(fs.readFileSync('${{ github.workspace }}/frontend/lhci_reports/manifest.json')); + let comments = ""; + + const { summary, jsonPath } = results[0]; + + const details = JSON.parse(fs.readFileSync(jsonPath)); + const { audits } = details; + + const formatResult = (res) => Math.round(res * 100); + + Object.keys(summary).forEach( + (key) => (summary[key] = formatResult(summary[key])) + ); + + const score = (res) => (res >= 90 ? "🟒" : res >= 50 ? "🟠" : "πŸ”΄"); + + const comment = [ + `## ⚑️ Lighthouse Report`, + `| Category | Score |`, + `| --- | --- |`, + `| ${score(summary.performance)} Performance | ${summary.performance} |`, + `| ${score(summary.accessibility)} Accessibility | ${summary.accessibility} |`, + `| ${score(summary[`best-practices`])} Best Practices | ${summary[`best-practices`]} |`, + `| ${score(summary.seo)} Seo | ${summary.seo} |`, + `| ${score(summary.pwa)} Pwa | ${summary.pwa} |` + ].join("\n"); + + const detail = [ + ``, + `| Category | Score |`, + `| --- | --- |`, + `| ${score( + audits[`first-contentful-paint`].score * 100 + )} First Contentful Paint | ${ + audits[`first-contentful-paint`].displayValue + } |`, + `| ${score( + audits[`speed-index`].score * 100 + )} Speed Index | ${ + audits[`speed-index`].displayValue + } |`, + `| ${score( + audits[`total-blocking-time`].score * 100 + )} Total Blocking Time | ${ + audits[`total-blocking-time`].displayValue + } |`, + `| ${score( + audits[`largest-contentful-paint`].score * 100 + )} Largest Contentful Paint | ${ + audits[`largest-contentful-paint`].displayValue + } |`, + `| ${score( + audits[`cumulative-layout-shift`].score * 100 + )} Cumulative Layout Shift | ${ + audits[`cumulative-layout-shift`].displayValue + } |` + ].join("\n"); + + comments += comment + "\n" + detail + "\n"; + + core.setOutput('comments', comments); + + - name: Comment PR + uses: unsplash/comment-on-pr@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + msg: ${{ steps.format_lighthouse_score.outputs.comments}} + + cypress-run: + name: Cypress E2E CI + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cypress CI + uses: cypress-io/github-action@v4 + with: + browser: chrome + headed: true + build: npm run build + start: npm run start + wait-on: 'http://localhost:3000' + record: true + parallel: true + working-directory: frontend + env: + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_INFO_MESSAGE: ${{github.event.pull_request.title}} + COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha}} + continue-on-error: true diff --git a/backend/build.gradle b/backend/build.gradle index 09b4bb5b..00d8e455 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -13,6 +13,7 @@ sourceCompatibility = '11' configurations { asciidoctorExtensions + cucumberRuntime.extendsFrom(implementation, testImplementation, runtimeOnly) } repositories { @@ -58,6 +59,11 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' + //cucumber + testImplementation 'io.cucumber:cucumber-java:6.10.4' + testImplementation 'io.cucumber:cucumber-spring:6.10.4' + testImplementation 'io.cucumber:cucumber-junit-platform-engine:6.10.4' + // slack implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.slack.api:slack-app-backend:1.22.2' @@ -119,12 +125,27 @@ jacocoTestReport { } } +task cucumber() { + dependsOn assemble, compileTestJava + doLast { + javaexec { + main = "io.cucumber.core.cli.Main" + classpath = configurations.cucumberRuntime + sourceSets.main.output + sourceSets.test.output + args = [ + '--plugin', 'pretty', + '--plugin', 'html:build/reports/cucumber/cucumber-report.html', + '--glue', 'com.woowacourse.gongcheck', + 'src/test/resources/features'] + } + } +} + +build { + dependsOn cucumber +} + sonarqube { properties { - property "sonar.projectKey", "woowacourse-teams_2022-gong-check" - property "sonar.organization", "woowacourse-teams" - property "sonar.host.url", "https://sonarcloud.io" - property 'sonar.coverage.jacoco.xmlReportPaths', 'build/reports/jacoco/test/jacocoTestReport.xml' - property 'sonar.language', 'java' + property "sonar.projectKey", "woowacourse-teams_2022-gong-check_AYKm4KvOS_3Pe1LEsoLu" } } diff --git a/backend/src/docs/asciidoc/errorCode.adoc b/backend/src/docs/asciidoc/errorCode.adoc new file mode 100644 index 00000000..f90d23b2 --- /dev/null +++ b/backend/src/docs/asciidoc/errorCode.adoc @@ -0,0 +1,3 @@ +[[ErrorCode]] +== μ—λŸ¬ μ½”λ“œ +operation::errorCode[snippets='error-code'] diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index eee5f468..ada478ee 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -14,3 +14,4 @@ include::task.adoc[] include::runningTask.adoc[] include::submission.adoc[] include::image-upload.adoc[] +include::errorCode.adoc[] diff --git a/backend/src/docs/asciidoc/runningTask.adoc b/backend/src/docs/asciidoc/runningTask.adoc index f1b81954..9738333f 100644 --- a/backend/src/docs/asciidoc/runningTask.adoc +++ b/backend/src/docs/asciidoc/runningTask.adoc @@ -3,7 +3,7 @@ === RunningTask 정보 쑰회 -operation::runningTasks/find/success[snippets='http-request,http-response,path-parameters,response-fields'] +operation::runningTasks/connect/success[snippets='http-request,http-response,path-parameters'] === RunningTask μ™„λ£Œ μ—¬λΆ€ 확인 @@ -16,3 +16,7 @@ operation::runningTasks/check/success[snippets='http-request,http-response,path- === RunningTask 생성 operation::runningTasks/create/success[snippets='http-request,http-response,path-parameters'] + +=== Section의 RunningTask λͺ¨λ‘ μ²΄ν¬μƒνƒœλ‘œ λ³€ν™˜ + +operation::runningTasks/allCheck/success[snippets='http-request,http-response,path-parameters'] diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProvider.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProvider.java index dd68d412..5d1c5acd 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProvider.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProvider.java @@ -1,6 +1,7 @@ package com.woowacourse.gongcheck.auth.application; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import org.springframework.stereotype.Component; @Component @@ -26,13 +27,15 @@ public Long parseId(final String entranceCode) { validateIdSize(id); return id; } catch (NumberFormatException | BusinessException e) { - throw new BusinessException("μœ νš¨ν•˜μ§€ μ•Šμ€ μž…μž₯μ½”λ“œμž…λ‹ˆλ‹€."); + String message = String.format("μœ νš¨ν•˜μ§€ μ•Šμ€ μž…μž₯μ½”λ“œμž…λ‹ˆλ‹€. entranceCode = %s", entranceCode); + throw new BusinessException(message, ErrorCode.H002); } } private void validateIdSize(final Long id) { if (id < MINIMUM_ID_SIZE) { - throw new BusinessException("μœ νš¨ν•˜μ§€ μ•Šμ€ idμž…λ‹ˆλ‹€."); + String message = String.format("μœ νš¨ν•˜μ§€ μ•Šμ€ idμž…λ‹ˆλ‹€. id = %d", id); + throw new BusinessException(message, ErrorCode.H003); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HostAuthService.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HostAuthService.java index 2dbacf32..8a6672f0 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HostAuthService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HostAuthService.java @@ -2,7 +2,7 @@ import static com.woowacourse.gongcheck.auth.domain.Authority.HOST; -import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.application.response.SocialProfileResponse; import com.woowacourse.gongcheck.auth.application.response.TokenResponse; import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; import com.woowacourse.gongcheck.core.domain.host.Host; @@ -28,17 +28,17 @@ public HostAuthService(final JwtTokenProvider jwtTokenProvider, final GithubOaut @Transactional public TokenResponse createToken(final TokenRequest request) { - GithubProfileResponse githubProfileResponse = githubOauthClient.requestGithubProfileByCode(request.getCode()); - boolean alreadyJoin = hostRepository.existsByGithubId(githubProfileResponse.getGithubId()); - Host host = findOrCreateHost(alreadyJoin, githubProfileResponse); + SocialProfileResponse socialProfileResponse = githubOauthClient.requestSocialProfileByCode(request.getCode()); + boolean alreadyJoin = hostRepository.existsByGithubId(socialProfileResponse.getGithubId()); + Host host = findOrCreateHost(alreadyJoin, socialProfileResponse); String token = jwtTokenProvider.createToken(String.valueOf(host.getId()), HOST); return TokenResponse.of(token, alreadyJoin); } - private Host findOrCreateHost(final boolean alreadyJoin, final GithubProfileResponse githubProfileResponse) { + private Host findOrCreateHost(final boolean alreadyJoin, final SocialProfileResponse socialProfileResponse) { if (alreadyJoin) { - return hostRepository.getByGithubId(githubProfileResponse.getGithubId()); + return hostRepository.getByGithubId(socialProfileResponse.getGithubId()); } - return hostRepository.save(githubProfileResponse.toHost()); + return hostRepository.save(socialProfileResponse.toHost()); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/OAuthClient.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/OAuthClient.java new file mode 100644 index 00000000..408cec4e --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/OAuthClient.java @@ -0,0 +1,7 @@ +package com.woowacourse.gongcheck.auth.application; + +import com.woowacourse.gongcheck.auth.application.response.SocialProfileResponse; + +public interface OAuthClient { + SocialProfileResponse requestSocialProfileByCode(String code); +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubAccessTokenResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/OAuthAccessTokenResponse.java similarity index 64% rename from backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubAccessTokenResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/OAuthAccessTokenResponse.java index 25682b10..73c7deba 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubAccessTokenResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/OAuthAccessTokenResponse.java @@ -4,15 +4,15 @@ import lombok.Getter; @Getter -public class GithubAccessTokenResponse { +public class OAuthAccessTokenResponse { @JsonProperty("access_token") private String accessToken; - private GithubAccessTokenResponse() { + private OAuthAccessTokenResponse() { } - public GithubAccessTokenResponse(final String accessToken) { + public OAuthAccessTokenResponse(final String accessToken) { this.accessToken = accessToken; } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubProfileResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/SocialProfileResponse.java similarity index 86% rename from backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubProfileResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/SocialProfileResponse.java index 3088ef13..b6dfb9c9 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubProfileResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/SocialProfileResponse.java @@ -5,7 +5,7 @@ import lombok.Getter; @Getter -public class GithubProfileResponse { +public class SocialProfileResponse { @JsonProperty("name") private String nickname; @@ -16,10 +16,10 @@ public class GithubProfileResponse { @JsonProperty("avatar_url") private String imageUrl; - private GithubProfileResponse() { + private SocialProfileResponse() { } - public GithubProfileResponse(final String nickname, final String loginName, final String githubId, + public SocialProfileResponse(final String nickname, final String loginName, final String githubId, final String imageUrl) { this.nickname = nickname; this.loginName = loginName; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/AuthenticationContext.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/AuthenticationContext.java index 0c57ce99..f18c3034 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/AuthenticationContext.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/AuthenticationContext.java @@ -10,13 +10,8 @@ public class AuthenticationContext { private String principal; - private Authority authority; public void setPrincipal(final String principal) { this.principal = principal; } - - public void setAuthority(final Authority authority) { - this.authority = authority; - } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationInterceptor.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationInterceptor.java index 88816762..3f922031 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationInterceptor.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationInterceptor.java @@ -4,11 +4,14 @@ import com.woowacourse.gongcheck.auth.domain.AuthenticationContext; import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.auth.support.JwtTokenExtractor; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.UnauthorizedException; +import java.util.Objects; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.cors.CorsUtils; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @Component @@ -31,12 +34,29 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp } String token = JwtTokenExtractor.extractToken(request) - .orElseThrow(() -> new UnauthorizedException("헀더에 토큰 값이 μ •μƒμ μœΌλ‘œ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")); + .orElseThrow(() -> { + String message = "헀더에 토큰값이 μ •μƒμ μœΌλ‘œ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."; + throw new UnauthorizedException(message, ErrorCode.A003); + }); String subject = jwtTokenProvider.extractSubject(token); authenticationContext.setPrincipal(subject); + + if (isNotHostOnlyAnnotated((HandlerMethod) handler)) { + return true; + } Authority authority = jwtTokenProvider.extractAuthority(token); - authenticationContext.setAuthority(authority); + authorize(authority); return HandlerInterceptor.super.preHandle(request, response, handler); } + + private boolean isNotHostOnlyAnnotated(final HandlerMethod handlerMethod) { + return Objects.isNull(handlerMethod.getMethodAnnotation(HostOnly.class)); + } + + private void authorize(final Authority authority) { + if (!authority.isHost()) { + throw new UnauthorizedException("호슀트만 μž…μž₯ κ°€λŠ₯ν•©λ‹ˆλ‹€.", ErrorCode.A001); + } + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipalArgumentResolver.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipalArgumentResolver.java index a41e9aec..2dda9361 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipalArgumentResolver.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipalArgumentResolver.java @@ -24,8 +24,7 @@ public boolean supportsParameter(final MethodParameter parameter) { @Override public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, - final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) - throws Exception { + final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) { return Long.valueOf(authenticationContext.getPrincipal()); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostOnly.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/HostOnly.java similarity index 81% rename from backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostOnly.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/HostOnly.java index d7d32718..e431931c 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostOnly.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/HostOnly.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.auth.presentation.aop; +package com.woowacourse.gongcheck.auth.presentation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostVerifier.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostVerifier.java deleted file mode 100644 index 5133c6c5..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostVerifier.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.woowacourse.gongcheck.auth.presentation.aop; - -import com.woowacourse.gongcheck.auth.domain.AuthenticationContext; -import com.woowacourse.gongcheck.auth.domain.Authority; -import com.woowacourse.gongcheck.exception.UnauthorizedException; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.springframework.stereotype.Component; - -@Aspect -@Component -public class HostVerifier { - - private final AuthenticationContext authenticationContext; - - public HostVerifier(final AuthenticationContext authenticationContext) { - this.authenticationContext = authenticationContext; - } - - @Before("@annotation(com.woowacourse.gongcheck.auth.presentation.aop.HostOnly)") - public void checkHost() { - Authority authority = authenticationContext.getAuthority(); - if (!authority.isHost()) { - throw new UnauthorizedException("호슀트만 μž…μž₯ κ°€λŠ₯ν•©λ‹ˆλ‹€."); - } - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GuestEnterRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GuestEnterRequest.java index 46e77da0..e35b620b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GuestEnterRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GuestEnterRequest.java @@ -6,8 +6,7 @@ @Getter public class GuestEnterRequest { - - @NotNull + @NotNull(message = "GuestEnterRequest의 passwordλŠ” null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String password; private GuestEnterRequest() { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GithubAccessTokenRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/OAuthAccessTokenRequest.java similarity index 69% rename from backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GithubAccessTokenRequest.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/OAuthAccessTokenRequest.java index 19849aa7..8580a319 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GithubAccessTokenRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/OAuthAccessTokenRequest.java @@ -4,7 +4,7 @@ import lombok.Getter; @Getter -public class GithubAccessTokenRequest { +public class OAuthAccessTokenRequest { private String code; @@ -14,10 +14,10 @@ public class GithubAccessTokenRequest { @JsonProperty("client_secret") private String clientSecret; - private GithubAccessTokenRequest() { + private OAuthAccessTokenRequest() { } - public GithubAccessTokenRequest(final String code, final String clientId, final String clientSecret) { + public OAuthAccessTokenRequest(final String code, final String clientId, final String clientSecret) { this.code = code; this.clientId = clientId; this.clientSecret = clientSecret; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/JobService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/JobService.java index baf49459..f6ee4fb6 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/application/JobService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/JobService.java @@ -72,15 +72,15 @@ public void removeJob(final Long hostId, final Long jobId) { } @Transactional - public Long updateJob(final Long hostId, final Long jobId, final JobCreateRequest request) { + public void updateJob(final Long hostId, final Long jobId, final JobCreateRequest request) { Host host = hostRepository.getById(hostId); Job job = jobRepository.getBySpaceHostAndId(host, jobId); - Space space = job.getSpace(); List
sections = sectionRepository.findAllByJob(job); List tasks = taskRepository.findAllBySectionIn(sections); - deleteJob(jobId, sections, tasks); - return saveJob(request, space); + job.changeName(new Name(request.getName())); + deleteSectionsAndTasks(sections, tasks); + createSectionsAndTasks(request.getSections(), job); } public SlackUrlResponse findSlackUrl(final Long hostId, final Long jobId) { @@ -140,11 +140,15 @@ private Task createTask(final TaskCreateRequest taskCreateRequest, final Section } private void deleteJob(final Long jobId, final List
sections, final List tasks) { + deleteSectionsAndTasks(sections, tasks); + jobRepository.deleteById(jobId); + } + + private void deleteSectionsAndTasks(final List
sections, final List tasks) { runningTaskRepository.deleteAllByIdInBatch(tasks.stream() .map(Task::getId) .collect(toList())); taskRepository.deleteAllInBatch(tasks); sectionRepository.deleteAllInBatch(sections); - jobRepository.deleteById(jobId); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/AlertService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/NotificationService.java similarity index 85% rename from backend/src/main/java/com/woowacourse/gongcheck/core/application/AlertService.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/NotificationService.java index 5283569d..2421db15 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/application/AlertService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/NotificationService.java @@ -2,7 +2,7 @@ import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; -public interface AlertService { +public interface NotificationService { void sendMessage(final SubmissionCreatedResponse submissionCreatedResponse); } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java index ff0be4e0..8f625352 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java @@ -19,6 +19,7 @@ import com.woowacourse.gongcheck.core.presentation.request.SpaceChangeRequest; import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -102,13 +103,17 @@ public void removeSpace(final Long hostId, final Long spaceId) { private void checkDuplicateSpaceName(final Name spaceName, final Host host) { if (spaceRepository.existsByHostAndName(host, spaceName)) { - throw new BusinessException("이미 μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€."); + String message = String.format("이미 μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€. hostId = %d, spaceName = %s", host.getId(), + spaceName.getValue()); + throw new BusinessException(message, ErrorCode.SP01); } } private void checkDuplicateSpaceName(final Name spaceName, final Host host, final Space space) { if (spaceRepository.existsByHostAndNameAndIdNot(host, spaceName, space.getId())) { - throw new BusinessException("이미 μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€."); + String message = String.format("이미 μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€. hostId = %d, spaceName = %s, spaceId = %d", host.getId(), + spaceName.getValue(), space.getId()); + throw new BusinessException(message, ErrorCode.SP02); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SubmissionService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SubmissionService.java index a001bd18..1200f8aa 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SubmissionService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SubmissionService.java @@ -2,6 +2,7 @@ import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; +import com.woowacourse.gongcheck.core.application.support.LoggingFormatConverter; import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.core.domain.job.Job; @@ -11,13 +12,14 @@ import com.woowacourse.gongcheck.core.domain.submission.Submission; import com.woowacourse.gongcheck.core.domain.submission.SubmissionRepository; import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskSseEmitterContainer; import com.woowacourse.gongcheck.core.domain.task.RunningTasks; import com.woowacourse.gongcheck.core.domain.task.TaskRepository; import com.woowacourse.gongcheck.core.domain.task.Tasks; import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.util.List; -import org.springframework.core.task.TaskRejectedException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -34,38 +36,39 @@ public class SubmissionService { private final TaskRepository taskRepository; private final RunningTaskRepository runningTaskRepository; private final SubmissionRepository submissionRepository; - private final AlertService alertService; + private final NotificationService notificationService; + private final RunningTaskSseEmitterContainer runningTaskSseEmitterContainer; public SubmissionService(final HostRepository hostRepository, final JobRepository jobRepository, final SpaceRepository spaceRepository, final TaskRepository taskRepository, final RunningTaskRepository runningTaskRepository, - final SubmissionRepository submissionRepository, final AlertService alertService) { + final SubmissionRepository submissionRepository, + final NotificationService notificationService, + final RunningTaskSseEmitterContainer runningTaskSseEmitterContainer) { this.hostRepository = hostRepository; this.jobRepository = jobRepository; this.spaceRepository = spaceRepository; this.taskRepository = taskRepository; this.runningTaskRepository = runningTaskRepository; this.submissionRepository = submissionRepository; - this.alertService = alertService; + this.notificationService = notificationService; + this.runningTaskSseEmitterContainer = runningTaskSseEmitterContainer; } @Transactional(isolation = Isolation.SERIALIZABLE) public void submitJobCompletion(final Long hostId, final Long jobId, - final SubmissionRequest request) { + final SubmissionRequest request) { Host host = hostRepository.getById(hostId); Job job = jobRepository.getBySpaceHostAndId(host, jobId); saveSubmissionAndClearRunningTasks(request, job); if (job.hasUrl()) { - alertMessage(request, job); + sendNotification(request, job); } + runningTaskSseEmitterContainer.publishSubmitEvent(jobId); } - private void alertMessage(final SubmissionRequest request, final Job job) { - try { - alertService.sendMessage(SubmissionCreatedResponse.of(request.getAuthor(), job)); - } catch (TaskRejectedException e) { - throw new RuntimeException(e); - } + private void sendNotification(final SubmissionRequest request, final Job job) { + notificationService.sendMessage(SubmissionCreatedResponse.of(request.getAuthor(), job)); } public SubmissionsResponse findPage(final Long hostId, final Long spaceId, final Pageable pageable) { @@ -86,8 +89,11 @@ private void saveSubmissionAndClearRunningTasks(final SubmissionRequest request, } private void validateRunning(final Tasks tasks) { - if (!runningTaskRepository.existsByTaskIdIn(tasks.getTaskIds())) { - throw new BusinessException("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + List taskIds = tasks.getTaskIds(); + if (!runningTaskRepository.existsByTaskIdIn(taskIds)) { + String message = String.format("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. taskIds = %s", + LoggingFormatConverter.convertIdsToString(taskIds)); + throw new BusinessException(message, ErrorCode.S001); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/TaskService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/TaskService.java index e307979c..1a978397 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/application/TaskService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/TaskService.java @@ -7,15 +7,21 @@ import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.core.domain.job.Job; import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; import com.woowacourse.gongcheck.core.domain.task.RunningTask; import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskSseEmitterContainer; +import com.woowacourse.gongcheck.core.domain.task.RunningTasks; import com.woowacourse.gongcheck.core.domain.task.Task; import com.woowacourse.gongcheck.core.domain.task.TaskRepository; import com.woowacourse.gongcheck.core.domain.task.Tasks; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.NotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Service @Transactional(readOnly = true) @@ -23,25 +29,34 @@ public class TaskService { private final HostRepository hostRepository; private final JobRepository jobRepository; + private final SectionRepository sectionRepository; private final TaskRepository taskRepository; private final RunningTaskRepository runningTaskRepository; + private final RunningTaskSseEmitterContainer runningTaskSseEmitterContainer; public TaskService(final HostRepository hostRepository, final JobRepository jobRepository, - final TaskRepository taskRepository, final RunningTaskRepository runningTaskRepository) { + final SectionRepository sectionRepository, final TaskRepository taskRepository, + final RunningTaskRepository runningTaskRepository, + final RunningTaskSseEmitterContainer runningTaskSseEmitterContainer) { this.hostRepository = hostRepository; this.jobRepository = jobRepository; + this.sectionRepository = sectionRepository; this.taskRepository = taskRepository; this.runningTaskRepository = runningTaskRepository; + this.runningTaskSseEmitterContainer = runningTaskSseEmitterContainer; } @Transactional public void createNewRunningTasks(final Long hostId, final Long jobId) { Tasks tasks = findTasksByHostIdAndJobId(hostId, jobId); if (tasks.isEmpty()) { - throw new NotFoundException("μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + String message = String.format("μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. hostId = %d, jobId = %d", hostId, jobId); + throw new NotFoundException(message, ErrorCode.T002); } if (existsAnyRunningTaskIn(tasks)) { - throw new BusinessException("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ—¬ μƒˆλ‘œμš΄ μž‘μ—…μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."); + String message = String.format("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ—¬ μƒˆλ‘œμš΄ μž‘μ—…μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€. hostId = %d, jobId = %d", hostId, + jobId); + throw new BusinessException(message, ErrorCode.T001); } runningTaskRepository.saveAll(tasks.createRunningTasks()); } @@ -51,9 +66,9 @@ public JobActiveResponse isJobActivated(final Long hostId, final Long jobId) { return JobActiveResponse.from(existsAnyRunningTaskIn(tasks)); } - public RunningTasksResponse findRunningTasks(final Long hostId, final Long jobId) { - Tasks tasks = findTasksByHostIdAndJobId(hostId, jobId); - return findExistingRunningTasks(tasks); + public SseEmitter connectRunningTasks(final Long hostId, final Long jobId) { + RunningTasksResponse runningTasks = findExistingRunningTasks(hostId, jobId); + return runningTaskSseEmitterContainer.createEmitterWithConnectionEvent(jobId, runningTasks); } @Transactional @@ -61,9 +76,17 @@ public void flipRunningTask(final Long hostId, final Long taskId) { Host host = hostRepository.getById(hostId); Task task = taskRepository.getBySectionJobSpaceHostAndId(host, taskId); RunningTask runningTask = runningTaskRepository.findByTaskId(task.getId()) - .orElseThrow(() -> new BusinessException("ν˜„μž¬ 진행 쀑인 μž‘μ—…μ΄ μ•„λ‹™λ‹ˆλ‹€.")); + .orElseThrow(() -> { + String message = String.format("ν˜„μž¬ 진행 쀑인 μž‘μ—…μ΄ μ•„λ‹™λ‹ˆλ‹€. hostId = %d, taskId = %d", hostId, taskId); + throw new BusinessException(message, ErrorCode.R002); + }); runningTask.flipCheckedStatus(); + + Long jobId = task.getSection().getJob().getId(); + RunningTasksResponse runningTasks = findExistingRunningTasks(hostId, jobId); + + runningTaskSseEmitterContainer.publishFlipEvent(jobId, runningTasks); } public TasksResponse findTasks(final Long hostId, final Long jobId) { @@ -71,19 +94,39 @@ public TasksResponse findTasks(final Long hostId, final Long jobId) { return TasksResponse.from(tasks); } - private Tasks findTasksByHostIdAndJobId(final Long hostId, final Long jobId) { + @Transactional + public void checkRunningTasksInSection(final Long hostId, final Long sectionId) { Host host = hostRepository.getById(hostId); - Job job = jobRepository.getBySpaceHostAndId(host, jobId); - return new Tasks(taskRepository.findAllBySectionJob(job)); + Section section = sectionRepository.getByJobSpaceHostAndId(host, sectionId); + Long jobId = section.getJob().getId(); + Tasks tasks = new Tasks(taskRepository.findAllBySection(section)); + + if (!existsAnyRunningTaskIn(tasks)) { + String message = String.format("ν˜„μž¬ 진행쀑인 RunningTaskκ°€ μ—†μŠ΅λ‹ˆλ‹€ hostId = %d, sectionId = %d", hostId, sectionId); + throw new BusinessException(message, ErrorCode.R002); + } + RunningTasks runningTasks = tasks.getRunningTasks(); + runningTasks.check(); + + Tasks allTasks = new Tasks(taskRepository.findAllBySectionJob(section.getJob())); + runningTaskSseEmitterContainer.publishFlipEvent(jobId, RunningTasksResponse.from(allTasks)); } - private RunningTasksResponse findExistingRunningTasks(final Tasks tasks) { + private RunningTasksResponse findExistingRunningTasks(final Long hostId, final Long jobId) { + Tasks tasks = findTasksByHostIdAndJobId(hostId, jobId); if (!existsAnyRunningTaskIn(tasks)) { - throw new BusinessException("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•Šμ•„ μ‘°νšŒν•  수 μ—†μŠ΅λ‹ˆλ‹€"); + String message = String.format("ν˜„μž¬ 진행쀑인 RunningTaskκ°€ μ—†μŠ΅λ‹ˆλ‹€ hostId = %d, jobId = %d", hostId, jobId); + throw new BusinessException(message, ErrorCode.R001); } return RunningTasksResponse.from(tasks); } + private Tasks findTasksByHostIdAndJobId(final Long hostId, final Long jobId) { + Host host = hostRepository.getById(hostId); + Job job = jobRepository.getBySpaceHostAndId(host, jobId); + return new Tasks(taskRepository.findAllBySectionJob(job)); + } + private boolean existsAnyRunningTaskIn(final Tasks tasks) { return runningTaskRepository.existsByTaskIdIn(tasks.getTaskIds()); } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/support/LoggingFormatConverter.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/support/LoggingFormatConverter.java new file mode 100644 index 00000000..66a5108a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/support/LoggingFormatConverter.java @@ -0,0 +1,18 @@ +package com.woowacourse.gongcheck.core.application.support; + +import java.util.List; +import java.util.stream.Collectors; + +public class LoggingFormatConverter { + + private static final String ID_DELIMITER = ", "; + + private LoggingFormatConverter() { + } + + public static String convertIdsToString(final List ids) { + return ids.stream() + .map(String::valueOf) + .collect(Collectors.joining(ID_DELIMITER)); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/Host.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/Host.java index 9678207c..379a6d50 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/Host.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/Host.java @@ -1,7 +1,7 @@ package com.woowacourse.gongcheck.core.domain.host; import com.woowacourse.gongcheck.exception.BusinessException; -import com.woowacourse.gongcheck.exception.UnauthorizedException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; @@ -64,7 +64,8 @@ public Host(final Long id, final SpacePassword spacePassword, final Long githubI public void checkPassword(final SpacePassword spacePassword) { if (!this.spacePassword.equals(spacePassword)) { - throw new BusinessException("곡간 λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯ν•˜μ‹  λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + String message = String.format("곡간 λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯ν•˜μ‹  λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. spacePassword = %s", spacePassword); + throw new BusinessException(message, ErrorCode.H001); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/HostRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/HostRepository.java index d43b853e..fb074c5f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/HostRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/HostRepository.java @@ -1,5 +1,6 @@ package com.woowacourse.gongcheck.core.domain.host; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -11,10 +12,16 @@ public interface HostRepository extends JpaRepository { boolean existsByGithubId(final Long githubId); default Host getById(final Long id) throws NotFoundException { - return findById(id).orElseThrow(() -> new NotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€.")); + return findById(id).orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€. hostId = %d", id); + throw new NotFoundException(message, ErrorCode.H004); + }); } default Host getByGithubId(final Long githubId) throws NotFoundException { - return findByGithubId(githubId).orElseThrow(() -> new NotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€.")); + return findByGithubId(githubId).orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€. githubId = %d", githubId); + throw new NotFoundException(message, ErrorCode.H005); + }); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/SpacePassword.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/SpacePassword.java index 132ddb7b..b402c3f0 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/SpacePassword.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/SpacePassword.java @@ -1,6 +1,7 @@ package com.woowacourse.gongcheck.core.domain.host; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.util.Objects; import java.util.regex.Pattern; import javax.persistence.Column; @@ -22,7 +23,8 @@ protected SpacePassword() { public SpacePassword(final String value) { if (!SPACE_PASSWORD_PATTERN.matcher(value).find()) { - throw new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€."); + String message = String.format("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€. value = %s", value); + throw new BusinessException(message, ErrorCode.SP03); } this.value = value; } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFile.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFile.java index 43e7d3b6..50ed15db 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFile.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFile.java @@ -3,6 +3,7 @@ import static org.springframework.util.StringUtils.getFilenameExtension; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.io.IOException; import java.util.Objects; import java.util.UUID; @@ -40,25 +41,29 @@ public static ImageFile from(final MultipartFile multipartFile) { return new ImageFile(multipartFile.getOriginalFilename(), multipartFile.getContentType(), getFilenameExtension(multipartFile.getOriginalFilename()), multipartFile.getBytes()); } catch (IOException exception) { - throw new BusinessException("잘λͺ»λœ νŒŒμΌμž…λ‹ˆλ‹€."); + String message = String.format("잘λͺ»λœ νŒŒμΌμž…λ‹ˆλ‹€. multipartFile = %s", multipartFile.getName()); + throw new BusinessException(message, ErrorCode.IM05); } } private static void validateNullFile(final MultipartFile multipartFile) { if (Objects.isNull(multipartFile)) { - throw new BusinessException("이미지 νŒŒμΌμ€ null이 λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); + String message = "이미지 νŒŒμΌμ€ null이 λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."; + throw new BusinessException(message, ErrorCode.IM01); } } private static void validateEmptyFile(final MultipartFile multipartFile) { if (multipartFile.getSize() == 0) { - throw new BusinessException("이미지 νŒŒμΌμ€ λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); + String message = String.format("이미지 νŒŒμΌμ€ λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€. multipartFile = %s", multipartFile.getName()); + throw new BusinessException(message, ErrorCode.IM02); } } private static void validateNullFileName(final MultipartFile multipartFile) { if (Objects.requireNonNull(multipartFile.getOriginalFilename()).isEmpty()) { - throw new BusinessException("이미지 파일 이름은 λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); + String message = String.format("이미지 파일 이름은 λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€. multipartFile = %s", multipartFile.getName()); + throw new BusinessException(message, ErrorCode.IM03); } } @@ -66,7 +71,8 @@ private static void validateImageExtension(final MultipartFile multipartFile) { String fileExtension = getFilenameExtension(multipartFile.getOriginalFilename()); assert fileExtension != null; if (!IMAGE_FILE_EXTENSION_PATTERN.matcher(fileExtension).matches()) { - throw new BusinessException("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€."); + String message = String.format("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€. fileExtension = %s", fileExtension); + throw new BusinessException(message, ErrorCode.IM04); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/Job.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/Job.java index 7269a98f..ad3e5c05 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/Job.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/Job.java @@ -71,6 +71,10 @@ public Submission createSubmission(final String author) { .build(); } + public void changeName(final Name name) { + this.name = name; + } + public void changeSlackUrl(final String slackUrl) { this.slackUrl = slackUrl; } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/JobRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/JobRepository.java index 70586d50..e567416d 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/JobRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/JobRepository.java @@ -2,6 +2,7 @@ import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; import java.util.Optional; @@ -17,11 +18,17 @@ public interface JobRepository extends JpaRepository { default Job getBySpaceHostAndId(final Host host, final Long id) throws NotFoundException { return findBySpaceHostAndId(host, id) - .orElseThrow(() -> new NotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€.")); + .orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€. hostId = %d, jobId = %d", host.getId(), id); + throw new NotFoundException(message, ErrorCode.J001); + }); } default Job getById(final Long id) throws NotFoundException { return findById(id) - .orElseThrow(() -> new NotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€.")); + .orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€. jobId = %d", id); + throw new NotFoundException(message, ErrorCode.J002); + }); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/SectionRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/SectionRepository.java index 9ad81d08..83cab8e9 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/SectionRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/SectionRepository.java @@ -1,11 +1,34 @@ package com.woowacourse.gongcheck.core.domain.section; +import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.exception.ErrorCode; +import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface SectionRepository extends JpaRepository { + List
findAllByJobIn(final List jobs); List
findAllByJob(final Job job); + + Optional
findByJobSpaceHostAndId(final Host host, final Long id); + + default Section getByJobSpaceHostAndId(final Host host, final Long id) throws NotFoundException { + return findByJobSpaceHostAndId(host, id) + .orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ΅¬μ—­μž…λ‹ˆλ‹€. hostId = %d, sectionId = %d", host.getId(), id); + throw new NotFoundException(message, ErrorCode.SE01); + }); + } + + default Section getById(final Long id) throws NotFoundException { + return findById(id) + .orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ΅¬μ—­μž…λ‹ˆλ‹€. sectionId = %d", id); + throw new NotFoundException(message, ErrorCode.SE01); + }); + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepository.java index 19bbc9c9..33e5ffe7 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepository.java @@ -2,6 +2,7 @@ import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; import java.util.Optional; @@ -19,6 +20,9 @@ public interface SpaceRepository extends JpaRepository { default Space getByHostAndId(final Host host, final Long id) throws NotFoundException { return findByHostAndId(host, id) - .orElseThrow(() -> new NotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€.")); + .orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€. hostId = %d, spaceId = %d", host.getId(), id); + throw new NotFoundException(message, ErrorCode.SP04); + }); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/Submission.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/Submission.java index 1ff6f870..30718cd3 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/Submission.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/Submission.java @@ -2,6 +2,7 @@ import com.woowacourse.gongcheck.core.domain.job.Job; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; @@ -56,11 +57,13 @@ public Submission(final Long id, final Job job, final String author, final Local private void validateAuthorLength(final String author) { if (author.isBlank()) { - throw new BusinessException("제좜자 이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€."); + String message = String.format("제좜자 이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€. author = %s", author); + throw new BusinessException(message, ErrorCode.S002); } if (author.length() > AUTHOR_MAX_LENGTH) { - throw new BusinessException("제좜자 이름은 " + AUTHOR_MAX_LENGTH + "자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); + String message = String.format("제좜자 이름은 %d자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€. author = %s", AUTHOR_MAX_LENGTH, author); + throw new BusinessException(message, ErrorCode.S003); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTask.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTask.java index 546acb77..c7140469 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTask.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTask.java @@ -55,6 +55,10 @@ public void flipCheckedStatus() { isChecked = !isChecked; } + public void check() { + isChecked = true; + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskSseEmitterContainer.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskSseEmitterContainer.java new file mode 100644 index 00000000..90fac726 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskSseEmitterContainer.java @@ -0,0 +1,81 @@ +package com.woowacourse.gongcheck.core.domain.task; + +import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@Component +@Slf4j +public class RunningTaskSseEmitterContainer { + + private static final long DEFAULT_TIME_OUT_MILLIS = 10L * 60 * 1000; + + private final Map> values = new ConcurrentHashMap<>(); + + public SseEmitter createEmitterWithConnectionEvent(final Long jobId, final RunningTasksResponse runningTasks) { + SseEmitter emitter = saveByJobId(jobId); + try { + emitter.send(SseEmitter.event() + .name("connect") + .data(runningTasks)); + } catch (IOException e) { + log.error("데이터 전솑 쀑 μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. message = {}, emitterId = {}", e.getMessage(), emitter); + throw new RuntimeException(e); + } + return emitter; + } + + public void publishFlipEvent(final Long jobId, final RunningTasksResponse runningTasks) { + getValuesByJobId(jobId).forEach(emitter -> { + try { + emitter.send(SseEmitter.event() + .name("flip") + .data(runningTasks)); + } catch (IOException e) { + log.info("expired emitter. message = {}, emitterId = {}", e.getMessage(), emitter); + } + }); + } + + public void publishSubmitEvent(final Long jobId) { + getValuesByJobId(jobId).forEach(emitter -> { + try { + emitter.send(SseEmitter.event() + .name("submit") + .data("send submit event")); + log.info("send emitter = {}", emitter); + } catch (IOException e) { + log.info("expired emitter. message = {}, emitterId = {}", e.getMessage(), emitter); + } + }); + } + + public void deleteByJobId(final Long jobId, final SseEmitter emitter) { + List emitters = values.get(jobId); + if (Objects.isNull(emitters)) { + return; + } + emitters.remove(emitter); + } + + public List getValuesByJobId(final Long jobId) { + return values.get(jobId); + } + + private SseEmitter saveByJobId(final Long jobId) { + SseEmitter emitter = new SseEmitter(DEFAULT_TIME_OUT_MILLIS); + emitter.onCompletion(() -> deleteByJobId(jobId, emitter)); + if (!values.containsKey(jobId)) { + values.put(jobId, new CopyOnWriteArrayList<>()); + } + values.get(jobId).add(emitter); + return emitter; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTasks.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTasks.java index 2ae44958..c347c022 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTasks.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTasks.java @@ -1,7 +1,10 @@ package com.woowacourse.gongcheck.core.domain.task; +import com.woowacourse.gongcheck.core.application.support.LoggingFormatConverter; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.util.List; +import java.util.stream.Collectors; public class RunningTasks { @@ -13,7 +16,15 @@ public RunningTasks(final List runningTasks) { public void validateCompletion() { if (!isAllChecked()) { - throw new BusinessException("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€."); + String message = String.format("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€. runningTasksIds = %s", + LoggingFormatConverter.convertIdsToString(getIds())); + throw new BusinessException(message, ErrorCode.R003); + } + } + + public void check() { + for (RunningTask runningTask : runningTasks) { + runningTask.check(); } } @@ -21,4 +32,10 @@ private boolean isAllChecked() { return runningTasks.stream() .allMatch(RunningTask::isChecked); } + + private List getIds() { + return runningTasks.stream() + .map(RunningTask::getTaskId) + .collect(Collectors.toList()); + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/TaskRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/TaskRepository.java index 8d9dd488..6c2802e1 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/TaskRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/TaskRepository.java @@ -3,6 +3,7 @@ import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.domain.job.Job; import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; import java.util.Optional; @@ -21,8 +22,14 @@ public interface TaskRepository extends JpaRepository { void deleteAllBySectionIn(final List
sections); + @EntityGraph(attributePaths = {"runningTask"}, type = EntityGraphType.FETCH) + List findAllBySection(final Section section); + default Task getBySectionJobSpaceHostAndId(final Host host, final Long id) throws NotFoundException { return findBySectionJobSpaceHostAndId(host, id) - .orElseThrow(() -> new NotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€.")); + .orElseThrow(() -> { + String message = String.format("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€. hostId = %d, taskId = %d", host.getId(), id); + throw new NotFoundException(message, ErrorCode.T003); + }); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Tasks.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Tasks.java index b3ce2886..94aecabe 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Tasks.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Tasks.java @@ -20,14 +20,20 @@ public List createRunningTasks() { .collect(Collectors.toList()); } + public boolean isEmpty() { + return tasks.isEmpty(); + } + public List getTaskIds() { return tasks.stream() .map(Task::getId) .collect(Collectors.toList()); } - public boolean isEmpty() { - return tasks.isEmpty(); + public RunningTasks getRunningTasks() { + return new RunningTasks(tasks.stream() + .map(Task::getRunningTask) + .collect(Collectors.toList())); } @Override diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Description.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Description.java index 612f36e4..cc8a0bec 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Description.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Description.java @@ -1,6 +1,7 @@ package com.woowacourse.gongcheck.core.domain.vo; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embeddable; @@ -25,7 +26,8 @@ public Description(final String value) { private void validateLength(final String value) { if (value.length() > DESCRIPTION_MAX_LENGTH) { - throw new BusinessException("μ„€λͺ…은 " + DESCRIPTION_MAX_LENGTH + "자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); + String message = String.format("μ„€λͺ…은 %d자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€. value = %s", DESCRIPTION_MAX_LENGTH, value); + throw new BusinessException(message, ErrorCode.D001); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Name.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Name.java index e0c1c8ef..75ee32d1 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Name.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Name.java @@ -1,6 +1,7 @@ package com.woowacourse.gongcheck.core.domain.vo; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embeddable; @@ -25,11 +26,12 @@ public Name(final String value) { private void checkNameLength(final String name) { if (name.isBlank()) { - throw new BusinessException("이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€."); + throw new BusinessException("이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€.", ErrorCode.N001); } if (name.length() > NAME_MAX_LENGTH) { - throw new BusinessException("이름은 " + NAME_MAX_LENGTH + "자 μ΄ν•˜μ—¬μ•Όν•©λ‹ˆλ‹€."); + String message = String.format("이름은 " + NAME_MAX_LENGTH + "자 μ΄ν•˜μ—¬μ•Όν•©λ‹ˆλ‹€. name = %s", name); + throw new BusinessException(message, ErrorCode.N002); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/HostController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/HostController.java index d92038a4..1df9cc0c 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/HostController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/HostController.java @@ -1,7 +1,7 @@ package com.woowacourse.gongcheck.core.presentation; import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; -import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.auth.presentation.HostOnly; import com.woowacourse.gongcheck.core.application.HostService; import com.woowacourse.gongcheck.core.application.response.EntranceCodeResponse; import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/ImageUploadController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/ImageUploadController.java index 26995871..02dcf90b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/ImageUploadController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/ImageUploadController.java @@ -1,6 +1,6 @@ package com.woowacourse.gongcheck.core.presentation; -import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.auth.presentation.HostOnly; import com.woowacourse.gongcheck.core.application.ImageUploader; import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; import lombok.extern.slf4j.Slf4j; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/JobController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/JobController.java index 29aa7c98..2997eb4a 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/JobController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/JobController.java @@ -1,7 +1,7 @@ package com.woowacourse.gongcheck.core.presentation; import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; -import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.auth.presentation.HostOnly; import com.woowacourse.gongcheck.core.application.JobService; import com.woowacourse.gongcheck.core.application.response.JobsResponse; import com.woowacourse.gongcheck.core.application.response.SlackUrlResponse; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SpaceController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SpaceController.java index 6256d392..1b5a6233 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SpaceController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SpaceController.java @@ -1,7 +1,7 @@ package com.woowacourse.gongcheck.core.presentation; import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; -import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.auth.presentation.HostOnly; import com.woowacourse.gongcheck.core.application.SpaceService; import com.woowacourse.gongcheck.core.application.response.SpaceResponse; import com.woowacourse.gongcheck.core.application.response.SpacesResponse; @@ -9,19 +9,15 @@ import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; import java.net.URI; import javax.validation.Valid; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RestController @RequestMapping("/api") diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SubmissionController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SubmissionController.java index 91ea12d6..bb574962 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SubmissionController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SubmissionController.java @@ -1,7 +1,7 @@ package com.woowacourse.gongcheck.core.presentation; import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; -import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.auth.presentation.HostOnly; import com.woowacourse.gongcheck.core.application.SubmissionService; import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/TaskController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/TaskController.java index a0b13ed2..073e299d 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/TaskController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/TaskController.java @@ -1,18 +1,19 @@ package com.woowacourse.gongcheck.core.presentation; import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; -import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.auth.presentation.HostOnly; import com.woowacourse.gongcheck.core.application.TaskService; import com.woowacourse.gongcheck.core.application.response.JobActiveResponse; -import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; import com.woowacourse.gongcheck.core.application.response.TasksResponse; import java.net.URI; +import org.springframework.http.MediaType; 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @RestController @RequestMapping("/api") @@ -38,11 +39,11 @@ public ResponseEntity isJobActive(@AuthenticationPrincipal fi return ResponseEntity.ok(response); } - @GetMapping("/jobs/{jobId}/runningTasks") - public ResponseEntity showRunningTasks(@AuthenticationPrincipal final Long hostId, - @PathVariable final Long jobId) { - RunningTasksResponse response = taskService.findRunningTasks(hostId, jobId); - return ResponseEntity.ok(response); + @GetMapping(value = "/jobs/{jobId}/runningTasks/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public ResponseEntity connectRunningTasks(@AuthenticationPrincipal final Long hostId, + @PathVariable Long jobId) { + SseEmitter emitter = taskService.connectRunningTasks(hostId, jobId); + return ResponseEntity.ok(emitter); } @PostMapping("/tasks/{taskId}/flip") @@ -59,4 +60,11 @@ public ResponseEntity showTasks(@AuthenticationPrincipal final Lo TasksResponse response = taskService.findTasks(hostId, jobId); return ResponseEntity.ok(response); } + + @PostMapping("/sections/{sectionId}/runningTask/allCheck") + public ResponseEntity checkRunningTasksInSection(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long sectionId) { + taskService.checkRunningTasksInSection(hostId, sectionId); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/filter/RequestContext.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/filter/RequestContext.java new file mode 100644 index 00000000..70e3218e --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/filter/RequestContext.java @@ -0,0 +1,18 @@ +package com.woowacourse.gongcheck.core.presentation.filter; + +import lombok.Getter; +import org.springframework.stereotype.Component; +import org.springframework.web.context.annotation.RequestScope; +import org.springframework.web.util.ContentCachingRequestWrapper; + +@Component +@RequestScope +@Getter +public class RequestContext { + + private ContentCachingRequestWrapper request; + + public void setRequest(final ContentCachingRequestWrapper request) { + this.request = request; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/filter/RequestServletWrappingFilter.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/filter/RequestServletWrappingFilter.java new file mode 100644 index 00000000..258e3d89 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/filter/RequestServletWrappingFilter.java @@ -0,0 +1,30 @@ +package com.woowacourse.gongcheck.core.presentation.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +@Component +public class RequestServletWrappingFilter extends OncePerRequestFilter { + + private final RequestContext requestContext; + + public RequestServletWrappingFilter(final RequestContext requestContext) { + this.requestContext = requestContext; + } + + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) + throws ServletException, IOException { + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); + requestContext.setRequest(requestWrapper); + + filterChain.doFilter(requestWrapper, response); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/JobCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/JobCreateRequest.java index ca65589f..c6c6751f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/JobCreateRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/JobCreateRequest.java @@ -8,7 +8,7 @@ @Getter public class JobCreateRequest { - @NotNull(message = "이름은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "JobCreateRequest의 name은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String name; @Valid diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SectionCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SectionCreateRequest.java index 43f3506a..0986fc19 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SectionCreateRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SectionCreateRequest.java @@ -8,7 +8,7 @@ @Getter public class SectionCreateRequest { - @NotNull(message = "이름은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "SectionCreateRequest의 name은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String name; private String description; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SlackUrlChangeRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SlackUrlChangeRequest.java index 6a35144d..61ff2b36 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SlackUrlChangeRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SlackUrlChangeRequest.java @@ -6,7 +6,7 @@ @Getter public class SlackUrlChangeRequest { - @NotNull(message = "Slack URL은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "SlackUrlChangeRequest의 SlackURL은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String slackUrl; private SlackUrlChangeRequest() { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceChangeRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceChangeRequest.java index 14fa336c..5a2114a1 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceChangeRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceChangeRequest.java @@ -6,7 +6,7 @@ @Getter public class SpaceChangeRequest { - @NotNull(message = "이름은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "SpaceChangeRequest의 name은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String name; private String imageUrl; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceCreateRequest.java index 4595b361..0234606b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceCreateRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceCreateRequest.java @@ -6,7 +6,7 @@ @Getter public class SpaceCreateRequest { - @NotNull(message = "이름은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "SpaceCreateRequest name은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String name; private String imageUrl; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpacePasswordChangeRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpacePasswordChangeRequest.java index 5798f035..2fede96f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpacePasswordChangeRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpacePasswordChangeRequest.java @@ -6,7 +6,7 @@ @Getter public class SpacePasswordChangeRequest { - @NotNull(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” null일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "SpacePasswordChangeRequest의 passwordλŠ” null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String password; private SpacePasswordChangeRequest() { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SubmissionRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SubmissionRequest.java index 3656763e..36a0007f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SubmissionRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SubmissionRequest.java @@ -6,7 +6,7 @@ @Getter public class SubmissionRequest { - @NotNull(message = "제좜자 이름은 null 일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "SubmissionRequest의 author은 null 일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String author; private SubmissionRequest() { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/TaskCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/TaskCreateRequest.java index bc112e42..f917b842 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/TaskCreateRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/TaskCreateRequest.java @@ -6,7 +6,7 @@ @Getter public class TaskCreateRequest { - @NotNull(message = "이름은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") + @NotNull(message = "TaskCreateRequest의 name은 null일 수 μ—†μŠ΅λ‹ˆλ‹€.") private String name; private String description; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/BusinessException.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/BusinessException.java index dd967975..bc7af4c2 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/BusinessException.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/BusinessException.java @@ -1,8 +1,8 @@ package com.woowacourse.gongcheck.exception; -public class BusinessException extends RuntimeException { +public class BusinessException extends CustomException { - public BusinessException(final String message) { - super(message); + public BusinessException(final String message, final ErrorCode errorCode) { + super(message, errorCode); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java index 0f21be8b..32987e8a 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java @@ -1,5 +1,8 @@ package com.woowacourse.gongcheck.exception; +import static com.woowacourse.gongcheck.exception.ExceptionMessageGenerator.generate; + +import com.woowacourse.gongcheck.core.presentation.filter.RequestContext; import java.io.PrintWriter; import java.io.StringWriter; import lombok.extern.slf4j.Slf4j; @@ -13,39 +16,45 @@ @Slf4j public class ControllerAdvice { + private final RequestContext requestContext; + + public ControllerAdvice(final RequestContext requestContext) { + this.requestContext = requestContext; + } + @ExceptionHandler(UnauthorizedException.class) - public ResponseEntity handleUnauthorized(final RuntimeException e) { - log.info(e.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.from(e)); + public ResponseEntity handleUnauthorized(final CustomException e) { + log.info(generate(requestContext.getRequest(), e)); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.from(e.getErrorCode())); } @ExceptionHandler(NotFoundException.class) - public ResponseEntity handleNotFound(final RuntimeException e) { - log.warn(e.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e)); + public ResponseEntity handleNotFound(final CustomException e) { + log.warn(generate(requestContext.getRequest(), e)); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e.getErrorCode())); } @ExceptionHandler(BusinessException.class) - public ResponseEntity handleBusiness(final RuntimeException e) { - log.info(e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.from(e)); + public ResponseEntity handleBusiness(final CustomException e) { + log.info(generate(requestContext.getRequest(), e)); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.from(e.getErrorCode())); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException e) { - log.warn(e.getMessage()); - return ResponseEntity.badRequest().body(ErrorResponse.from(e)); + log.warn(generate(requestContext.getRequest(), e)); + return ResponseEntity.badRequest().body(ErrorResponse.from(ErrorCode.V001)); } @ExceptionHandler(InfrastructureException.class) - public ResponseEntity handleInfrastructureException(final InfrastructureException e) { - return ResponseEntity.internalServerError().body(ErrorResponse.from(e)); + public ResponseEntity handleInfrastructureException(final CustomException e) { + return ResponseEntity.internalServerError().body(ErrorResponse.from(e.getErrorCode())); } @ExceptionHandler(Exception.class) public ResponseEntity handleInternalServerError(final Exception e) { log.error("Stack Trace : {}", extractStackTrace(e)); - return ResponseEntity.internalServerError().body(ErrorResponse.from("μ„œλ²„ μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.")); + return ResponseEntity.internalServerError().body(ErrorResponse.from(ErrorCode.E001)); } private String extractStackTrace(final Exception e) { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/CustomException.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/CustomException.java new file mode 100644 index 00000000..83a4c033 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/CustomException.java @@ -0,0 +1,15 @@ +package com.woowacourse.gongcheck.exception; + +public class CustomException extends RuntimeException { + + private final ErrorCode errorCode; + + public CustomException(final String message, final ErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/ErrorCode.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/ErrorCode.java new file mode 100644 index 00000000..39f213cc --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/ErrorCode.java @@ -0,0 +1,82 @@ +package com.woowacourse.gongcheck.exception; + +import lombok.Getter; + +@Getter +public enum ErrorCode { + + // Host + H001("Space λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯받은 λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠλŠ” 경우"), + H002("entranceCode κ°€ μœ νš¨ν•˜μ§€ μ•Šμ€ μ½”λ“œμΈ 경우"), + H003("entranceCode μ—μ„œ hostId μΆ”μΆœ μ‹œ μœ νš¨ν•˜μ§€ μ•Šμ€ hostId인 경우"), + H004("Host 쑰회 μ‹œ, μž…λ ₯ 받은 id의 Hostκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + H005("Host 쑰회 μ‹œ, μž…λ ₯ 받은 githubId의 Hostκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + + // Job + J001("Job 쑰회 μ‹œ, μž…λ ₯ 받은 host, id에 ν•΄λ‹Ήν•˜λŠ” Job 이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + J002("Job 쑰회 μ‹œ, μž…λ ₯ 받은 id에 ν•΄λ‹Ήν•˜λŠ” Job 이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + + // SPACE + SP01("곡간 생성 μ‹œ, 곡간 이름이 이미 μ‘΄μž¬ν•˜λŠ” 경우"), + SP02("곡간 μˆ˜μ • μ‹œ, 곡간 이름이 이미 μ‘΄μž¬ν•˜λŠ” 경우"), + SP03("λΉ„λ°€λ²ˆν˜Έκ°€ 4자리둜 이루어지지 μ•Šμ€ 경우"), + SP04("곡간 쑰회 μ‹œ, μž…λ ₯ 받은 host, id에 ν•΄λ‹Ήν•˜λŠ” 곡간이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + + // AUTHORITY + A001("호슀트 κΆŒν•œμ΄ μ—†λŠ” ν† ν°μœΌλ‘œ 호슀트용 μ ‘κ·Ό 경둜둜 μ ‘κ·Όν•  경우"), + A002("Guest 의 토큰이 만료된 경우"), + A003("헀더에 토큰값이 μ •μƒμ μœΌλ‘œ μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우"), + + // TASK + T001("RunningTask κ°€ 이미 μ‘΄μž¬ν•˜λŠ”λ° 또 μƒμ„±ν•˜λ €λŠ” 경우"), + T002("RunningTask 생성할 Task κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + T003("Task 쑰회 μ‹œ, μž…λ ₯ 받은 host, id에 ν•΄λ‹Ήν•˜λŠ” Task κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + + // RUNNING_TASK + R001("RunningTask 쑰회 μ‹œ, RunningTask κ°€ 아직 μƒμ„±λ˜μ§€ μ•Šμ€ 경우"), + R002("RunningTask 체크 μ‹œ, RunningTask κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + R003("μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ λ‹€ μ²΄ν¬λ˜μ§€ μ•Šμ•˜μ§€λ§Œ, μ œμΆœν•œ 경우"), + + // SUBMISSION, + S001("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” RunningTask κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ° μ œμΆœν•œ 경우"), + S002("제좜 μ‹œ 제좜자 이름이 곡백인 경우"), + S003("제좜 μ‹œ 제좜자 이름이 10κΈ€μž 초과인 경우"), + + // SECTION + SE01("Sectoin 쑰회 μ‹œ, Section이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우"), + + // IMAGE + IM01("이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일이 null 인 경우"), + IM02("이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일이 λΉˆκ°’μΈ 경우"), + IM03("이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일 이름이 λΉˆκ°’μΈ 경우"), + IM04("이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일 ν™•μž₯μžκ°€ 잘λͺ»λœ 경우"), + IM05("이미지 μ—…λ‘œλ“œ μ‹œ, 파일이 잘λͺ»λœ 경우"), + + // DESCRIPTION + D001("μ„€λͺ… 길이가 128자 초과인 경우"), + + // NAME + N001("이름이 곡백인 경우"), + N002("이름이 10κΈ€μž 초과인 경우"), + + // INFRASTRUCTURE + I001("μŠ¬λž™ λ©”μ‹œμ§€ 전솑에 μ‹€νŒ¨ν•œ 경우"), + I002("ν•΄μ‹±μš© μ‹œν¬λ¦Ών‚€κ°€ 32자 미만인 경우"), + I003("ν•΄μ‹± 인코딩 μ‹€νŒ¨ν•  경우"), + I004("ν•΄μ‹± λ””μ½”λ”© μ‹€νŒ¨ν•œ 경우"), + I005("토큰이 값이 μ˜¬λ°”λ₯΄μ§€ μ•Šμ•„ μΆ”μΆœν•  수 μ—†λŠ” 경우"), + I006("κΉƒν—ˆλΈŒ μ—‘μ„ΈμŠ€ 토큰이 null 인 경우"), + I007("κΉƒν—ˆλΈŒ μ‚¬μš©μž ν”„λ‘œν•„μ„ κ°€μ Έμ˜¬ 수 μ—†λŠ” 경우"), + + // ERROR + E001("μ˜ˆμƒμΉ˜ λͺ»ν•œ μ˜ˆμ™Έκ°€ λ°œμƒν•œ 경우"), + + // DTO Valid Error + V001("μš”μ²­μ— λŒ€ν•œ DTO ν•„λ“œκ°’ 일뢀가 null 인 경우"); + + private final String description; + + ErrorCode(final String description) { + this.description = description; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/ErrorResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/ErrorResponse.java index b407e260..21eeb48f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/ErrorResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/ErrorResponse.java @@ -6,26 +6,16 @@ @Getter public class ErrorResponse { - private String message; + private String errorCode; private ErrorResponse() { } - private ErrorResponse(final String message) { - this.message = message; + private ErrorResponse(final String errorCode) { + this.errorCode = errorCode; } - public static ErrorResponse from(final RuntimeException e) { - return new ErrorResponse(e.getMessage()); - } - - public static ErrorResponse from(final MethodArgumentNotValidException e) { - return new ErrorResponse(e.getAllErrors() - .get(0) - .getDefaultMessage()); - } - - public static ErrorResponse from(final String message) { - return new ErrorResponse(message); + public static ErrorResponse from(final ErrorCode errorCode) { + return new ErrorResponse(errorCode.name()); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/ExceptionMessageGenerator.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/ExceptionMessageGenerator.java new file mode 100644 index 00000000..3bc70712 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/ExceptionMessageGenerator.java @@ -0,0 +1,75 @@ +package com.woowacourse.gongcheck.exception; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.web.util.ContentCachingRequestWrapper; + +public class ExceptionMessageGenerator { + + private static final String MESSAGE_FORMAT = "Request Information\n%s %s\n%s\nParams: %s\nBody : %s\nException Message : %s"; + private static final String GENERATE_MESSAGE_EXCEPTION = "μ˜ˆμ™Έ 메세지 생성 쀑 μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."; + private static final String KEY_VALUE_DELIMITER = ":"; + + private ExceptionMessageGenerator() { + } + + public static String generate(final ContentCachingRequestWrapper request, final Exception exception) { + try { + String method = request.getMethod(); + String requestURI = request.getRequestURI(); + String headers = getHeaders(request); + String params = getParams(request); + String body = getBody(request); + + return String.format(MESSAGE_FORMAT, method, requestURI, headers, params, body, exception.getMessage()); + } catch (Exception e) { + return GENERATE_MESSAGE_EXCEPTION; + } + } + + private static String getHeaders(final ContentCachingRequestWrapper request) { + Enumeration headerNames = request.getHeaderNames(); + return extractHeaders(request, headerNames); + } + + private static String extractHeaders(final ContentCachingRequestWrapper request, + final Enumeration parameterNames) { + Map headers = new HashMap<>(); + + while (parameterNames.hasMoreElements()) { + String headerName = parameterNames.nextElement(); + headers.put(headerName, request.getHeader(headerName)); + } + + return convertMapToString(headers); + } + + private static String getParams(final ContentCachingRequestWrapper request) { + Enumeration parameterNames = request.getParameterNames(); + return extractParameters(request, parameterNames); + } + + private static String extractParameters(final ContentCachingRequestWrapper request, + final Enumeration parameterNames) { + Map parameters = new HashMap<>(); + + while (parameterNames.hasMoreElements()) { + String parameterName = parameterNames.nextElement(); + parameters.put(parameterName, request.getParameter(parameterName)); + } + + return convertMapToString(parameters); + } + + private static String getBody(final ContentCachingRequestWrapper request) { + return new String(request.getContentAsByteArray()); + } + + private static String convertMapToString(final Map requestInfo) { + return requestInfo.entrySet().stream() + .map(i -> i.getKey() + KEY_VALUE_DELIMITER+ i.getValue()) + .collect(Collectors.joining("\n")); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/InfrastructureException.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/InfrastructureException.java index db7b7a90..ab4740fb 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/InfrastructureException.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/InfrastructureException.java @@ -1,8 +1,8 @@ package com.woowacourse.gongcheck.exception; -public class InfrastructureException extends RuntimeException { +public class InfrastructureException extends CustomException { - public InfrastructureException(final String message) { - super(message); + public InfrastructureException(final String message, final ErrorCode errorCode) { + super(message, errorCode); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/NotFoundException.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/NotFoundException.java index a905bfc8..3d766e48 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/NotFoundException.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/NotFoundException.java @@ -1,8 +1,8 @@ package com.woowacourse.gongcheck.exception; -public class NotFoundException extends RuntimeException { +public class NotFoundException extends CustomException { - public NotFoundException(final String message) { - super(message); + public NotFoundException(final String message, final ErrorCode errorCode) { + super(message, errorCode); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/UnauthorizedException.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/UnauthorizedException.java index 09e4bff9..19f55462 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/UnauthorizedException.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/UnauthorizedException.java @@ -1,8 +1,8 @@ package com.woowacourse.gongcheck.exception; -public class UnauthorizedException extends RuntimeException { +public class UnauthorizedException extends CustomException { - public UnauthorizedException(final String message) { - super(message); + public UnauthorizedException(final String message, final ErrorCode errorCode) { + super(message, errorCode); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/hash/AES256.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/hash/AES256.java index 06ef8587..485ec186 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/hash/AES256.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/hash/AES256.java @@ -1,6 +1,8 @@ package com.woowacourse.gongcheck.infrastructure.hash; import com.woowacourse.gongcheck.auth.application.HashTranslator; +import com.woowacourse.gongcheck.exception.ErrorCode; +import com.woowacourse.gongcheck.exception.InfrastructureException; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -32,8 +34,8 @@ public class AES256 implements HashTranslator { public AES256(@Value("${security.hash.secret-key}") final String secretKey) { if (secretKey.length() < SECRET_KEY_SIZE) { - throw new IllegalArgumentException( - "Minimum key size is " + SECRET_KEY_SIZE + ", current key size :" + secretKey.length()); + String message = "Minimum key size is " + SECRET_KEY_SIZE + ". current key size = " + secretKey.length(); + throw new InfrastructureException(message, ErrorCode.I002); } secretKeySpec = new SecretKeySpec(secretKey.substring(0, SECRET_KEY_SIZE).getBytes(), ALGORITHM); ivParamSpec = new IvParameterSpec(secretKey.substring(0, INITIALIZATION_VECTOR_SIZE).getBytes()); @@ -48,9 +50,8 @@ public String encode(String input) { return Base64.encodeBase64URLSafeString(encrypted); } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | NullPointerException e) { - // TODO: 2022/08/02 Infrastructure μ˜ˆμ™Έλ‘œ λ³€κ²½ ν•„μš” - log.error("encoding error, input = {}, message = {}", input, e.getMessage()); - throw new IllegalArgumentException(e); + log.error("encoding error. input = {}, message = {}", input, e.getMessage()); + throw new InfrastructureException(e.getMessage(), ErrorCode.I003); } } @@ -64,9 +65,8 @@ public String decode(String input) { return new String(decrypted, StandardCharsets.UTF_8); } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | NullPointerException e) { - // TODO: 2022/08/02 Infrastructure μ˜ˆμ™Έλ‘œ λ³€κ²½ ν•„μš” - log.error("decoding error, input = {}, message = {}", input, e.getMessage()); - throw new IllegalArgumentException(e); + log.error("decoding error. input = {}, message = {}", input, e.getMessage()); + throw new InfrastructureException(e.getMessage(), ErrorCode.I004); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProvider.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProvider.java index 05a2efa0..14c42f56 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProvider.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProvider.java @@ -2,6 +2,7 @@ import com.woowacourse.gongcheck.auth.application.JwtTokenProvider; import com.woowacourse.gongcheck.auth.domain.Authority; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.InfrastructureException; import com.woowacourse.gongcheck.exception.UnauthorizedException; import io.jsonwebtoken.Claims; @@ -13,10 +14,12 @@ import io.jsonwebtoken.security.Keys; import java.security.Key; import java.util.Date; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component +@Slf4j public class JjwtTokenProvider implements JwtTokenProvider { private static final String AUTHORITY = "authority"; @@ -63,9 +66,11 @@ private Claims extractBody(final String token) { .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { - throw new UnauthorizedException("만료된 ν† ν°μž…λ‹ˆλ‹€."); + String message = String.format("만료된 ν† ν°μž…λ‹ˆλ‹€. token = %s", token); + throw new UnauthorizedException(message, ErrorCode.A002); } catch (JwtException e) { - throw new InfrastructureException("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."); + log.error("jwt token error. token = {}", token); + throw new InfrastructureException("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€.", ErrorCode.I005); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/alert/SlackService.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/notification/SlackService.java similarity index 68% rename from backend/src/main/java/com/woowacourse/gongcheck/infrastructure/alert/SlackService.java rename to backend/src/main/java/com/woowacourse/gongcheck/infrastructure/notification/SlackService.java index a8a28eb8..4214c7ab 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/alert/SlackService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/notification/SlackService.java @@ -1,10 +1,11 @@ -package com.woowacourse.gongcheck.infrastructure.alert; +package com.woowacourse.gongcheck.infrastructure.notification; import com.slack.api.Slack; import com.slack.api.webhook.Payload; -import com.woowacourse.gongcheck.core.application.AlertService; +import com.woowacourse.gongcheck.core.application.NotificationService; import com.woowacourse.gongcheck.core.application.response.Attachments; import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.InfrastructureException; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -12,7 +13,7 @@ @Service @Slf4j -public class SlackService implements AlertService { +public class SlackService implements NotificationService { @Async @Override @@ -23,8 +24,9 @@ public void sendMessage(final SubmissionCreatedResponse submissionCreatedRespons .build(); slack.send(submissionCreatedResponse.getSlackUrl(), payload); } catch (Exception e) { - log.error(e.getMessage()); - throw new InfrastructureException("λ©”μ‹œμ§€ 전솑에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + log.error("slack message error. slackUrl = {}, message = {}", submissionCreatedResponse.getSlackUrl(), + e.getMessage()); + throw new InfrastructureException("λ©”μ‹œμ§€ 전솑에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.", ErrorCode.I001); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClient.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClient.java index b4c8797b..f36ac21b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClient.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClient.java @@ -1,10 +1,11 @@ package com.woowacourse.gongcheck.infrastructure.oauth; -import com.woowacourse.gongcheck.auth.application.response.GithubAccessTokenResponse; -import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; -import com.woowacourse.gongcheck.auth.presentation.request.GithubAccessTokenRequest; +import com.woowacourse.gongcheck.auth.application.OAuthClient; +import com.woowacourse.gongcheck.auth.application.response.OAuthAccessTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.SocialProfileResponse; +import com.woowacourse.gongcheck.auth.presentation.request.OAuthAccessTokenRequest; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.InfrastructureException; -import com.woowacourse.gongcheck.exception.UnauthorizedException; import java.util.Objects; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -18,7 +19,7 @@ @Component @Slf4j -public class GithubOauthClient { +public class GithubOauthClient implements OAuthClient { private static final String BEARER_TYPE = "Bearer "; @@ -40,31 +41,34 @@ public GithubOauthClient(@Value("${github.client.id}") final String clientId, this.restTemplate = restTemplate; } - public GithubProfileResponse requestGithubProfileByCode(final String code) { + @Override + public SocialProfileResponse requestSocialProfileByCode(final String code) { return requestGithubProfile(requestAccessToken(code)); } private String requestAccessToken(final String code) { - GithubAccessTokenRequest githubAccessTokenRequest = new GithubAccessTokenRequest(code, clientId, clientSecret); + OAuthAccessTokenRequest oAuthAccessTokenRequest = new OAuthAccessTokenRequest(code, clientId, clientSecret); HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); - HttpEntity httpEntity = new HttpEntity<>(githubAccessTokenRequest, headers); + HttpEntity httpEntity = new HttpEntity<>(oAuthAccessTokenRequest, headers); - GithubAccessTokenResponse githubAccessTokenResponse = exchangeRestTemplateBody(tokenUrl, HttpMethod.POST, - httpEntity, GithubAccessTokenResponse.class); - if (Objects.isNull(githubAccessTokenResponse)) { - throw new InfrastructureException("잘λͺ»λœ μš”μ²­μž…λ‹ˆλ‹€."); + OAuthAccessTokenResponse oAuthAccessTokenResponse = exchangeRestTemplateBody(tokenUrl, HttpMethod.POST, + httpEntity, OAuthAccessTokenResponse.class); + if (Objects.isNull(oAuthAccessTokenResponse)) { + log.error("github oauth error. clientId = {}, clientSecret = {}, tokenUrl = {}, profileUrl = {}", clientId, + clientSecret, tokenUrl, profileUrl); + throw new InfrastructureException("잘λͺ»λœ μš”μ²­μž…λ‹ˆλ‹€.", ErrorCode.I006); } - return githubAccessTokenResponse.getAccessToken(); + return oAuthAccessTokenResponse.getAccessToken(); } - private GithubProfileResponse requestGithubProfile(final String accessToken) { + private SocialProfileResponse requestGithubProfile(final String accessToken) { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, BEARER_TYPE + accessToken); HttpEntity httpEntity = new HttpEntity<>(headers); - return exchangeRestTemplateBody(profileUrl, HttpMethod.GET, httpEntity, GithubProfileResponse.class); + return exchangeRestTemplateBody(profileUrl, HttpMethod.GET, httpEntity, SocialProfileResponse.class); } private T exchangeRestTemplateBody(final String url, final HttpMethod httpMethod, @@ -74,8 +78,10 @@ private T exchangeRestTemplateBody(final String url, final HttpMethod httpMe .exchange(url, httpMethod, httpEntity, exchangeType) .getBody(); } catch (HttpClientErrorException | NullPointerException e) { - log.error(e.getMessage()); - throw new InfrastructureException("ν•΄λ‹Ή μ‚¬μš©μžμ˜ ν”„λ‘œν•„μ„ μš”μ²­ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); + log.error( + "github oauth error. clientId = {}, clientSecret = {}, tokenUrl = {}, profileUrl = {}, message = {}", + clientId, clientSecret, tokenUrl, profileUrl, e.getMessage()); + throw new InfrastructureException("ν•΄λ‹Ή μ‚¬μš©μžμ˜ ν”„λ‘œν•„μ„ μš”μ²­ν•  수 μ—†μŠ΅λ‹ˆλ‹€.", ErrorCode.I007); } } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/ApplicationTest.java b/backend/src/test/java/com/woowacourse/gongcheck/ApplicationTest.java new file mode 100644 index 00000000..a46d3dfb --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/ApplicationTest.java @@ -0,0 +1,15 @@ +package com.woowacourse.gongcheck; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SpringBootTest +@ExtendWith({DatabaseCleanerExtension.class}) +public @interface ApplicationTest { +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/DatabaseCleanerExtension.java b/backend/src/test/java/com/woowacourse/gongcheck/DatabaseCleanerExtension.java new file mode 100644 index 00000000..412f6dee --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/DatabaseCleanerExtension.java @@ -0,0 +1,20 @@ +package com.woowacourse.gongcheck; + +import com.woowacourse.gongcheck.acceptance.DatabaseInitializer; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +public class DatabaseCleanerExtension implements AfterEachCallback { + + @Override + public void afterEach(final ExtensionContext context) { + executeDatabaseInitialize(context); + } + + private void executeDatabaseInitialize(final ExtensionContext context) { + DatabaseInitializer databaseInitializer = (DatabaseInitializer) SpringExtension + .getApplicationContext(context).getBean("databaseInitializer"); + databaseInitializer.truncateTables(); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/SupportRepository.java b/backend/src/test/java/com/woowacourse/gongcheck/SupportRepository.java new file mode 100644 index 00000000..e4ae69d2 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/SupportRepository.java @@ -0,0 +1,57 @@ +package com.woowacourse.gongcheck; + +import java.util.List; +import java.util.Optional; +import javax.persistence.EntityManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Transactional +public class SupportRepository { + + @Autowired + private EntityManager entityManager; + + public T save(final T entity) { + entityManager.persist(entity); + entityManager.flush(); + entityManager.clear(); + return entity; + } + + public List saveAll(final List entities) { + for (T entity : entities) { + save(entity); + } + entityManager.flush(); + entityManager.clear(); + return entities; + } + + public Optional findById(final Class entityClass, final Object id) { + entityManager.clear(); + return Optional.ofNullable(entityManager.find(entityClass, id)); + } + + public T getById(final Class entityClass, final Object id) { + entityManager.clear(); + return Optional.ofNullable(entityManager.find(entityClass, id)) + .orElseThrow(EntityNotFoundExcpetion::new); + } + + public List findAll(final Class entityClass) { + entityManager.clear(); + String className = entityClass.getSimpleName(); + return entityManager.createQuery(String.format("SELECT entity FROM %s entity", className)) + .getResultList(); + } + + static class EntityNotFoundExcpetion extends RuntimeException { + + public EntityNotFoundExcpetion() { + super("Entityλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java index 2560676b..34099435 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java @@ -6,8 +6,9 @@ import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; import com.woowacourse.gongcheck.auth.application.response.TokenResponse; import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; -import com.woowacourse.gongcheck.core.application.AlertService; import com.woowacourse.gongcheck.core.application.ImageUploader; +import com.woowacourse.gongcheck.core.application.NotificationService; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskSseEmitterContainer; import io.restassured.RestAssured; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -22,7 +23,10 @@ class AcceptanceTest { @MockBean - private AlertService alertService; + private NotificationService notificationService; + + @MockBean + protected RunningTaskSseEmitterContainer runningTaskSseEmitterContainer; @MockBean protected ImageUploader imageUploader; @@ -48,6 +52,7 @@ void setUp() { void clean() { databaseInitializer.truncateTables(); } + public String 토큰을_μš”μ²­ν•œλ‹€(final GuestEnterRequest guestEnterRequest) { String entranceCode = entranceCodeProvider.createEntranceCode(1L); return RestAssured diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java index 943dd07a..c6823b37 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java @@ -7,8 +7,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.woowacourse.gongcheck.auth.application.HostAuthService; -import com.woowacourse.gongcheck.auth.application.response.GithubAccessTokenResponse; -import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.application.response.OAuthAccessTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.SocialProfileResponse; import com.woowacourse.gongcheck.auth.application.response.TokenResponse; import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; import org.springframework.beans.factory.annotation.Autowired; @@ -40,15 +40,15 @@ public ResponseEntity login() throws JsonProcessingException { .andExpect(method(HttpMethod.POST)) .andRespond(withStatus(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(new GithubAccessTokenResponse("token")))); + .body(objectMapper.writeValueAsString(new OAuthAccessTokenResponse("token")))); - GithubProfileResponse githubProfileResponse = new GithubProfileResponse("nickname", "loginName", + SocialProfileResponse socialProfileResponse = new SocialProfileResponse("nickname", "loginName", "2", "test.com"); mockRestServiceServer.expect(requestTo("https://api.github.com/user")) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(githubProfileResponse))); + .body(objectMapper.writeValueAsString(socialProfileResponse))); TokenResponse result = hostAuthService.createToken(new TokenRequest("code")); return ResponseEntity.ok(result); @@ -61,15 +61,15 @@ public ResponseEntity signup() throws JsonProcessingException { .andExpect(method(HttpMethod.POST)) .andRespond(withStatus(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(new GithubAccessTokenResponse("token")))); + .body(objectMapper.writeValueAsString(new OAuthAccessTokenResponse("token")))); - GithubProfileResponse githubProfileResponse = new GithubProfileResponse("nickname", "loginName", + SocialProfileResponse socialProfileResponse = new SocialProfileResponse("nickname", "loginName", "3", "test.com"); mockRestServiceServer.expect(requestTo("https://api.github.com/user")) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(githubProfileResponse))); + .body(objectMapper.writeValueAsString(socialProfileResponse))); TokenResponse result = hostAuthService.createToken(new TokenRequest("code")); return ResponseEntity.ok(result); diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java index 157bab1e..62289110 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; -import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; import com.woowacourse.gongcheck.core.application.response.TasksResponse; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; @@ -13,18 +12,13 @@ import org.springframework.http.HttpStatus; class TaskAcceptanceTest extends AcceptanceTest { - + @Test void RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_μš”μ²­ν•œλ‹€(guestEnterRequest); - ExtractableResponse response = RestAssured - .given().log().all() - .auth().oauth2(token) - .when().post("/api/jobs/1/runningTasks/new") - .then().log().all() - .extract(); + ExtractableResponse response = RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(token); assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); } @@ -33,60 +27,34 @@ class TaskAcceptanceTest extends AcceptanceTest { void 이미_RunningTaskκ°€_μ‘΄μž¬ν•˜λŠ”_경우_생성에_μ‹€νŒ¨ν•œλ‹€() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_μš”μ²­ν•œλ‹€(guestEnterRequest); + RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(token); - RestAssured - .given().log().all() - .auth().oauth2(token) - .when().post("/api/jobs/1/runningTasks/new") - .then().log().all() - .extract(); - - ExtractableResponse response = RestAssured - .given().log().all() - .auth().oauth2(token) - .when().post("/api/jobs/1/runningTasks/new") - .then().log().all() - .extract(); + ExtractableResponse response = TaskAcceptanceTest.this.RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(token); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @Test - void RunningTaskλ₯Ό_μ‘°νšŒν•œλ‹€() { + void RunningTask에_λŒ€ν•œ_Sse_connection을_μ—°κ²°ν•œλ‹€() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_μš”μ²­ν•œλ‹€(guestEnterRequest); - RestAssured - .given().log().all() - .auth().oauth2(token) - .when().post("/api/jobs/1/runningTasks/new") - .then().log().all() - .extract(); + RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(token); ExtractableResponse response = RestAssured .given().log().all() .auth().oauth2(token) - .when().get("/api/jobs/1/runningTasks") + .when().get("/api/jobs/1/runningTasks/connect") .then().log().all() .extract(); - RunningTasksResponse runningTasksResponse = response.as(RunningTasksResponse.class); - assertAll( - () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), - () -> assertThat(runningTasksResponse.getSections()).hasSize(2) - ); + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); } @Test void RunningTaskλ₯Ό_μƒμ„±ν•˜κ³ _쑴재_μ—¬λΆ€λ₯Ό_ν™•μΈν•œλ‹€() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_μš”μ²­ν•œλ‹€(guestEnterRequest); - - RestAssured - .given().log().all() - .auth().oauth2(token) - .when().post("/api/jobs/1/runningTasks/new") - .then().log().all() - .extract(); + RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(token); ExtractableResponse response = RestAssured .given().log().all() @@ -121,7 +89,7 @@ class TaskAcceptanceTest extends AcceptanceTest { ExtractableResponse response = RestAssured .given().log().all() .auth().oauth2(token) - .when().get("/api/jobs/1/runningTasks") + .when().get("/api/jobs/1/runningTasks/connect") .then().log().all() .extract(); @@ -132,12 +100,7 @@ class TaskAcceptanceTest extends AcceptanceTest { void RunningTask의_μ²΄ν¬μƒνƒœλ₯Ό_λ³€κ²½ν•œλ‹€() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_μš”μ²­ν•œλ‹€(guestEnterRequest); - RestAssured - .given().log().all() - .auth().oauth2(token) - .when().post("/api/jobs/1/runningTasks/new") - .then().log().all() - .extract(); + RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(token); ExtractableResponse response = RestAssured .given().log().all() @@ -181,4 +144,44 @@ class TaskAcceptanceTest extends AcceptanceTest { () -> assertThat(taskResponse.getSections()).hasSize(2) ); } + + @Test + void ν•΄λ‹Ή_Section의_RunningTaskλ₯Ό_λͺ¨λ‘_μ²΄ν¬ν•œλ‹€() { + GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); + String token = 토큰을_μš”μ²­ν•œλ‹€(guestEnterRequest); + RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(token); + + ExtractableResponse response = RestAssured + .given().log().all() + .auth().oauth2(token) + .when().post("/api/sections/1/runningTask/allCheck") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + void μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_RunningTaskλ₯Ό_λͺ¨λ‘_μ²΄ν¬ν•˜λ €λŠ”_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); + String token = 토큰을_μš”μ²­ν•œλ‹€(guestEnterRequest); + + ExtractableResponse response = RestAssured + .given().log().all() + .auth().oauth2(token) + .when().post("/api/sections/1/runningTask/allCheck") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + private ExtractableResponse RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€(final String token) { + return RestAssured + .given().log().all() + .auth().oauth2(token) + .when().post("/api/jobs/1/runningTasks/new") + .then().log().all() + .extract(); + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProviderTest.java b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProviderTest.java index 5e995f0d..dc8eccfd 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProviderTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProviderTest.java @@ -37,7 +37,7 @@ class μž…λ ₯받은_idκ°€_μ–‘μˆ˜κ°€_μ•„λ‹Œ_경우 { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€(final Long hostId) { assertThatThrownBy(() -> entranceCodeProvider.createEntranceCode(hostId)) .isInstanceOf(BusinessException.class) - .hasMessage("μœ νš¨ν•˜μ§€ μ•Šμ€ idμž…λ‹ˆλ‹€."); + .hasMessageContaining("μœ νš¨ν•˜μ§€ μ•Šμ€ idμž…λ‹ˆλ‹€."); } } @@ -71,7 +71,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> entranceCodeProvider.parseId(invalidEntranceCode)) .isInstanceOf(BusinessException.class) - .hasMessage("μœ νš¨ν•˜μ§€ μ•Šμ€ μž…μž₯μ½”λ“œμž…λ‹ˆλ‹€."); + .hasMessageContaining("μœ νš¨ν•˜μ§€ μ•Šμ€ μž…μž₯μ½”λ“œμž…λ‹ˆλ‹€."); } } @@ -89,7 +89,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> entranceCodeProvider.parseId(invalidEntranceCode)) .isInstanceOf(BusinessException.class) - .hasMessage("μœ νš¨ν•˜μ§€ μ•Šμ€ μž…μž₯μ½”λ“œμž…λ‹ˆλ‹€."); + .hasMessageContaining("μœ νš¨ν•˜μ§€ μ•Šμ€ μž…μž₯μ½”λ“œμž…λ‹ˆλ‹€."); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/GuestAuthServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/GuestAuthServiceTest.java index e0502b8f..7f7b30f9 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/GuestAuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/GuestAuthServiceTest.java @@ -9,7 +9,6 @@ import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.exception.UnauthorizedException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; @@ -73,7 +72,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> guestAuthService.createToken(NON_EXIST_ID, guestEnterRequest)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -96,7 +95,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> guestAuthService.createToken(hostId, errorGuestEnterRequest)) .isInstanceOf(BusinessException.class) - .hasMessage("곡간 λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯ν•˜μ‹  λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("곡간 λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯ν•˜μ‹  λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); } } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/HostAuthServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/HostAuthServiceTest.java index 8c46bf42..1d4630e5 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/HostAuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/HostAuthServiceTest.java @@ -8,7 +8,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.application.response.SocialProfileResponse; import com.woowacourse.gongcheck.auth.application.response.TokenResponse; import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; import com.woowacourse.gongcheck.core.domain.host.HostRepository; @@ -60,8 +60,8 @@ class μƒˆλ‘œ_κ°€μž…ν•œ_Host의_oauth_토큰_μ½”λ“œκ°€_μž…λ ₯될_경우 { @BeforeEach void setUp() { - when(githubOauthClient.requestGithubProfileByCode(OAUTH_CODE)) - .thenReturn(new GithubProfileResponse(GITHUB_NICKNAME, GITHUB_LOGIN_NAME, GITHUB_ID, + when(githubOauthClient.requestSocialProfileByCode(OAUTH_CODE)) + .thenReturn(new SocialProfileResponse(GITHUB_NICKNAME, GITHUB_LOGIN_NAME, GITHUB_ID, GITHUB_IMAGE_URL)); when(jwtTokenProvider.createToken(any(), eq(HOST))).thenReturn(JWT_ACCESS_TOKEN); tokenRequest = new TokenRequest(OAUTH_CODE); @@ -86,8 +86,8 @@ class 기쑴에_κ°€μž…ν•œ_Host의_oauth_토큰_μ½”λ“œκ°€_μž…λ ₯될_경우 { @BeforeEach void setUp() { - when(githubOauthClient.requestGithubProfileByCode(OAUTH_CODE)) - .thenReturn(new GithubProfileResponse(GITHUB_NICKNAME, GITHUB_LOGIN_NAME, GITHUB_ID, + when(githubOauthClient.requestSocialProfileByCode(OAUTH_CODE)) + .thenReturn(new SocialProfileResponse(GITHUB_NICKNAME, GITHUB_LOGIN_NAME, GITHUB_ID, GITHUB_IMAGE_URL)); when(jwtTokenProvider.createToken(any(), eq(HOST))).thenReturn(JWT_ACCESS_TOKEN); hostRepository.save(Host_생성("1234", Long.parseLong(GITHUB_ID))); @@ -112,7 +112,7 @@ class 잘λͺ»λœ_oauth_토큰_μ½”λ“œκ°€_μž…λ ₯될_경우 { @BeforeEach void setUp() { - when(githubOauthClient.requestGithubProfileByCode(ERROR_OAUTH_CODE)) + when(githubOauthClient.requestSocialProfileByCode(ERROR_OAUTH_CODE)) .thenThrow(UnauthorizedException.class); tokenRequest = new TokenRequest(ERROR_OAUTH_CODE); } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java index 36c307b5..b5276855 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java @@ -3,11 +3,11 @@ import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; +import com.woowacourse.gongcheck.ApplicationTest; +import com.woowacourse.gongcheck.SupportRepository; import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; import com.woowacourse.gongcheck.core.domain.host.Host; -import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; import com.woowacourse.gongcheck.exception.NotFoundException; import org.junit.jupiter.api.BeforeEach; @@ -17,11 +17,8 @@ import org.junit.jupiter.api.Nested; 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; -@SpringBootTest -@Transactional +@ApplicationTest @DisplayName("HostService 클래슀") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class HostServiceTest { @@ -30,7 +27,7 @@ class HostServiceTest { private HostService hostService; @Autowired - private HostRepository hostRepository; + private SupportRepository repository; @Autowired private EntranceCodeProvider entranceCodeProvider; @@ -51,20 +48,16 @@ class μ‘΄μž¬ν•˜λŠ”_Host의_id와_μˆ˜μ •ν• _νŒ¨μŠ€μ›Œλ“œλ₯Ό_λ°›λŠ”_경우 { @BeforeEach void setUp() { spacePasswordChangeRequest = new SpacePasswordChangeRequest(CHANGING_PASSWORD); - hostId = hostRepository.save(Host_생성(ORIGIN_PASSWORD, GITHUB_ID)) + hostId = repository.save(Host_생성(ORIGIN_PASSWORD, GITHUB_ID)) .getId(); } @Test void νŒ¨μŠ€μ›Œλ“œλ₯Ό_μˆ˜μ •ν•œλ‹€() { hostService.changeSpacePassword(hostId, spacePasswordChangeRequest); - Host actual = hostRepository.getById(hostId); + Host actual = repository.getById(Host.class, hostId); - assertAll( - () -> assertThat(actual.getSpacePassword().getValue()).isEqualTo(CHANGING_PASSWORD), - () -> assertThat(actual.getGithubId()).isEqualTo(GITHUB_ID), - () -> assertThat(actual.getId()).isEqualTo(hostId) - ); + assertThat(actual.getSpacePassword().getValue()).isEqualTo(CHANGING_PASSWORD); } } @@ -86,7 +79,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> hostService.changeSpacePassword(hostId, spacePasswordChangeRequest)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } } @@ -102,7 +95,7 @@ class μ‘΄μž¬ν•˜λŠ”_Host의_idλ₯Ό_λ°›λŠ”_경우 { @BeforeEach void setUp() { - hostId = hostRepository.save(Host_생성("1234", 1111L)) + hostId = repository.save(Host_생성("1234", 1111L)) .getId(); expected = entranceCodeProvider.createEntranceCode(hostId); } @@ -121,7 +114,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host의_idλ₯Ό_λ°›λŠ”_경우 { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> hostService.createEntranceCode(0L)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/JobServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/JobServiceTest.java index 3abbd0e0..70b5d9a2 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/application/JobServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/JobServiceTest.java @@ -10,17 +10,16 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import com.woowacourse.gongcheck.ApplicationTest; +import com.woowacourse.gongcheck.SupportRepository; import com.woowacourse.gongcheck.core.application.response.JobResponse; +import com.woowacourse.gongcheck.core.application.response.SlackUrlResponse; import com.woowacourse.gongcheck.core.domain.host.Host; -import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.core.domain.job.Job; -import com.woowacourse.gongcheck.core.domain.job.JobRepository; import com.woowacourse.gongcheck.core.domain.section.Section; import com.woowacourse.gongcheck.core.domain.section.SectionRepository; import com.woowacourse.gongcheck.core.domain.space.Space; -import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; import com.woowacourse.gongcheck.core.domain.task.RunningTask; -import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; import com.woowacourse.gongcheck.core.domain.task.Task; import com.woowacourse.gongcheck.core.domain.task.TaskRepository; import com.woowacourse.gongcheck.core.presentation.request.JobCreateRequest; @@ -30,7 +29,6 @@ import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; import java.util.stream.Collectors; -import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; @@ -38,29 +36,17 @@ import org.junit.jupiter.api.Nested; 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; -@SpringBootTest -@Transactional +@ApplicationTest @DisplayName("JobService 클래슀") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class JobServiceTest { - @Autowired - private EntityManager entityManager; - @Autowired private JobService jobService; @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; + private SupportRepository repository; @Autowired private SectionRepository sectionRepository; @@ -68,9 +54,6 @@ class JobServiceTest { @Autowired private TaskRepository taskRepository; - @Autowired - private RunningTaskRepository runningTaskRepository; - @Nested class findJobs_λ©”μ†Œλ“œλŠ” { @@ -83,9 +66,9 @@ class Job_λͺ©λ‘μ΄_μ‘΄μž¬ν•˜λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - List jobs = jobRepository.saveAll( + host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€")); + List jobs = repository.saveAll( List.of(Job_생성(space, "μ˜€ν”ˆ"), Job_생성(space, "μ²­μ†Œ"), Job_생성(space, "마감"))); jobNames = jobs.stream() .map(job -> job.getName().getValue()) @@ -115,15 +98,15 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host둜_μ‘°νšŒν• _경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€")); } @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> jobService.findJobs(NON_EXIST_HOST_ID, space.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -136,7 +119,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Space의_Job_λͺ©λ‘μ„_μ‘°νšŒν• _경우 { @BeforeEach void setUp() { - hostId = hostRepository.save(Host_생성("1234", 1234L)) + hostId = repository.save(Host_생성("1234", 1234L)) .getId(); } @@ -144,7 +127,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> jobService.findJobs(hostId, NON_EXIST_SPACE_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } @@ -156,16 +139,16 @@ class λ‹€λ₯Έ_Host의_Space의_Job_λͺ©λ‘μ„_μ‘°νšŒν• _경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Host otherHost = hostRepository.save(Host_생성("1234", 2345L)); - space = spaceRepository.save(Space_생성(otherHost, "μž μ‹€")); + host = repository.save(Host_생성("1234", 1234L)); + Host otherHost = repository.save(Host_생성("1234", 2345L)); + space = repository.save(Space_생성(otherHost, "μž μ‹€")); } @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> jobService.findJobs(host.getId(), space.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } } @@ -184,8 +167,8 @@ class Job_Sectionλ“€κ³Ό_Task듀을_μž…λ ₯_λ°›λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€")); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), new TaskCreateRequest("칠판 닦기", "칠판 닦기 μ„€λͺ…", "https://image.gongcheck.shop/chilpan123")); @@ -204,7 +187,7 @@ void setUp() { void ν•œ_λ²ˆμ—_μƒμ„±ν•œλ‹€() { long savedJobId = jobService.createJob(host.getId(), space.getId(), request); - Job savedJob = jobRepository.getById(savedJobId); + Job savedJob = repository.getById(Job.class, savedJobId); List
savedSections = sectionRepository.findAllByJob(savedJob); List savedTasks = taskRepository.findAllBySectionIn(savedSections); @@ -230,8 +213,8 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host의_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + Host host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€")); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), new TaskCreateRequest("칠판 닦기", "칠판 닦기 μ„€λͺ…", "https://image.gongcheck.shop/chilpan123")); @@ -244,7 +227,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.createJob(NON_EXIST_HOST_ID, space.getId(), request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -258,7 +241,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Space의_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), new TaskCreateRequest("칠판 닦기", "칠판 닦기 μ„€λͺ…", "https://image.gongcheck.shop/chilpan123")); @@ -271,7 +254,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.createJob(host.getId(), NON_EXIST_SPACE_ID, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } @@ -284,10 +267,10 @@ class λ‹€λ₯Έ_host의_space_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - spaceRepository.save(Space_생성(host, "μž μ‹€")); - Host otherHost = hostRepository.save(Host_생성("5678", 5678L)); - otherSpace = spaceRepository.save(Space_생성(otherHost, "μž μ‹€")); + host = repository.save(Host_생성("1234", 1234L)); + repository.save(Space_생성(host, "μž μ‹€")); + Host otherHost = repository.save(Host_생성("5678", 5678L)); + otherSpace = repository.save(Space_생성(otherHost, "μž μ‹€")); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), new TaskCreateRequest("칠판 닦기", "칠판 닦기 μ„€λͺ…", "https://image.gongcheck.shop/chilpan123")); @@ -300,7 +283,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.createJob(host.getId(), otherSpace.getId(), request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } } @@ -318,15 +301,15 @@ class 기쑴의_Job이_μ‘΄μž¬ν•˜λŠ”_경우 { private Task originTask; private JobCreateRequest request; private List requestSectionNames; - List requestTaskNames; + private List requestTaskNames; @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - originJob = jobRepository.save(Job_생성(space, "마감")); - originSection = sectionRepository.save(Section_생성(originJob, "μ†Œκ°•μ˜μ‹€")); - originTask = taskRepository.save(Task_생성(originSection, "뢈 끄기")); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + originJob = repository.save(Job_생성(space, "마감")); + originSection = repository.save(Section_생성(originJob, "μ†Œκ°•μ˜μ‹€")); + originTask = repository.save(Task_생성(originSection, "뢈 끄기")); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), @@ -346,11 +329,10 @@ void setUp() { } @Test - void 기쑴에_μ‘΄μž¬ν•˜λ˜_Job을_μ‚­μ œν•œ_ν›„_μƒˆλ‘œμš΄_Job을_μƒμ„±ν•œλ‹€() { - long updateJobId = jobService.updateJob(host.getId(), originJob.getId(), request); + void Job을_μˆ˜μ •ν•œλ‹€() { + jobService.updateJob(host.getId(), originJob.getId(), request); - Job updateJob = jobRepository.findBySpaceHostAndId(host, updateJobId) - .get(); + Job updateJob = repository.getById(Job.class, originJob.getId()); List
updateSections = sectionRepository.findAllByJob(updateJob); List updateTasks = taskRepository.findAllBySectionIn(updateSections); @@ -378,8 +360,8 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host의_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + Host host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), new TaskCreateRequest("칠판 닦기", "칠판 닦기 μ„€λͺ…", "https://image.gongcheck.shop/chilpan123")); @@ -393,20 +375,22 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.updateJob(NON_EXIST_HOST_ID, savedJobId, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @Nested class λ‹€λ₯Έ_host의_job_idλ₯Ό_μž…λ ₯받은_경우 { + private Host anotherHost; private JobCreateRequest request; private long savedJobId; @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), new TaskCreateRequest("칠판 닦기", "칠판 닦기 μ„€λͺ…", "https://image.gongcheck.shop/chilpan123")); @@ -418,10 +402,9 @@ void setUp() { @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - Host anotherHost = hostRepository.save(Host_생성("1234", 2345L)); assertThatThrownBy(() -> jobService.updateJob(anotherHost.getId(), savedJobId, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -435,7 +418,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_job_idλ₯Ό_μž…λ ₯받을_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); List tasks = List .of(new TaskCreateRequest("책상 닦기", "책상 닦기 μ„€λͺ…", "https://image.gongcheck.shop/checksang123"), new TaskCreateRequest("칠판 닦기", "칠판 닦기 μ„€λͺ…", "https://image.gongcheck.shop/chilpan123")); @@ -448,7 +431,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.updateJob(host.getId(), NON_EXIST_JOB_ID, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } } @@ -465,16 +448,16 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host의_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + Host host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.removeJob(NON_EXIST_HOST_ID, job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -487,14 +470,14 @@ class Job이_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.removeJob(host.getId(), NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -506,17 +489,17 @@ class λ‹€λ₯Έ_Host의_Job_μ œκ±°ν• _경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - otherHost = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + Host host = repository.save(Host_생성("1234", 1234L)); + otherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.removeJob(otherHost.getId(), job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -531,25 +514,23 @@ class μ‘΄μž¬ν•˜λŠ”_Job이_μžˆλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - section = sectionRepository.save(Section_생성(job, "λŒ€κ°•μ˜μ‹€")); - task = taskRepository.save(Task_생성(section, "책상 닦기")); - runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), false)); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + section = repository.save(Section_생성(job, "λŒ€κ°•μ˜μ‹€")); + task = repository.save(Task_생성(section, "책상 닦기")); + runningTask = repository.save(RunningTask_생성(task.getId(), false)); } @Test void Jobκ³Ό_κ΄€λ ¨λœ_Section_Task_RunningTaskλ₯Ό_ν•¨κ»˜_μ‚­μ œν•œλ‹€() { jobService.removeJob(host.getId(), job.getId()); - entityManager.flush(); - entityManager.clear(); assertAll( - () -> assertThat(jobRepository.findById(job.getId())).isEmpty(), - () -> assertThat(sectionRepository.findById(section.getId())).isEmpty(), - () -> assertThat(taskRepository.findById(task.getId())).isEmpty(), - () -> assertThat(runningTaskRepository.findById(runningTask.getTaskId())).isEmpty() + () -> assertThat(repository.findById(Job.class, job.getId())).isEmpty(), + () -> assertThat(repository.findById(Section.class, section.getId())).isEmpty(), + () -> assertThat(repository.findById(Task.class, task.getId())).isEmpty(), + () -> assertThat(repository.findById(RunningTask.class, runningTask.getTaskId())).isEmpty() ); } } @@ -567,7 +548,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host의_idλ₯Ό_μž…λ ₯받은_경우 { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.findSlackUrl(NON_EXIST_HOST_ID, NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -580,14 +561,14 @@ class Job이_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.findSlackUrl(host.getId(), NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } } @@ -600,17 +581,17 @@ class Job이_μ‘΄μž¬ν•˜λŠ”λ°_λ‹€λ₯Έ_Host의_Job의_Slack_Url_μ‘°νšŒν•˜λŠ”_경우 @BeforeEach void setUp() { - myHost = hostRepository.save(Host_생성("1234", 1234L)); - Host otherHost = hostRepository.save(Host_생성("1234", 2456L)); - Space otherSpace = spaceRepository.save(Space_생성(otherHost, "μž μ‹€")); - otherJob = jobRepository.save(Job_생성(otherSpace, "ν†±μ˜€λΈŒμŠ€μœ™λ°©")); + myHost = repository.save(Host_생성("1234", 1234L)); + Host otherHost = repository.save(Host_생성("1234", 2456L)); + Space otherSpace = repository.save(Space_생성(otherHost, "μž μ‹€")); + otherJob = repository.save(Job_생성(otherSpace, "ν†±μ˜€λΈŒμŠ€μœ™λ°©")); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.findSlackUrl(myHost.getId(), otherJob.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -622,15 +603,16 @@ class Job이_μ‘΄μž¬ν•˜λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "ν†±μ˜€λΈŒμŠ€μœ™λ°©", "http://slackurl.com")); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "ν†±μ˜€λΈŒμŠ€μœ™λ°©", "http://slackurl.com")); } @Test void Slack_Url을_μ‘°νšŒν•œλ‹€() { - assertThat(jobService.findSlackUrl(host.getId(), job.getId()).getSlackUrl()).isEqualTo( - "http://slackurl.com"); + SlackUrlResponse actual = jobService.findSlackUrl(host.getId(), job.getId()); + + assertThat(actual.getSlackUrl()).isEqualTo("http://slackurl.com"); } } } @@ -656,7 +638,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.changeSlackUrl(NON_EXIST_HOST_ID, JOB_ID, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -670,7 +652,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Job을_μž…λ ₯_λ°›λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); request = new SlackUrlChangeRequest("https://newslackurl.com"); } @@ -678,7 +660,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.changeSlackUrl(host.getId(), NON_EXIST_JOB_ID, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -691,10 +673,10 @@ class λ‹€λ₯Έ_Host의_JobIdλ₯Ό_μž…λ ₯_λ°›λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Host otherHost = hostRepository.save(Host_생성("1234", 2456L)); - Space otherSpace = spaceRepository.save(Space_생성(otherHost, "μž μ‹€")); - otherJob = jobRepository.save(Job_생성(otherSpace, "ν†±μ˜€λΈŒμŠ€μœ™λ°©")); + host = repository.save(Host_생성("1234", 1234L)); + Host otherHost = repository.save(Host_생성("1234", 2456L)); + Space otherSpace = repository.save(Space_생성(otherHost, "μž μ‹€")); + otherJob = repository.save(Job_생성(otherSpace, "ν†±μ˜€λΈŒμŠ€μœ™λ°©")); request = new SlackUrlChangeRequest("https://newslackurl.com"); } @@ -702,7 +684,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobService.changeSlackUrl(host.getId(), otherJob.getId(), request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -713,22 +695,24 @@ class μ‘΄μž¬ν•˜λŠ”_Job을_μž…λ ₯_λ°›λŠ”_경우 { private static final String NEW_SLACK_URL = "https://newslackurl.com"; private Host host; - private Job job; + private Long jobId; private SlackUrlChangeRequest request; @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "ν†±μ˜€λΈŒμŠ€μœ™λ°©", SLACK_URL)); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + jobId = repository.save(Job_생성(space, "ν†±μ˜€λΈŒμŠ€μœ™λ°©", SLACK_URL)) + .getId(); request = new SlackUrlChangeRequest(NEW_SLACK_URL); } @Test void Slack_Url을_μˆ˜μ •ν•œλ‹€() { - jobService.changeSlackUrl(host.getId(), job.getId(), request); + jobService.changeSlackUrl(host.getId(), jobId, request); + Job actual = repository.getById(Job.class, jobId); - assertThat(job.getSlackUrl()).isEqualTo(NEW_SLACK_URL); + assertThat(actual.getSlackUrl()).isEqualTo(NEW_SLACK_URL); } } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java index 083574a2..08fa625d 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java @@ -10,25 +10,20 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import com.woowacourse.gongcheck.ApplicationTest; +import com.woowacourse.gongcheck.SupportRepository; import com.woowacourse.gongcheck.core.application.response.SpaceResponse; import com.woowacourse.gongcheck.core.application.response.SpacesResponse; import com.woowacourse.gongcheck.core.domain.host.Host; -import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.core.domain.job.Job; -import com.woowacourse.gongcheck.core.domain.job.JobRepository; import com.woowacourse.gongcheck.core.domain.section.Section; -import com.woowacourse.gongcheck.core.domain.section.SectionRepository; import com.woowacourse.gongcheck.core.domain.space.Space; -import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; import com.woowacourse.gongcheck.core.domain.task.RunningTask; -import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; import com.woowacourse.gongcheck.core.domain.task.Task; -import com.woowacourse.gongcheck.core.domain.task.TaskRepository; import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; -import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; @@ -36,38 +31,17 @@ import org.junit.jupiter.api.Nested; 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; -@SpringBootTest -@Transactional +@ApplicationTest @DisplayName("SpaceService 클래슀") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class SpaceServiceTest { - @Autowired - EntityManager entityManager; - @Autowired private SpaceService spaceService; @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; - - @Autowired - private RunningTaskRepository runningTaskRepository; + private SupportRepository repository; @Nested class findSpaces_λ©”μ„œλ“œλŠ” { @@ -79,7 +53,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host_idλ₯Ό_μž…λ ₯λ°›λŠ”_경우 { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.findSpaces(0L)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -91,12 +65,12 @@ class μ‘΄μž¬ν•˜λŠ”_Host의_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); Space space_1 = Space_생성(host, "μž μ‹€ 캠퍼슀"); Space space_2 = Space_생성(host, "선릉 캠퍼슀"); Space space_3 = Space_생성(host, "양평같은방"); - List spaces = spaceRepository.saveAll(List.of(space_1, space_2, space_3)); + List spaces = repository.saveAll(List.of(space_1, space_2, space_3)); expected = SpacesResponse.from(spaces); } @@ -123,8 +97,8 @@ class Hostκ°€_μž…λ ₯받은_Space_이름과_같은_Spaceλ₯Ό_이미_가지고_있 @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); request = new SpaceCreateRequest(space.getName().getValue(), "https://image.gongcheck.shop/123sdf5"); } @@ -132,7 +106,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.createSpace(host.getId(), request)) .isInstanceOf(BusinessException.class) - .hasMessage("이미 μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€."); + .hasMessageContaining("이미 μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€."); } } @@ -152,7 +126,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.createSpace(NON_EXIST_HOST_ID, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -167,14 +141,14 @@ class μž…λ ₯받은_Hostκ°€_μ‘΄μž¬ν•˜λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); request = new SpaceCreateRequest(SPACE_NAME, SPACE_IMAGE_URL); } @Test void Spaceλ₯Ό_μƒμ„±ν•œλ‹€() { Long spaceId = spaceService.createSpace(host.getId(), request); - Space actual = spaceRepository.getById(spaceId); + Space actual = repository.getById(Space.class, spaceId); assertAll( () -> assertThat(actual.getName().getValue()).isEqualTo(SPACE_NAME), @@ -197,8 +171,8 @@ class Space_λͺ©λ‘μ΄_μ‘΄μž¬ν•˜λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 2345L)); - space = spaceRepository.save(Space_생성(host, SPACE_NAME)); + host = repository.save(Host_생성("1234", 2345L)); + space = repository.save(Space_생성(host, SPACE_NAME)); } @Test @@ -220,16 +194,16 @@ class μž…λ ₯받은_Hostκ°€_μž…λ ₯받은_Spaceλ₯Ό_가지고_μžˆμ§€_μ•Šμ€_경우 @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Host host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); + anotherHost = repository.save(Host_생성("1234", 2345L)); } @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.findSpace(anotherHost.getId(), space.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } @@ -242,15 +216,15 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); + Host host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); } @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.findSpace(NON_EXIST_HOST_ID, space.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -261,14 +235,14 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Space_idλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); } @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.findSpace(host.getId(), 0L)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } @@ -282,8 +256,8 @@ class μž…λ ₯받은_Hostκ°€_μž…λ ₯받은_Spaceλ₯Ό_μ†Œμœ ν•˜κ³ _μžˆλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, SPACE_NAME)); + host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, SPACE_NAME)); } @Test @@ -310,15 +284,15 @@ class μž…λ ₯받은_Host_idκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); + Host host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); } @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.removeSpace(NON_EXIST_HOST_ID, space.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -330,16 +304,15 @@ class μž…λ ₯받은_Hostκ°€_μž…λ ₯받은_Spaceλ₯Ό_μ†Œμœ ν•˜κ³ _μžˆμ§€_μ•Šμ€_κ²½ @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 4567L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 4567L)); + space = repository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); } @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> spaceService.removeSpace(anotherHost.getId(), space.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .isInstanceOf(NotFoundException.class); } } @@ -355,26 +328,24 @@ class μž…λ ₯받은_Space_idκ°€_μ‘΄μž¬ν•˜λ©΄ { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - section = sectionRepository.save(Section_생성(job, "λŒ€κ°•μ˜μ‹€")); - task = taskRepository.save(Task_생성(section, "책상 닦기")); - runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), false)); + host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€ 캠퍼슀")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + section = repository.save(Section_생성(job, "λŒ€κ°•μ˜μ‹€")); + task = repository.save(Task_생성(section, "책상 닦기")); + runningTask = repository.save(RunningTask_생성(task.getId(), false)); } @Test void ν•΄λ‹Ή_Space_및_κ΄€λ ¨λœ_Job_Section_Task_RunningTaskλ₯Ό_μ‚­μ œν•œλ‹€() { spaceService.removeSpace(host.getId(), space.getId()); - entityManager.flush(); - entityManager.clear(); assertAll( - () -> assertThat(spaceRepository.findById(space.getId())).isEmpty(), - () -> assertThat(jobRepository.findById(job.getId())).isEmpty(), - () -> assertThat(sectionRepository.findById(section.getId())).isEmpty(), - () -> assertThat(taskRepository.findById(task.getId())).isEmpty(), - () -> assertThat(runningTaskRepository.findById(runningTask.getTaskId())).isEmpty() + () -> assertThat(repository.findById(Space.class, space.getId())).isEmpty(), + () -> assertThat(repository.findById(Job.class, job.getId())).isEmpty(), + () -> assertThat(repository.findById(Section.class, section.getId())).isEmpty(), + () -> assertThat(repository.findById(Task.class, task.getId())).isEmpty(), + () -> assertThat(repository.findById(RunningTask.class, runningTask.getTaskId())).isEmpty() ); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SubmissionServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SubmissionServiceTest.java index ed6ecc28..9ecd3eb4 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SubmissionServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SubmissionServiceTest.java @@ -10,22 +10,23 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import com.woowacourse.gongcheck.ApplicationTest; +import com.woowacourse.gongcheck.SupportRepository; import com.woowacourse.gongcheck.core.application.response.SubmissionResponse; import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; import com.woowacourse.gongcheck.core.domain.host.Host; -import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.core.domain.job.Job; -import com.woowacourse.gongcheck.core.domain.job.JobRepository; import com.woowacourse.gongcheck.core.domain.section.Section; -import com.woowacourse.gongcheck.core.domain.section.SectionRepository; import com.woowacourse.gongcheck.core.domain.space.Space; -import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; import com.woowacourse.gongcheck.core.domain.submission.Submission; import com.woowacourse.gongcheck.core.domain.submission.SubmissionRepository; import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskSseEmitterContainer; import com.woowacourse.gongcheck.core.domain.task.Task; -import com.woowacourse.gongcheck.core.domain.task.TaskRepository; import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.NotFoundException; @@ -37,12 +38,10 @@ import org.junit.jupiter.api.Nested; 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.data.domain.PageRequest; -import org.springframework.transaction.annotation.Transactional; -@SpringBootTest -@Transactional +@ApplicationTest @DisplayName("SubmissionService 클래슀") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class SubmissionServiceTest { @@ -51,19 +50,7 @@ class SubmissionServiceTest { private SubmissionService submissionService; @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; + private SupportRepository repository; @Autowired private RunningTaskRepository runningTaskRepository; @@ -71,6 +58,9 @@ class SubmissionServiceTest { @Autowired private SubmissionRepository submissionRepository; + @MockBean + private RunningTaskSseEmitterContainer runningTaskSseEmitterContainer; + @Nested class submitJobCompletion_λ©”μ†Œλ“œλŠ” { @@ -92,7 +82,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> submissionService.submitJobCompletion(NON_EXIST_HOST_ID, jobId, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -106,7 +96,7 @@ class μž…λ ₯받은_Job이_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); request = new SubmissionRequest("제좜자"); } @@ -114,7 +104,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> submissionService.submitJobCompletion(host.getId(), NON_EXIST_JOB_ID, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -127,10 +117,10 @@ class λ‹€λ₯Έ_Host의_Job을_μž…λ ₯λ°›λŠ”_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); request = new SubmissionRequest("제좜자"); } @@ -139,7 +129,7 @@ void setUp() { assertThatThrownBy( () -> submissionService.submitJobCompletion(anotherHost.getId(), job.getId(), request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -152,9 +142,9 @@ class RunningTaskκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); request = new SubmissionRequest("제좜자"); } @@ -162,7 +152,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> submissionService.submitJobCompletion(host.getId(), job.getId(), request)) .isInstanceOf(BusinessException.class) - .hasMessage("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); } } @@ -175,14 +165,14 @@ class λͺ¨λ“ _RunningTaskκ°€_μ²΄ν¬μƒνƒœκ°€_μ•„λ‹Œ_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - Task task_1 = taskRepository.save(Task_생성(section, "책상 μ²­μ†Œ")); - Task task_2 = taskRepository.save(Task_생성(section, "의자 λ„£κΈ°")); - runningTaskRepository.save(RunningTask_생성(task_1.getId(), false)); - runningTaskRepository.save(RunningTask_생성(task_2.getId(), false)); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + Task task_1 = repository.save(Task_생성(section, "책상 μ²­μ†Œ")); + Task task_2 = repository.save(Task_생성(section, "의자 λ„£κΈ°")); + repository.save(RunningTask_생성(task_1.getId(), false)); + repository.save(RunningTask_생성(task_2.getId(), false)); request = new SubmissionRequest("제좜자"); } @@ -190,7 +180,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> submissionService.submitJobCompletion(host.getId(), job.getId(), request)) .isInstanceOf(BusinessException.class) - .hasMessage("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€."); + .hasMessageContaining("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€."); } } @@ -204,14 +194,14 @@ class λͺ¨λ“ _RunningTaskκ°€_체크_μƒνƒœμΈ_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - Task task_1 = taskRepository.save(Task_생성(section, "책상 μ²­μ†Œ")); - Task task_2 = taskRepository.save(Task_생성(section, "의자 λ„£κΈ°")); - runningTaskRepository.save(RunningTask_생성(task_1.getId(), true)); - runningTaskRepository.save(RunningTask_생성(task_2.getId(), true)); + host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + Task task_1 = repository.save(Task_생성(section, "책상 μ²­μ†Œ")); + Task task_2 = repository.save(Task_생성(section, "의자 λ„£κΈ°")); + repository.save(RunningTask_생성(task_1.getId(), true)); + repository.save(RunningTask_생성(task_2.getId(), true)); request = new SubmissionRequest("제좜자"); } @@ -228,6 +218,14 @@ void setUp() { () -> assertThat(runningTaskSize).isZero() ); } + + @Test + void Submission_Sse_Eventλ₯Ό_λ°œν–‰ν•œλ‹€() { + submissionService.submitJobCompletion(host.getId(), job.getId(), request); + + verify(runningTaskSseEmitterContainer, times(1)) + .publishSubmitEvent(anyLong()); + } } } @@ -252,7 +250,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> submissionService.findPage(NON_EXIST_HOST_ID, spaceId, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -266,7 +264,7 @@ class μž…λ ₯받은_Spaceκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); + host = repository.save(Host_생성("1234", 1234L)); request = PageRequest.of(0, 2); } @@ -274,7 +272,7 @@ void setUp() { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> submissionService.findPage(host.getId(), NON_EXIST_SPACE_ID, request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } @@ -287,9 +285,9 @@ class λ‹€λ₯Έ_Host의_Spaceλ₯Ό_μž…λ ₯λ°›λŠ”_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + space = repository.save(Space_생성(host, "μž μ‹€")); request = PageRequest.of(0, 2); } @@ -298,7 +296,7 @@ void setUp() { assertThatThrownBy( () -> submissionService.findPage(anotherHost.getId(), space.getId(), request)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } @@ -315,13 +313,13 @@ class μ˜¬λ°”λ₯Έ_Host의_Spaceλ₯Ό_μž…λ ₯λ°›λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + host = repository.save(Host_생성("1234", 1234L)); + space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); request = PageRequest.of(0, 2); - submissionRepository.save(Submission_생성(job, SUBMISSION_AUTHOR_1)); - submissionRepository.save(Submission_생성(job, SUBMISSION_AUTHOR_2)); - submissionRepository.save(Submission_생성(job, SUBMISSION_AUTHOR_2)); + repository.saveAll( + List.of(Submission_생성(job, SUBMISSION_AUTHOR_1), Submission_생성(job, SUBMISSION_AUTHOR_2), + Submission_생성(job, SUBMISSION_AUTHOR_2))); } @Test diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/TaskServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/TaskServiceTest.java index 152f4b30..9c1cc13c 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/application/TaskServiceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/TaskServiceTest.java @@ -10,29 +10,28 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import com.woowacourse.gongcheck.ApplicationTest; +import com.woowacourse.gongcheck.SupportRepository; import com.woowacourse.gongcheck.core.application.response.JobActiveResponse; -import com.woowacourse.gongcheck.core.application.response.RunningTaskResponse; -import com.woowacourse.gongcheck.core.application.response.RunningTasksWithSectionResponse; import com.woowacourse.gongcheck.core.application.response.TaskResponse; import com.woowacourse.gongcheck.core.application.response.TasksResponse; import com.woowacourse.gongcheck.core.application.response.TasksWithSectionResponse; import com.woowacourse.gongcheck.core.domain.host.Host; -import com.woowacourse.gongcheck.core.domain.host.HostRepository; import com.woowacourse.gongcheck.core.domain.job.Job; -import com.woowacourse.gongcheck.core.domain.job.JobRepository; import com.woowacourse.gongcheck.core.domain.section.Section; -import com.woowacourse.gongcheck.core.domain.section.SectionRepository; import com.woowacourse.gongcheck.core.domain.space.Space; -import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; import com.woowacourse.gongcheck.core.domain.task.RunningTask; import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskSseEmitterContainer; import com.woowacourse.gongcheck.core.domain.task.Task; -import com.woowacourse.gongcheck.core.domain.task.TaskRepository; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; -import javax.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; @@ -40,11 +39,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.boot.test.mock.mockito.MockBean; -@SpringBootTest -@Transactional +@ApplicationTest @DisplayName("TaskService 클래슀") @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class TaskServiceTest { @@ -53,25 +50,13 @@ class TaskServiceTest { private TaskService taskService; @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; + private SupportRepository repository; @Autowired private RunningTaskRepository runningTaskRepository; - @Autowired - private EntityManager entityManager; + @MockBean + private RunningTaskSseEmitterContainer runningTaskSseEmitterContainer; @Nested class createNewRunningTasks_λ©”μ†Œλ“œλŠ” { @@ -85,11 +70,11 @@ class μ‘΄μž¬ν•˜λŠ”_Host와_Job을_μž…λ ₯λ°›λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - List tasks = taskRepository.saveAll( + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + List tasks = repository.saveAll( List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); taskIds = tasks.stream() .map(Task::getId) @@ -120,17 +105,17 @@ class Taskκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.createNewRunningTasks(host.getId(), job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); } } @@ -144,7 +129,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host둜_RunningTaskλ₯Ό_생성할_경우 { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.createNewRunningTasks(NON_EXIST_HOST_ID, JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -157,7 +142,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Task둜_RunningTaskλ₯Ό_생성할_경우 { @BeforeEach void setUp() { - hostId = hostRepository.save(Host_생성("1234", 1234567L)) + hostId = repository.save(Host_생성("1234", 1234567L)) .getId(); } @@ -165,7 +150,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.createNewRunningTasks(hostId, NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -177,19 +162,19 @@ class λ‹€λ₯Έ_Host의_Task둜_RunningTaskλ₯Ό_생성할_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - taskRepository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + repository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.createNewRunningTasks(anotherHost.getId(), job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -201,13 +186,13 @@ class RunningTaskκ°€_이미_μ‘΄μž¬ν• _λ•Œ_μƒˆλ‘œμš΄_RunningTaskλ₯Ό_생성할_κ²½ @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - List tasks = taskRepository.saveAll( + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + List tasks = repository.saveAll( List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); - runningTaskRepository.saveAll(tasks.stream() + repository.saveAll(tasks.stream() .map(Task::getId) .map(id -> RunningTask_생성(id, true)) .collect(toList())); @@ -217,7 +202,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.createNewRunningTasks(host.getId(), job.getId())) .isInstanceOf(BusinessException.class) - .hasMessage("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ—¬ μƒˆλ‘œμš΄ μž‘μ—…μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ—¬ μƒˆλ‘œμš΄ μž‘μ—…μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€."); } } } @@ -233,13 +218,13 @@ class μ‘΄μž¬ν•˜λŠ”_Host와_Job의_RunningTaskκ°€_μ‘΄μž¬ν•˜λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - List tasks = taskRepository.saveAll(List.of( + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + List tasks = repository.saveAll(List.of( Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); - runningTaskRepository.saveAll(tasks.stream() + repository.saveAll(tasks.stream() .map(Task::getId) .map(id -> RunningTask_생성(id, true)) .collect(toList())); @@ -261,11 +246,11 @@ class μ‘΄μž¬ν•˜λŠ”_Host와_Job의_RunningTaskκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - taskRepository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + repository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); } @Test @@ -286,7 +271,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Host둜_ν™•μΈν•˜λ €λŠ”_경우 { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.isJobActivated(NON_EXIST_HOST_ID, JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -299,7 +284,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Task둜_ν™•μΈν•˜λ €λŠ”_경우 { @BeforeEach void setUp() { - hostId = hostRepository.save(Host_생성("1234", 1111L)) + hostId = repository.save(Host_생성("1234", 1111L)) .getId(); } @@ -307,7 +292,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.isJobActivated(hostId, NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -319,68 +304,54 @@ class λ‹€λ₯Έ_Host의_Task둜_ν™•μΈν•˜λ €λŠ”_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - taskRepository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + repository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.isJobActivated(anotherHost.getId(), job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } } @Nested - class findRunningTasks_λ©”μ†Œλ“œλŠ” { + class connectRunningTasks_λ©”μ„œλ“œλŠ” { @Nested class μ‘΄μž¬ν•˜λŠ”_Host와_RunningTasksκ°€_μƒμ„±λœ_Job을_μž…λ ₯받은_경우 { - private static final String SECTION_NAME = "νŠΈλž™λ£Έ"; - private static final String TASK_NAME_1 = "책상 μ²­μ†Œ"; - private static final String TASK_NAME_2 = "의자 λ„£κΈ°"; - private static final int TASK_INDEX = 0; - - private Host host; - private Job job; - private Section section; + private Long hostId; + private Long jobId; @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, SECTION_NAME)); - List tasks = taskRepository.saveAll(List.of( - Task_생성(section, TASK_NAME_1), Task_생성(section, TASK_NAME_2))); - runningTaskRepository.saveAll(tasks.stream() + Host host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + List tasks = repository.saveAll(List.of( + Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + repository.saveAll(tasks.stream() .map(task -> RunningTask_생성(task.getId(), false)) .collect(toList())); - entityManager.flush(); - entityManager.clear(); + + hostId = host.getId(); + jobId = job.getId(); } @Test - void μ •μƒμ μœΌλ‘œ_RunningTasksλ₯Ό_μ‘°νšŒν•œλ‹€() { - List actual = taskService.findRunningTasks(host.getId(), job.getId()) - .getSections(); - List actualTasks = actual.get(TASK_INDEX).getTasks(); + void emitterλ₯Ό_μƒμ„±ν•˜λŠ”_λ©”μ„œλ“œλ₯Ό_ν˜ΈμΆœν•œλ‹€() { + taskService.connectRunningTasks(hostId, jobId); - assertAll( - () -> assertThat(actual) - .extracting(RunningTasksWithSectionResponse::getName) - .containsExactly(SECTION_NAME), - () -> assertThat(actual).hasSize(1), - () -> assertThat(actualTasks) - .extracting(RunningTaskResponse::getName) - .containsExactly(TASK_NAME_1, TASK_NAME_2) - ); + verify(runningTaskSseEmitterContainer, times(1)) + .createEmitterWithConnectionEvent(anyLong(), any()); } } @@ -392,9 +363,9 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Hostλ₯Ό_μž…λ ₯받은_경우 { @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - assertThatThrownBy(() -> taskService.findRunningTasks(NON_EXIST_HOST_ID, JOB_ID)) + assertThatThrownBy(() -> taskService.connectRunningTasks(NON_EXIST_HOST_ID, JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -407,15 +378,15 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Job을_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - hostId = hostRepository.save(Host_생성("1234", 1111L)) + hostId = repository.save(Host_생성("1234", 1111L)) .getId(); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - assertThatThrownBy(() -> taskService.findRunningTasks(hostId, NON_EXIST_JOB_ID)) + assertThatThrownBy(() -> taskService.connectRunningTasks(hostId, NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -427,23 +398,23 @@ class λ‹€λ₯Έ_Host의_Task의_RunningTaskλ₯Ό_μ‘°νšŒν•˜λ©΄ { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - List tasks = taskRepository.saveAll(List.of( + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + List tasks = repository.saveAll(List.of( Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); - runningTaskRepository.saveAll(tasks.stream() + repository.saveAll(tasks.stream() .map(task -> RunningTask_생성(task.getId(), false)) .collect(toList())); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - assertThatThrownBy(() -> taskService.findRunningTasks(anotherHost.getId(), job.getId())) + assertThatThrownBy(() -> taskService.connectRunningTasks(anotherHost.getId(), job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -455,22 +426,21 @@ class RunningTasksκ°€_μƒμ„±λ˜μ§€_μ•Šμ€_Job을_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - taskRepository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + repository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - assertThatThrownBy(() -> taskService.findRunningTasks(host.getId(), job.getId())) + assertThatThrownBy(() -> taskService.connectRunningTasks(host.getId(), job.getId())) .isInstanceOf(BusinessException.class) - .hasMessage("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•Šμ•„ μ‘°νšŒν•  수 μ—†μŠ΅λ‹ˆλ‹€"); + .hasMessageContaining("ν˜„μž¬ 진행쀑인 RunningTaskκ°€ μ—†μŠ΅λ‹ˆλ‹€"); } } } - @Nested class flipRunningTask_λ©”μ†Œλ“œλŠ” { @@ -479,23 +449,25 @@ class μ‘΄μž¬ν•˜λŠ”_Host와_μ²΄ν¬λ˜μ§€_μ•Šμ€_RunningTaskλ₯Ό_가진_Taskλ₯Ό_μž… private Host host; private Task task; - private RunningTask runningTask; + private Long runningTaskId; @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - Job job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - task = taskRepository.save(Task_생성(section, "책상 μ²­μ†Œ")); - runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), false)); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + task = repository.save(Task_생성(section, "책상 μ²­μ†Œ")); + runningTaskId = repository.save(RunningTask_생성(task.getId(), false)) + .getTaskId(); } @Test void μ²΄ν¬μƒνƒœλ₯Ό_True둜_λ³€κ²½ν•œλ‹€() { taskService.flipRunningTask(host.getId(), task.getId()); + RunningTask actual = repository.getById(RunningTask.class, runningTaskId); - assertThat(runningTask.isChecked()).isTrue(); + assertThat(actual.isChecked()).isTrue(); } } @@ -504,23 +476,25 @@ class μ‘΄μž¬ν•˜λŠ”_Host와_체크된_RunningTaskλ₯Ό_가진_Taskλ₯Ό_μž…λ ₯받은_ private Host host; private Task task; - private RunningTask runningTask; + private Long runningTaskId; @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - Job job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - task = taskRepository.save(Task_생성(section, "책상 μ²­μ†Œ")); - runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), true)); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + task = repository.save(Task_생성(section, "책상 μ²­μ†Œ")); + runningTaskId = repository.save(RunningTask_생성(task.getId(), true)) + .getTaskId(); } @Test void μ²΄ν¬μƒνƒœλ₯Ό_False둜_λ³€κ²½ν•œλ‹€() { taskService.flipRunningTask(host.getId(), task.getId()); + RunningTask actual = repository.getById(RunningTask.class, runningTaskId); - assertThat(runningTask.isChecked()).isFalse(); + assertThat(actual.isChecked()).isFalse(); } } @@ -534,7 +508,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Hostλ₯Ό_μž…λ ₯받은_경우 { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.flipRunningTask(NON_EXIST_HOST_ID, TASK_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -547,7 +521,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Taskλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - hostId = hostRepository.save(Host_생성("1234", 1111L)) + hostId = repository.save(Host_생성("1234", 1111L)) .getId(); } @@ -555,7 +529,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.flipRunningTask(hostId, NON_EXIST_TASK_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -567,12 +541,12 @@ class λ‹€λ₯Έ_Host의_Taskλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - Job job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - taskId = taskRepository.save(Task_생성(section, "책상 μ²­μ†Œ")) + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + taskId = repository.save(Task_생성(section, "책상 μ²­μ†Œ")) .getId(); } @@ -580,7 +554,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.flipRunningTask(anotherHost.getId(), taskId)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -592,11 +566,11 @@ class 진행쀑이지_μ•Šμ€_Taskλ₯Ό_μž…λ ₯받은_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - Job job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - taskId = taskRepository.save(Task_생성(section, "책상 μ²­μ†Œ")) + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + taskId = repository.save(Task_생성(section, "책상 μ²­μ†Œ")) .getId(); } @@ -604,7 +578,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.flipRunningTask(host.getId(), taskId)) .isInstanceOf(BusinessException.class) - .hasMessage("ν˜„μž¬ 진행 쀑인 μž‘μ—…μ΄ μ•„λ‹™λ‹ˆλ‹€."); + .hasMessageContaining("ν˜„μž¬ 진행 쀑인 μž‘μ—…μ΄ μ•„λ‹™λ‹ˆλ‹€."); } } } @@ -624,11 +598,11 @@ class μ‘΄μž¬ν•˜λŠ”_Host와_Job을_μž…λ ₯ν•˜λŠ”_경우 { @BeforeEach void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, SECTION_NAME)); - taskRepository.saveAll(List.of(Task_생성(section, TASK_NAME_1), Task_생성(section, TASK_NAME_2))); + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, SECTION_NAME)); + repository.saveAll(List.of(Task_생성(section, TASK_NAME_1), Task_생성(section, TASK_NAME_2))); } @Test @@ -656,7 +630,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Hostλ₯Ό_μž…λ ₯ν•˜λŠ”_경우 { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.findTasks(NON_EXIST_HOST_ID, JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } @@ -669,7 +643,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Job을_μž…λ ₯ν•˜λŠ”_경우 { @BeforeEach void setUp() { - hostId = hostRepository.save(Host_생성("1234", 1111L)) + hostId = repository.save(Host_생성("1234", 1111L)) .getId(); } @@ -677,7 +651,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.findTasks(hostId, NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -689,19 +663,133 @@ class λ‹€λ₯Έ_Host의_Job을_μž…λ ₯ν•˜λŠ”_경우 { @BeforeEach void setUp() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - anotherHost = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); - job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); - Section section = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); - taskRepository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + job = repository.save(Job_생성(space, "μ²­μ†Œ")); + Section section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + repository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); } @Test void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskService.findTasks(anotherHost.getId(), job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + } + } + } + + @Nested + class checkRunningTasksInSection_λ©”μ†Œλ“œλŠ” { + + @Nested + class μ‘΄μž¬ν•˜λŠ”_Host와_Section을_μž…λ ₯ν• _경우 { + + private Host host; + private Section section; + + @BeforeEach + void setUp() { + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + Task task1 = repository.save(Task_생성(section, "책상 μ²­μ†Œ")); + Task task2 = repository.save(Task_생성(section, "의자 정리")); + repository.saveAll(List.of(RunningTask_생성(task1.getId(), false), + RunningTask_생성(task2.getId(), false))); + } + + @Test + void ν•΄λ‹Ή_Section의_RunningTaskλ₯Ό_λͺ¨λ‘_μ²΄ν¬ν•œλ‹€() { + taskService.checkRunningTasksInSection(host.getId(), section.getId()); + List actual = repository.findAll(RunningTask.class); + + assertThat(actual).extracting("isChecked") + .containsExactly(true, true); + } + } + + @Nested + class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Hostλ₯Ό_μž…λ ₯ν•˜λŠ”_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long JOB_ID = 1L; + + @Test + void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + assertThatThrownBy(() -> taskService.checkRunningTasksInSection(NON_EXIST_HOST_ID, JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + } + } + + @Nested + class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_Section을_μž…λ ₯ν•˜λŠ”_경우 { + + private static final long NON_EXIST_SECTION_ID = 0L; + + private long hostId; + + @BeforeEach + void setUp() { + hostId = repository.save(Host_생성("1234", 1111L)) + .getId(); + } + + @Test + void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + assertThatThrownBy(() -> taskService.checkRunningTasksInSection(hostId, NON_EXIST_SECTION_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ΅¬μ—­μž…λ‹ˆλ‹€."); + } + } + + @Nested + class λ‹€λ₯Έ_Host의_Section을_μž…λ ₯ν•˜λŠ”_경우 { + + private Host anotherHost; + private Section section; + + @BeforeEach + void setUp() { + Host host = repository.save(Host_생성("1234", 1234L)); + anotherHost = repository.save(Host_생성("1234", 2345L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + repository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + } + + @Test + void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + assertThatThrownBy(() -> taskService.checkRunningTasksInSection(anotherHost.getId(), section.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ΅¬μ—­μž…λ‹ˆλ‹€."); + } + } + + @Nested + class ν•΄λ‹Ή_Section의_진행쀑인_RunningTaskκ°€_μ—†λŠ”_경우 { + + private Host host; + private Section section; + + @BeforeEach + void setUp() { + host = repository.save(Host_생성("1234", 1234L)); + Space space = repository.save(Space_생성(host, "μž μ‹€")); + Job job = repository.save(Job_생성(space, "μ²­μ†Œ")); + section = repository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + repository.saveAll(List.of(Task_생성(section, "책상 μ²­μ†Œ"), Task_생성(section, "의자 λ„£κΈ°"))); + } + + @Test + void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + assertThatThrownBy(() -> taskService.checkRunningTasksInSection(host.getId(), section.getId())) + .isInstanceOf(BusinessException.class) + .hasMessageContaining("ν˜„μž¬ 진행쀑인 RunningTaskκ°€ μ—†μŠ΅λ‹ˆλ‹€"); } } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/support/LoggingFormatConverterTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/support/LoggingFormatConverterTest.java new file mode 100644 index 00000000..5ed80e8d --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/support/LoggingFormatConverterTest.java @@ -0,0 +1,22 @@ +package com.woowacourse.gongcheck.core.application.support; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class LoggingFormatConverterTest { + + @Test + void Longνƒ€μž…_리슀트λ₯Ό_μž…λ ₯λ°›μ•„_λ¬Έμžμ—΄λ‘œ_λ°˜ν™˜ν•œλ‹€() { + List ids = List.of(1L, 2L, 3L, 4L); + String result = LoggingFormatConverter.convertIdsToString(ids); + + List expected = ids.stream() + .map(String::valueOf) + .collect(Collectors.toList()); + + assertThat(result).contains(expected); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostRepositoryTest.java index ceefff8e..41e1412b 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostRepositoryTest.java @@ -79,7 +79,7 @@ class μ‘΄μž¬ν•˜μ§€μ•ŠλŠ”_Host의_idλ₯Ό_μž…λ ₯λ°›λŠ”_경우 { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> hostRepository.getById(NON_EXIST_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } } @@ -114,7 +114,7 @@ class μ‘΄μž¬ν•˜μ§€μ•ŠλŠ”_Host의_githubIdλ₯Ό_μž…λ ₯λ°›λŠ”_경우 { void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> hostRepository.getByGithubId(NON_EXIST_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” ν˜ΈμŠ€νŠΈμž…λ‹ˆλ‹€."); } } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostTest.java index a1777563..acf583e6 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostTest.java @@ -30,7 +30,7 @@ class λΉ„λ°€λ²ˆν˜Έλ₯Ό_검사할_λ•Œ { assertThatThrownBy(() -> host.checkPassword(spacePassword)) .isInstanceOf(BusinessException.class) - .hasMessage("곡간 λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯ν•˜μ‹  λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("곡간 λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯ν•˜μ‹  λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); } @Test diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/SpacePasswordTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/SpacePasswordTest.java index 53bdc882..81820484 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/SpacePasswordTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/SpacePasswordTest.java @@ -23,7 +23,7 @@ class SpacePasswordTest { void λ„€μžλ¦¬_μˆ«μžκ°€_μ•„λ‹ˆλ©΄_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€(final String password) { assertThatThrownBy(() -> new SpacePassword(password)) .isInstanceOf(BusinessException.class) - .hasMessage("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€."); + .hasMessageContaining("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€."); } @Test diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFileTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFileTest.java index 5d4451e5..18a7fb3c 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFileTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFileTest.java @@ -23,7 +23,7 @@ class null인_multipartFile이_μž…λ ₯된_경우 { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> ImageFile.from(null)) .isInstanceOf(BusinessException.class) - .hasMessage("이미지 νŒŒμΌμ€ null이 λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("이미지 νŒŒμΌμ€ null이 λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); } } @@ -44,7 +44,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> ImageFile.from(emptyFile)) .isInstanceOf(BusinessException.class) - .hasMessage("이미지 νŒŒμΌμ€ λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("이미지 νŒŒμΌμ€ λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); } } @@ -65,7 +65,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> ImageFile.from(nullNameFile)) .isInstanceOf(BusinessException.class) - .hasMessage("이미지 파일 이름은 λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("이미지 파일 이름은 λΉˆκ°’μ΄ λ“€μ–΄μ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€."); } } @@ -86,7 +86,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> ImageFile.from(textFile)) .isInstanceOf(BusinessException.class) - .hasMessage("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€."); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobRepositoryTest.java index 7a5603a0..ce9f6cd7 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobRepositoryTest.java @@ -139,7 +139,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobRepository.getBySpaceHostAndId(otherHost, job.getId())) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } } @@ -200,7 +200,7 @@ class μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_jobIdλ₯Ό_μž…λ ₯_λ°›λŠ”_경우 { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> jobRepository.getById(NON_EXIST_JOB_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobTest.java index edafef6a..12149640 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.woowacourse.gongcheck.core.domain.vo.Name; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -9,7 +10,9 @@ class JobTest { @Test void SlackUrl을_μˆ˜μ •ν•œλ‹€() { - Job job = Job.builder().slackUrl("https://slackurl.com").build(); + Job job = Job.builder() + .slackUrl("https://slackurl.com") + .build(); job.changeSlackUrl("https://newslackurl.com"); @@ -36,4 +39,17 @@ void nonExist() { assertThat(job.hasUrl()).isFalse(); } } + + @Test + void 이름을_μˆ˜μ •ν•œλ‹€() { + Name name = new Name("μˆ˜μ • μ „ 이름"); + Name changeName = new Name("μˆ˜μ • ν›„ 이름"); + Job job = Job.builder() + .name(name) + .build(); + + job.changeName(changeName); + + assertThat(job.getName()).isEqualTo(changeName); + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionRepositoryTest.java index 74ea0029..4457b1ee 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionRepositoryTest.java @@ -5,6 +5,7 @@ import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.woowacourse.gongcheck.config.JpaConfig; import com.woowacourse.gongcheck.core.domain.host.Host; @@ -14,8 +15,14 @@ import com.woowacourse.gongcheck.core.domain.space.Space; import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.exception.NotFoundException; import java.time.LocalDateTime; import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; @@ -23,6 +30,8 @@ @DataJpaTest @Import(JpaConfig.class) +@DisplayName("SectionRepositoryTest 클래슀") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class SectionRepositoryTest { @Autowired @@ -79,4 +88,84 @@ class SectionRepositoryTest { assertThat(result).containsExactly(section1, section2); } + + @Nested + class findAllBySpaceHostAndSpace_λ©”μ†Œλ“œλŠ” { + + @Nested + class Host와_Spaceλ₯Ό_μž…λ ₯받은_경우 { + + private Host host; + private Space space; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + Job job1 = Job_생성(space, "μ˜€ν”ˆ"); + Job job2 = Job_생성(space, "μ²­μ†Œ"); + Job job3 = Job_생성(space, "마감"); + jobRepository.saveAll(List.of(job1, job2, job3)); + } + + @Test + void μ—°κ΄€λœ_Job_λͺ©λ‘μ„_μ‘°νšŒν•œλ‹€() { + List result = jobRepository.findAllBySpaceHostAndSpace(host, space); + + assertThat(result).hasSize(3) + .extracting("name") + .extracting("value") + .containsExactly("μ˜€ν”ˆ", "μ²­μ†Œ", "마감"); + } + } + } + + @Nested + class getByJobSpaceHostAndId_λ©”μ†Œλ“œλŠ” { + + @Nested + class Host와_SectionIdλ₯Ό_μž…λ ₯λ°›λŠ”_경우 { + + private Host host; + private Section section; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + Job job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + section = sectionRepository.save(Section_생성(job, "λŒ€κ°•μ˜μ‹€")); + } + + @Test + void ν•΄λ‹Ήλ˜λŠ”_Section을_μ‘°νšŒν•œλ‹€() { + Section result = sectionRepository.getByJobSpaceHostAndId(host, section.getId()); + + assertThat(result).isEqualTo(section); + } + } + + @Nested + class μž…λ ₯받은_Host와_SectionIdκ°€_μ—°κ΄€λ˜μ§€_μ•Šμ€_경우 { + + private Host otherHost; + private Section section; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + otherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + Job job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + section = sectionRepository.save(Section_생성(job, "λŒ€κ°•μ˜μ‹€")); + } + + @Test + void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + assertThatThrownBy(() -> sectionRepository.getByJobSpaceHostAndId(otherHost, section.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ΅¬μ—­μž…λ‹ˆλ‹€."); + } + } + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepositoryTest.java index 61c1f13b..3b824a75 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepositoryTest.java @@ -108,7 +108,7 @@ void setUp() { assertThatThrownBy(() -> spaceRepository.getByHostAndId(anotherHost, spaceId)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } @@ -129,7 +129,7 @@ void setUp() { assertThatThrownBy(() -> spaceRepository.getByHostAndId(host, NON_EXIST_SPACE_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” κ³΅κ°„μž…λ‹ˆλ‹€."); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionTest.java index 08e52063..225b7c41 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionTest.java @@ -13,6 +13,6 @@ class SubmissionTest { .author("12345678901") .build()) .isInstanceOf(BusinessException.class) - .hasMessage("제좜자 이름은 10자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); + .hasMessageContaining("제좜자 이름은 10자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskSseEmitterContainerTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskSseEmitterContainerTest.java new file mode 100644 index 00000000..370388c5 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskSseEmitterContainerTest.java @@ -0,0 +1,46 @@ +package com.woowacourse.gongcheck.core.domain.task; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +class RunningTaskSseEmitterContainerTest { + + private RunningTaskSseEmitterContainer container; + + @BeforeEach + void setUp() { + container = new RunningTaskSseEmitterContainer(); + } + + @Test + void SseEmitterλ₯Ό_μƒμ„±ν•œλ‹€() { + SseEmitter emitter = container.createEmitterWithConnectionEvent(1L, null); + + assertThat(emitter).isNotNull(); + } + + @Test + void SseEmitterλ₯Ό_생성할_λ•Œ_μž…λ ₯받은_id와_λ§€ν•‘ν•˜μ—¬_μ €μž₯ν•œλ‹€() { + Long jobId = 1L; + SseEmitter emitter = container.createEmitterWithConnectionEvent(jobId, null); + + List emittersById = container.getValuesByJobId(jobId); + + assertThat(emittersById).containsExactly(emitter); + } + + @Test + void μž…λ ₯받은_id와_emitter에_ν•΄λ‹Ήν•˜λŠ”_emitterλ₯Ό_μ œκ±°ν•œλ‹€() { + Long jobId = 1L; + SseEmitter emitter = container.createEmitterWithConnectionEvent(jobId, null); + + container.deleteByJobId(jobId, emitter); + + List emitters = container.getValuesByJobId(jobId); + assertThat(emitters).isEmpty(); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskTest.java index 9df1ba1f..a83b0ee4 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -18,4 +19,15 @@ class RunningTaskTest { assertThat(runningTask.isChecked()).isEqualTo(expected); } + + @Test + void RunningTaskλ₯Ό_μ²΄ν¬ν•œλ‹€() { + RunningTask runningTask = RunningTask.builder() + .isChecked(false) + .build(); + + runningTask.check(); + + assertThat(runningTask.isChecked()).isTrue(); + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTasksTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTasksTest.java index 831cb361..06ebd593 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTasksTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTasksTest.java @@ -1,12 +1,16 @@ package com.woowacourse.gongcheck.core.domain.task; import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.woowacourse.gongcheck.exception.BusinessException; import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; class RunningTasksTest { @@ -27,6 +31,21 @@ class RunningTasksTest { assertThatThrownBy(runningTasks::validateCompletion) .isInstanceOf(BusinessException.class) - .hasMessage("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€."); + .hasMessageContaining("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€."); + } + + @ParameterizedTest + @CsvSource(value = {"false,false", "true,false"}) + void RunningTask듀을_λͺ¨λ‘_μ²΄ν¬ν•œλ‹€(boolean isChecked1, boolean isChecked2) { + RunningTask runningTask1 = RunningTask_생성(1L, isChecked1); + RunningTask runningTask2 = RunningTask_생성(2L, isChecked2); + RunningTasks runningTasks = new RunningTasks(List.of(runningTask1, runningTask2)); + + runningTasks.check(); + + assertAll( + () -> assertThat(runningTask1.isChecked()).isTrue(), + () -> assertThat(runningTask2.isChecked()).isTrue() + ); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskRepositoryTest.java index 6facd3ed..05f54e7f 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskRepositoryTest.java @@ -8,6 +8,7 @@ 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.groups.Tuple.tuple; import com.woowacourse.gongcheck.config.JpaConfig; import com.woowacourse.gongcheck.core.domain.host.Host; @@ -177,7 +178,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskRepository.getBySectionJobSpaceHostAndId(host, NON_EXIST_TASK_ID)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } @@ -202,7 +203,7 @@ void setUp() { void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { assertThatThrownBy(() -> taskRepository.getBySectionJobSpaceHostAndId(anotherHost, taskId)) .isInstanceOf(NotFoundException.class) - .hasMessage("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€."); } } } @@ -266,4 +267,43 @@ void setUp() { } } } + + @Nested + class findAllBySection_λ©”μ†Œλ“œλŠ” { + + @Nested + class μž…λ ₯받은_Job이_Taskλ₯Ό_가지고_μžˆλŠ”_경우 { + + private Job job; + private Section expectedSection1; + private Section expectedSection2; + private List expected; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "μž μ‹€")); + job = jobRepository.save(Job_생성(space, "μ²­μ†Œ")); + expectedSection1 = sectionRepository.save(Section_생성(job, "νŠΈλž™λ£Έ")); + expectedSection2 = sectionRepository.save(Section_생성(job, "κ΅Ώμƒ· κ°•μ˜μž₯")); + expected = List.of( + Task_생성(expectedSection1, "책상 μ²­μ†Œ"), Task_생성(expectedSection1, "빈백 정리"), + Task_생성(expectedSection2, "책상 μ²­μ†Œ"), Task_생성(expectedSection2, "의자 λ„£κΈ°") + ); + taskRepository.saveAll(expected); + } + + @Test + void 가지고_μžˆλŠ”_λͺ¨λ“ _Taskλ₯Ό_λ°˜ν™˜ν•œλ‹€() { + List actual = taskRepository.findAllBySectionJob(job); + + assertThat(actual).hasSize(expected.size()) + .extracting("section", "name") + .containsExactly(tuple(expectedSection1, new Name("책상 μ²­μ†Œ")), + tuple(expectedSection1, new Name("빈백 정리")), + tuple(expectedSection2, new Name("책상 μ²­μ†Œ")), + tuple(expectedSection2, new Name("의자 λ„£κΈ°"))); + } + } + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/DescriptionTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/DescriptionTest.java index 06f62067..8960448c 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/DescriptionTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/DescriptionTest.java @@ -17,7 +17,7 @@ class DescriptionTest { String value = sb.toString(); assertThatThrownBy(() -> new Description(value)) .isInstanceOf(BusinessException.class) - .hasMessage("μ„€λͺ…은 128자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); + .hasMessageContaining("μ„€λͺ…은 128자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); } @Test diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/NameTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/NameTest.java index c536e6f9..92fe4580 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/NameTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/NameTest.java @@ -12,14 +12,14 @@ class NameTest { void 이름은_10자_μ΄ν•˜μ—¬μ•Ό_ν•œλ‹€() { assertThatThrownBy(() -> new Name("12345678901")) .isInstanceOf(BusinessException.class) - .hasMessage("이름은 10자 μ΄ν•˜μ—¬μ•Όν•©λ‹ˆλ‹€."); + .hasMessageContaining("이름은 10자 μ΄ν•˜μ—¬μ•Όν•©λ‹ˆλ‹€."); } @Test void 이름은_빈_값일_수_μ—†λ‹€() { assertThatThrownBy(() -> new Name("")) .isInstanceOf(BusinessException.class) - .hasMessage("이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€."); } @Test diff --git a/backend/src/test/java/com/woowacourse/gongcheck/cucumber/AcceptanceContext.java b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/AcceptanceContext.java new file mode 100644 index 00000000..a62db647 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/AcceptanceContext.java @@ -0,0 +1,38 @@ +package com.woowacourse.gongcheck.cucumber; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import java.util.HashMap; +import java.util.Map; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Component +@Scope(scopeName = "cucumber-glue") +public class AcceptanceContext { + + public RequestSpecification request; + public Response response; + public Map storage; + + public AcceptanceContext() { + reset(); + } + + private void reset() { + request = null; + response = null; + storage = new HashMap<>(); + } + + public void invokeHttpPost(String path, Object data) { + request = RestAssured + .given().log().all() + .body(data) + .contentType(ContentType.JSON); + response = request.post(path); + response.then().log().all(); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/cucumber/AcceptanceSteps.java b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/AcceptanceSteps.java new file mode 100644 index 00000000..bc43cac1 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/AcceptanceSteps.java @@ -0,0 +1,13 @@ +package com.woowacourse.gongcheck.cucumber; + +import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; +import org.springframework.beans.factory.annotation.Autowired; + +public class AcceptanceSteps { + + @Autowired + public AcceptanceContext context; + + @Autowired + public EntranceCodeProvider entranceCodeProvider; +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/cucumber/CucumberIntegrationTest.java b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/CucumberIntegrationTest.java new file mode 100644 index 00000000..6188d3cd --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/CucumberIntegrationTest.java @@ -0,0 +1,29 @@ +package com.woowacourse.gongcheck.cucumber; + +import com.woowacourse.gongcheck.acceptance.DatabaseInitializer; +import io.cucumber.java.Before; +import io.cucumber.spring.CucumberContextConfiguration; +import io.restassured.RestAssured; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +@CucumberContextConfiguration +@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) +@ActiveProfiles("test") +public class CucumberIntegrationTest { + + @LocalServerPort + private int port; + + @Autowired + private DatabaseInitializer databaseInitializer; + + @Before("@api") + public void setupForApi() { + RestAssured.port = port; + databaseInitializer.initTable(); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/cucumber/steps/GuestAuthStepDefinitions.java b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/steps/GuestAuthStepDefinitions.java new file mode 100644 index 00000000..6a467a50 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/cucumber/steps/GuestAuthStepDefinitions.java @@ -0,0 +1,31 @@ +package com.woowacourse.gongcheck.cucumber.steps; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.cucumber.AcceptanceSteps; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.springframework.http.HttpStatus; + +public class GuestAuthStepDefinitions extends AcceptanceSteps { + + @When("Space의 νŒ¨μŠ€μ›Œλ“œλ₯Ό μž…λ ₯ν•˜λ©΄") + public void Space의_νŒ¨μŠ€μ›Œλ“œλ₯Ό_μž…λ ₯ν•˜λ©΄() { + GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); + String entranceCode = entranceCodeProvider.createEntranceCode(1L); + + context.invokeHttpPost("/api/hosts/" + entranceCode + "/enter/", guestEnterRequest); + } + + @Then("μ—‘μ„ΈμŠ€ 토큰을 λ°›λŠ”λ‹€") + public void μ—‘μ„ΈμŠ€_토큰을_λ°›λŠ”λ‹€() { + assertAll( + () -> assertThat(context.response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat( + context.response.body().jsonPath().getObject(".", GuestTokenResponse.class)).isNotNull() + ); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java index 00776280..6ce39ad0 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java @@ -6,13 +6,14 @@ import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; import com.woowacourse.gongcheck.auth.application.GuestAuthService; import com.woowacourse.gongcheck.auth.application.HostAuthService; +import com.woowacourse.gongcheck.auth.application.JwtTokenProvider; import com.woowacourse.gongcheck.auth.domain.AuthenticationContext; import com.woowacourse.gongcheck.auth.presentation.GuestAuthController; import com.woowacourse.gongcheck.auth.presentation.HostAuthController; -import com.woowacourse.gongcheck.core.application.AlertService; import com.woowacourse.gongcheck.core.application.HostService; import com.woowacourse.gongcheck.core.application.ImageUploader; import com.woowacourse.gongcheck.core.application.JobService; +import com.woowacourse.gongcheck.core.application.NotificationService; import com.woowacourse.gongcheck.core.application.SpaceService; import com.woowacourse.gongcheck.core.application.SubmissionService; import com.woowacourse.gongcheck.core.application.TaskService; @@ -22,7 +23,8 @@ import com.woowacourse.gongcheck.core.presentation.SpaceController; import com.woowacourse.gongcheck.core.presentation.SubmissionController; import com.woowacourse.gongcheck.core.presentation.TaskController; -import com.woowacourse.gongcheck.infrastructure.jwt.JjwtTokenProvider; +import com.woowacourse.gongcheck.core.presentation.filter.RequestContext; +import com.woowacourse.gongcheck.documentation.support.ErrorCodeController; import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification; import org.junit.jupiter.api.BeforeEach; @@ -42,7 +44,8 @@ TaskController.class, SubmissionController.class, HostController.class, - ImageUploadController.class + ImageUploadController.class, + ErrorCodeController.class }) @ExtendWith(RestDocumentationExtension.class) class DocumentationTest { @@ -68,13 +71,13 @@ class DocumentationTest { protected SubmissionService submissionService; @MockBean - protected AlertService alertService; + protected NotificationService notificationService; @MockBean protected HostService hostService; @MockBean - protected JjwtTokenProvider jwtTokenProvider; + protected JwtTokenProvider jwtTokenProvider; @MockBean protected EntranceCodeProvider entranceCodeProvider; @@ -85,6 +88,9 @@ class DocumentationTest { @MockBean protected ImageUploader imageUploader; + @MockBean + private RequestContext requestContext; + @BeforeEach void setDocsGiven(final WebApplicationContext webApplicationContext, final RestDocumentationContextProvider restDocumentation) { diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/ErrorCodeDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/ErrorCodeDocumentation.java new file mode 100644 index 00000000..2ece1dab --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/ErrorCodeDocumentation.java @@ -0,0 +1,19 @@ +package com.woowacourse.gongcheck.documentation; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; + +import com.woowacourse.gongcheck.documentation.support.ErrorCodeFieldsSnippet; +import java.io.IOException; +import org.junit.jupiter.api.Test; + +class ErrorCodeDocumentation extends DocumentationTest { + + @Test + public void errorCodes() throws IOException { + ErrorCodeFieldsSnippet errorCodeFieldsSnippet = new ErrorCodeFieldsSnippet("error-code", "error-code-template"); + docsGiven + .when().get("/test/errorCodes") + .then().log().all() + .apply(document("errorCode", errorCodeFieldsSnippet)); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java index 59e9be75..7dcf94f6 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java @@ -12,10 +12,12 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.snippet.Attributes.key; import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.ErrorResponse; import io.restassured.module.mockmvc.response.MockMvcResponse; import io.restassured.response.ExtractableResponse; @@ -46,9 +48,12 @@ class 게슀트_토큰을_μš”μ²­ν•œλ‹€ { pathParameters( parameterWithName("entranceCode").description("ν˜ΈμŠ€νŠΈκ°€ μ œκ³΅ν•˜λŠ” μž…μž₯μ½”λ“œ")), requestFields( - fieldWithPath("password").type(JsonFieldType.STRING).description("곡간 λΉ„λ°€λ²ˆν˜Έ")), + fieldWithPath("password").type(JsonFieldType.STRING).description("곡간 λΉ„λ°€λ²ˆν˜Έ") + .attributes(key("length").value(4)) + .attributes(key("nullable").value(true))), responseFields( - fieldWithPath("token").type(JsonFieldType.STRING).description("Access Token") + fieldWithPath("token").type(JsonFieldType.STRING) + .description("Access Token") ) )) .statusCode(HttpStatus.OK.value()); @@ -57,7 +62,7 @@ class 게슀트_토큰을_μš”μ²­ν•œλ‹€ { @Test void λΉ„λ°€λ²ˆν˜Έ_길이가_λ§žμ§€_μ•Šμ„_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("12345"); - doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.")) + doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.", ErrorCode.SP03)) .when(guestAuthService).createToken(anyLong(), any()); ExtractableResponse response = docsGiven @@ -70,15 +75,15 @@ class 게슀트_토큰을_μš”μ²­ν•œλ‹€ { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()), - () -> assertThat(response.jsonPath().getObject(".", ErrorResponse.class).getMessage()) - .isEqualTo("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.") + () -> assertThat(response.jsonPath().getObject(".", ErrorResponse.class).getErrorCode()) + .isEqualTo(ErrorCode.SP03.name()) ); } @Test void λΉ„λ°€λ²ˆν˜Έκ°€_μˆ«μžκ°€_아닐_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("abcd"); - doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.")) + doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.", ErrorCode.SP03)) .when(guestAuthService).createToken(anyLong(), any()); ExtractableResponse response = docsGiven @@ -91,8 +96,8 @@ class 게슀트_토큰을_μš”μ²­ν•œλ‹€ { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()), - () -> assertThat(response.jsonPath().getObject(".", ErrorResponse.class).getMessage()) - .isEqualTo("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.") + () -> assertThat(response.jsonPath().getObject(".", ErrorResponse.class).getErrorCode()) + .isEqualTo(ErrorCode.SP03.name()) ); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java index 76ab2288..9efcade0 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java @@ -6,6 +6,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.snippet.Attributes.key; import com.woowacourse.gongcheck.auth.application.response.TokenResponse; import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; @@ -33,7 +34,8 @@ class 호슀트_토큰을_μš”μ²­ν•œλ‹€ { .then().log().all() .apply(document("hosts/auth/success", requestFields( - fieldWithPath("code").type(JsonFieldType.STRING).description("Authorization Code")), + fieldWithPath("code").type(JsonFieldType.STRING).description("Authorization Code") + .attributes(key("nullable").value(true))), responseFields( fieldWithPath("token").type(JsonFieldType.STRING).description("Access Token"), fieldWithPath("alreadyJoin").type(JsonFieldType.BOOLEAN).description("κ°€μž… μ—¬λΆ€") diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java index dcb95bca..d7499932 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java @@ -2,6 +2,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -9,8 +10,10 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; @@ -25,6 +28,7 @@ class λΉ„λ°€λ²ˆν˜Έ_λ³€κ²½ { @Test void λΉ„λ°€λ²ˆν˜Έ_변경에_μ„±κ³΅ν•œλ‹€() { doNothing().when(hostService).changeSpacePassword(anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven @@ -39,9 +43,10 @@ class λΉ„λ°€λ²ˆν˜Έ_λ³€κ²½ { @Test void λΉ„λ°€λ²ˆν˜Έ_길이가_λ§žμ§€_μ•ŠλŠ”_경우_변경에_μ‹€νŒ¨ν•œλ‹€() { - doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.")) + doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.", ErrorCode.SP03)) .when(hostService) .changeSpacePassword(anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven @@ -56,9 +61,10 @@ class λΉ„λ°€λ²ˆν˜Έ_λ³€κ²½ { @Test void λΉ„λ°€λ²ˆν˜Έ_ν˜•μ‹μ΄_λ§žμ§€_μ•ŠλŠ”_경우_변경에_μ‹€νŒ¨ν•œλ‹€() { - doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.")) + doThrow(new BusinessException("λΉ„λ°€λ²ˆν˜ΈλŠ” λ„€ 자리 숫자둜 이루어져야 ν•©λ‹ˆλ‹€.", ErrorCode.SP03)) .when(hostService) .changeSpacePassword(anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven @@ -75,6 +81,7 @@ class λΉ„λ°€λ²ˆν˜Έ_λ³€κ²½ { @Test void 호슀트_μž…μž₯μ½”λ“œλ₯Ό_μ‘°νšŒν•œλ‹€() { when(hostService.createEntranceCode(anyLong())).thenReturn("random_entrance_code"); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/ImageUploadDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/ImageUploadDocumentation.java index 05e44329..f41f583b 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/ImageUploadDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/ImageUploadDocumentation.java @@ -11,7 +11,9 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.snippet.Attributes.key; +import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; import java.io.File; import java.io.IOException; @@ -27,6 +29,7 @@ class ImageUploadDocumentation extends DocumentationTest { File fakeImage = createFakeImage(); when(imageUploader.upload(any(), anyString())) .thenReturn(ImageUrlResponse.from("https://image.gongcheck.com/12sdf124sx.jpg")); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven @@ -37,7 +40,8 @@ class ImageUploadDocumentation extends DocumentationTest { .then().log().all() .apply(document("image-upload", requestParts(partWithName("image") - .description("The version of the image")), + .description("The version of the image") + .attributes(key("nullable").value(true))), responseFields( fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("μ €μž₯된 Image Url") ) diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java index f0d4e10e..e4a6c5df 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java @@ -6,6 +6,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -16,7 +17,9 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.snippet.Attributes.key; +import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.core.application.response.JobsResponse; import com.woowacourse.gongcheck.core.application.response.SlackUrlResponse; import com.woowacourse.gongcheck.core.domain.host.Host; @@ -26,6 +29,7 @@ import com.woowacourse.gongcheck.core.presentation.request.SlackUrlChangeRequest; import com.woowacourse.gongcheck.core.presentation.request.TaskCreateRequest; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import io.restassured.module.mockmvc.response.MockMvcResponse; import io.restassured.response.ExtractableResponse; import java.util.List; @@ -86,6 +90,7 @@ class Job을_생성_μ‹œ { @Test void Job을_μƒμ„±ν•œλ‹€() { + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); JobCreateRequest request = new JobCreateRequest("μ²­μ†Œ", sections); @@ -99,19 +104,29 @@ class Job을_생성_μ‹œ { pathParameters( parameterWithName("spaceId").description("Job을 생성할 Space Id")), requestFields( - fieldWithPath("name").type(JsonFieldType.STRING).description("Job 이름"), + fieldWithPath("name").type(JsonFieldType.STRING) + .description("Job 이름") + .attributes(key("length").value(10)), fieldWithPath("sections.[].name").type(JsonFieldType.STRING) - .description("Section 이름"), + .description("Section 이름") + .attributes(key("length").value(10)), fieldWithPath("sections.[].description").type(JsonFieldType.STRING) - .description("Section μ„€λͺ…"), + .description("Section μ„€λͺ…") + .attributes(key("length").value(128)) + .attributes(key("nullable").value(true)), fieldWithPath("sections.[].imageUrl").type(JsonFieldType.STRING) - .description("Section Image Url"), + .description("Section Image Url") + .attributes(key("nullable").value(true)), fieldWithPath("sections.[].tasks.[].name").type(JsonFieldType.STRING) - .description("Task 이름"), + .description("Task 이름") + .attributes(key("length").value(10)), fieldWithPath("sections.[].tasks.[].description").type(JsonFieldType.STRING) - .description("Task μ„€λͺ…"), + .description("Task μ„€λͺ…") + .attributes(key("length").value(128)) + .attributes(key("nullable").value(true)), fieldWithPath("sections.[].tasks.[].imageUrl").type(JsonFieldType.STRING) .description("Task Image Url") + .attributes(key("nullable").value(true)) ) )) .statusCode(HttpStatus.CREATED.value()); @@ -119,9 +134,10 @@ class Job을_생성_μ‹œ { @Test void Job의_이름_길이가_μ˜¬λ°”λ₯΄μ§€_μ•Šμ„_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(BusinessException.class) + doThrow(new BusinessException("이름이 곡백인 경우", ErrorCode.N001)) .when(jobService) .createJob(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); JobCreateRequest wrongRequest = new JobCreateRequest("10μžμ΄ˆκ³Όμ˜μ΄λ¦„μ€μ•ˆλΌ", sections); @@ -140,9 +156,10 @@ class Job을_생성_μ‹œ { @Test void Section_이름_길이가_μ˜¬λ°”λ₯΄μ§€_μ•Šμ„_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(BusinessException.class) + doThrow(new BusinessException("이름이 곡백인 경우", ErrorCode.N001)) .when(jobService) .createJob(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); List sections = List.of(new SectionCreateRequest("10μžμ΄ˆκ³Όμ˜μ΄λ¦„μ€μ•ˆλΌ", "λŒ€κ°•μ˜μ‹€ μ„€λͺ…", @@ -163,9 +180,10 @@ class Job을_생성_μ‹œ { @Test void Task_이름_길이가_μ˜¬λ°”λ₯΄μ§€_μ•Šμ„_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(BusinessException.class) + doThrow(new BusinessException("이름이 곡백인 경우", ErrorCode.N001)) .when(jobService) .createJob(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); List tasks1 = List @@ -212,7 +230,9 @@ class Job을_μˆ˜μ •_μ‹œ { .of(new SectionCreateRequest("λŒ€κ°•μ˜μ‹€", "λŒ€κ°•μ˜μ‹€ μ„€λͺ…", "https://image.gongcheck.shop/degang123", tasks1), new SectionCreateRequest("μ†Œκ°•μ˜μ‹€", "μ†Œκ°•μ˜μ‹€ μ„€λͺ…", "https://image.gongcheck.shop/sogang123", tasks2)); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + JobCreateRequest request = new JobCreateRequest("μ²­μ†Œ", sections); docsGiven @@ -225,19 +245,28 @@ class Job을_μˆ˜μ •_μ‹œ { pathParameters( parameterWithName("jobId").description("μˆ˜μ •ν•  Job Id")), requestFields( - fieldWithPath("name").type(JsonFieldType.STRING).description("Job 이름"), + fieldWithPath("name").type(JsonFieldType.STRING).description("Job 이름") + .attributes(key("length").value(10)), fieldWithPath("sections.[].name").type(JsonFieldType.STRING) - .description("Section 이름"), + .description("Section 이름") + .attributes(key("length").value(10)), fieldWithPath("sections.[].description").type(JsonFieldType.STRING) - .description("Section μ„€λͺ…"), + .description("Section μ„€λͺ…") + .attributes(key("length").value(128)) + .attributes(key("nullable").value(true)), fieldWithPath("sections.[].imageUrl").type(JsonFieldType.STRING) - .description("Section Image Url"), + .description("Section Image Url") + .attributes(key("nullable").value(true)), fieldWithPath("sections.[].tasks.[].name").type(JsonFieldType.STRING) - .description("Task 이름"), + .description("Task 이름") + .attributes(key("length").value(10)), fieldWithPath("sections.[].tasks.[].description").type(JsonFieldType.STRING) - .description("Task μ„€λͺ…"), + .description("Task μ„€λͺ…") + .attributes(key("length").value(128)) + .attributes(key("nullable").value(true)), fieldWithPath("sections.[].tasks.[].imageUrl").type(JsonFieldType.STRING) .description("Task Image Url") + .attributes(key("nullable").value(true)) ) )) .statusCode(HttpStatus.NO_CONTENT.value()); @@ -247,9 +276,10 @@ class Job을_μˆ˜μ •_μ‹œ { @NullSource @ValueSource(strings = {"", "10μžμ΄ˆκ³Όμ˜μ΄λ¦„μ€μ•ˆλΌ"}) void Job_이름이_1κΈ€μž_미만_10κΈ€μž_초과_nul_인_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€(final String input) { - doThrow(BusinessException.class) + doThrow(new BusinessException("이름이 곡백인 경우", ErrorCode.N001)) .when(jobService) .updateJob(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); JobCreateRequest wrongRequest = new JobCreateRequest(input, sections); @@ -270,10 +300,12 @@ class Job을_μˆ˜μ •_μ‹œ { @NullSource @ValueSource(strings = {"", "10μžμ΄ˆκ³Όμ˜μ΄λ¦„μ€μ•ˆλΌ"}) void Section_이름이_1κΈ€μž_미만_10κΈ€μž_초과_null_일_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€(final String input) { - doThrow(BusinessException.class) + doThrow(new BusinessException("이름이 곡백인 경우", ErrorCode.N001)) .when(jobService) .updateJob(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + List sections = List .of(new SectionCreateRequest(input, "λŒ€κ°•μ˜μ‹€ μ„€λͺ…", "https://image.gongcheck.shop/degang123", tasks1)); JobCreateRequest wrongRequest = new JobCreateRequest("μ²­μ†Œ", sections); @@ -294,9 +326,10 @@ class Job을_μˆ˜μ •_μ‹œ { @NullSource @ValueSource(strings = {"", "10μžμ΄ˆκ³Όμ˜μ΄λ¦„μ€μ•ˆλΌ"}) void Task_이름이_1κΈ€μž_미만_10κΈ€μž_μ΄ˆκ³Όν•˜κ±°λ‚˜_null일_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€(final String input) { - doThrow(BusinessException.class) + doThrow(new BusinessException("이름이 곡백인 경우", ErrorCode.N001)) .when(jobService) .updateJob(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); List tasks1 = List.of( @@ -321,6 +354,7 @@ class Job을_μˆ˜μ •_μ‹œ { @Test void Job을_μ‚­μ œν•œλ‹€() { doNothing().when(jobService).removeJob(anyLong(), anyLong()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven @@ -337,6 +371,7 @@ class Job을_μˆ˜μ •_μ‹œ { @Test void Job의_Slack_Url을_μ‘°νšŒν•œλ‹€() { when(jobService.findSlackUrl(anyLong(), anyLong())).thenReturn(new SlackUrlResponse("http://slackurl.com")); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); ExtractableResponse response = docsGiven @@ -360,6 +395,7 @@ class SlackUrl_μˆ˜μ •_μ‹œ { @Test void μ •μƒμ μœΌλ‘œ_μˆ˜μ •ν•œλ‹€() { doNothing().when(jobService).changeSlackUrl(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SlackUrlChangeRequest request = new SlackUrlChangeRequest("https://newslackurl.com"); @@ -374,15 +410,19 @@ class SlackUrl_μˆ˜μ •_μ‹œ { pathParameters( parameterWithName("jobId").description("Slack Url을 μˆ˜μ •ν•  Job Id")), requestFields( - fieldWithPath("slackUrl").type(JsonFieldType.STRING).description("μˆ˜μ •ν•  Slack Url") + fieldWithPath("slackUrl").type(JsonFieldType.STRING) + .description("μˆ˜μ •ν•  Slack Url") + .attributes(key("nullable").value(true)) ) )) .statusCode(HttpStatus.NO_CONTENT.value()); + } @Test void null이_전달될_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { doNothing().when(jobService).changeSlackUrl(anyLong(), anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SlackUrlChangeRequest wrongRequest = new SlackUrlChangeRequest(null); diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java index 8c6c2493..07e078f3 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java @@ -4,6 +4,7 @@ import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_아이디_지정_생성; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -14,13 +15,16 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.snippet.Attributes.key; +import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.core.application.response.SpaceResponse; import com.woowacourse.gongcheck.core.application.response.SpacesResponse; import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.presentation.request.SpaceChangeRequest; import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -67,6 +71,7 @@ class Spaceλ₯Ό_μƒμ„±ν•œλ‹€ { @Test void Space_생성에_μ„±κ³΅ν•œλ‹€() { when(spaceService.createSpace(anyLong(), any())).thenReturn(1L); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SpaceCreateRequest request = new SpaceCreateRequest("μž μ‹€ 캠퍼슀", "https://image.gongcheck.shop/123sdf5"); @@ -80,9 +85,11 @@ class Spaceλ₯Ό_μƒμ„±ν•œλ‹€ { .apply(document("spaces/create/success", requestFields( fieldWithPath("name").type(JsonFieldType.STRING) - .description("Space 이름"), + .description("Space 이름") + .attributes(key("length").value(10)), fieldWithPath("imageUrl").type(JsonFieldType.STRING) .description("Space Image Url") + .attributes(key("nullable").value(true)) ) )) .statusCode(HttpStatus.CREATED.value()); @@ -90,6 +97,7 @@ class Spaceλ₯Ό_μƒμ„±ν•œλ‹€ { @Test void Space_이름이_null_인_경우_생성에_μ‹€νŒ¨ν•œλ‹€() { + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SpaceCreateRequest request = new SpaceCreateRequest(null, "https://image.gongcheck.shop/123sdf5"); @@ -106,9 +114,10 @@ class Spaceλ₯Ό_μƒμ„±ν•œλ‹€ { @Test void Space_이름이_빈_κ°’_인_경우_생성에_μ‹€νŒ¨ν•œλ‹€() { - doThrow(BusinessException.class) + doThrow(new BusinessException("이름은 곡백일 수 μ—†μŠ΅λ‹ˆλ‹€", ErrorCode.N001)) .when(spaceService) .createSpace(anyLong(), any()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SpaceCreateRequest request = new SpaceCreateRequest("", "https://image.gongcheck.shop/123sdf5"); @@ -174,9 +183,11 @@ class Spaceλ₯Ό_μˆ˜μ •ν•œλ‹€ { parameterWithName("spaceId").description("μˆ˜μ •ν•  Space Id")), requestFields( fieldWithPath("name").type(JsonFieldType.STRING) - .description("Space 이름"), + .description("Space 이름") + .attributes(key("length").value(10)), fieldWithPath("imageUrl").type(JsonFieldType.STRING) .description("Space Image Url") + .attributes(key("nullable").value(true)) ) )) .statusCode(HttpStatus.NO_CONTENT.value()); @@ -202,6 +213,7 @@ class Spaceλ₯Ό_μˆ˜μ •ν•œλ‹€ { @Test void Spaceλ₯Ό_μ‚­μ œν•œλ‹€() { doNothing().when(spaceService).removeSpace(anyLong(), anyLong()); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java index abf82e7c..5311a1ed 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -18,7 +19,9 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.snippet.Attributes.key; +import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; import com.woowacourse.gongcheck.core.domain.host.Host; @@ -27,6 +30,7 @@ import com.woowacourse.gongcheck.core.domain.submission.Submission; import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.ErrorResponse; import io.restassured.module.mockmvc.response.MockMvcResponse; import io.restassured.response.ExtractableResponse; @@ -65,6 +69,7 @@ class Submission을_μƒμ„±ν•œλ‹€ { requestFields( fieldWithPath("author").type(JsonFieldType.STRING) .description("제좜자") + .attributes(key("length").value(10)) ) )) .statusCode(HttpStatus.OK.value()); @@ -72,7 +77,7 @@ class Submission을_μƒμ„±ν•œλ‹€ { @Test void author_길이가_μ˜¬λ°”λ₯΄μ§€_μ•Šμ€_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new BusinessException("제좜자 μ΄λ¦„μ˜ 길이가 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")) + doThrow(new BusinessException("제좜자 μ΄λ¦„μ˜ 길이가 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", ErrorCode.S003)) .when(submissionService) .submitJobCompletion(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -89,8 +94,8 @@ class Submission을_μƒμ„±ν•œλ‹€ { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()), - () -> assertThat(response.as(ErrorResponse.class).getMessage()) - .isEqualTo("제좜자 μ΄λ¦„μ˜ 길이가 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.") + () -> assertThat(response.as(ErrorResponse.class).getErrorCode()) + .isEqualTo(ErrorCode.S003.name()) ); } @@ -110,14 +115,14 @@ class Submission을_μƒμ„±ν•œλ‹€ { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()), - () -> assertThat(response.as(ErrorResponse.class).getMessage()) - .isEqualTo("제좜자 이름은 null 일 수 μ—†μŠ΅λ‹ˆλ‹€.") + () -> assertThat(response.as(ErrorResponse.class).getErrorCode()) + .isEqualTo(ErrorCode.V001.name()) ); } @Test void RunningTaskκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠμœΌλ©΄_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new BusinessException("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")) + doThrow(new BusinessException("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", ErrorCode.S001)) .when(submissionService) .submitJobCompletion(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -134,14 +139,14 @@ class Submission을_μƒμ„±ν•œλ‹€ { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()), - () -> assertThat(response.as(ErrorResponse.class).getMessage()) - .isEqualTo("ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.") + () -> assertThat(response.as(ErrorResponse.class).getErrorCode()) + .isEqualTo(ErrorCode.S001.name()) ); } @Test void λͺ¨λ“ _RunningTaskκ°€_μ²΄ν¬μƒνƒœκ°€_μ•„λ‹ˆλ©΄_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new BusinessException("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€.")) + doThrow(new BusinessException("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€.", ErrorCode.R003)) .when(submissionService) .submitJobCompletion(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -158,8 +163,8 @@ class Submission을_μƒμ„±ν•œλ‹€ { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()), - () -> assertThat(response.as(ErrorResponse.class).getMessage()) - .isEqualTo("λͺ¨λ“  μž‘μ—…μ΄ μ™„λ£Œλ˜μ§€μ•Šμ•„ 제좜이 λΆˆκ°€ν•©λ‹ˆλ‹€.") + () -> assertThat(response.as(ErrorResponse.class).getErrorCode()) + .isEqualTo(ErrorCode.R003.name()) ); } } @@ -178,6 +183,7 @@ class Submission_λͺ©λ‘_쑰회 { SubmissionsResponse response = SubmissionsResponse.of(List.of(submission1, submission2), true); when(submissionService.findPage(anyLong(), anyLong(), any())).thenReturn(response); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java index ee73fba2..2881fe19 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java @@ -2,15 +2,13 @@ import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask둜_Task_아이디_지정_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_아이디_지정_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_아이디_지정_생성; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @@ -20,17 +18,17 @@ import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.core.application.response.JobActiveResponse; -import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; import com.woowacourse.gongcheck.core.application.response.TasksResponse; import com.woowacourse.gongcheck.core.domain.host.Host; import com.woowacourse.gongcheck.core.domain.job.Job; import com.woowacourse.gongcheck.core.domain.section.Section; import com.woowacourse.gongcheck.core.domain.space.Space; -import com.woowacourse.gongcheck.core.domain.task.RunningTask; import com.woowacourse.gongcheck.core.domain.task.Task; import com.woowacourse.gongcheck.core.domain.task.Tasks; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.ErrorCode; import com.woowacourse.gongcheck.exception.NotFoundException; import io.restassured.module.mockmvc.response.MockMvcResponse; import io.restassured.response.ExtractableResponse; @@ -40,6 +38,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; class TaskDocumentation extends DocumentationTest { @@ -64,7 +63,7 @@ class RunningTaskλ₯Ό_μƒμ„±ν•œλ‹€ { @Test void 이미_RunningTaskκ°€_μ‘΄μž¬ν•˜λŠ”λ°_μƒˆλ‘œμš΄_RunningTaskλ₯Ό_μƒμ„±ν•˜λ €λŠ”_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new BusinessException("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ—¬ μƒˆλ‘œμš΄ μž‘μ—…μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€.")).when(taskService) + doThrow(new BusinessException("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ—¬ μƒˆλ‘œμš΄ μž‘μ—…μ„ 생성할 수 μ—†μŠ΅λ‹ˆλ‹€.", ErrorCode.T001)).when(taskService) .createNewRunningTasks(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -105,65 +104,36 @@ class RunningTask_생성_μ—¬λΆ€λ₯Ό_ν™•μΈν•œλ‹€ { } @Nested - class Running_Taskλ₯Ό_μ‘°νšŒν•œλ‹€ { + class Running_Task에_λŒ€ν•œ_SSEλ₯Ό_μ—°κ²°ν•œλ‹€ { @Test - void RunningTaskκ°€_μ‘΄μž¬ν•˜λ©΄_μ„±κ³΅μ μœΌλ‘œ_μ‘°νšŒν•œλ‹€() { - Host host = Host_생성("1234", 1234L); - Space space = Space_생성(host, "μž μ‹€"); - Job job = Job_생성(space, "μ²­μ†Œ"); - Section section1 = Section_아이디_지정_생성(1L, job, "νŠΈλž™λ£Έ"); - Section section2 = Section_아이디_지정_생성(2L, job, "κ΅Ώμƒ·κ°•μ˜μž₯"); - RunningTask runningTask1 = RunningTask_생성(Task_생성(section1, "책상 μ²­μ†Œ").getId(), false); - RunningTask runningTask2 = RunningTask_생성(Task_생성(section2, "책상 μ²­μ†Œ").getId(), true); - Task task1 = RunningTask둜_Task_아이디_지정_생성(1L, runningTask1, section1, "책상 μ²­μ†Œ"); - Task task2 = RunningTask둜_Task_아이디_지정_생성(2L, runningTask2, section2, "의자 μ²­μ†Œ"); - when(taskService.findRunningTasks(anyLong(), any())).thenReturn( - RunningTasksResponse.from(new Tasks(List.of(task1, task2))) - ); + void RunningTaskκ°€_μ‘΄μž¬ν•˜λ©΄_μ„±κ³΅μ μœΌλ‘œ_μ—°κ²°ν•œλ‹€() { + when(taskService.connectRunningTasks(anyLong(), any())) + .thenReturn(new SseEmitter()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven .header("Authorization", "Bearer jwt.token.here") - .when().get("/api/jobs/{jobId}/runningTasks", 1) + .accept(MediaType.TEXT_EVENT_STREAM_VALUE) + .when().get("/api/jobs/{jobId}/runningTasks/connect", 1) .then().log().all() - .apply(document("runningTasks/find/success", + .apply(document("runningTasks/connect/success", pathParameters( - parameterWithName("jobId").description("ν•΄λ‹Ή RunningTaskλ₯Ό μ‘°νšŒν•  Job Id")), - responseFields( - fieldWithPath("sections.[].id").type(JsonFieldType.NUMBER) - .description("Section Id"), - fieldWithPath("sections.[].name").type(JsonFieldType.STRING) - .description("Section 이름"), - fieldWithPath("sections.[].imageUrl").type(JsonFieldType.STRING) - .description("Section Image Url"), - fieldWithPath("sections.[].description").type(JsonFieldType.STRING) - .description("Section μ„€λͺ…"), - fieldWithPath("sections.[].tasks.[].id").type(JsonFieldType.NUMBER) - .description("Task Id"), - fieldWithPath("sections.[].tasks.[].name").type(JsonFieldType.STRING) - .description("Task 이름"), - fieldWithPath("sections.[].tasks.[].imageUrl").type(JsonFieldType.STRING) - .description("Task Image Url"), - fieldWithPath("sections.[].tasks.[].description").type(JsonFieldType.STRING) - .description("Task μ„€λͺ…"), - fieldWithPath("sections.[].tasks.[].checked").type(JsonFieldType.BOOLEAN) - .description("μ™„λ£Œ μ—¬λΆ€") - ) + parameterWithName("jobId").description("ν•΄λ‹Ή RunningTaskλ₯Ό μ‘°νšŒν•  Job Id")) )) .statusCode(HttpStatus.OK.value()); } @Test - void RunningTaskκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_μƒνƒœμ—μ„œ_μ‘°νšŒν•˜λ €λŠ”_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new BusinessException("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•Šμ•„ μ‘°νšŒν•  수 μ—†μŠ΅λ‹ˆλ‹€")).when(taskService) - .findRunningTasks(anyLong(), anyLong()); + void RunningTaskκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_μƒνƒœμ—μ„œ_SSE_연결을_ν•˜λ €λŠ”_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + doThrow(new BusinessException("ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•Šμ•„ μ‘°νšŒν•  수 μ—†μŠ΅λ‹ˆλ‹€", ErrorCode.R001)).when(taskService) + .connectRunningTasks(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); ExtractableResponse response = docsGiven .header("Authorization", "Bearer jwt.token.here") - .contentType(MediaType.APPLICATION_JSON_VALUE) - .when().get("/api/jobs/1/runningTasks") + .contentType(MediaType.TEXT_EVENT_STREAM_VALUE) + .when().get("/api/jobs/1/runningTasks/connect") .then().log().all() .apply(document("runningTasks/find/fail/active")) .extract(); @@ -193,7 +163,7 @@ class RunningTask의_μ²΄ν¬μƒνƒœλ₯Ό_λ³€κ²½ν•œλ‹€ { @Test void μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_RunningTask의_μ²΄ν¬μƒνƒœλ₯Ό_λ³€κ²½ν•˜λ €λŠ”_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new BusinessException("ν˜„μž¬ 진행 쀑인 μž‘μ—…μ΄ μ•„λ‹™λ‹ˆλ‹€.")).when(taskService) + doThrow(new BusinessException("ν˜„μž¬ 진행 쀑인 μž‘μ—…μ΄ μ•„λ‹™λ‹ˆλ‹€.", ErrorCode.R002)).when(taskService) .flipRunningTask(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -210,7 +180,7 @@ class RunningTask의_μ²΄ν¬μƒνƒœλ₯Ό_λ³€κ²½ν•œλ‹€ { @Test void RunningTask의_아이디와_Host_아이디가_μ—°κ΄€λ˜μ§€_μ•ŠλŠ”_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new NotFoundException("진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")).when(taskService) + doThrow(new NotFoundException("진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", ErrorCode.T003)).when(taskService) .flipRunningTask(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -227,7 +197,7 @@ class RunningTask의_μ²΄ν¬μƒνƒœλ₯Ό_λ³€κ²½ν•œλ‹€ { @Test void RunningTask와_μ—°κ΄€λœ_Hostκ°€_μ‘΄μž¬ν•˜μ§€_μ•ŠλŠ”_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - doThrow(new NotFoundException("진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")).when(taskService) + doThrow(new NotFoundException("진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.", ErrorCode.T003)).when(taskService) .flipRunningTask(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -255,6 +225,7 @@ class RunningTask의_μ²΄ν¬μƒνƒœλ₯Ό_λ³€κ²½ν•œλ‹€ { when(taskService.findTasks(anyLong(), any())).thenReturn( TasksResponse.from(new Tasks(List.of(task1, task2))) ); + when(jwtTokenProvider.extractAuthority(anyString())).thenReturn(Authority.HOST); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven @@ -285,4 +256,24 @@ class RunningTask의_μ²΄ν¬μƒνƒœλ₯Ό_λ³€κ²½ν•œλ‹€ { )) .statusCode(HttpStatus.OK.value()); } + + @Nested + class ν•΄λ‹Ή_Section의_RunningTaskλ₯Ό_μ „λΆ€_μ²΄ν¬ν•œλ‹€ { + + @Test + void μ „λΆ€_체크에_μ„±κ³΅ν•œλ‹€() { + doNothing().when(taskService).checkRunningTasksInSection(anyLong(), any()); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + docsGiven + .header("Authorization", "Bearer jwt.token.here") + .when().post("/api/sections/{sectionId}/runningTask/allCheck", 1) + .then().log().all() + .apply(document("runningTasks/allCheck/success", + pathParameters( + parameterWithName("sectionId").description("RunningTaskλ₯Ό λͺ¨λ‘ 체크 μƒνƒœλ‘œ λ°”κΏ€ sectionId")) + )) + .statusCode(HttpStatus.OK.value()); + } + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/support/ErrorCodeController.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/support/ErrorCodeController.java new file mode 100644 index 00000000..5acf97de --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/support/ErrorCodeController.java @@ -0,0 +1,14 @@ +package com.woowacourse.gongcheck.documentation.support; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test") +public class ErrorCodeController { + + @GetMapping("/errorCodes") + public void findErrorCodes() { + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/support/ErrorCodeFieldsSnippet.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/support/ErrorCodeFieldsSnippet.java new file mode 100644 index 00000000..b1a5d44a --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/support/ErrorCodeFieldsSnippet.java @@ -0,0 +1,36 @@ +package com.woowacourse.gongcheck.documentation.support; + +import com.woowacourse.gongcheck.exception.ErrorCode; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.TemplatedSnippet; + +public class ErrorCodeFieldsSnippet extends TemplatedSnippet { + + public ErrorCodeFieldsSnippet(final String snippetName, final String templateName) { + super(snippetName, templateName, null); + } + + @Override + protected Map createModel(final Operation operation) { + Map model = new HashMap<>(); + List> fields = new ArrayList<>(); + model.put("fields", fields); + addErrorCodes(fields); + return model; + } + + private void addErrorCodes(final List> fields) { + for (ErrorCode errorCode : ErrorCode.values()) { + Map model = new HashMap<>(); + model.put("path", errorCode.name()); + model.put("type", "String"); + model.put("description", errorCode.getDescription()); + model.put("optional", false); + fields.add(model); + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/hash/AES256Test.java b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/hash/AES256Test.java index 41b26f9f..6405efa9 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/hash/AES256Test.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/hash/AES256Test.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.woowacourse.gongcheck.exception.InfrastructureException; import org.junit.jupiter.api.Test; class AES256Test { @@ -19,13 +20,13 @@ class AES256Test { @Test void 인코딩_μ‹œ_null을_μž…λ ₯λ°›λŠ”_경우_μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> aes256.encode(null)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(InfrastructureException.class); } @Test void λ””μ½”λ”©_μ‹œ_null을_μž…λ ₯λ°›λŠ”_경우_μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { assertThatThrownBy(() -> aes256.decode(null)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(InfrastructureException.class); } @Test diff --git a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProviderTest.java b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProviderTest.java index bfd83082..f149586e 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProviderTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProviderTest.java @@ -35,7 +35,7 @@ class JjwtTokenProviderTest { assertThatThrownBy(() -> tokenProvider.extractSubject(expiredToken)) .isInstanceOf(UnauthorizedException.class) - .hasMessage("만료된 ν† ν°μž…λ‹ˆλ‹€."); + .hasMessageContaining("만료된 ν† ν°μž…λ‹ˆλ‹€."); } @Test @@ -44,7 +44,7 @@ class JjwtTokenProviderTest { assertThatThrownBy(() -> tokenProvider.extractSubject(invalidToken)) .isInstanceOf(InfrastructureException.class) - .hasMessage("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."); } @Test @@ -75,6 +75,6 @@ class JjwtTokenProviderTest { assertThatThrownBy(() -> tokenProvider.extractAuthority(invalidToken)) .isInstanceOf(InfrastructureException.class) - .hasMessage("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."); + .hasMessageContaining("μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClientTest.java b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClientTest.java index 5f77c50b..3bb8b017 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClientTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClientTest.java @@ -8,11 +8,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.woowacourse.gongcheck.auth.application.response.GithubAccessTokenResponse; -import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.application.response.OAuthAccessTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.SocialProfileResponse; import com.woowacourse.gongcheck.exception.InfrastructureException; -import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.exception.UnauthorizedException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -61,9 +59,9 @@ void setUp() { @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { - assertThatThrownBy(() -> githubOauthClient.requestGithubProfileByCode("code")) + assertThatThrownBy(() -> githubOauthClient.requestSocialProfileByCode("code")) .isInstanceOf(InfrastructureException.class) - .hasMessage("ν•΄λ‹Ή μ‚¬μš©μžμ˜ ν”„λ‘œν•„μ„ μš”μ²­ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("ν•΄λ‹Ή μ‚¬μš©μžμ˜ ν”„λ‘œν•„μ„ μš”μ²­ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); mockRestServiceServer.verify(); } } @@ -82,9 +80,9 @@ void setUp() throws JsonProcessingException { @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { - assertThatThrownBy(() -> githubOauthClient.requestGithubProfileByCode("code")) + assertThatThrownBy(() -> githubOauthClient.requestSocialProfileByCode("code")) .isInstanceOf(InfrastructureException.class) - .hasMessage("잘λͺ»λœ μš”μ²­μž…λ‹ˆλ‹€."); + .hasMessageContaining("잘λͺ»λœ μš”μ²­μž…λ‹ˆλ‹€."); mockRestServiceServer.verify(); } } @@ -94,7 +92,7 @@ class Githubμ—μ„œ_ν”„λ‘œν•„_λ°˜ν™˜μ΄_λΆˆκ°€λŠ₯ν•œ_경우 { @BeforeEach void setUp() throws JsonProcessingException { - GithubAccessTokenResponse token = new GithubAccessTokenResponse("access_token"); + OAuthAccessTokenResponse token = new OAuthAccessTokenResponse("access_token"); mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) .andExpect(method(HttpMethod.POST)) .andRespond(withStatus(HttpStatus.OK) @@ -109,9 +107,9 @@ void setUp() throws JsonProcessingException { @Test void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { - assertThatThrownBy(() -> githubOauthClient.requestGithubProfileByCode("code")) + assertThatThrownBy(() -> githubOauthClient.requestSocialProfileByCode("code")) .isInstanceOf(InfrastructureException.class) - .hasMessage("ν•΄λ‹Ή μ‚¬μš©μžμ˜ ν”„λ‘œν•„μ„ μš”μ²­ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); + .hasMessageContaining("ν•΄λ‹Ή μ‚¬μš©μžμ˜ ν”„λ‘œν•„μ„ μš”μ²­ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); mockRestServiceServer.verify(); } } @@ -119,32 +117,32 @@ void setUp() throws JsonProcessingException { @Nested class κΆŒν•œμ΄_μžˆλŠ”_code둜_Github에_ν”„λ‘œν•„μ ‘κ·Όμ΄_κ°€λŠ₯ν•œ_경우 { - private GithubProfileResponse githubProfileResponse; + private SocialProfileResponse socialProfileResponse; @BeforeEach void setUp() throws JsonProcessingException { - GithubAccessTokenResponse token = new GithubAccessTokenResponse("access_token"); + OAuthAccessTokenResponse token = new OAuthAccessTokenResponse("access_token"); mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) .andExpect(method(HttpMethod.POST)) .andRespond(withStatus(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) .body(objectMapper.writeValueAsString(token))); - githubProfileResponse = new GithubProfileResponse("nickname", "loginName", "1", "test.com"); + socialProfileResponse = new SocialProfileResponse("nickname", "loginName", "1", "test.com"); mockRestServiceServer.expect(requestTo("https://api.github.com/user")) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK) .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(githubProfileResponse))); + .body(objectMapper.writeValueAsString(socialProfileResponse))); } @Test void Github_ν”„λ‘œν•„μ •λ³΄λ₯Ό_λ°˜ν™˜ν•œλ‹€() { - GithubProfileResponse actual = githubOauthClient.requestGithubProfileByCode("access_token"); + SocialProfileResponse actual = githubOauthClient.requestSocialProfileByCode("access_token"); assertThat(actual) .usingRecursiveComparison() - .isEqualTo(githubProfileResponse); + .isEqualTo(socialProfileResponse); mockRestServiceServer.verify(); } } diff --git a/backend/src/test/resources/features/guestAuth.feature b/backend/src/test/resources/features/guestAuth.feature new file mode 100644 index 00000000..d63bed1f --- /dev/null +++ b/backend/src/test/resources/features/guestAuth.feature @@ -0,0 +1,7 @@ +@api +Feature: Space μž…μž₯ κΈ°λŠ₯ + + Scenario: Space μž…μž₯ν•˜κΈ° + When Space의 νŒ¨μŠ€μ›Œλ“œλ₯Ό μž…λ ₯ν•˜λ©΄ + Then μ—‘μ„ΈμŠ€ 토큰을 λ°›λŠ”λ‹€ + diff --git a/backend/src/test/resources/org/springframework/restdocs/templates/error-code-template.snippet b/backend/src/test/resources/org/springframework/restdocs/templates/error-code-template.snippet new file mode 100644 index 00000000..effb04c9 --- /dev/null +++ b/backend/src/test/resources/org/springframework/restdocs/templates/error-code-template.snippet @@ -0,0 +1,9 @@ +|=== +|μ½”λ“œ|μ½”λ“œλͺ… + +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} +|=== diff --git a/backend/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/backend/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet new file mode 100644 index 00000000..b301a7fa --- /dev/null +++ b/backend/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet @@ -0,0 +1,13 @@ +|=== +|Path|Type|Nullable|Length|Description + +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{#nullable}}true{{/nullable}}{{/tableCellContent}} +|{{#tableCellContent}}{{#length}}{{length}}{{/length}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} + +|=== diff --git a/backend/submodule b/backend/submodule index 86818e0a..3c38096b 160000 --- a/backend/submodule +++ b/backend/submodule @@ -1 +1 @@ -Subproject commit 86818e0aa73864ada829ecd1e88a1fa7f0807da0 +Subproject commit 3c38096b2ccc8603659f64cc73b3168397a3c58d diff --git a/frontend/.gitignore b/frontend/.gitignore index ce31cbb3..19ed7902 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -10,3 +10,6 @@ cypress/videos cypress/screenshots node_modules + +.lighthouseci +lhci_reports diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index 66933adb..1c270e7d 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'cypress'; export default defineConfig({ + projectId: 'abtuyq', e2e: { setupNodeEvents(on, config) { // implement node event listeners here @@ -13,4 +14,5 @@ export default defineConfig({ toast: '#toast', }, }, + chromeWebSecurity: false, }); diff --git a/frontend/cypress/e2e/guest.cy.js b/frontend/cypress/e2e/guest.cy.js new file mode 100644 index 00000000..930b29ea --- /dev/null +++ b/frontend/cypress/e2e/guest.cy.js @@ -0,0 +1,183 @@ +const WRONG_PASSWORD = '0000'; +const CORRECT_PASSWORD = '1234'; + +const PAGE = { + PASSWORD: 'http://localhost:3000/enter/1/pwd', + SPACE_LIST: 'http://localhost:3000/enter/1/spaces', + JOB_LIST: 'http://localhost:3000/enter/1/spaces/1', + TASK_LIST: 'http://localhost:3000/enter/1/spaces/1/1', +}; + +describe('μ‚¬μš©μž, - λΉ„λ°€λ²ˆν˜Έ μž…λ ₯ νŽ˜μ΄μ§€', () => { + beforeEach(() => { + cy.visit(PAGE.PASSWORD); + cy.spaceEnter(); + cy.getSpaces(); + }); + + it('μ‚¬μš©μžκ°€ 잘λͺ»λœ λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜λ©΄, ν† μŠ€νŠΈλ°”λ‘œ μ•ˆλ‚΄ν•΄μ€€λ‹€.', () => { + cy.get('input') + .type(WRONG_PASSWORD) + .then(() => { + cy.get('button') + .click() + .then(() => { + cy.get('#toast > div').should('be.contain', 'λΉ„λ°€λ²ˆν˜Έλ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”.'); + }); + }); + }); +}); + +describe('μ‚¬μš©μž, - 곡간 선택 νŽ˜μ΄μ§€', () => { + beforeEach(() => { + cy.setToken(); + cy.visit(PAGE.SPACE_LIST); + cy.spaceEnter(); + cy.getSpaces(); + cy.getJobs(); + cy.getSpaceInfo(); + }); + + it('μ‚¬μš©μžκ°€ 곡간을 ν΄λ¦­ν•˜λ©΄, κ³΅κ°„μ˜ 업무 선택 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.', () => { + cy.get('[class$=-spaceCard]') + .first() + .click() + .then(() => { + cy.setToken(); + }) + .then(() => { + cy.url().should('eq', PAGE.JOB_LIST); + }); + }); +}); + +describe('μ‚¬μš©μž - 업무 선택 νŽ˜μ΄μ§€', () => { + beforeEach(() => { + cy.setToken(); + cy.visit(PAGE.JOB_LIST); + cy.getJobs(); + cy.getSpaceInfo(); + cy.getRunningTasks(); + }); + + it('μ‚¬μš©μžκ°€ μž‘μ—…μ΄ 진행쀑이지 μ•Šμ€ 업무λ₯Ό ν΄λ¦­ν•˜λ©΄, confirm 창이 λ…ΈμΆœλœλ‹€.', () => { + cy.getRunningTaskActive_false(1).then(() => { + cy.postNewRunningTasks(1).then(() => { + cy.get('[class$=-jobCard]') + .first() + .click() + .then(() => { + cy.on('window:confirm', text => { + expect(text).to.contains('진행쀑인 μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€. μƒˆλ‘­κ²Œ μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'); + return false; + }); + }); + }); + }); + }); + + it('μ‚¬μš©μžκ°€ μž‘μ—…μ΄ 진행쀑인 업무λ₯Ό ν΄λ¦­ν•˜λ©΄, ν•΄λ‹Ή 체크리슀트 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.', () => { + cy.getRunningTaskActive_true(1).then(() => { + cy.postNewRunningTasks(1).then(() => { + cy.get('[class$=-jobCard]') + .first() + .click() + .then(() => { + cy.url().should('eq', PAGE.TASK_LIST); + }); + }); + }); + }); + + it('μ‚¬μš©μžκ°€ μƒˆλ‘œμš΄ 체크리슀트 μƒμ„±μ‹œ, 업무 λͺ©λ‘μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ ν† μŠ€νŠΈλ°”λ‘œ μ•ˆλ‚΄ν•΄μ€€λ‹€.', () => { + cy.getRunningTaskActive_false(2).then(() => { + cy.postNewRunningTasks(2).then(() => { + cy.get('[class$=-jobCard]') + .last() + .click() + .then(() => { + cy.on('window:confirm', text => { + expect(text).to.contains('진행쀑인 μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€. μƒˆλ‘­κ²Œ μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'); + return true; + }); + }) + .then(() => { + cy.get('#toast > div').should('be.contain', 'μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'); + }); + }); + }); + }); +}); + +describe('μ‚¬μš©μž - 체크리슀트 체크 νŽ˜μ΄μ§€', () => { + beforeEach(() => { + cy.setToken(); + cy.visit(PAGE.TASK_LIST); + cy.getSpaceInfo(); + cy.getRunningTasks(); + cy.postJobComplete(); + }); + + // it('μ‚¬μš©μžκ°€ 체크 λ°•μŠ€λ₯Ό ν΄λ¦­ν•˜λ©΄, ν•΄λ‹Ή 사항이 μ²΄ν¬ν‘œμ‹œλœλ‹€.', () => { + // cy.postCheckTask(1); + + // cy.get('label') + // .first() + // .click() + // .then(() => { + // cy.get('label').first().should('have.css', 'background-color', 'rgb(126, 217, 87)'); + // }); + // }); + + // it('μ‚¬μš©μžκ°€ λͺ¨λ“  체크 λ°•μŠ€λ₯Ό ν΄λ¦­ν•˜λ©΄, 제좜 λ²„νŠΌμ΄ ν™œμ„±ν™”λœλ‹€.', () => { + // cy.postCheckTask(1).then(() => { + // cy.get('label') + // .first() + // .click() + // .then(() => { + // cy.postCheckTask(2).then(() => { + // cy.get('label') + // .last() + // .click() + // .then(() => { + // cy.get('button').should('not.be.disabled'); + // }); + // }); + // }); + // }); + // }); + + // it('μ‚¬μš©μžκ°€ 제좜 λͺ¨λ‹¬μ—μ„œ 제좜자λ₯Ό μž…λ ₯ν•˜κ³  λ²„νŠΌμ„ λˆ„λ₯΄λ©΄, 제좜 alertκ°€ λ°œμƒν•œλ‹€.', () => { + // cy.getSpaces().then(() => { + // cy.postCheckTask(1).then(() => { + // cy.get('label') + // .first() + // .click() + // .then(() => { + // cy.postCheckTask(2).then(() => { + // cy.get('label') + // .last() + // .click() + // .then(() => { + // cy.get('button') + // .click() + // .then(() => { + // cy.get('#modal input') + // .type('coke') + // .then(() => { + // cy.get('#modal button') + // .click() + // .then(() => { + // cy.on('window:alert', text => { + // expect(text).to.include('제좜 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + // }); + // }); + // }); + // }); + // }); + // }); + // }); + // }); + // }); + // }); +}); diff --git a/frontend/cypress/e2e/guest.cy.ts b/frontend/cypress/e2e/guest.cy.ts deleted file mode 100644 index f829783d..00000000 --- a/frontend/cypress/e2e/guest.cy.ts +++ /dev/null @@ -1,155 +0,0 @@ -const WRONG_PASSWORD = '0000'; -const CORRECT_PASSWORD = '1234'; - -const PAGE = { - PASSWORD: 'http://localhost:3000/enter/1/pwd', - SPACE_LIST: 'http://localhost:3000/enter/1/spaces', - JOB_LIST: 'http://localhost:3000/enter/1/spaces/1', - TASK_LIST: 'http://localhost:3000/enter/1/spaces/1/1', -}; - -describe('μ‚¬μš©μž, - λΉ„λ°€λ²ˆν˜Έ μž…λ ₯ νŽ˜μ΄μ§€', () => { - beforeEach(() => { - cy.visit(PAGE.PASSWORD); - cy.spaceEnter(); - cy.getSpaces(); - }); - - it('μ‚¬μš©μžκ°€ 잘λͺ»λœ λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜λ©΄, API 호좜이 μ‹€νŒ¨ν•˜κ³  ν† μŠ€νŠΈλ°”λ‘œ μ•ˆλ‚΄ν•΄μ€€λ‹€.', () => { - cy.get('input').type(WRONG_PASSWORD); - - cy.get('button').click(); - - cy.get('@spaceEnter').then(({ response }: any) => { - cy.get('#toast > div').should('be.contain', 'λΉ„λ°€λ²ˆν˜Έλ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”.'); - expect(response.statusCode).to.equal(401); - }); - }); - - it('μ‚¬μš©μžκ°€ μ˜¬λ°”λ₯Έ λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•˜λ©΄, API 호좜이 μ„±κ³΅ν•˜κ³  곡간 선택 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.', () => { - cy.get('input').type(CORRECT_PASSWORD); - - cy.get('button').click(); - - cy.get('@spaceEnter').then(({ response }: any) => { - expect(response.statusCode).to.equal(200); - }); - }); -}); - -describe('μ‚¬μš©μž, - 곡간 선택 νŽ˜μ΄μ§€', () => { - beforeEach(() => { - cy.setToken(); - cy.visit(PAGE.SPACE_LIST); - cy.spaceEnter(); - cy.getSpaces(); - cy.getJobs(); - cy.getSpaceInfo(); - }); - - it('μ‚¬μš©μžκ°€ 곡간을 ν΄λ¦­ν•˜λ©΄, κ³΅κ°„μ˜ 업무 선택 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.', () => { - cy.get('[class$=-spaceCard]').first().click(); - - cy.url().should('eq', PAGE.JOB_LIST); - }); -}); - -describe('μ‚¬μš©μž - 업무 선택 νŽ˜μ΄μ§€', () => { - beforeEach(() => { - cy.setToken(); - cy.visit(PAGE.JOB_LIST); - cy.getJobs(); - cy.getSpaceInfo(); - cy.getRunningTasks(); - }); - - it('μ‚¬μš©μžκ°€ μž‘μ—…μ΄ 진행쀑이지 μ•Šμ€ 업무λ₯Ό ν΄λ¦­ν•˜λ©΄, confirm 창이 λ…ΈμΆœλœλ‹€.', () => { - cy.getRunningTaskActive_false(1); - cy.postNewRunningTasks(1); - cy.get('[class$=-jobCard]').first().click(); - - cy.on('window:confirm', text => { - expect(text).to.contains('진행쀑인 μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€. μƒˆλ‘­κ²Œ μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'); - return false; - }); - }); - - it('μ‚¬μš©μžκ°€ μž‘μ—…μ΄ 진행쀑인 업무λ₯Ό ν΄λ¦­ν•˜λ©΄, ν•΄λ‹Ή 체크리슀트 νŽ˜μ΄μ§€λ‘œ μ΄λ™ν•œλ‹€.', () => { - cy.getRunningTaskActive_true(1); - cy.postNewRunningTasks(1); - cy.get('[class$=-jobCard]').first().click(); - - cy.url().should('eq', PAGE.TASK_LIST); - }); - - it('μ‚¬μš©μžκ°€ μƒˆλ‘œμš΄ 체크리슀트 μƒμ„±μ‹œ, 업무 λͺ©λ‘μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμœΌλ©΄ ν† μŠ€νŠΈλ°”λ‘œ μ•ˆλ‚΄ν•΄μ€€λ‹€.', () => { - cy.getRunningTaskActive_false(2); - cy.postNewRunningTasks(2); - cy.get('[class$=-jobCard]').last().click(); - - cy.on('window:confirm', text => { - expect(text).to.contains('진행쀑인 μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€. μƒˆλ‘­κ²Œ μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?'); - return true; - }); - - cy.get('@postNewRunningTasks').then(({ response }: any) => { - cy.get('#toast > div').should('be.contain', 'μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'); - expect(response.statusCode).to.equal(401); - }); - }); -}); - -describe('μ‚¬μš©μž - 체크리슀트 체크 νŽ˜μ΄μ§€', () => { - beforeEach(() => { - cy.setToken(); - cy.visit(PAGE.TASK_LIST); - cy.getSpaceInfo(); - cy.getRunningTasks(); - cy.postJobComplete(); - }); - - it('μ‚¬μš©μžκ°€ 체크 λ°•μŠ€λ₯Ό ν΄λ¦­ν•˜λ©΄, ν•΄λ‹Ή 사항이 μ²΄ν¬ν‘œμ‹œλœλ‹€.', () => { - cy.postCheckTask(1); - cy.get('label').first().click(); - - cy.get('label').first().should('have.css', 'background-color', 'rgb(126, 217, 87)'); - }); - - it('μ‚¬μš©μžκ°€ λͺ¨λ“  체크 λ°•μŠ€λ₯Ό ν΄λ¦­ν•˜λ©΄, 제좜 λ²„νŠΌμ΄ ν™œμ„±ν™”λœλ‹€.', () => { - cy.postCheckTask(1); - cy.get('label').first().click(); - cy.postCheckTask(2); - cy.get('label').last().click(); - - cy.get('button').should('not.be.disabled'); - }); - - it('μ‚¬μš©μžκ°€ 제좜 λ²„νŠΌμ„ ν΄λ¦­ν•˜λ©΄, 제좜 λͺ¨λ‹¬μ΄ λ…ΈμΆœλœλ‹€.', () => { - cy.postCheckTask(1); - cy.get('label').first().click(); - cy.postCheckTask(2); - cy.get('label').last().click(); - - cy.get('button').click(); - - cy.get('#modal > div').should('be.visible'); - }); - - it('μ‚¬μš©μžκ°€ 제좜 λͺ¨λ‹¬μ—μ„œ 제좜자λ₯Ό μž…λ ₯ν•˜κ³  λ²„νŠΌμ„ λˆ„λ₯΄λ©΄, 제좜 alertκ°€ λ°œμƒν•œλ‹€.', () => { - cy.getSpaces(); - - cy.postCheckTask(1); - cy.get('label').first().click(); - cy.postCheckTask(2); - cy.get('label').last().click(); - - cy.get('button').click(); - - cy.get('#modal input').type('coke'); - cy.get('#modal button').click(); - - cy.on('window:alert', text => { - expect(text).to.include('제좜 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); - }); - }); -}); diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.js similarity index 69% rename from frontend/cypress/support/commands.ts rename to frontend/cypress/support/commands.js index 9d33cbc0..38f5cd80 100644 --- a/frontend/cypress/support/commands.ts +++ b/frontend/cypress/support/commands.js @@ -1,6 +1,3 @@ -/// -// *********************************************** - const SECTIONS = [ { id: 1, @@ -26,8 +23,11 @@ const SECTIONS = [ }, ]; -Cypress.Commands.add('setToken', () => { - localStorage.setItem('token', 'json_web_token'); +const HOST_ID = 1; + +Cypress.Commands.add('setToken', () => { + sessionStorage.setItem('tokenKey', `${HOST_ID}`); + localStorage.setItem(`${HOST_ID}`, 'json_web_token'); }); // mockAPIs @@ -35,7 +35,7 @@ Cypress.Commands.addAll({ spaceEnter() { const PASSWORD = '1234'; - cy.intercept('POST', 'http://localhost:8080/api/hosts/1/enter', (request: any) => { + cy.intercept('POST', '/api/hosts/1/enter', request => { request.reply( request.body.password === PASSWORD ? { @@ -49,7 +49,7 @@ Cypress.Commands.addAll({ }, getSpaces() { - cy.intercept('GET', 'http://localhost:8080/api/spaces', (request: any) => { + cy.intercept('GET', '/api/spaces', request => { request.reply({ statusCode: 200, body: { @@ -71,7 +71,7 @@ Cypress.Commands.addAll({ }, getJobs() { - cy.intercept('GET', 'http://localhost:8080/api/spaces/1/jobs', (request: any) => { + cy.intercept('GET', '/api/spaces/1/jobs', request => { request.reply({ statusCode: 200, body: { @@ -91,7 +91,7 @@ Cypress.Commands.addAll({ }, getSpaceInfo() { - cy.intercept('GET', 'http://localhost:8080/api/spaces/1', (request: any) => { + cy.intercept('GET', '/api/spaces/1', request => { request.reply({ statusCode: 200, body: { @@ -103,8 +103,8 @@ Cypress.Commands.addAll({ }).as('getSpaceInfo'); }, - getRunningTaskActive_true(jobId: number) { - cy.intercept('GET', `http://localhost:8080/api/jobs/${jobId}/active`, (request: any) => { + getRunningTaskActive_true(jobId) { + cy.intercept('GET', `/api/jobs/${jobId}/active`, request => { request.reply({ statusCode: 200, body: { @@ -114,8 +114,8 @@ Cypress.Commands.addAll({ }).as('getRunningTaskActive_true'); }, - getRunningTaskActive_false(jobId: number) { - cy.intercept('GET', `http://localhost:8080/api/jobs/${jobId}/active`, (request: any) => { + getRunningTaskActive_false(jobId) { + cy.intercept('GET', `/api/jobs/${jobId}/active`, request => { request.reply({ statusCode: 200, body: { @@ -125,8 +125,8 @@ Cypress.Commands.addAll({ }).as('getRunningTaskActive_false'); }, - postNewRunningTasks(jobId: number) { - cy.intercept('POST', `http://localhost:8080/api/jobs/${jobId}/runningTasks/new`, (request: any) => { + postNewRunningTasks(jobId) { + cy.intercept('POST', `/api/jobs/${jobId}/runningTasks/new`, request => { request.reply( jobId === 1 ? { @@ -135,7 +135,7 @@ Cypress.Commands.addAll({ : { statusCode: 401, body: { - message: 'μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', + errorCode: 'T002', }, } ); @@ -143,7 +143,7 @@ Cypress.Commands.addAll({ }, getRunningTasks() { - cy.intercept('GET', 'http://localhost:8080/api/jobs/1/runningTasks', (request: any) => { + cy.intercept('GET', '/api/jobs/1/runningTasks', request => { request.reply({ statusCode: 200, body: { sections: SECTIONS }, @@ -151,8 +151,8 @@ Cypress.Commands.addAll({ }).as('getRunningTasks'); }, - postCheckTask(taskId: number) { - cy.intercept('POST', `http://localhost:8080/api/tasks/${taskId}/flip`, (request: any) => { + postCheckTask(taskId) { + cy.intercept('POST', `/api/tasks/${taskId}/flip`, request => { SECTIONS.forEach(section => section.tasks.forEach(task => { if (task.id === taskId) { @@ -167,10 +167,12 @@ Cypress.Commands.addAll({ }, postJobComplete() { - cy.intercept('POST', `http://localhost:8080/api/jobs/1/complete`, (request: any) => { + cy.intercept('POST', `/api/jobs/1/complete`, request => { request.reply({ statusCode: 200, }); }).as('postJobComplete'); }, }); + +export {}; diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.js similarity index 100% rename from frontend/cypress/support/e2e.ts rename to frontend/cypress/support/e2e.js diff --git a/frontend/cypress/support/index.d.ts b/frontend/cypress/support/index.d.ts deleted file mode 100644 index 263d7ae2..00000000 --- a/frontend/cypress/support/index.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare namespace Cypress { - interface Chainable { - setToken(): void; - spaceEnter(): void; - getSpaces(): void; - getJobs(): void; - getSpaceInfo(): void; - getRunningTaskActive_true(jobId: number): void; - getRunningTaskActive_false(jobId: number): void; - postNewRunningTasks(jobId: number): void; - getRunningTasks(): void; - postCheckTask(taskId: number): void; - postJobComplete(): void; - } -} diff --git a/frontend/cypress/videos/guest.cy.ts.mp4 b/frontend/cypress/videos/guest.cy.ts.mp4 deleted file mode 100644 index b63bb163..00000000 Binary files a/frontend/cypress/videos/guest.cy.ts.mp4 and /dev/null differ diff --git a/frontend/frontend-security b/frontend/frontend-security index b7d26630..dc5dc992 160000 --- a/frontend/frontend-security +++ b/frontend/frontend-security @@ -1 +1 @@ -Subproject commit b7d266308a50ad392d9dbf591eccbdb6a0a8d0b8 +Subproject commit dc5dc992222c55f2eb0dce9d618885ed928bc022 diff --git a/frontend/lighthouserc.js b/frontend/lighthouserc.js new file mode 100644 index 00000000..3b6d7943 --- /dev/null +++ b/frontend/lighthouserc.js @@ -0,0 +1,20 @@ +module.exports = { + ci: { + collect: { + staticDistDir: './dist', + startServerCommand: 'npm run start', + url: ['http://localhost:3000/index.html'], + collect: { + numberOfRuns: 3, + }, + }, + upload: { + target: 'filesystem', + outputDir: './lhci_reports', + reportFilenamePattern: '%%PATHNAME%%-%%DATETIME%%-report.%%EXTENSION%%', + }, + assert: { + preset: 'lighthouse:recommended', + }, + }, +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ed247c2b..06b24265 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,7 +28,9 @@ "@emotion/css": "^11.9.0", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", + "@lhci/cli": "^0.9.0", "@trivago/prettier-plugin-sort-imports": "^3.2.0", + "@types/event-source-polyfill": "^1.0.0", "@types/node": "^18.0.0", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", @@ -47,11 +49,13 @@ "eslint": "^8.19.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-react": "^7.30.1", + "event-source-polyfill": "1.0.28", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "nanoid": "^4.0.0", "prettier": "^2.7.1", "react-icons": "^4.4.0", + "start-server-and-test": "^1.14.0", "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.73.0", @@ -2174,6 +2178,21 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -2277,6 +2296,270 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "node_modules/@lhci/cli": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@lhci/cli/-/cli-0.9.0.tgz", + "integrity": "sha512-cEfWHCWuBQKUrwy3minHxRYtiAZ//m8XRSZvFQ2zX2Lzz+J/3xlBKQTfSiln3sIXTTWzEHaucDs70rQS8EQ/vQ==", + "dev": true, + "dependencies": { + "@lhci/utils": "0.9.0", + "chrome-launcher": "^0.13.4", + "compression": "^1.7.4", + "debug": "^4.3.1", + "express": "^4.17.1", + "inquirer": "^6.3.1", + "isomorphic-fetch": "^3.0.0", + "lighthouse": "9.3.0", + "lighthouse-logger": "1.2.0", + "open": "^7.1.0", + "tmp": "^0.1.0", + "update-notifier": "^3.0.1", + "uuid": "^8.3.1", + "yargs": "^15.4.1", + "yargs-parser": "^13.1.2" + }, + "bin": { + "lhci": "src/cli.js" + } + }, + "node_modules/@lhci/cli/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@lhci/cli/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "node_modules/@lhci/cli/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@lhci/cli/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "dev": true + }, + "node_modules/@lhci/cli/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@lhci/cli/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/@lhci/cli/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@lhci/cli/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/string-width/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@lhci/cli/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@lhci/cli/node_modules/tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dev": true, + "dependencies": { + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@lhci/cli/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@lhci/utils": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@lhci/utils/-/utils-0.9.0.tgz", + "integrity": "sha512-uHpGX71Mqay4XLxkuMdZhH+goKKygTW9Uc2s2SewRW64TLjUNRSMn2L6wG9cZx6gntD4zBdKFZWoF+dg2yx9MA==", + "dev": true, + "dependencies": { + "debug": "^4.3.1", + "isomorphic-fetch": "^3.0.0", + "js-yaml": "^3.13.1", + "lighthouse": "9.3.0", + "tree-kill": "^1.2.1" + } + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -2604,6 +2887,48 @@ "read-package-json-fast": "^2.0.1" } }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -2786,6 +3111,12 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, + "node_modules/@types/event-source-polyfill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.0.tgz", + "integrity": "sha512-b8O8/rg7NIW0iJ8i9MNDBZqPljHA+b7AjC3QFqH3dSyW6vgrl3oBgyIv5dw2fibh5enHHDkkPZG5PHza7U4NRw==", + "dev": true + }, "node_modules/@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", @@ -3592,6 +3923,15 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "dependencies": { + "string-width": "^4.1.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3921,6 +4261,15 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "node_modules/axe-core": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz", + "integrity": "sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -5249,6 +5598,84 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "node_modules/boxen": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-3.2.0.tgz", + "integrity": "sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A==", + "dev": true, + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^2.4.2", + "cli-boxes": "^2.2.0", + "string-width": "^3.0.0", + "term-size": "^1.2.0", + "type-fest": "^0.3.0", + "widest-line": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/boxen/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/boxen/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/boxen/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/boxen/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5416,8 +5843,50 @@ "node": ">=0.10.0" } }, - "node_modules/cachedir": { - "version": "2.3.0", + "node_modules/cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cachedir": { + "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", "dev": true, @@ -5463,6 +5932,15 @@ "tslib": "^2.0.3" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001361", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001361.tgz", @@ -5514,6 +5992,15 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -5559,6 +6046,32 @@ "node": ">=10" } }, + "node_modules/chrome-launcher": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", + "integrity": "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^1.0.5", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^0.5.3", + "rimraf": "^3.0.2" + } + }, + "node_modules/chrome-launcher/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -5708,6 +6221,18 @@ "webpack": ">=4.0.0 <6.0.0" } }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5784,6 +6309,64 @@ "node": ">= 10" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -5816,6 +6399,18 @@ "node": ">=6" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -6015,6 +6610,35 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -6201,6 +6825,30 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/csp_evaluator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.0.tgz", + "integrity": "sha512-TcB+ZH9wZBG314jAUpKHPl1oYbRJV+nAT2YwZ9y4fmUN0FkEJa8e/hKZoOgzLYp1Z/CJdFhbhhGIGh0XG8W54Q==", + "dev": true + }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -6229,6 +6877,21 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", + "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", + "dev": true, + "dependencies": { + "cssom": "0.3.x" + } + }, "node_modules/csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", @@ -6536,6 +7199,15 @@ "node": "*" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -6545,6 +7217,18 @@ "node": ">=0.10" } }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -6590,6 +7274,12 @@ "node": ">=0.8" } }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -6907,6 +7597,18 @@ "tslib": "^2.0.3" } }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", @@ -6955,6 +7657,12 @@ "node": ">=0.10.0" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -7687,6 +8395,27 @@ "node": ">= 0.6" } }, + "node_modules/event-source-polyfill": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.28.tgz", + "integrity": "sha512-S4Je04Br394hKTiyUXKAkNpha/7r172rLNu76sdxB3Nw2g9sjuEc4kxMu/miaikvWA4jvJ2x6Xkg6BOAb1aM7g==", + "dev": true + }, + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, "node_modules/eventemitter2": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.6.tgz", @@ -8595,6 +9324,12 @@ "node": ">= 0.6" } }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -8721,6 +9456,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", @@ -9177,6 +9921,15 @@ "node": ">=0.10.0" } }, + "node_modules/has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -9370,6 +10123,12 @@ "node": ">= 0.8" } }, + "node_modules/http-link-header": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-0.8.0.tgz", + "integrity": "sha512-qsh/wKe1Mk1vtYEFr+LpQBFWTO1gxZQBdii2D0Umj+IUQ23r5sT088Rhpq4XzpSyIpaX7vwjB8Rrtx8u9JTg+Q==", + "dev": true + }, "node_modules/http-parser-js": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", @@ -9526,6 +10285,12 @@ "node": ">=10" } }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9551,6 +10316,15 @@ "node": ">=4" } }, + "node_modules/import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -9736,6 +10510,22 @@ "node": ">= 0.10" } }, + "node_modules/intl-messageformat": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-4.4.0.tgz", + "integrity": "sha512-z+Bj2rS3LZSYU4+sNitdHrwnBhr0wO80ZJSW8EzKDBowwUe3Q/UsvgCGjrwa+HPzoGCLEb9HAjfJgo4j2Sac8w==", + "dev": true, + "dependencies": { + "intl-messageformat-parser": "^1.8.1" + } + }, + "node_modules/intl-messageformat-parser": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.8.1.tgz", + "integrity": "sha512-IMSCKVf0USrM/959vj3xac7s8f87sc+80Y/ipBzdKy4ifBv5Gsj2tZ41EAaURVg01QU71fYr77uA8Meh6kELbg==", + "deprecated": "We've written a new parser that's 6x faster and is backwards compatible. Please use @formatjs/icu-messageformat-parser", + "dev": true + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -10045,6 +10835,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-npm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-3.0.0.tgz", + "integrity": "sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -10069,6 +10868,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -10303,6 +11111,12 @@ "node": ">=8" } }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -10336,6 +11150,16 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -10515,8 +11339,36 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/js-sha3": { - "version": "0.8.0", + "node_modules/joi": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", + "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, + "node_modules/js-library-detector": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.5.0.tgz", + "integrity": "sha512-Kq7VckJ5kb26kHMAu1sDO8t2qr7M5Uw6Gf7fVGtu1YceoHdqTcobwnB5kStcktusPuPmiCE8PbCaiLzhiBsSAw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" }, @@ -10556,6 +11408,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10685,6 +11543,15 @@ "integrity": "sha512-dgFenZnMsc1xGNqgdtgnh7DK+Oy352CE3VZLbzcbQpsBs9iI2K3M0IRrdgREZ72eItTjbl0suRyvKRdVQa9GbA==", "dev": true }, + "node_modules/keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10694,6 +11561,18 @@ "node": ">=0.10.0" } }, + "node_modules/latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lazy-ass": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", @@ -10729,6 +11608,214 @@ "node": ">= 0.8.0" } }, + "node_modules/lighthouse": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-9.3.0.tgz", + "integrity": "sha512-jooRAn9LQYk/KgALmwd9fPcmfGecVnd15pr7Ya4pZ1mhG9SKgVOIWwj8cjxZlWATrMV31ySwkX37dw/Jepm9gw==", + "dev": true, + "dependencies": { + "axe-core": "4.3.5", + "chrome-launcher": "^0.15.0", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.0", + "cssstyle": "1.2.1", + "enquirer": "^2.3.6", + "http-link-header": "^0.8.0", + "intl-messageformat": "^4.4.0", + "jpeg-js": "^0.4.3", + "js-library-detector": "^6.4.0", + "lighthouse-logger": "^1.3.0", + "lighthouse-stack-packs": "^1.7.0", + "lodash.clonedeep": "^4.5.0", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "lodash.set": "^4.3.2", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.2.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "ps-list": "^8.0.0", + "raven": "^2.2.1", + "robots-parser": "^3.0.0", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.12.7", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "lighthouse-core/scripts/manual-chrome-launcher.js", + "lighthouse": "lighthouse-cli/index.js", + "smokehouse": "lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=14.15" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", + "integrity": "sha512-wzUvdIeJZhRsG6gpZfmSCfysaxNEr43i+QT+Hie94wvHDKFLi4n7C2GqZ4sTC+PH5b5iktmXJvU87rWvhP3lHw==", + "dev": true, + "dependencies": { + "debug": "^2.6.8", + "marky": "^1.2.0" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.8.2.tgz", + "integrity": "sha512-vlCUxxQAB8Nu6LQHqPpDRiMi06Du593/my/6JbMttQeEfJ7pf4OS8obSTh5xSOS80U/O7fq59Q8rQGAUxQatUQ==", + "dev": true + }, + "node_modules/lighthouse/node_modules/chrome-launcher": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.1.tgz", + "integrity": "sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/lighthouse/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/lighthouse/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lighthouse/node_modules/lighthouse-logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.3.0.tgz", + "integrity": "sha512-BbqAKApLb9ywUli+0a+PcV04SyJ/N1q/8qgCNe6U97KbPCS1BTksEuHFLYdvc8DltuhfxIUBqDZsC0bBGtl3lA==", + "dev": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/lighthouse/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/lighthouse/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/lighthouse/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lighthouse/node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/lighthouse/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10836,12 +11923,30 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10854,6 +11959,12 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -11022,6 +12133,12 @@ "node": ">=8" } }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11114,6 +12231,12 @@ "node": ">=0.10.0" } }, + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, "node_modules/map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -11126,6 +12249,12 @@ "node": ">=0.10.0" } }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "dev": true + }, "node_modules/match-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", @@ -11142,6 +12271,17 @@ "dev": true, "optional": true }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -11234,6 +12374,12 @@ "node": ">= 8" } }, + "node_modules/metaviewport-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.2.0.tgz", + "integrity": "sha512-qL5NtY18LGs7lvZCkj3ep2H4Pes9rIiSLZRUyfDdvVw7pWFA0eLwmqaIxApD74RGvUrNEtk9e5Wt1rT+VlCvGw==", + "dev": true + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -11301,6 +12447,15 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -11608,7 +12763,6 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, - "optional": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -11776,6 +12930,15 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/npm-api": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-api/-/npm-api-1.0.1.tgz", @@ -12654,6 +13817,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -12768,6 +13940,76 @@ "node": ">=6" } }, + "node_modules/package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json/node_modules/got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/package-json/node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/package-json/node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/pacote": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/pacote/-/pacote-12.0.3.tgz", @@ -12846,6 +14088,12 @@ "node": ">=6" } }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, "node_modules/parse-conflict-json": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-2.0.2.tgz", @@ -13004,6 +14252,15 @@ "node": ">=8" } }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -13389,20 +14646,53 @@ "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", "dev": true }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "node_modules/ps-list": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-8.1.0.tgz", + "integrity": "sha512-NoGBqJe7Ou3kfQxEvDzDyKGAyEgwIuD3YrfXinjcCmBRv0hTld0Xb71hrXvtsNPj7HSFATfemvzB8PPJtq6Yag==", "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "node_modules/punycode": { @@ -13492,6 +14782,45 @@ "node": ">= 0.6" } }, + "node_modules/raven": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/raven/-/raven-2.6.4.tgz", + "integrity": "sha512-6PQdfC4+DQSFncowthLf+B6Hr0JpPsFBgTVYTAOq7tCmx/kR4SXbeawtPch20+3QfUcQDoJBLjWW1ybvZ4kXTw==", + "deprecated": "Please upgrade to @sentry/node. See the migration guide https://bit.ly/3ybOlo7", + "dev": true, + "dependencies": { + "cookie": "0.3.1", + "md5": "^2.2.1", + "stack-trace": "0.0.10", + "timed-out": "4.0.1", + "uuid": "3.3.2" + }, + "bin": { + "raven": "bin/raven" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/raven/node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raven/node_modules/uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -13516,6 +14845,36 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -13932,6 +15291,30 @@ "node": ">=4" } }, + "node_modules/registry-auth-token": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", + "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", + "dev": true, + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regjsgen": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", @@ -14040,6 +15423,15 @@ "throttleit": "^1.0.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -14049,6 +15441,12 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -14106,6 +15504,15 @@ "deprecated": "https://github.com/lydell/resolve-url#deprecated", "dev": true }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, "node_modules/resq": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/resq/-/resq-1.10.2.tgz", @@ -14182,6 +15589,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robots-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.0.tgz", + "integrity": "sha512-6xkze3WRdneibICBAzMKcXyTKQw5shA3GbwoEJy7RSvxpZNGF0GMuYKE1T0VMP4fwx/fQs0n0mtriOqRtk5L1w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -14306,6 +15722,27 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", + "dev": true, + "dependencies": { + "semver": "^5.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -15009,6 +16446,32 @@ "wbuf": "^1.7.3" } }, + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "dev": true, + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -15064,6 +16527,55 @@ "node": ">= 8" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/start-server-and-test": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.14.0.tgz", + "integrity": "sha512-on5ELuxO2K0t8EmNj9MtVlFqwBMxfWOhu4U7uZD1xccVpFlOQKR93CSe0u98iQzfNxRyaNTb/CdadbNllplTsw==", + "dev": true, + "dependencies": { + "bluebird": "3.7.2", + "check-more-types": "2.24.0", + "debug": "4.3.2", + "execa": "5.1.1", + "lazy-ass": "1.6.0", + "ps-tree": "1.2.0", + "wait-on": "6.0.0" + }, + "bin": { + "server-test": "src/bin/start.js", + "start-server-and-test": "src/bin/start.js", + "start-test": "src/bin/start.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/start-server-and-test/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -15169,6 +16681,15 @@ "node": ">= 0.8" } }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -15314,6 +16835,15 @@ "node": ">=0.10.0" } }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -15391,81 +16921,216 @@ "node": ">= 10" } }, - "node_modules/terser": { - "version": "5.14.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz", - "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==", + "node_modules/term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==", "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" + "execa": "^0.7.0" }, "engines": { - "node": ">=10" + "node": ">=4" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz", - "integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==", + "node_modules/term-size/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.7", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "terser": "^5.7.2" + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/term-size/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } + "node": ">=4" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "node_modules/term-size/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "node_modules/textextensions": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-5.15.0.tgz", - "integrity": "sha512-MeqZRHLuaGamUXGuVn2ivtU3LA3mLCCIO5kUGoohTCoGmCBg/+8yPhWVX9WSl9telvVd8erftjFk9Fwb2dD6rw==", + "node_modules/term-size/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "dev": true, "engines": { - "node": ">=0.8" - }, - "funding": { - "url": "https://bevry.me/fund" + "node": ">=0.10.0" } }, - "node_modules/throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==", - "dev": true + "node_modules/term-size/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/term-size/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/term-size/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/term-size/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/term-size/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/term-size/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/term-size/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz", + "integrity": "sha512-+ahUAE+iheqBTDxXhTisdA8hgvbEG1hHOQ9xmNjeUJSoi6DU/gMrKNcfZjHkyY6Alnuyc+ikYJaxxfHkT3+WuQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz", + "integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.7", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "terser": "^5.7.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/textextensions": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-5.15.0.tgz", + "integrity": "sha512-MeqZRHLuaGamUXGuVn2ivtU3LA3mLCCIO5kUGoohTCoGmCBg/+8yPhWVX9WSl9telvVd8erftjFk9Fwb2dD6rw==", + "dev": true, + "engines": { + "node": ">=0.8" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/third-party-web": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.12.7.tgz", + "integrity": "sha512-9d/OfjEOjyeOpnm4F9o0KSK6BI6ytvi9DINSB5h1+jdlCvQlhKpViMSxWpBN9WstdfDQ61BS6NxWqcPCuQCAJg==", + "dev": true + }, + "node_modules/throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==", + "dev": true }, "node_modules/through": { "version": "2.3.8", @@ -15543,6 +17208,15 @@ "node": ">=0.10.0" } }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -15596,8 +17270,16 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, - "optional": true + "bin": { + "tree-kill": "cli.js" + } }, "node_modules/treeverse": { "version": "1.0.4", @@ -15800,6 +17482,15 @@ "node": ">= 0.6" } }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", @@ -15910,6 +17601,18 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -16029,6 +17732,190 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/update-notifier": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-3.0.1.tgz", + "integrity": "sha512-grrmrB6Zb8DUiyDIaeRTBCkgISYUgETNe7NglEbVsrLWXeESnlCSP50WfRSj/GmzMPl6Uchj24S/p80nP/ZQrQ==", + "dev": true, + "dependencies": { + "boxen": "^3.0.0", + "chalk": "^2.0.1", + "configstore": "^4.0.0", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.1.0", + "is-npm": "^3.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/update-notifier/node_modules/configstore": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-4.0.0.tgz", + "integrity": "sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ==", + "dev": true, + "dependencies": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/update-notifier/node_modules/crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-notifier/node_modules/dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "dev": true, + "dependencies": { + "is-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-notifier/node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-notifier/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/update-notifier/node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/update-notifier/node_modules/is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==", + "dev": true, + "dependencies": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-notifier/node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-notifier/node_modules/is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-notifier/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-notifier/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-notifier/node_modules/unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", + "dev": true, + "dependencies": { + "crypto-random-string": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-notifier/node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/update-notifier/node_modules/xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -16207,6 +18094,34 @@ "node": ">=4" } }, + "node_modules/wait-on": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.0.tgz", + "integrity": "sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==", + "dev": true, + "dependencies": { + "axios": "^0.21.1", + "joi": "^17.4.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.1.0" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/wait-on/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/walk-up-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-1.0.0.tgz", @@ -16248,8 +18163,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "optional": true + "dev": true }, "node_modules/webpack": { "version": "5.73.0", @@ -16583,12 +18497,17 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "dev": true + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, - "optional": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -16625,6 +18544,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "dev": true + }, "node_modules/which-pm": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.0.0.tgz", @@ -16647,6 +18572,61 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "dependencies": { + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/widest-line/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -16774,6 +18754,21 @@ } } }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -16789,6 +18784,51 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -19635,6 +21675,21 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -19725,6 +21780,217 @@ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", "dev": true }, + "@lhci/cli": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@lhci/cli/-/cli-0.9.0.tgz", + "integrity": "sha512-cEfWHCWuBQKUrwy3minHxRYtiAZ//m8XRSZvFQ2zX2Lzz+J/3xlBKQTfSiln3sIXTTWzEHaucDs70rQS8EQ/vQ==", + "dev": true, + "requires": { + "@lhci/utils": "0.9.0", + "chrome-launcher": "^0.13.4", + "compression": "^1.7.4", + "debug": "^4.3.1", + "express": "^4.17.1", + "inquirer": "^6.3.1", + "isomorphic-fetch": "^3.0.0", + "lighthouse": "9.3.0", + "lighthouse-logger": "1.2.0", + "open": "^7.1.0", + "tmp": "^0.1.0", + "update-notifier": "^3.0.1", + "uuid": "^8.3.1", + "yargs": "^15.4.1", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "inquirer": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", + "dev": true + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", + "dev": true, + "requires": { + "rimraf": "^2.6.3" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "@lhci/utils": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@lhci/utils/-/utils-0.9.0.tgz", + "integrity": "sha512-uHpGX71Mqay4XLxkuMdZhH+goKKygTW9Uc2s2SewRW64TLjUNRSMn2L6wG9cZx6gntD4zBdKFZWoF+dg2yx9MA==", + "dev": true, + "requires": { + "debug": "^4.3.1", + "isomorphic-fetch": "^3.0.0", + "js-yaml": "^3.13.1", + "lighthouse": "9.3.0", + "tree-kill": "^1.2.1" + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -19998,6 +22264,42 @@ "read-package-json-fast": "^2.0.1" } }, + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", + "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", + "dev": true + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "dev": true + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "dev": true, + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -20160,6 +22462,12 @@ "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", "dev": true }, + "@types/event-source-polyfill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.0.tgz", + "integrity": "sha512-b8O8/rg7NIW0iJ8i9MNDBZqPljHA+b7AjC3QFqH3dSyW6vgrl3oBgyIv5dw2fibh5enHHDkkPZG5PHza7U4NRw==", + "dev": true + }, "@types/expect": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", @@ -20809,6 +23117,15 @@ "dev": true, "requires": {} }, + "ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "requires": { + "string-width": "^4.1.0" + } + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -21040,6 +23357,12 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, + "axe-core": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz", + "integrity": "sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA==", + "dev": true + }, "axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -22159,6 +24482,68 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "boxen": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-3.2.0.tgz", + "integrity": "sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A==", + "dev": true, + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^2.4.2", + "cli-boxes": "^2.2.0", + "string-width": "^3.0.0", + "term-size": "^1.2.0", + "type-fest": "^0.3.0", + "widest-line": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "type-fest": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", + "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", + "dev": true + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -22281,6 +24666,38 @@ "unset-value": "^1.0.0" } }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "dev": true, + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true + } + } + }, "cachedir": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", @@ -22319,6 +24736,12 @@ "tslib": "^2.0.3" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, "caniuse-lite": { "version": "1.0.30001361", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001361.tgz", @@ -22354,6 +24777,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true + }, "check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -22382,6 +24811,31 @@ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true }, + "chrome-launcher": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.13.4.tgz", + "integrity": "sha512-nnzXiDbGKjDSK6t2I+35OAPBy5Pw/39bgkb/ZAFwMhwJbdYBp6aH+vW28ZgtjdU890Q7D+3wN/tB8N66q5Gi2A==", + "dev": true, + "requires": { + "@types/node": "*", + "escape-string-regexp": "^1.0.5", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^0.5.3", + "rimraf": "^3.0.2" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + } + } + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -22498,6 +24952,12 @@ "del": "^4.1.1" } }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "dev": true + }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -22548,6 +25008,54 @@ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -22571,6 +25079,15 @@ "shallow-clone": "^3.0.0" } }, + "clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -22750,6 +25267,34 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "dev": true, + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + } + } + }, "connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -22884,6 +25429,24 @@ "which": "^2.0.1" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true + }, + "csp_evaluator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.0.tgz", + "integrity": "sha512-TcB+ZH9wZBG314jAUpKHPl1oYbRJV+nAT2YwZ9y4fmUN0FkEJa8e/hKZoOgzLYp1Z/CJdFhbhhGIGh0XG8W54Q==", + "dev": true + }, "css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -22903,6 +25466,21 @@ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true }, + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "cssstyle": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", + "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", + "dev": true, + "requires": { + "cssom": "0.3.x" + } + }, "csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", @@ -23133,12 +25711,27 @@ "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", "dev": true }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", "dev": true }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dev": true, + "requires": { + "mimic-response": "^1.0.0" + } + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -23177,6 +25770,12 @@ } } }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", + "dev": true + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -23422,6 +26021,15 @@ "tslib": "^2.0.3" } }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "requires": { + "is-obj": "^2.0.0" + } + }, "dotenv": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", @@ -23458,6 +26066,12 @@ "moment": "^2.15.1" } }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -24009,6 +26623,27 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true }, + "event-source-polyfill": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.28.tgz", + "integrity": "sha512-S4Je04Br394hKTiyUXKAkNpha/7r172rLNu76sdxB3Nw2g9sjuEc4kxMu/miaikvWA4jvJ2x6Xkg6BOAb1aM7g==", + "dev": true + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, "eventemitter2": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.6.tgz", @@ -24734,6 +27369,12 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -24832,6 +27473,12 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-intrinsic": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", @@ -25183,6 +27830,12 @@ } } }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", + "dev": true + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -25342,6 +27995,12 @@ "toidentifier": "1.0.1" } }, + "http-link-header": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-0.8.0.tgz", + "integrity": "sha512-qsh/wKe1Mk1vtYEFr+LpQBFWTO1gxZQBdii2D0Umj+IUQ23r5sT088Rhpq4XzpSyIpaX7vwjB8Rrtx8u9JTg+Q==", + "dev": true + }, "http-parser-js": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", @@ -25449,6 +28108,12 @@ "minimatch": "^3.0.4" } }, + "image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -25467,6 +28132,12 @@ } } }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", + "dev": true + }, "import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -25606,6 +28277,21 @@ "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, + "intl-messageformat": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-4.4.0.tgz", + "integrity": "sha512-z+Bj2rS3LZSYU4+sNitdHrwnBhr0wO80ZJSW8EzKDBowwUe3Q/UsvgCGjrwa+HPzoGCLEb9HAjfJgo4j2Sac8w==", + "dev": true, + "requires": { + "intl-messageformat-parser": "^1.8.1" + } + }, + "intl-messageformat-parser": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-1.8.1.tgz", + "integrity": "sha512-IMSCKVf0USrM/959vj3xac7s8f87sc+80Y/ipBzdKy4ifBv5Gsj2tZ41EAaURVg01QU71fYr77uA8Meh6kELbg==", + "dev": true + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -25824,6 +28510,12 @@ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true }, + "is-npm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-3.0.0.tgz", + "integrity": "sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA==", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -25839,6 +28531,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true + }, "is-path-cwd": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", @@ -25998,6 +28696,12 @@ "is-docker": "^2.0.0" } }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -26022,6 +28726,16 @@ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dev": true, + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -26150,6 +28864,31 @@ } } }, + "joi": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.0.tgz", + "integrity": "sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, + "jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, + "js-library-detector": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.5.0.tgz", + "integrity": "sha512-Kq7VckJ5kb26kHMAu1sDO8t2qr7M5Uw6Gf7fVGtu1YceoHdqTcobwnB5kStcktusPuPmiCE8PbCaiLzhiBsSAw==", + "dev": true + }, "js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -26182,6 +28921,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true + }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -26285,12 +29030,30 @@ "integrity": "sha512-dgFenZnMsc1xGNqgdtgnh7DK+Oy352CE3VZLbzcbQpsBs9iI2K3M0IRrdgREZ72eItTjbl0suRyvKRdVQa9GbA==", "dev": true }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "dev": true, + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "dev": true, + "requires": { + "package-json": "^6.3.0" + } + }, "lazy-ass": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", @@ -26317,6 +29080,172 @@ "type-check": "~0.4.0" } }, + "lighthouse": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-9.3.0.tgz", + "integrity": "sha512-jooRAn9LQYk/KgALmwd9fPcmfGecVnd15pr7Ya4pZ1mhG9SKgVOIWwj8cjxZlWATrMV31ySwkX37dw/Jepm9gw==", + "dev": true, + "requires": { + "axe-core": "4.3.5", + "chrome-launcher": "^0.15.0", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.0", + "cssstyle": "1.2.1", + "enquirer": "^2.3.6", + "http-link-header": "^0.8.0", + "intl-messageformat": "^4.4.0", + "jpeg-js": "^0.4.3", + "js-library-detector": "^6.4.0", + "lighthouse-logger": "^1.3.0", + "lighthouse-stack-packs": "^1.7.0", + "lodash.clonedeep": "^4.5.0", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "lodash.set": "^4.3.2", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.2.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "ps-list": "^8.0.0", + "raven": "^2.2.1", + "robots-parser": "^3.0.0", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.12.7", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "dependencies": { + "chrome-launcher": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.1.tgz", + "integrity": "sha512-UugC8u59/w2AyX5sHLZUHoxBAiSiunUhZa3zZwMH6zPVis0C3dDKiRWyUGIo14tTbZHGVviWxv3PQWZ7taZ4fg==", + "dev": true, + "requires": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + } + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "lighthouse-logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.3.0.tgz", + "integrity": "sha512-BbqAKApLb9ywUli+0a+PcV04SyJ/N1q/8qgCNe6U97KbPCS1BTksEuHFLYdvc8DltuhfxIUBqDZsC0bBGtl3lA==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, + "lighthouse-logger": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.2.0.tgz", + "integrity": "sha512-wzUvdIeJZhRsG6gpZfmSCfysaxNEr43i+QT+Hie94wvHDKFLi4n7C2GqZ4sTC+PH5b5iktmXJvU87rWvhP3lHw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "marky": "^1.2.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "lighthouse-stack-packs": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.8.2.tgz", + "integrity": "sha512-vlCUxxQAB8Nu6LQHqPpDRiMi06Du593/my/6JbMttQeEfJ7pf4OS8obSTh5xSOS80U/O7fq59Q8rQGAUxQatUQ==", + "dev": true + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -26397,12 +29326,30 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -26415,6 +29362,12 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -26536,6 +29489,12 @@ } } }, + "lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -26607,6 +29566,12 @@ "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", + "dev": true + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -26616,6 +29581,12 @@ "object-visit": "^1.0.0" } }, + "marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "dev": true + }, "match-sorter": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", @@ -26632,6 +29603,17 @@ "dev": true, "optional": true }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -26703,6 +29685,12 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, + "metaviewport-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.2.0.tgz", + "integrity": "sha512-qL5NtY18LGs7lvZCkj3ep2H4Pes9rIiSLZRUyfDdvVw7pWFA0eLwmqaIxApD74RGvUrNEtk9e5Wt1rT+VlCvGw==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -26749,6 +29737,12 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -26994,7 +29988,6 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, - "optional": true, "requires": { "whatwg-url": "^5.0.0" } @@ -27119,6 +30112,12 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "dev": true + }, "npm-api": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-api/-/npm-api-1.0.1.tgz", @@ -27804,6 +30803,12 @@ } } }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "dev": true + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -27882,6 +30887,63 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "dev": true, + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "dev": true, + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } + } + } + }, "pacote": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/pacote/-/pacote-12.0.3.tgz", @@ -27950,6 +31012,12 @@ "callsites": "^3.0.0" } }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, "parse-conflict-json": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-2.0.2.tgz", @@ -28074,6 +31142,15 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "requires": { + "through": "~2.3" + } + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -28359,6 +31436,27 @@ "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", "dev": true }, + "ps-list": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-8.1.0.tgz", + "integrity": "sha512-NoGBqJe7Ou3kfQxEvDzDyKGAyEgwIuD3YrfXinjcCmBRv0hTld0Xb71hrXvtsNPj7HSFATfemvzB8PPJtq6Yag==", + "dev": true + }, + "ps-tree": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.2.0.tgz", + "integrity": "sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==", + "dev": true, + "requires": { + "event-stream": "=3.3.4" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -28432,6 +31530,33 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true }, + "raven": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/raven/-/raven-2.6.4.tgz", + "integrity": "sha512-6PQdfC4+DQSFncowthLf+B6Hr0JpPsFBgTVYTAOq7tCmx/kR4SXbeawtPch20+3QfUcQDoJBLjWW1ybvZ4kXTw==", + "dev": true, + "requires": { + "cookie": "0.3.1", + "md5": "^2.2.1", + "stack-trace": "0.0.10", + "timed-out": "4.0.1", + "uuid": "3.3.2" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha512-+IJOX0OqlHCszo2mBUq+SrEbCj6w7Kpffqx60zYbPTFaO4+yYgRjHwcZNpWvaTylDHaV7PPmBHzSecZiMhtPgw==", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + } + } + }, "raw-body": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", @@ -28452,6 +31577,32 @@ } } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + } + } + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -28756,6 +31907,24 @@ "unicode-match-property-value-ecmascript": "^2.0.0" } }, + "registry-auth-token": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", + "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", + "dev": true, + "requires": { + "rc": "1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "dev": true, + "requires": { + "rc": "^1.2.8" + } + }, "regjsgen": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", @@ -28845,12 +32014,24 @@ "throttleit": "^1.0.0" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -28895,6 +32076,15 @@ "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "resq": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/resq/-/resq-1.10.2.tgz", @@ -28954,6 +32144,12 @@ "glob": "^7.1.3" } }, + "robots-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.0.tgz", + "integrity": "sha512-6xkze3WRdneibICBAzMKcXyTKQw5shA3GbwoEJy7RSvxpZNGF0GMuYKE1T0VMP4fwx/fQs0n0mtriOqRtk5L1w==", + "dev": true + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -29045,6 +32241,23 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", + "dev": true, + "requires": { + "semver": "^5.0.3" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -29637,6 +32850,26 @@ "wbuf": "^1.7.3" } }, + "speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", + "dev": true, + "requires": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" + } + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==", + "dev": true, + "requires": { + "through": "2" + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -29675,7 +32908,39 @@ "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", "dev": true, "requires": { - "minipass": "^3.1.1" + "minipass": "^3.1.1" + } + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true + }, + "start-server-and-test": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.14.0.tgz", + "integrity": "sha512-on5ELuxO2K0t8EmNj9MtVlFqwBMxfWOhu4U7uZD1xccVpFlOQKR93CSe0u98iQzfNxRyaNTb/CdadbNllplTsw==", + "dev": true, + "requires": { + "bluebird": "3.7.2", + "check-more-types": "2.24.0", + "debug": "4.3.2", + "execa": "5.1.1", + "lazy-ass": "1.6.0", + "ps-tree": "1.2.0", + "wait-on": "6.0.0" + }, + "dependencies": { + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } } }, "static-extend": { @@ -29762,6 +33027,15 @@ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==", + "dev": true, + "requires": { + "duplexer": "~0.1.1" + } + }, "string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -29871,6 +33145,12 @@ "strip-bom": "^2.0.0" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -29924,6 +33204,110 @@ "yallist": "^4.0.0" } }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==", + "dev": true, + "requires": { + "execa": "^0.7.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + } + } + }, "terser": { "version": "5.14.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.1.tgz", @@ -29961,6 +33345,12 @@ "integrity": "sha512-MeqZRHLuaGamUXGuVn2ivtU3LA3mLCCIO5kUGoohTCoGmCBg/+8yPhWVX9WSl9telvVd8erftjFk9Fwb2dD6rw==", "dev": true }, + "third-party-web": { + "version": "0.12.7", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.12.7.tgz", + "integrity": "sha512-9d/OfjEOjyeOpnm4F9o0KSK6BI6ytvi9DINSB5h1+jdlCvQlhKpViMSxWpBN9WstdfDQ61BS6NxWqcPCuQCAJg==", + "dev": true + }, "throttleit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", @@ -30030,6 +33420,12 @@ } } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "dev": true + }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -30071,8 +33467,13 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "optional": true + "dev": true + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true }, "treeverse": { "version": "1.0.4", @@ -30221,6 +33622,15 @@ "mime-types": "~2.1.24" } }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", @@ -30305,6 +33715,15 @@ "imurmurhash": "^0.1.4" } }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "requires": { + "crypto-random-string": "^2.0.0" + } + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -30388,6 +33807,153 @@ "picocolors": "^1.0.0" } }, + "update-notifier": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-3.0.1.tgz", + "integrity": "sha512-grrmrB6Zb8DUiyDIaeRTBCkgISYUgETNe7NglEbVsrLWXeESnlCSP50WfRSj/GmzMPl6Uchj24S/p80nP/ZQrQ==", + "dev": true, + "requires": { + "boxen": "^3.0.0", + "chalk": "^2.0.1", + "configstore": "^4.0.0", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.1.0", + "is-npm": "^3.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "semver-diff": "^2.0.0", + "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "configstore": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-4.0.0.tgz", + "integrity": "sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ==", + "dev": true, + "requires": { + "dot-prop": "^4.1.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "unique-string": "^1.0.0", + "write-file-atomic": "^2.0.0", + "xdg-basedir": "^3.0.0" + } + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==", + "dev": true + }, + "dot-prop": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==", + "dev": true, + "requires": { + "global-dirs": "^0.1.0", + "is-path-inside": "^1.0.0" + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", + "dev": true, + "requires": { + "crypto-random-string": "^1.0.0" + } + }, + "write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", + "dev": true + } + } + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -30534,6 +34100,30 @@ "vinyl": "^2.0.1" } }, + "wait-on": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.0.tgz", + "integrity": "sha512-tnUJr9p5r+bEYXPUdRseolmz5XqJTTj98JgOsfBn7Oz2dxfE2g3zw1jE+Mo8lopM3j3et/Mq1yW7kKX6qw7RVw==", + "dev": true, + "requires": { + "axios": "^0.21.1", + "joi": "^17.4.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.1.0" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.0" + } + } + } + }, "walk-up-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-1.0.0.tgz", @@ -30572,8 +34162,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "optional": true + "dev": true }, "webpack": { "version": "5.73.0", @@ -30800,12 +34389,17 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "dev": true + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "dev": true, - "optional": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -30833,6 +34427,12 @@ "is-symbol": "^1.0.3" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "dev": true + }, "which-pm": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.0.0.tgz", @@ -30852,6 +34452,48 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "widest-line": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", + "dev": true, + "requires": { + "string-width": "^2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "wildcard": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", @@ -30942,6 +34584,18 @@ "dev": true, "requires": {} }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -30954,6 +34608,47 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d9c0cc8b..2bf45369 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,12 @@ "build-local": "cross-env NODE_ENV=development webpack", "build-staging": "cross-env NODE_ENV=staging webpack", "build-release": "cross-env NODE_ENV=production webpack", - "prettier": "npx prettier --write 'src/**/*.{tsx,ts,css,html,json}'" + "lhci": "lhci autorun", + "prettier": "npx prettier --write 'src/**/*.{tsx,ts,css,html,json}'", + "cypress:open": "cypress open", + "cypress:run": "cypress run --browser electron", + "test:dev": "start-server-and-test start http://localhost:3000 cypress:open", + "test:ci": "start-server-and-test start http://localhost:3000 cypress:run" }, "repository": { "type": "git", @@ -36,7 +41,9 @@ "@emotion/css": "^11.9.0", "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", + "@lhci/cli": "^0.9.0", "@trivago/prettier-plugin-sort-imports": "^3.2.0", + "@types/event-source-polyfill": "^1.0.0", "@types/node": "^18.0.0", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", @@ -55,11 +62,13 @@ "eslint": "^8.19.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-react": "^7.30.1", + "event-source-polyfill": "1.0.28", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "nanoid": "^4.0.0", "prettier": "^2.7.1", "react-icons": "^4.4.0", + "start-server-and-test": "^1.14.0", "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.73.0", diff --git a/frontend/src/ErrorBoundary/ErrorHostToken.tsx b/frontend/src/ErrorBoundary/ErrorHostToken.tsx index 45eee6c9..99045495 100644 --- a/frontend/src/ErrorBoundary/ErrorHostToken.tsx +++ b/frontend/src/ErrorBoundary/ErrorHostToken.tsx @@ -3,8 +3,7 @@ import { ErrorBoundary } from 'react-error-boundary'; import { QueryErrorResetBoundary } from 'react-query'; import { useNavigate } from 'react-router-dom'; -const EXPIRED_TOKEN_TEXT = '만료된 ν† ν°μž…λ‹ˆλ‹€.'; -const NOT_TOKEN_TEXT = '헀더에 토큰 값이 μ •μƒμ μœΌλ‘œ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'; +import errorMessage from '@/constants/errorMessage'; interface ErrorHostTokenProps { children: React.ReactNode; @@ -17,11 +16,10 @@ const ErrorHostToken: React.FC = ({ children }) => { { - const err = error as AxiosError<{ message: string }>; - const message = err.response?.data.message; + const err = error as AxiosError<{ errorCode: keyof typeof errorMessage }>; + const errorCode = err.response?.data.errorCode; - if (message === EXPIRED_TOKEN_TEXT || message === NOT_TOKEN_TEXT) { - localStorage.removeItem('token'); + if (errorCode === 'A002' || errorCode === 'A003') { navigate(`/host`); } diff --git a/frontend/src/ErrorBoundary/ErrorUserTask.tsx b/frontend/src/ErrorBoundary/ErrorUserTask.tsx index 6b307d19..43007bba 100644 --- a/frontend/src/ErrorBoundary/ErrorUserTask.tsx +++ b/frontend/src/ErrorBoundary/ErrorUserTask.tsx @@ -6,12 +6,9 @@ import { useNavigate, useParams } from 'react-router-dom'; import useToast from '@/hooks/useToast'; -// μ‚¬μš©μžκ°€ 체크리슀트 νŽ˜μ΄μ§€μ— 접속 쀑일 λ•Œ, κ΄€λ¦¬μžκ°€ μž‘μ—…μ„ μˆ˜μ • ν•  경우 λ°œμƒ -const NOT_TASK_TEXT = 'μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μž‘μ—…μž…λ‹ˆλ‹€.'; +import { ID } from '@/types'; -// μ‚¬μš©μžλ“€μ΄ 체크리슀트 νŽ˜μ΄μ§€μ— 접속 쀑일 λ•Œ, λ‹€λ₯Έ μ‚¬μš©μžκ°€ μ²΄ν¬λ¦¬μ‹œνŠΈλ₯Ό 제좜 ν•œ 경우 λ°œμƒ -const NOT_JOB_TEXT = 'μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'; -const NOT_JOB_WORK_TEXT = 'ν˜„μž¬ 진행쀑인 μž‘μ—…μ΄ μ‘΄μž¬ν•˜μ§€ μ•Šμ•„ μ‘°νšŒν•  수 μ—†μŠ΅λ‹ˆλ‹€'; +import errorMessage from '@/constants/errorMessage'; interface ErrorUserTaskProps { children: React.ReactNode; @@ -19,35 +16,26 @@ interface ErrorUserTaskProps { const ErrorUserTask: React.FC = ({ children }) => { const navigate = useNavigate(); - const { hostId } = useParams(); - const [message, setMessage] = useState(''); + const { hostId } = useParams() as { hostId: ID }; + const [errorCode, setErrorCode] = useState(); const { openToast } = useToast(); - const isTaskListPage = location.pathname.split('/').length === 6; - useEffect(() => { - if (message === NOT_TASK_TEXT) { - openToast('ERROR', `κ΄€λ¦¬μžκ°€ 체크리슀트λ₯Ό μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.`); + if (errorCode === 'R001' || errorCode === 'R002') { + openToast('ERROR', errorMessage[`${errorCode}`]); navigate(`/enter/${hostId}/spaces`); } - - if (message === NOT_JOB_TEXT || message === NOT_JOB_WORK_TEXT) { - if (isTaskListPage) { - openToast('ERROR', `λ‹€λ₯Έ μ‚¬μš©μžκ°€ 체크리슀트λ₯Ό 제좜 ν–ˆμŠ΅λ‹ˆλ‹€.`); - navigate(`/enter/${hostId}/spaces`); - } - } - }, [message]); + }, [errorCode]); return ( { - const err = error as AxiosError<{ message: string }>; - const message = err.response?.data.message; + const err = error as AxiosError<{ errorCode: keyof typeof errorMessage }>; + const newErrorCode = err.response?.data.errorCode; - setMessage(message); + setErrorCode(newErrorCode); return <>; }} diff --git a/frontend/src/ErrorBoundary/ErrorUserToken.tsx b/frontend/src/ErrorBoundary/ErrorUserToken.tsx index 6cc3dab6..d0ff5b61 100644 --- a/frontend/src/ErrorBoundary/ErrorUserToken.tsx +++ b/frontend/src/ErrorBoundary/ErrorUserToken.tsx @@ -4,10 +4,9 @@ import { ErrorBoundary } from 'react-error-boundary'; import { QueryErrorResetBoundary } from 'react-query'; import { useNavigate, useParams } from 'react-router-dom'; -import useToast from '@/hooks/useToast'; +import { ID } from '@/types'; -const EXPIRED_TOKEN_TEXT = '만료된 ν† ν°μž…λ‹ˆλ‹€.'; -const NOT_TOKEN_TEXT = '헀더에 토큰 값이 μ •μƒμ μœΌλ‘œ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.'; +import errorMessage from '@/constants/errorMessage'; interface ErrorUserTokenProps { children: React.ReactNode; @@ -15,24 +14,23 @@ interface ErrorUserTokenProps { const ErrorUserToken: React.FC = ({ children }) => { const navigate = useNavigate(); - const { hostId } = useParams(); - const [message, setMessage] = useState(''); + const { hostId } = useParams() as { hostId: ID }; + const [errorCode, setErrorCode] = useState(); useEffect(() => { - if (message === EXPIRED_TOKEN_TEXT || message === NOT_TOKEN_TEXT) { - localStorage.removeItem('token'); + if (errorCode === 'A002' || errorCode === 'A003') { navigate(`/enter/${hostId}/pwd`); } - }, [message]); + }, [errorCode]); return ( { - const err = error as AxiosError<{ message: string }>; - const message = err.response?.data.message; + const err = error as AxiosError<{ errorCode: keyof typeof errorMessage }>; + const newErrorCode = err.response?.data.errorCode; - setMessage(message); + setErrorCode(newErrorCode); return <>; }} diff --git a/frontend/src/apis/config.ts b/frontend/src/apis/config.ts index 402f4252..8ed96efe 100644 --- a/frontend/src/apis/config.ts +++ b/frontend/src/apis/config.ts @@ -1,6 +1,6 @@ import axios from 'axios'; -const API_URL = process.env.REACT_APP_API_URL!; +const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080'; export const axiosInstance = axios.create({ baseURL: API_URL, @@ -12,7 +12,10 @@ export const axiosInstanceToken = axios.create({ axiosInstanceToken.interceptors.request.use( config => { - const accessToken = localStorage.getItem('token'); + const tokenKey = sessionStorage.getItem('tokenKey'); + if (!tokenKey) return; + + const accessToken = localStorage.getItem(tokenKey); if (accessToken) { config.headers = { diff --git a/frontend/src/apis/githubAuth.ts b/frontend/src/apis/githubAuth.ts index 856a2189..9e873b61 100644 --- a/frontend/src/apis/githubAuth.ts +++ b/frontend/src/apis/githubAuth.ts @@ -4,7 +4,7 @@ import { ApiHostTokenData } from '@/types/apis'; import { axiosInstance } from './config'; -const getToken = async (code: string | null) => { +const getToken = async (code: string) => { const { data }: AxiosResponse = await axiosInstance({ method: 'POST', url: '/api/login', diff --git a/frontend/src/apis/image.ts b/frontend/src/apis/image.ts index 7bc1a697..37216e0b 100644 --- a/frontend/src/apis/image.ts +++ b/frontend/src/apis/image.ts @@ -1,6 +1,6 @@ import { axiosInstanceToken } from './config'; -const postImageUpload = async (formData: any) => { +const postImageUpload = async (formData: FormData) => { const { data } = await axiosInstanceToken({ method: 'POST', url: `/api/imageUpload`, diff --git a/frontend/src/apis/job.ts b/frontend/src/apis/job.ts index daff6591..c424f67a 100644 --- a/frontend/src/apis/job.ts +++ b/frontend/src/apis/job.ts @@ -5,7 +5,7 @@ import { ApiJobActiveData, ApiJobData } from '@/types/apis'; import { axiosInstanceToken } from './config'; -const getJobs = async (spaceId: ID | undefined) => { +const getJobs = async (spaceId: ID) => { const { data }: AxiosResponse = await axiosInstanceToken({ method: 'GET', url: `/api/spaces/${spaceId}/jobs`, @@ -23,9 +23,7 @@ const getJobActive = async (jobId: ID) => { return data; }; -// job 생성 -// Location : /api/jobs/{jobId} -const postNewJob = (spaceId: ID | undefined, name: string, sections: SectionType[]) => { +const postNewJob = (spaceId: ID, name: string, sections: SectionType[]) => { return axiosInstanceToken({ method: 'POST', url: `/api/spaces/${spaceId}/jobs`, @@ -36,8 +34,7 @@ const postNewJob = (spaceId: ID | undefined, name: string, sections: SectionType }); }; -// job μˆ˜μ • -const putJob = (jobId: ID | undefined, name: string, sections: SectionType[]) => { +const putJob = (jobId: ID, name: string, sections: SectionType[]) => { return axiosInstanceToken({ method: 'PUT', url: `/api/jobs/${jobId}`, @@ -48,7 +45,6 @@ const putJob = (jobId: ID | undefined, name: string, sections: SectionType[]) => }); }; -// job μ‚­μ œ const deleteJob = (jobId: ID) => { return axiosInstanceToken({ method: 'DELETE', diff --git a/frontend/src/apis/password.ts b/frontend/src/apis/password.ts index 5dc5b9cf..f95329bb 100644 --- a/frontend/src/apis/password.ts +++ b/frontend/src/apis/password.ts @@ -1,10 +1,11 @@ import { AxiosResponse } from 'axios'; +import { ID } from '@/types'; import { ApiTokenData } from '@/types/apis'; import { axiosInstance, axiosInstanceToken } from './config'; -const postPassword = async ({ hostId, password }: any) => { +const postPassword = async (hostId: ID, password: string) => { const { data }: AxiosResponse = await axiosInstance({ method: 'POST', url: `api/hosts/${hostId}/enter`, @@ -16,9 +17,7 @@ const postPassword = async ({ hostId, password }: any) => { return data; }; -// /api/spacePassword -// space password μˆ˜μ • -const patchSpacePassword = (password: number | string) => { +const patchSpacePassword = (password: string) => { return axiosInstanceToken({ method: 'PATCH', url: `/api/spacePassword`, diff --git a/frontend/src/apis/slack.ts b/frontend/src/apis/slack.ts index d2327fac..110285db 100644 --- a/frontend/src/apis/slack.ts +++ b/frontend/src/apis/slack.ts @@ -8,7 +8,6 @@ type ApiSlackUrlData = { slackUrl: string; }; -// slack URL 쑰회 const getSlackUrl = async (jobId: ID) => { const { data }: AxiosResponse = await axiosInstanceToken({ method: 'GET', @@ -18,7 +17,6 @@ const getSlackUrl = async (jobId: ID) => { return data; }; -// slack URL μˆ˜μ • const putSlackUrl = (jobId: ID, slackUrl: string) => { return axiosInstanceToken({ method: 'PUT', diff --git a/frontend/src/apis/space.ts b/frontend/src/apis/space.ts index 3734522a..1f2bb828 100644 --- a/frontend/src/apis/space.ts +++ b/frontend/src/apis/space.ts @@ -14,8 +14,7 @@ const getSpaces = async () => { return data; }; -// space 생성 -const postNewSpace = (name: string, imageUrl: string | undefined) => { +const postNewSpace = (name: string, imageUrl: string) => { return axiosInstanceToken({ method: 'POST', url: `/api/spaces`, @@ -23,16 +22,14 @@ const postNewSpace = (name: string, imageUrl: string | undefined) => { }); }; -// space μ‚­μ œ -const deleteSpace = (spaceId: string | undefined) => { +const deleteSpace = (spaceId: ID) => { return axiosInstanceToken({ method: 'DELETE', url: `/api/spaces/${spaceId}`, }); }; -// space 단건 쑰회 -const getSpace = async (spaceId: ID | undefined) => { +const getSpace = async (spaceId: ID) => { const { data }: AxiosResponse = await axiosInstanceToken({ method: 'GET', url: `/api/spaces/${spaceId}`, @@ -41,8 +38,7 @@ const getSpace = async (spaceId: ID | undefined) => { return data; }; -// space μˆ˜μ • -const putSpace = (spaceId: ID | undefined, name: string, imageUrl: string | undefined) => { +const putSpace = (spaceId: ID, name: string, imageUrl: string) => { return axiosInstanceToken({ method: 'PUT', url: `/api/spaces/${spaceId}`, diff --git a/frontend/src/apis/submission.ts b/frontend/src/apis/submission.ts index d4439b71..6ab09b81 100644 --- a/frontend/src/apis/submission.ts +++ b/frontend/src/apis/submission.ts @@ -1,10 +1,11 @@ import { AxiosResponse } from 'axios'; +import { ID } from '@/types'; import { ApiSubmissionData } from '@/types/apis'; import { axiosInstanceToken } from './config'; -const postJobComplete = ({ jobId, author }: any) => { +const postJobComplete = (jobId: ID, author: string) => { return axiosInstanceToken({ method: 'POST', url: `/api/jobs/${jobId}/complete`, @@ -14,8 +15,7 @@ const postJobComplete = ({ jobId, author }: any) => { }); }; -// submission λͺ©λ‘ 쑰회 -const getSubmission = async ({ spaceId }: any) => { +const getSubmission = async (spaceId: ID) => { const { data }: AxiosResponse = await axiosInstanceToken({ method: 'GET', url: `/api/spaces/${spaceId}/submissions`, diff --git a/frontend/src/apis/task.ts b/frontend/src/apis/task.ts index 3ebb2598..cedc7b06 100644 --- a/frontend/src/apis/task.ts +++ b/frontend/src/apis/task.ts @@ -5,14 +5,14 @@ import { ApiTaskData } from '@/types/apis'; import { axiosInstanceToken } from './config'; -const postNewRunningTasks = async (jobId: ID | undefined) => { +const postNewRunningTasks = async (jobId: ID) => { return axiosInstanceToken({ method: 'POST', url: `/api/jobs/${jobId}/runningTasks/new`, }); }; -const getRunningTasks = async (jobId: ID | undefined) => { +const getRunningTasks = async (jobId: ID) => { const { data }: AxiosResponse = await axiosInstanceToken({ method: 'GET', url: `/api/jobs/${jobId}/runningTasks`, @@ -21,7 +21,7 @@ const getRunningTasks = async (jobId: ID | undefined) => { return data; }; -const getTasks = async (jobId: ID | undefined) => { +const getTasks = async (jobId: ID) => { const { data }: AxiosResponse = await axiosInstanceToken({ method: 'GET', url: `/api/jobs/${jobId}/tasks`, @@ -30,13 +30,20 @@ const getTasks = async (jobId: ID | undefined) => { return data; }; -const postCheckTask = (taskId: ID | undefined) => { +const postCheckTask = (taskId: ID) => { return axiosInstanceToken({ method: 'POST', url: `/api/tasks/${taskId}/flip`, }); }; -const apiTask = { postCheckTask, getRunningTasks, getTasks, postNewRunningTasks }; +const postSectionAllCheckTask = (sectionId: ID) => { + return axiosInstanceToken({ + method: 'POST', + url: `/api/sections/${sectionId}/runningTask/allCheck`, + }); +}; + +const apiTask = { postCheckTask, getRunningTasks, getTasks, postNewRunningTasks, postSectionAllCheckTask }; export default apiTask; diff --git a/frontend/src/assets/createSpace.png b/frontend/src/assets/createSpace.png new file mode 100644 index 00000000..e4b8db43 Binary files /dev/null and b/frontend/src/assets/createSpace.png differ diff --git a/frontend/src/assets/dashboard.png b/frontend/src/assets/dashboard.png new file mode 100644 index 00000000..2e191734 Binary files /dev/null and b/frontend/src/assets/dashboard.png differ diff --git a/frontend/src/assets/edit.png b/frontend/src/assets/edit.png new file mode 100644 index 00000000..bc60f266 Binary files /dev/null and b/frontend/src/assets/edit.png differ diff --git a/frontend/src/assets/edit2.png b/frontend/src/assets/edit2.png new file mode 100644 index 00000000..d630d536 Binary files /dev/null and b/frontend/src/assets/edit2.png differ diff --git a/frontend/src/assets/mobileView1.png b/frontend/src/assets/mobileView1.png new file mode 100644 index 00000000..0fb21d37 Binary files /dev/null and b/frontend/src/assets/mobileView1.png differ diff --git a/frontend/src/assets/mobileView2.png b/frontend/src/assets/mobileView2.png new file mode 100644 index 00000000..0362fab7 Binary files /dev/null and b/frontend/src/assets/mobileView2.png differ diff --git a/frontend/src/assets/mobileView3.png b/frontend/src/assets/mobileView3.png new file mode 100644 index 00000000..887b87f9 Binary files /dev/null and b/frontend/src/assets/mobileView3.png differ diff --git a/frontend/src/assets/mobileView4.png b/frontend/src/assets/mobileView4.png new file mode 100644 index 00000000..9a3ca1b6 Binary files /dev/null and b/frontend/src/assets/mobileView4.png differ diff --git a/frontend/src/components/common/Button/index.tsx b/frontend/src/components/common/Button/index.tsx index 7e26e427..f93b4a6b 100644 --- a/frontend/src/components/common/Button/index.tsx +++ b/frontend/src/components/common/Button/index.tsx @@ -1,6 +1,4 @@ -import { css } from '@emotion/react'; - -import theme from '@/styles/theme'; +import styles from './styles'; interface ButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; @@ -8,19 +6,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes { const Button: React.FC = ({ children, ...props }) => { return ( - ); diff --git a/frontend/src/components/common/Button/styles.ts b/frontend/src/components/common/Button/styles.ts new file mode 100644 index 00000000..4018ab41 --- /dev/null +++ b/frontend/src/components/common/Button/styles.ts @@ -0,0 +1,18 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const button = css` + background: ${theme.colors.primary}; + width: 224px; + height: 48px; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + color: ${theme.colors.white}; + margin: 24px; +`; + +const styles = { button }; + +export default styles; diff --git a/frontend/src/components/common/Dimmer/index.tsx b/frontend/src/components/common/Dimmer/index.tsx index 8f7088af..ef47027d 100644 --- a/frontend/src/components/common/Dimmer/index.tsx +++ b/frontend/src/components/common/Dimmer/index.tsx @@ -1,8 +1,6 @@ -import { css } from '@emotion/react'; - import useModal from '@/hooks/useModal'; -import theme from '@/styles/theme'; +import styles from './styles'; interface DimmerProps { children: React.ReactNode; @@ -18,18 +16,7 @@ const Dimmer: React.FC = ({ children, isAbleClick = true, mode = 'f }; return ( -
+
{children}
); diff --git a/frontend/src/components/common/Dimmer/styles.ts b/frontend/src/components/common/Dimmer/styles.ts new file mode 100644 index 00000000..1bd2914c --- /dev/null +++ b/frontend/src/components/common/Dimmer/styles.ts @@ -0,0 +1,17 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const dimmer = (mode: 'full' | 'mobile') => css` + background-color: ${theme.colors.shadow80}; + max-width: ${mode === 'full' ? '100vw' : '414px'}; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +`; + +const styles = { dimmer }; + +export default styles; diff --git a/frontend/src/components/common/GitHubLoginButton/styles.ts b/frontend/src/components/common/GitHubLoginButton/styles.ts index 3f00bee1..84339728 100644 --- a/frontend/src/components/common/GitHubLoginButton/styles.ts +++ b/frontend/src/components/common/GitHubLoginButton/styles.ts @@ -4,7 +4,7 @@ const wrapper = css` display: flex; justify-content: center; align-items: center; - width: 240px; + width: 260px; border-radius: 24px; color: white; background-color: #21262c; diff --git a/frontend/src/components/common/Input/index.tsx b/frontend/src/components/common/Input/index.tsx index 600f1c19..6e16663a 100644 --- a/frontend/src/components/common/Input/index.tsx +++ b/frontend/src/components/common/Input/index.tsx @@ -1,33 +1,9 @@ -import { css } from '@emotion/react'; - -import theme from '@/styles/theme'; +import styles from './styles'; type InputProps = React.ClassAttributes & React.InputHTMLAttributes; const Input: React.FC = ({ placeholder, onChange, ...props }) => { - return ( - - ); + return ; }; export default Input; diff --git a/frontend/src/components/common/Input/styles.ts b/frontend/src/components/common/Input/styles.ts new file mode 100644 index 00000000..b40eb98e --- /dev/null +++ b/frontend/src/components/common/Input/styles.ts @@ -0,0 +1,24 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const input = css` + border: none; + background-color: ${theme.colors.gray200}; + width: 100%; + border-radius: 12px; + width: 256px; + height: 48px; + padding: 8px 16px; + font-size: 16px; + &::placeholder { + color: ${theme.colors.gray400}; + } + &:focus { + outline: none; + } +`; + +const styles = { input }; + +export default styles; diff --git a/frontend/src/components/common/Loading/styles.ts b/frontend/src/components/common/Loading/styles.ts index 7cf564d6..88bee35c 100644 --- a/frontend/src/components/common/Loading/styles.ts +++ b/frontend/src/components/common/Loading/styles.ts @@ -6,6 +6,8 @@ import theme from '@/styles/theme'; const layout = css` width: 100vw; height: 100vh; + z-index: 10; + opacity: 1; `; const spinner = css` @@ -13,6 +15,7 @@ const spinner = css` height: 100%; margin: 0 0 0 -1px; display: flex; + flex-direction: column; align-items: center; box-sizing: border-box; justify-content: center; @@ -34,6 +37,7 @@ const faceSpinner = css` border-radius: 4rem; display: flex; justify-content: center; + background-color: ${theme.colors.background}; `; const faceSpinnerEye = css` diff --git a/frontend/src/components/common/LoadingOverlay/index.tsx b/frontend/src/components/common/LoadingOverlay/index.tsx new file mode 100644 index 00000000..8a5241b9 --- /dev/null +++ b/frontend/src/components/common/LoadingOverlay/index.tsx @@ -0,0 +1,13 @@ +import Loading from '../Loading'; + +import styles from './styles'; + +const LoadingOverlay: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default LoadingOverlay; diff --git a/frontend/src/components/common/LoadingOverlay/styles.ts b/frontend/src/components/common/LoadingOverlay/styles.ts new file mode 100644 index 00000000..2e649092 --- /dev/null +++ b/frontend/src/components/common/LoadingOverlay/styles.ts @@ -0,0 +1,20 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const loadingOverlay = css` + top: 0; + left: 0; + position: absolute; + min-width: 100vw; + height: 100vh; + background-image: linear-gradient(${theme.colors.shadow50}, ${theme.colors.shadow50}); + z-index: 100; + cursor: wait; +`; + +const styles = { + loadingOverlay, +}; + +export default styles; diff --git a/frontend/src/components/host/ImageBox/index.tsx b/frontend/src/components/host/ImageBox/index.tsx index 0ab38533..f4a22f85 100644 --- a/frontend/src/components/host/ImageBox/index.tsx +++ b/frontend/src/components/host/ImageBox/index.tsx @@ -2,40 +2,33 @@ import { HiPlus } from 'react-icons/hi'; import styles from './styles'; -interface ImageBoxProps { - type: 'read' | 'create' | 'update'; - data?: { name: string; imageUrl: string; id: number }; - imageUrl: string | undefined; - onChangeImg?: (e: React.FormEvent) => void; +interface ImagePaintedLabelProps extends React.LabelHTMLAttributes { + imageUrl: string; } -interface ImageLabelBoxProps extends React.LabelHTMLAttributes { - children?: React.ReactNode; - imageUrl: string | undefined; +interface ImageChangeBoxProps { + imageUrl: string; + onChangeImage?: (e: React.FormEvent) => void; } -const ImageLabelBox: React.FC = ({ children, imageUrl, ...props }) => { - return ( - - ); -}; +interface ImageBoxMainProps { + children: React.ReactNode; +} -const ImageBox: React.FC = ({ type, imageUrl, onChangeImg }) => { - if (type === 'read') { - return ; - } +const ImagePaintedLabel: React.FC = ({ imageUrl }) => { + return ); }; -export default ImageBox; +const ImageBoxMain: React.FC = ({ children }) => { + return <>{children}; +}; + +export const ImageBox = Object.assign(ImageBoxMain, { + paintedLabel: ImagePaintedLabel, + changeBox: ImageChangeBox, +}); diff --git a/frontend/src/components/host/ImageBox/styles.ts b/frontend/src/components/host/ImageBox/styles.ts index af6342ea..aeb717c8 100644 --- a/frontend/src/components/host/ImageBox/styles.ts +++ b/frontend/src/components/host/ImageBox/styles.ts @@ -15,6 +15,17 @@ const imageBox = (imageUrl: string | undefined, borderStyle?: string) => css` position: relative; margin: 0 1.5rem; background-image: url(${imageUrl}); +`; + +const imagePaintedLabel = (imageUrl: string) => css` + ${imageBox(imageUrl)}; + + border-style: 'none'; +`; + +const imageChangeBox = (imageUrl: string, borderStyle?: string) => css` + ${imageBox(imageUrl, borderStyle)}; + cursor: pointer; `; @@ -43,6 +54,6 @@ const iconBox = css` transform: translate(-50%, -50%); `; -const styles = { imageBox, imageInput, imageCoverText, iconBox }; +const styles = { imageInput, imageCoverText, iconBox, imagePaintedLabel, imageChangeBox }; export default styles; diff --git a/frontend/src/components/host/JobControl/index.tsx b/frontend/src/components/host/JobControl/index.tsx index cdd24913..64977871 100644 --- a/frontend/src/components/host/JobControl/index.tsx +++ b/frontend/src/components/host/JobControl/index.tsx @@ -1,7 +1,5 @@ import Button from '@/components/common/Button'; -import useEditInput from '@/hooks/useEditInput'; - import styles from './styles'; interface JobControlProps { diff --git a/frontend/src/components/host/JobListCard/index.tsx b/frontend/src/components/host/JobListCard/index.tsx index e24638ea..a14e29c6 100644 --- a/frontend/src/components/host/JobListCard/index.tsx +++ b/frontend/src/components/host/JobListCard/index.tsx @@ -10,7 +10,7 @@ import emptyFolder from '@/assets/emptyFolder.png'; import styles from './styles'; interface JobListCardProps { - jobs: JobType[] | []; + jobs: JobType[]; } const JobListCard: React.FC = ({ jobs }) => { diff --git a/frontend/src/components/host/Navigation/useHostNavigation.ts b/frontend/src/components/host/Navigation/useHostNavigation.ts index a3a43951..a876eb4c 100644 --- a/frontend/src/components/host/Navigation/useHostNavigation.ts +++ b/frontend/src/components/host/Navigation/useHostNavigation.ts @@ -9,19 +9,17 @@ import { ID } from '@/types'; const useHostNavigation = () => { const navigate = useNavigate(); - const { spaceId } = useParams(); + const { spaceId } = useParams() as { spaceId: ID }; - const [selectedSpaceId, setSelectedSpaceId] = useState(spaceId); + const [selectedSpaceId, setSelectedSpaceId] = useState(spaceId); - const { data: spaceData } = useQuery(['spaces'], apiSpace.getSpaces, { - suspense: true, - }); + const { data: spaceData } = useQuery(['spaces'], apiSpace.getSpaces); const onClickPasswordUpdate = () => { navigate('/host/manage/passwordUpdate'); }; - const onClickSpace = (spaceId: number) => { + const onClickSpace = (spaceId: ID) => { setSelectedSpaceId(spaceId); navigate(`${spaceId}`); }; diff --git a/frontend/src/components/host/SectionDetailModal/index.tsx b/frontend/src/components/host/SectionDetailModal/index.tsx index 8f454cad..9eeea7c2 100644 --- a/frontend/src/components/host/SectionDetailModal/index.tsx +++ b/frontend/src/components/host/SectionDetailModal/index.tsx @@ -3,6 +3,7 @@ import { BiX } from 'react-icons/bi'; import Button from '@/components/common/Button'; import Dimmer from '@/components/common/Dimmer'; +import LoadingOverlay from '@/components/common/LoadingOverlay'; import ModalPortal from '@/portals/ModalPortal'; @@ -30,6 +31,7 @@ const SectionDetailModal: React.FC = props => { closeModal, imageUrl, description, + isImageLoading, } = useSectionDetailModal(props); return ( @@ -63,6 +65,7 @@ const SectionDetailModal: React.FC = props => { μ €μž₯
+ {isImageLoading && } ); diff --git a/frontend/src/components/host/SectionDetailModal/useSectionDetailModal.ts b/frontend/src/components/host/SectionDetailModal/useSectionDetailModal.ts index 6a885274..b081daac 100644 --- a/frontend/src/components/host/SectionDetailModal/useSectionDetailModal.ts +++ b/frontend/src/components/host/SectionDetailModal/useSectionDetailModal.ts @@ -9,6 +9,8 @@ import useToast from '@/hooks/useToast'; import apiImage from '@/apis/image'; +import errorMessage from '@/constants/errorMessage'; + const DEFAULT_NO_IMAGE = 'https://velog.velcdn.com/images/cks3066/post/7f506718-7a3c-4d63-b9ac-f21b6417f3c2/image.png'; interface SectionDetailModalProps { @@ -33,14 +35,17 @@ const useSectionDetailModal = (props: SectionDetailModalProps) => { const [description, setDescription] = useState(previousDescription); const [isDisabledButton, setIsDisabledButton] = useState(true); - const { mutateAsync: uploadImage } = useMutation((formData: FormData) => apiImage.postImageUpload(formData), { - onSuccess: data => { - setImageUrl(data.imageUrl); - }, - onError: (err: AxiosError<{ message: string }>) => { - openToast('ERROR', `${err.response?.data.message}`); - }, - }); + const { mutateAsync: uploadImage, isLoading: isImageLoading } = useMutation( + (formData: FormData) => apiImage.postImageUpload(formData), + { + onSuccess: data => { + setImageUrl(data.imageUrl); + }, + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); + }, + } + ); const onChangeImage = async (e: React.ChangeEvent) => { if (!e.target.files) return; @@ -88,6 +93,7 @@ const useSectionDetailModal = (props: SectionDetailModalProps) => { closeModal, imageUrl, description, + isImageLoading, }; }; diff --git a/frontend/src/components/host/SlackUrlBox/index.tsx b/frontend/src/components/host/SlackUrlBox/index.tsx index dda9a8c5..e98db4c2 100644 --- a/frontend/src/components/host/SlackUrlBox/index.tsx +++ b/frontend/src/components/host/SlackUrlBox/index.tsx @@ -19,9 +19,7 @@ interface SlackUrlBoxProps { const SlackUrlBox: React.FC = ({ jobName, jobId }) => { const { openToast } = useToast(); - const { data: slackUrlData } = useQuery(['slackUrl', jobId], () => slackApi.getSlackUrl(jobId), { - suspense: true, - }); + const { data: slackUrlData } = useQuery(['slackUrl', jobId], () => slackApi.getSlackUrl(jobId)); const { mutate: putSlackUrl } = useMutation((url: string) => slackApi.putSlackUrl(jobId, url), { onSuccess: () => { diff --git a/frontend/src/components/host/SpaceDeleteButton/index.tsx b/frontend/src/components/host/SpaceDeleteButton/index.tsx index d6dc5bf3..e0b7a8e9 100644 --- a/frontend/src/components/host/SpaceDeleteButton/index.tsx +++ b/frontend/src/components/host/SpaceDeleteButton/index.tsx @@ -10,11 +10,13 @@ import useToast from '@/hooks/useToast'; import apiSpace from '@/apis/space'; +import { ID } from '@/types'; + import styles from './styles'; interface SpaceDeleteButtonProps { - spaceId: string | undefined; - spaceName: string | undefined; + spaceId: ID; + spaceName: string; } const SpaceDeleteButton: React.FC = ({ spaceId, spaceName }) => { @@ -26,7 +28,6 @@ const SpaceDeleteButton: React.FC = ({ spaceId, spaceNam const text = useMemo(() => `${spaceName} 곡간을 μ‚­μ œν•©λ‹ˆλ‹€`, [spaceName]); const { refetch } = useQuery(['deleteSpaces'], apiSpace.getSpaces, { - suspense: true, enabled: false, onSuccess: data => { const { spaces } = data; @@ -41,7 +42,7 @@ const SpaceDeleteButton: React.FC = ({ spaceId, spaceNam }, }); - const { mutate: deleteSpace } = useMutation((spaceId: string | undefined) => apiSpace.deleteSpace(spaceId), { + const { mutate: deleteSpace } = useMutation((spaceId: ID) => apiSpace.deleteSpace(spaceId), { onSuccess: () => { refetch(); closeModal(); diff --git a/frontend/src/components/host/SpaceInfo/index.tsx b/frontend/src/components/host/SpaceInfo/index.tsx index 8093d04b..7dc8a09d 100644 --- a/frontend/src/components/host/SpaceInfo/index.tsx +++ b/frontend/src/components/host/SpaceInfo/index.tsx @@ -1,58 +1,67 @@ -import useSpaceInfo from './useSpaceInfo'; - -import Button from '@/components/common/Button'; +import React from 'react'; import styles from './styles'; -interface SpaceInfoProps { - type: 'read' | 'create' | 'update'; - inputText?: '' | string; - data?: { name: string; imageUrl: string; id: number } | undefined; +interface SpaceInfoHeaderProps { + children: React.ReactNode; +} + +interface SpaceInfoImageProps { + children: React.ReactNode; +} + +interface SpaceInfoInputProps { children: React.ReactNode; } -const SpaceInfo: React.FC = ({ type = 'read', inputText = '', data, children }) => { - const { name, isActiveSubmit, onChangeSpaceName, onClickEditSpaceInfo } = useSpaceInfo(data, type); +interface SpaceInfoMainProps { + children: React.ReactNode; +} +interface SpaceNameTextProps { + children: React.ReactNode; +} + +const SpaceInfoHeader: React.FC = ({ children }) => { return ( -
-
-

곡간 정보

- {type === 'read' ? ( - - ) : ( - - )} -
-
-

λŒ€ν‘œμ΄λ―Έμ§€

-
{children}
-
-
-
-

곡간 이름

- {type === 'read' ? ( - - ) : ( - - )} -
+
+

곡간 정보

+ {children} +
+ ); +}; + +const SpaceInfoImage: React.FC = ({ children }) => { + return ( +
+

λŒ€ν‘œμ΄λ―Έμ§€

+
{children}
+
+ ); +}; + +const SpaceInfoInput: React.FC = ({ children }) => { + return ( +
+
+

곡간 이름

+ {children}
); }; -export default SpaceInfo; +const SpaceNameText: React.FC = ({ children }) => { + return

{children}

; +}; + +const SpaceInfoMain: React.FC = ({ children }) => { + return
{children}
; +}; + +export const SpaceInfo = Object.assign(SpaceInfoMain, { + header: SpaceInfoHeader, + ImageBox: SpaceInfoImage, + InputBox: SpaceInfoInput, + nameText: SpaceNameText, +}); diff --git a/frontend/src/components/host/SpaceInfo/styles.ts b/frontend/src/components/host/SpaceInfo/styles.ts index 3afc0883..641ae5fa 100644 --- a/frontend/src/components/host/SpaceInfo/styles.ts +++ b/frontend/src/components/host/SpaceInfo/styles.ts @@ -7,15 +7,6 @@ const contentWith = css` margin: 0 auto; `; -const button = ({ isActive }: { isActive?: boolean }) => css` - width: 5rem; - height: 2rem; - margin: 0; - font-size: 1rem; - padding: 8px 0; - background: ${isActive ? theme.colors.primary : theme.colors.gray400}; -`; - const spaceInfo = css` height: 100%; min-height: 28rem; @@ -65,21 +56,17 @@ const inputWrapper = css` ${contentWith} `; -const input = css` +const nameText = css` border: none; font-size: 1.4rem; font-weight: bold; - cursor: pointer; - width: 100%; - &:focus { - outline: none; - } + margin: 0; + padding: 0; `; const styles = { spaceInfo, - button, titleWrapper, title, subTitle, @@ -87,7 +74,7 @@ const styles = { inputContainer, inputWrapper, imageWrapper, - input, + nameText, }; export default styles; diff --git a/frontend/src/components/host/SpaceInfo/useSpaceInfo.ts b/frontend/src/components/host/SpaceInfo/useSpaceInfo.ts deleted file mode 100644 index debaa92b..00000000 --- a/frontend/src/components/host/SpaceInfo/useSpaceInfo.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; - -const useSpaceInfo = (data?: { name: string; imageUrl: string; id: number }, type?: 'read' | 'create' | 'update') => { - const navigate = useNavigate(); - const [name, setName] = useState(''); - - const [isActiveSubmit, setIsActiveSubmit] = useState(type === 'update'); - - const onChangeSpaceName = (e: React.ChangeEvent) => { - const input = e.target as HTMLInputElement; - - const isExistValue = input.value.length > 0; - setIsActiveSubmit(isExistValue); - }; - - const onClickEditSpaceInfo = () => { - navigate(`/host/manage/${data?.id}/spaceUpdate`); - }; - - useEffect(() => { - if (data?.name) { - setName(data?.name); - } - }, [data]); - - return { name, isActiveSubmit, onChangeSpaceName, onClickEditSpaceInfo }; -}; - -export default useSpaceInfo; diff --git a/frontend/src/components/host/SpaceInfoCreateBox/index.tsx b/frontend/src/components/host/SpaceInfoCreateBox/index.tsx new file mode 100644 index 00000000..38eda4a4 --- /dev/null +++ b/frontend/src/components/host/SpaceInfoCreateBox/index.tsx @@ -0,0 +1,48 @@ +import useSpaceCreateForm from './useSpaceCreateForm'; + +import Button from '@/components/common/Button'; +import LoadingOverlay from '@/components/common/LoadingOverlay'; +import { ImageBox } from '@/components/host/ImageBox'; +import { SpaceInfo } from '@/components/host/SpaceInfo'; + +import useImage from '@/hooks/useImage'; + +import styles from './styles'; + +const SpaceInfoCreateBox: React.FC = () => { + const { isActiveSubmit, onSubmitCreateSpace, onChangeSpaceName } = useSpaceCreateForm(); + const { imageUrl, onChangeImage, isImageLoading } = useImage(); + + return ( + <> +
onSubmitCreateSpace(e, imageUrl)} encType="multipart/form-data"> + + + + + + + + + + + + + +
+ {isImageLoading && } + + ); +}; + +export default SpaceInfoCreateBox; diff --git a/frontend/src/components/host/SpaceInfoCreateBox/styles.ts b/frontend/src/components/host/SpaceInfoCreateBox/styles.ts new file mode 100644 index 00000000..c5cfd324 --- /dev/null +++ b/frontend/src/components/host/SpaceInfoCreateBox/styles.ts @@ -0,0 +1,31 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const button = ({ isActive }: { isActive?: boolean }) => css` + width: 5rem; + height: 2rem; + margin: 0; + font-size: 1rem; + padding: 8px 0; + background: ${isActive ? theme.colors.primary : theme.colors.gray400}; +`; + +const input = css` + border: none; + font-size: 1.4rem; + font-weight: bold; + cursor: pointer; + + width: 100%; + &:focus { + outline: none; + } +`; + +const styles = { + button, + input, +}; + +export default styles; diff --git a/frontend/src/components/host/SpaceInfoCreateBox/useSpaceCreateForm.ts b/frontend/src/components/host/SpaceInfoCreateBox/useSpaceCreateForm.ts new file mode 100644 index 00000000..5bd314e7 --- /dev/null +++ b/frontend/src/components/host/SpaceInfoCreateBox/useSpaceCreateForm.ts @@ -0,0 +1,54 @@ +import { AxiosError } from 'axios'; +import { useState } from 'react'; +import { useMutation } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import useToast from '@/hooks/useToast'; + +import apiSpace from '@/apis/space'; + +import { ID } from '@/types'; + +import errorMessage from '@/constants/errorMessage'; + +const useSpaceCreateForm = () => { + const navigate = useNavigate(); + const [isActiveSubmit, setIsActiveSubmit] = useState(false); + + const { openToast } = useToast(); + + const onChangeSpaceName = (e: React.ChangeEvent) => { + const input = e.target as HTMLInputElement; + + const isExistValue = input.value.length > 0; + setIsActiveSubmit(isExistValue); + }; + + const { mutate: createSpace } = useMutation( + ({ name, imageUrl }: { name: string; imageUrl: string }) => apiSpace.postNewSpace(name, imageUrl), + { + onSuccess: res => { + const locationSplitted = res.headers.location.split('/'); + const spaceId: ID = locationSplitted[locationSplitted.length - 1]; + + openToast('SUCCESS', '곡간이 생성 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + navigate(`/host/manage/${spaceId}`); + }, + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); + }, + } + ); + + const onSubmitCreateSpace = (e: React.FormEvent, imageUrl: string) => { + e.preventDefault(); + const form = e.target as HTMLFormElement; + const name = form['nameInput'].value; + + createSpace({ name, imageUrl }); + }; + + return { isActiveSubmit, onSubmitCreateSpace, onChangeSpaceName }; +}; + +export default useSpaceCreateForm; diff --git a/frontend/src/components/host/SpaceInfoDisplayBox/index.tsx b/frontend/src/components/host/SpaceInfoDisplayBox/index.tsx new file mode 100644 index 00000000..0fee43c4 --- /dev/null +++ b/frontend/src/components/host/SpaceInfoDisplayBox/index.tsx @@ -0,0 +1,37 @@ +import useSpaceInfoDisplayBox from './useSpaceInfoDisplayBox'; + +import Button from '@/components/common/Button'; +import { ImageBox } from '@/components/host/ImageBox'; +import { SpaceInfo } from '@/components/host/SpaceInfo'; + +import { SpaceType } from '@/types'; + +import styles from './styles'; + +interface SpaceInfoDisplayBoxProps { + spaceData: SpaceType; +} + +const SpaceInfoDisplayBox: React.FC = ({ spaceData }) => { + const { onClickEditSpaceInfo } = useSpaceInfoDisplayBox(spaceData); + + return ( + + + + + + + + + + + {spaceData.name} + + + ); +}; + +export default SpaceInfoDisplayBox; diff --git a/frontend/src/components/host/SpaceInfoDisplayBox/styles.ts b/frontend/src/components/host/SpaceInfoDisplayBox/styles.ts new file mode 100644 index 00000000..dbe2a575 --- /dev/null +++ b/frontend/src/components/host/SpaceInfoDisplayBox/styles.ts @@ -0,0 +1,28 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const button = css` + width: 5rem; + height: 2rem; + margin: 0; + font-size: 1rem; + padding: 8px 0; + background: ${theme.colors.primary}; +`; + +const input = css` + border: none; + font-size: 1.4rem; + font-weight: bold; + cursor: pointer; + + width: 100%; + &:focus { + outline: none; + } +`; + +const styles = { button, input }; + +export default styles; diff --git a/frontend/src/components/host/SpaceInfoDisplayBox/useSpaceInfoDisplayBox.ts b/frontend/src/components/host/SpaceInfoDisplayBox/useSpaceInfoDisplayBox.ts new file mode 100644 index 00000000..86d641dc --- /dev/null +++ b/frontend/src/components/host/SpaceInfoDisplayBox/useSpaceInfoDisplayBox.ts @@ -0,0 +1,15 @@ +import { useNavigate } from 'react-router-dom'; + +import { SpaceType } from '@/types'; + +const useSpaceInfoDisplayBox = (spaceData: SpaceType) => { + const navigate = useNavigate(); + + const onClickEditSpaceInfo = () => { + navigate(`/host/manage/${spaceData.id}/spaceUpdate`); + }; + + return { onClickEditSpaceInfo }; +}; + +export default useSpaceInfoDisplayBox; diff --git a/frontend/src/components/host/SpaceInfoUpdateBox/index.tsx b/frontend/src/components/host/SpaceInfoUpdateBox/index.tsx new file mode 100644 index 00000000..e4f952dd --- /dev/null +++ b/frontend/src/components/host/SpaceInfoUpdateBox/index.tsx @@ -0,0 +1,56 @@ +import useSpaceUpdateForm from './useSpaceUpdateForm'; + +import Button from '@/components/common/Button'; +import LoadingOverlay from '@/components/common/LoadingOverlay'; +import { ImageBox } from '@/components/host/ImageBox'; +import { SpaceInfo } from '@/components/host/SpaceInfo'; + +import useImage from '@/hooks/useImage'; + +import { ID, SpaceType } from '@/types'; + +import styles from './styles'; + +interface SpaceInfoUpdateBox { + spaceId: ID; + spaceData: SpaceType; +} + +const SpaceInfoUpdateBox: React.FC = ({ spaceData, spaceId }) => { + const { isActiveSubmit, onChangeSpaceName, onSubmitUpdateSpace } = useSpaceUpdateForm(); + const { imageUrl, onChangeImage, isImageLoading } = useImage(spaceData?.imageUrl); + + return ( + <> +
onSubmitUpdateSpace(e, imageUrl, spaceId)} encType="multipart/form-data"> + + + + + + + + + + + + + +
+ {isImageLoading && } + + ); +}; + +export default SpaceInfoUpdateBox; diff --git a/frontend/src/components/host/SpaceInfoUpdateBox/styles.ts b/frontend/src/components/host/SpaceInfoUpdateBox/styles.ts new file mode 100644 index 00000000..b9d0ab52 --- /dev/null +++ b/frontend/src/components/host/SpaceInfoUpdateBox/styles.ts @@ -0,0 +1,28 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const input = css` + border: none; + font-size: 1.4rem; + font-weight: bold; + cursor: pointer; + + width: 100%; + &:focus { + outline: none; + } +`; + +const button = ({ isActive }: { isActive?: boolean }) => css` + width: 5rem; + height: 2rem; + margin: 0; + font-size: 1rem; + padding: 8px 0; + background: ${isActive ? theme.colors.primary : theme.colors.gray400}; +`; + +const styles = { input, button }; + +export default styles; diff --git a/frontend/src/components/host/SpaceInfoUpdateBox/useSpaceUpdateForm.ts b/frontend/src/components/host/SpaceInfoUpdateBox/useSpaceUpdateForm.ts new file mode 100644 index 00000000..c8cc309f --- /dev/null +++ b/frontend/src/components/host/SpaceInfoUpdateBox/useSpaceUpdateForm.ts @@ -0,0 +1,53 @@ +import { AxiosError } from 'axios'; +import { useState } from 'react'; +import { useMutation } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import useToast from '@/hooks/useToast'; + +import apiSpace from '@/apis/space'; + +import { ID } from '@/types'; + +import errorMessage from '@/constants/errorMessage'; + +const useSpaceUpdateForm = () => { + const navigate = useNavigate(); + + const [isActiveSubmit, setIsActiveSubmit] = useState(true); + + const { openToast } = useToast(); + + const onChangeSpaceName = (e: React.ChangeEvent) => { + const input = e.target as HTMLInputElement; + + const isExistValue = input.value.length > 0; + setIsActiveSubmit(isExistValue); + }; + + const { mutate: updateSpace } = useMutation( + ({ spaceId, name, imageUrl }: { spaceId: ID; name: string; imageUrl: string }) => + apiSpace.putSpace(spaceId, name, imageUrl), + { + onSuccess: (_, { spaceId }) => { + openToast('SUCCESS', '곡간 정보가 μˆ˜μ • λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + navigate(`/host/manage/${spaceId}`); + }, + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); + }, + } + ); + + const onSubmitUpdateSpace = async (e: React.FormEvent, imageUrl: string, spaceId: ID) => { + e.preventDefault(); + const form = e.target as HTMLFormElement; + const name = form['nameInput'].value; + + updateSpace({ spaceId, name, imageUrl }); + }; + + return { isActiveSubmit, onChangeSpaceName, onSubmitUpdateSpace }; +}; + +export default useSpaceUpdateForm; diff --git a/frontend/src/components/host/Submissions/index.tsx b/frontend/src/components/host/Submissions/index.tsx index 1a1ef52b..ec3b2df9 100644 --- a/frontend/src/components/host/Submissions/index.tsx +++ b/frontend/src/components/host/Submissions/index.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; import React from 'react'; +import { useNavigate } from 'react-router-dom'; import Button from '@/components/common/Button'; @@ -22,17 +23,22 @@ interface Submission { interface SubmissionsProps { submissions: Submission[]; isFullSize?: boolean; - onClick?: (e: React.MouseEvent) => void; } -const Submissions: React.FC = ({ submissions, isFullSize = false, onClick }) => { +const Submissions: React.FC = ({ submissions, isFullSize = false }) => { + const navigate = useNavigate(); + + const onClickSubmissionsDetail = () => { + navigate('spaceRecord'); + }; + return (

곡간 μ‚¬μš© λ‚΄μ—­

{!isFullSize && ( - )} @@ -61,13 +67,7 @@ const Submissions: React.FC = ({ submissions, isFullSize = fal {author} {jobName} - - {createdAt} - + {createdAt} ))} diff --git a/frontend/src/components/host/Submissions/styles.ts b/frontend/src/components/host/Submissions/styles.ts index 660dfc55..3294538f 100644 --- a/frontend/src/components/host/Submissions/styles.ts +++ b/frontend/src/components/host/Submissions/styles.ts @@ -121,6 +121,10 @@ const empty = css` } `; -const styles = { layout, header, detailButton, table, empty }; +const greenText = css` + color: ${theme.colors.green}; +`; + +const styles = { layout, header, detailButton, table, empty, greenText }; export default styles; diff --git a/frontend/src/components/user/DetailedInfoCardModal/index.tsx b/frontend/src/components/user/DetailInfoModal/index.tsx similarity index 75% rename from frontend/src/components/user/DetailedInfoCardModal/index.tsx rename to frontend/src/components/user/DetailInfoModal/index.tsx index 21fa501c..bb939413 100644 --- a/frontend/src/components/user/DetailedInfoCardModal/index.tsx +++ b/frontend/src/components/user/DetailInfoModal/index.tsx @@ -4,13 +4,13 @@ import ModalPortal from '@/portals/ModalPortal'; import styles from './styles'; -interface DetailedInfoCardModalProps { +export interface DetailInfoModalProps { name: string; imageUrl: string; description: string; } -const DetailedInfoCardModal: React.FC = ({ name, imageUrl, description }) => { +const DetailInfoModal: React.FC = ({ name, imageUrl, description }) => { return ( @@ -26,4 +26,4 @@ const DetailedInfoCardModal: React.FC = ({ name, ima ); }; -export default DetailedInfoCardModal; +export default DetailInfoModal; diff --git a/frontend/src/components/user/DetailedInfoCardModal/styles.ts b/frontend/src/components/user/DetailInfoModal/styles.ts similarity index 100% rename from frontend/src/components/user/DetailedInfoCardModal/styles.ts rename to frontend/src/components/user/DetailInfoModal/styles.ts diff --git a/frontend/src/components/user/JobCard/index.tsx b/frontend/src/components/user/JobCard/index.tsx index 546b6150..d49b4259 100644 --- a/frontend/src/components/user/JobCard/index.tsx +++ b/frontend/src/components/user/JobCard/index.tsx @@ -20,7 +20,7 @@ const JobCard: React.FC = ({ jobName, jobId }) => { {jobName} 체크리슀트
- 체크리슀트 μ•„μ΄μ½˜ +
); }; diff --git a/frontend/src/components/user/JobCard/useJobCard.ts b/frontend/src/components/user/JobCard/useJobCard.ts index c2190e56..c656c4e3 100644 --- a/frontend/src/components/user/JobCard/useJobCard.ts +++ b/frontend/src/components/user/JobCard/useJobCard.ts @@ -1,3 +1,4 @@ +import { AxiosError } from 'axios'; import { useMutation, useQuery } from 'react-query'; import { useNavigate } from 'react-router-dom'; @@ -6,7 +7,8 @@ import useToast from '@/hooks/useToast'; import apis from '@/apis'; import { ID } from '@/types'; -import { ApiError } from '@/types/apis'; + +import errorMessage from '@/constants/errorMessage'; const useJobCard = (jobName: string, jobId: ID) => { const navigate = useNavigate(); @@ -14,14 +16,14 @@ const useJobCard = (jobName: string, jobId: ID) => { const { openToast } = useToast(); const { refetch: getJobActive } = useQuery(['jobActive', jobId], () => apis.getJobActive(jobId), { - retry: false, + suspense: false, enabled: false, onSuccess: data => { if (data.active) { navigate(jobId.toString(), { state: { jobName } }); - } else { - if (confirm('진행쀑인 μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€. μƒˆλ‘­κ²Œ μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) createNewRunningTask(); + return; } + if (confirm('진행쀑인 μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€. μƒˆλ‘­κ²Œ μƒμ„±ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?')) createNewRunningTask(); }, }); @@ -31,8 +33,8 @@ const useJobCard = (jobName: string, jobId: ID) => { { retry: false, onSuccess: () => navigate(jobId.toString(), { state: { jobName } }), - onError: (err: ApiError) => { - openToast('ERROR', `${err.response?.data.message}`); + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); }, } ); diff --git a/frontend/src/components/user/NameModal/index.tsx b/frontend/src/components/user/NameModal/index.tsx index 368cfd52..59f472a8 100644 --- a/frontend/src/components/user/NameModal/index.tsx +++ b/frontend/src/components/user/NameModal/index.tsx @@ -4,6 +4,8 @@ import Button from '@/components/common/Button'; import Dimmer from '@/components/common/Dimmer'; import Input from '@/components/common/Input'; +import { ID } from '@/types'; + import ModalPortal from '@/portals/ModalPortal'; import styles from './styles'; @@ -13,12 +15,11 @@ interface NameModalProps { detail: string; placeholder: string; buttonText: string; - jobId: string | undefined; - hostId: string | undefined; + jobId: ID; } -const NameModal: React.FC = ({ title, detail, placeholder, buttonText, jobId, hostId }) => { - const { name, isDisabledButton, onChangeInput, onClickButton } = useNameModal(jobId, hostId); +const NameModal: React.FC = ({ title, detail, placeholder, buttonText, jobId }) => { + const { name, isDisabledButton, onChangeInput, onClickButton } = useNameModal(jobId); return ( diff --git a/frontend/src/components/user/NameModal/useNameModal.ts b/frontend/src/components/user/NameModal/useNameModal.ts index 2177b979..962fafa2 100644 --- a/frontend/src/components/user/NameModal/useNameModal.ts +++ b/frontend/src/components/user/NameModal/useNameModal.ts @@ -8,22 +8,20 @@ import useToast from '@/hooks/useToast'; import apis from '@/apis'; -const useNameModal = (jobId: string | undefined, hostId: string | undefined) => { - const navigate = useNavigate(); +import { ID } from '@/types'; +import errorMessage from '@/constants/errorMessage'; + +const useNameModal = (jobId: ID) => { const { openToast } = useToast(); const { closeModal } = useModal(); const [name, setName] = useState(''); const [isDisabledButton, setIsDisabledButton] = useState(true); - const { mutate: postJobComplete } = useMutation(() => apis.postJobComplete({ jobId, author: name }), { - onSuccess: () => { - alert('μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ •μƒμ μœΌλ‘œ μ œμΆœλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); - navigate(`/enter/${hostId}/spaces`); - }, - onError: (err: AxiosError<{ message: string }>) => { - openToast('ERROR', err.response?.data.message!); + const { mutate: postJobComplete } = useMutation(() => apis.postJobComplete(jobId, name), { + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); closeModal(); }, }); diff --git a/frontend/src/components/user/SectionInfoPreview/index.tsx b/frontend/src/components/user/SectionInfoPreview/index.tsx new file mode 100644 index 00000000..3f43566f --- /dev/null +++ b/frontend/src/components/user/SectionInfoPreview/index.tsx @@ -0,0 +1,25 @@ +import { FaMapMarkedAlt } from 'react-icons/fa'; + +import useLazyImage from '@/hooks/useLazyLoading'; + +import styles from './styles'; + +interface SectionInfoPreviewProps { + imageUrl: string; + onClick: () => void; +} + +const SectionInfoPreview: React.FC = ({ imageUrl, onClick }) => { + const { isLoaded, targetRef: imageRef } = useLazyImage(); + + return ( +
+
+ +
+ +
+ ); +}; + +export default SectionInfoPreview; diff --git a/frontend/src/components/user/SectionInfoPreviewBox/styles.ts b/frontend/src/components/user/SectionInfoPreview/styles.ts similarity index 100% rename from frontend/src/components/user/SectionInfoPreviewBox/styles.ts rename to frontend/src/components/user/SectionInfoPreview/styles.ts diff --git a/frontend/src/components/user/SectionInfoPreviewBox/index.tsx b/frontend/src/components/user/SectionInfoPreviewBox/index.tsx deleted file mode 100644 index 074a4f42..00000000 --- a/frontend/src/components/user/SectionInfoPreviewBox/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { FaMapMarkedAlt } from 'react-icons/fa'; - -import styles from './styles'; - -interface SectionInfoPreviewBoxProps { - imageUrl: string; - onClick: () => void; -} - -const SectionInfoPreviewBox: React.FC = ({ imageUrl, onClick }) => { - return ( -
-
- -
- -
- ); -}; - -export default SectionInfoPreviewBox; diff --git a/frontend/src/components/user/TaskCard/index.tsx b/frontend/src/components/user/TaskCard/index.tsx index ee935a7a..da660ff8 100644 --- a/frontend/src/components/user/TaskCard/index.tsx +++ b/frontend/src/components/user/TaskCard/index.tsx @@ -1,7 +1,7 @@ import { RiInformationLine } from 'react-icons/ri'; import CheckBox from '@/components/common/Checkbox'; -import DetailedInfoCardModal from '@/components/user/DetailedInfoCardModal'; +import DetailInfoModal from '@/components/user/DetailInfoModal'; import useModal from '@/hooks/useModal'; @@ -13,23 +13,18 @@ import styles from './styles'; type TaskCardProps = { tasks: TaskType[]; - getSections: () => void; }; -const TaskCard: React.FC = ({ tasks, getSections }) => { +const TaskCard: React.FC = ({ tasks }) => { const { openModal } = useModal(); - const onClickCheckBox = async ( - e: React.MouseEvent | React.ChangeEvent, - id: ID - ) => { + const onClickCheckBox = (e: React.MouseEvent | React.ChangeEvent, id: ID) => { e.preventDefault(); - await apis.postCheckTask(id); - getSections(); + apis.postCheckTask(id); }; - const onClickTaskDetail = (task: any) => { - openModal(); + const onClickTaskDetail = (task: TaskType) => { + openModal(); }; return ( diff --git a/frontend/src/constants/errorMessage.ts b/frontend/src/constants/errorMessage.ts new file mode 100644 index 00000000..cf7e6a1e --- /dev/null +++ b/frontend/src/constants/errorMessage.ts @@ -0,0 +1,45 @@ +const errorMessage = Object.freeze({ + H001: 'λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', //'Space λΉ„λ°€λ²ˆν˜Έμ™€ μž…λ ₯받은 λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠλŠ” 경우' + H002: 'μœ νš¨ν•˜μ§€ μ•ŠλŠ” μ½”λ“œμž…λ‹ˆλ‹€.', //'entranceCode κ°€ μœ νš¨ν•˜μ§€ μ•Šμ€ μ½”λ“œμΈ 경우' + H003: 'μœ νš¨ν•˜μ§€ μ•Šμ€ hostId μž…λ‹ˆλ‹€.', //'entranceCode μ—μ„œ hostId μΆ”μΆœ μ‹œ μœ νš¨ν•˜μ§€ μ•Šμ€ hostId인 경우' + H004: 'μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ‚¬μš©μžμž…λ‹ˆλ‹€.', //'Host 쑰회 μ‹œ, μž…λ ₯ 받은 id의 Hostκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + H005: 'μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ‚¬μš©μžμž…λ‹ˆλ‹€.', //'Host 쑰회 μ‹œ, μž…λ ₯ 받은 githubId의 Hostκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + J001: 'μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ—…λ¬΄μž…λ‹ˆλ‹€.', //'Job 쑰회 μ‹œ, μž…λ ₯ 받은 host, id에 ν•΄λ‹Ήν•˜λŠ” Job 이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + J002: 'μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” μ—…λ¬΄μž…λ‹ˆλ‹€.', //'Job 쑰회 μ‹œ, μž…λ ₯ 받은 id에 ν•΄λ‹Ήν•˜λŠ” Job 이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + SP01: 'μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€.', //'곡간 생성 μ‹œ, 곡간 이름이 이미 μ‘΄μž¬ν•˜λŠ” 경우' + SP02: 'μ‘΄μž¬ν•˜λŠ” μ΄λ¦„μž…λ‹ˆλ‹€.', //'곡간 μˆ˜μ • μ‹œ, 곡간 이름이 이미 μ‘΄μž¬ν•˜λŠ” 경우' + SP03: '4자리의 λΉ„λ°€λ²ˆν˜Έλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.', //'λΉ„λ°€λ²ˆν˜Έκ°€ 4자리둜 이루어지지 μ•Šμ€ 경우' + SP04: 'ν•΄λ‹Ή 곡간이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', //'곡간 쑰회 μ‹œ, μž…λ ₯ 받은 host, id에 ν•΄λ‹Ήν•˜λŠ” 곡간이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + A001: '잘λͺ»λœ μ ‘κ·Ό μž…λ‹ˆλ‹€.', //'호슀트 κΆŒν•œμ΄ μ—†λŠ” ν† ν°μœΌλ‘œ 호슀트용 μ ‘κ·Ό 경둜둜 μ ‘κ·Όν•  경우' + A002: '만료된 ν† ν°μž…λ‹ˆλ‹€.', //'Guest 의 토큰이 만료된 경우' + A003: '헀더에 토큰 값이 μ •μƒμ μœΌλ‘œ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', //'헀더에 토큰값이 μ •μƒμ μœΌλ‘œ μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우' + T001: '이미 μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μƒμ„±λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.', //'RunningTask κ°€ 이미 μ‘΄μž¬ν•˜λŠ”λ° 또 μƒμ„±ν•˜λ €λŠ” 경우' + T002: 'μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', //'RunningTask 생성할 Task κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + T003: 'μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', //'Task 쑰회 μ‹œ, μž…λ ₯ 받은 host, id에 ν•΄λ‹Ήν•˜λŠ” Task κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + R001: 'μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μˆ˜μ • λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', //'RunningTask 쑰회 μ‹œ, RunningTask κ°€ 아직 μƒμ„±λ˜μ§€ μ•Šμ€ 경우' (κ΄€λ¦¬μžκ°€ 체크리슀트λ₯Ό μˆ˜μ • ν•˜κ±°λ‚˜, λ‹€λ₯Έ μ‚¬μš©μžκ°€ 체크리슀트λ₯Ό 제좜 ν•  경우) + R002: 'μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μˆ˜μ • λ˜μ—ˆμŠ΅λ‹ˆλ‹€.', //'RunningTask 체크 μ‹œ, RunningTask κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + R003: '체크리슀트의 λͺ¨λ“  ν•­λͺ©μ„ 체크 ν•΄μ•Ό ν•©λ‹ˆλ‹€.', //'μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ λ‹€ μ²΄ν¬λ˜μ§€ μ•Šμ•˜μ§€λ§Œ, μ œμΆœν•œ 경우' + S001: 'ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” RunningTask κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ° μ œμΆœν–ˆμŠ΅λ‹ˆλ‹€.', //'ν˜„μž¬ μ œμΆœν•  수 μžˆλŠ” RunningTask κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ° μ œμΆœν•œ 경우' + S002: '곡백은 μž…λ ₯ ν•  수 μ—†μŠ΅λ‹ˆλ‹€.', //'제좜 μ‹œ 제좜자 이름이 곡백인 경우' + S003: '이름은 10자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€.', //'제좜 μ‹œ 제좜자 이름이 λ„ˆλ¬΄ κΈ΄ 경우' + SE01: 'Section이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', //'Section 쑰회 μ‹œ, Section이 μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 경우' + IM01: 'μ΄λ―Έμ§€λŠ” png, gif, jpeg, jpg, svg, webp ν™•μž₯자만 μ—…λ‘œλ“œ κ°€λŠ₯ ν•©λ‹ˆλ‹€.', //'이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일이 null 인 경우' + IM02: 'μ΄λ―Έμ§€λŠ” png, gif, jpeg, jpg, svg, webp ν™•μž₯자만 μ—…λ‘œλ“œ κ°€λŠ₯ ν•©λ‹ˆλ‹€.', //'이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일이 λΉˆκ°’μΈ 경우' + IM03: '이미지 파일 이름이 μ—†μŠ΅λ‹ˆλ‹€.', //'이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일 이름이 λΉˆκ°’μΈ 경우' + IM04: 'μ΄λ―Έμ§€λŠ” png, gif, jpeg, jpg, svg, webp ν™•μž₯자만 μ—…λ‘œλ“œ κ°€λŠ₯ ν•©λ‹ˆλ‹€.', //'이미지 μ—…λ‘œλ“œ μ‹œ, 이미지 파일 ν™•μž₯μžκ°€ 잘λͺ»λœ 경우' + IM05: 'μ΄λ―Έμ§€λŠ” png, gif, jpeg, jpg, svg, webp ν™•μž₯자만 μ—…λ‘œλ“œ κ°€λŠ₯ ν•©λ‹ˆλ‹€.', //'이미지 μ—…λ‘œλ“œ μ‹œ, 파일이 잘λͺ»λœ 경우' + D001: 'μ„€λͺ… κΈΈμ΄λŠ” 128자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€.', //'μ„€λͺ… 길이가 λ„ˆλ¬΄ κΈ΄ 경우' + N001: '곡백은 μž…λ ₯ ν•  수 μ—†μŠ΅λ‹ˆλ‹€.', //'이름이 곡백인 경우' + N002: '이름은 10자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€.', //'이름이 λ„ˆλ¬΄ κΈ΄ 경우' + I001: 'μŠ¬λž™ λ©”μ‹œμ§€ 전솑에 μ‹€νŒ¨ ν–ˆμŠ΅λ‹ˆλ‹€.', //'μŠ¬λž™ λ©”μ‹œμ§€ 전솑에 μ‹€νŒ¨ν•œ 경우' + I002: 'ν•΄μ‹±μš© μ‹œν¬λ¦Ών‚€κ°€ μ§§μŠ΅λ‹ˆλ‹€.', //'ν•΄μ‹±μš© μ‹œν¬λ¦Ών‚€κ°€ λ„ˆλ¬΄ 짧은 경우' + I003: 'ν•΄μ‹± 인코딩 μ‹€νŒ¨ ν–ˆμŠ΅λ‹ˆλ‹€.', //'ν•΄μ‹± 인코딩 μ‹€νŒ¨ν•  경우' + I004: 'ν•΄μ‹± λ””μ½”λ”© μ‹€νŒ¨ ν–ˆμŠ΅λ‹ˆλ‹€.', //'ν•΄μ‹± λ””μ½”λ”© μ‹€νŒ¨ν•œ 경우' + I005: '토큰 값이 μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.', //'토큰이 값이 μ˜¬λ°”λ₯΄μ§€ μ•Šμ•„ μΆ”μΆœν•  수 μ—†λŠ” 경우' + I006: 'κΉƒν—ˆλΈŒ μ—‘μ„ΈμŠ€ 토큰이 nullμž…λ‹ˆλ‹€.', //'κΉƒν—ˆλΈŒ μ—‘μ„ΈμŠ€ 토큰이 null 인 경우' + I007: 'κΉƒν—ˆλΈŒ μ‚¬μš©μž ν”„λ‘œν•„μ„ κ°€μ Έμ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€.', //'κΉƒν—ˆλΈŒ μ‚¬μš©μž ν”„λ‘œν•„μ„ κ°€μ Έμ˜¬ 수 μ—†λŠ” 경우' + E001: 'μ˜ˆμƒμΉ˜ λͺ»ν•œ μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.', //'μ˜ˆμƒμΉ˜ λͺ»ν•œ μ˜ˆμ™Έκ°€ λ°œμƒν•œ 경우' + V001: 'μš”μ²­μ— λŒ€ν•œ DTO ν•„λ“œκ°’ 일뢀가 nullμž…λ‹ˆλ‹€.', //'μš”μ²­μ— λŒ€ν•œ DTO ν•„λ“œκ°’ 일뢀가 null 인 경우' +}); + +export default errorMessage; diff --git a/frontend/src/hooks/useGithubLogin.ts b/frontend/src/hooks/useGithubLogin.ts index 3f7aa6d7..315d045b 100644 --- a/frontend/src/hooks/useGithubLogin.ts +++ b/frontend/src/hooks/useGithubLogin.ts @@ -4,17 +4,16 @@ import { useQuery } from 'react-query'; import apiAuth from '@/apis/githubAuth'; const useGitHubLogin = () => { - const code = new URL(location.href).searchParams.get('code'); + const code = new URL(location.href).searchParams.get('code') || ''; - const { isSuccess: isSuccessGithubLogin, data } = useQuery(['hostToken'], () => apiAuth.getToken(code), { - suspense: true, - }); + const { isSuccess: isSuccessGithubLogin, data: tokenData } = useQuery(['hostToken'], () => apiAuth.getToken(code)); useEffect(() => { - if (data) { - localStorage.setItem('token', data.token); + if (tokenData) { + localStorage.setItem('host', tokenData.token); + sessionStorage.setItem('tokenKey', 'host'); } - }, [data]); + }, [tokenData]); return { isSuccessGithubLogin }; }; diff --git a/frontend/src/components/host/ImageBox/useImageBox.ts b/frontend/src/hooks/useImage.ts similarity index 56% rename from frontend/src/components/host/ImageBox/useImageBox.ts rename to frontend/src/hooks/useImage.ts index 997c3f50..5c15cc18 100644 --- a/frontend/src/components/host/ImageBox/useImageBox.ts +++ b/frontend/src/hooks/useImage.ts @@ -7,17 +7,24 @@ import useToast from '@/hooks/useToast'; import apiImage from '@/apis/image'; -const useImageBox = (prevImageUrl?: string | undefined) => { - const [imageUrl, setImageUrl] = useState(''); +import errorMessage from '@/constants/errorMessage'; + +const useImage = (prevImageUrl?: string) => { const { openToast } = useToast(); - const { mutateAsync: uploadImage } = useMutation((formData: any) => apiImage.postImageUpload(formData), { - onError: (err: AxiosError<{ message: string }>) => { - openToast('ERROR', `${err.response?.data.message}`); - }, - }); + const [imageUrl, setImageUrl] = useState(''); + + const { mutateAsync: uploadImage, isLoading: isImageLoading } = useMutation( + (formData: FormData) => apiImage.postImageUpload(formData), - const onChangeImg = async (e: React.FormEvent) => { + { + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); + }, + } + ); + + const onChangeImage = async (e: React.FormEvent) => { const input = e.target as HTMLInputElement; if (!input.files?.length) { @@ -41,12 +48,12 @@ const useImageBox = (prevImageUrl?: string | undefined) => { }; useEffect(() => { - if (prevImageUrl) { - setImageUrl(prevImageUrl); - } + if (!prevImageUrl) return; + + setImageUrl(prevImageUrl); }, [prevImageUrl]); - return { imageUrl, onChangeImg }; + return { imageUrl, onChangeImage, isImageLoading }; }; -export default useImageBox; +export default useImage; diff --git a/frontend/src/hooks/useLazyLoading.ts b/frontend/src/hooks/useLazyLoading.ts new file mode 100644 index 00000000..b80e0814 --- /dev/null +++ b/frontend/src/hooks/useLazyLoading.ts @@ -0,0 +1,34 @@ +import { useEffect, useRef, useState } from 'react'; + +const useLazyLoading = (threshold: number = 0) => { + const targetRef = useRef(null); + const observerRef = useRef(); + const [isLoaded, setIsLoaded] = useState(false); + + const intersectionCallBack = (entries: IntersectionObserverEntry[], io: IntersectionObserver) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setIsLoaded(true); + return; + } + if (!entry.isIntersecting) { + setIsLoaded(false); + return; + } + }); + }; + + useEffect(() => { + if (!observerRef.current) { + observerRef.current = new IntersectionObserver(intersectionCallBack, { + threshold, + }); + } + + targetRef.current && observerRef.current.observe(targetRef.current); + }, []); + + return { isLoaded, targetRef }; +}; + +export default useLazyLoading; diff --git a/frontend/src/hooks/useScroll.ts b/frontend/src/hooks/useScroll.ts new file mode 100644 index 00000000..31ac0a18 --- /dev/null +++ b/frontend/src/hooks/useScroll.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +const useScroll = () => { + const [scrollPosition, setScrollPosition] = useState(0); + + const updateScroll = () => { + setScrollPosition((scrollY / innerHeight) * 100); + }; + + useEffect(() => { + window.addEventListener('scroll', updateScroll); + }, []); + + return { scrollPosition }; +}; + +export default useScroll; diff --git a/frontend/src/hooks/useSectionCheck.ts b/frontend/src/hooks/useSectionCheck.ts index 0ec41811..81d3f263 100644 --- a/frontend/src/hooks/useSectionCheck.ts +++ b/frontend/src/hooks/useSectionCheck.ts @@ -3,22 +3,34 @@ import { useMemo } from 'react'; import { SectionType } from '@/types'; const useSectionCheck = (sections: SectionType[]) => { - const tasks = useMemo(() => sections.map(section => section.tasks.map(task => task.checked)), [sections]); - const checkList = useMemo( - () => - tasks.reduce((prev, cur) => { - return prev.concat(...cur); - }, []), - [tasks] - ); - const totalCount = useMemo(() => checkList.length, [checkList]); - const checkCount = useMemo(() => checkList.filter(check => check === true).length, [checkList]); - const percent = useMemo(() => Math.ceil((checkCount / totalCount) * 100), [checkCount, totalCount]); - const isAllChecked = totalCount === checkCount; + if (sections.length === 0) + return { totalCount: 0, checkedCount: false, percent: 0, isAllChecked: false, sectionsAllCheckMap: new Map() }; + + const sectionsAllCheckMap = new Map(); + const sectionCheckLists = sections.map(section => { + const newSectionCheckList = section.tasks.map(task => task.checked); + + sectionsAllCheckMap.set( + `${section.id}`, + newSectionCheckList.every(isCheck => isCheck) + ); + + return newSectionCheckList; + }); + + const jobCheckList = sectionCheckLists?.reduce((prev, cur) => prev.concat(...cur)); + + const totalCount = useMemo(() => jobCheckList?.length || 0, [jobCheckList]); + const checkedCount = useMemo(() => jobCheckList?.filter(check => check === true).length || 0, [jobCheckList]); + + const percent = useMemo(() => Math.ceil((checkedCount / totalCount) * 100), [checkedCount, totalCount]); + + const isAllChecked = totalCount === checkedCount; return { + sectionsAllCheckMap, totalCount, - checkCount, + checkedCount, percent, isAllChecked, }; diff --git a/frontend/src/hooks/useSpaceForm.ts b/frontend/src/hooks/useSpaceForm.ts deleted file mode 100644 index 83d7fffe..00000000 --- a/frontend/src/hooks/useSpaceForm.ts +++ /dev/null @@ -1,74 +0,0 @@ -import useToast from './useToast'; -import { AxiosError } from 'axios'; -import { useMutation } from 'react-query'; -import { useNavigate } from 'react-router-dom'; - -import apiImage from '@/apis/image'; -import apiSpace from '@/apis/space'; - -import { ID } from '@/types'; - -const useSpaceForm = () => { - const navigate = useNavigate(); - const { openToast } = useToast(); - - const { mutate: createSpace } = useMutation( - ({ name, imageUrl }: { name: string; imageUrl: string | undefined }) => apiSpace.postNewSpace(name, imageUrl), - { - onSuccess: res => { - const locationSplitted = res.headers.location.split('/'); - const spaceId: ID = locationSplitted[locationSplitted.length - 1]; - - openToast('SUCCESS', '곡간이 생성 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); - navigate(`/host/manage/${spaceId}`); - }, - onError: (err: AxiosError<{ message: string }>) => { - openToast('ERROR', `${err.response?.data.message}`); - }, - } - ); - - const { mutateAsync: uploadImage } = useMutation((formData: any) => apiImage.postImageUpload(formData), { - onError: (err: AxiosError<{ message: string }>) => { - openToast('ERROR', `${err.response?.data.message}`); - }, - }); - - const { mutate: updateSpace } = useMutation( - ({ spaceId, name, imageUrl }: { spaceId: ID | undefined; name: string; imageUrl: string | undefined }) => - apiSpace.putSpace(spaceId, name, imageUrl), - { - onSuccess: (_, { spaceId }) => { - openToast('SUCCESS', '곡간 정보가 μˆ˜μ • λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); - navigate(`/host/manage/${spaceId}`); - }, - onError: (err: AxiosError<{ message: string }>) => { - openToast('ERROR', `${err.response?.data.message}`); - }, - } - ); - - const onSubmitCreateSpace = (e: React.FormEvent, imageUrl: string | undefined) => { - e.preventDefault(); - const form = e.target as HTMLFormElement; - const name = form['nameInput'].value; - - createSpace({ name, imageUrl }); - }; - - const onSubmitUpdateSpace = async ( - e: React.FormEvent, - imageUrl: string | undefined, - spaceId: ID | undefined - ) => { - e.preventDefault(); - const form = e.target as HTMLFormElement; - const name = form['nameInput'].value; - - updateSpace({ spaceId, name, imageUrl }); - }; - - return { onSubmitCreateSpace, onSubmitUpdateSpace }; -}; - -export default useSpaceForm; diff --git a/frontend/src/hooks/useToast.ts b/frontend/src/hooks/useToast.ts index 6a096e3d..d5b358ff 100644 --- a/frontend/src/hooks/useToast.ts +++ b/frontend/src/hooks/useToast.ts @@ -2,6 +2,8 @@ import { useSetRecoilState } from 'recoil'; import { isShowToastState, toastState } from '@/recoil/toast'; +const TOAST_ACTIVE_TIME = 2000; + const useToast = () => { const setState = useSetRecoilState(toastState); const setIsShowToast = useSetRecoilState(isShowToastState); @@ -14,7 +16,7 @@ const useToast = () => { }); setTimeout(() => { setIsShowToast(false); - }, 2000); + }, TOAST_ACTIVE_TIME); }; const closeToast = () => { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 5fed5bda..c7d00c16 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -8,7 +8,11 @@ import { RecoilRoot } from 'recoil'; import globalStyle from './styles/global'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); -const queryClient = new QueryClient(); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { suspense: true, retry: false }, + }, +}); root.render( diff --git a/frontend/src/layouts/DefaultLayout/MainPage.tsx b/frontend/src/layouts/DefaultLayout/MainPage.tsx new file mode 100644 index 00000000..a7a100a6 --- /dev/null +++ b/frontend/src/layouts/DefaultLayout/MainPage.tsx @@ -0,0 +1,698 @@ +import MainSection from './MainSection'; +import { css } from '@emotion/react'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import useLazyLoading from '@/hooks/useLazyLoading'; +import useScroll from '@/hooks/useScroll'; + +import createSpace from '@/assets/createSpace.png'; +import dashboard from '@/assets/dashboard.png'; +import edit2 from '@/assets/edit2.png'; +import edit from '@/assets/edit.png'; +import mobileView1 from '@/assets/mobileView1.png'; +import mobileView2 from '@/assets/mobileView2.png'; +import mobileView3 from '@/assets/mobileView3.png'; +import mobileView4 from '@/assets/mobileView4.png'; + +import animation from '@/styles/animation'; +import theme from '@/styles/theme'; + +const FloatingActionButton: React.FC = () => { + const navigate = useNavigate(); + const { scrollPosition } = useScroll(); + const [eventNumber, setEventNumber] = useState(0); + + const onClick = () => { + navigate('/host'); + }; + + useEffect(() => { + if (scrollPosition >= 550) { + setEventNumber(1); + return; + } + setEventNumber(0); + }, [scrollPosition]); + + return ( +
+
+ μ‹œμž‘ν•˜κΈ° +
+
+ ); +}; + +const MainPage: React.FC = () => { + return ( +
+ + + + + + + + +
+ ); +}; + +const UserViewSection1 = () => { + const { isLoaded, targetRef: sectionRef } = useLazyLoading(); + const { scrollPosition } = useScroll(); + const [eventNumber, setEventNumber] = useState(0); + + useEffect(() => { + if (scrollPosition >= 60 && scrollPosition < 100) { + setEventNumber(1); + return; + } + if (scrollPosition >= 100) { + setEventNumber(2); + return; + } + setEventNumber(0); + }, [scrollPosition]); + + return ( +
+ {isLoaded && ( +
+

+ μ‰½κ²Œ ν™•μΈν•΄μš”. +

+

+ ν•¨κ»˜ μ‚¬μš©ν•  +

+ {eventNumber >= 1 && ( + <> + +

+ 곡간과 +

+ + )} + {eventNumber === 2 && ( + <> + +

+ 업무λ₯Ό +

+ + )} +
+ )} +
+ ); +}; + +const UserViewSection2 = () => { + const { isLoaded, targetRef: sectionRef } = useLazyLoading(); + const { scrollPosition } = useScroll(); + const [eventNumber, setEventNumber] = useState(0); + + useEffect(() => { + if (scrollPosition >= 160 && scrollPosition < 200) { + setEventNumber(1); + return; + } + if (scrollPosition >= 200) { + setEventNumber(2); + return; + } + setEventNumber(0); + }, [scrollPosition]); + + return ( +
+ {isLoaded && ( +
+

+ κ°„λ‹¨ν•˜κ²Œ μ²΄ν¬ν•΄μš”. +

+ {eventNumber >= 1 && ( + <> + +

+ μ²΄ν¬λ¦¬μŠ€νŠΈμ™€ +

+ + )} + {eventNumber === 2 && ( + <> + +

+ 상세정보 제곡 +

+ {innerWidth > 600 && ( +

+ How, Where +

+ )} + + )} +
+ )} +
+ ); +}; + +const UserViewSection3 = () => { + const { isLoaded, targetRef: sectionRef } = useLazyLoading(); + const { scrollPosition } = useScroll(); + const [eventNumber, setEventNumber] = useState(0); + + useEffect(() => { + if (scrollPosition >= 270) { + setEventNumber(1); + return; + } + setEventNumber(0); + }, [scrollPosition]); + + return ( +
+ {isLoaded && ( +
+

+ ν•¨κ»˜ μ‚¬μš©ν•΄μš”. +

+ {eventNumber === 1 && ( + <> + + + {innerWidth > 600 && ( +

+ μ—¬λŸ¬λͺ…이 λ™μ‹œμ— 같은 업무 체크 κ°€λŠ₯ +

+ )} + + )} +
+ )} +
+ ); +}; + +const HostViewSection1 = () => { + const { isLoaded, targetRef: sectionRef } = useLazyLoading(); + const { scrollPosition } = useScroll(); + const [eventNumber, setEventNumber] = useState(0); + + useEffect(() => { + if (scrollPosition >= 410) { + setEventNumber(1); + return; + } + setEventNumber(0); + }, [scrollPosition]); + + return ( +
+ {isLoaded && ( +
+

+ 직접 λ‚΄ 곡간을 μƒμ„±ν•˜κ³  κ΄€λ¦¬ν•˜κ³ μ‹Άλ‹€λ©΄? +

+ {eventNumber === 1 && ( +

+ Gong + Checkκ³Ό ν•¨κ»˜ ν•˜μ„Έμš”. +

+ )} +
+ )} +
+ ); +}; + +const HostViewSection2 = () => { + const { isLoaded, targetRef: sectionRef } = useLazyLoading(0.2); + const { scrollPosition } = useScroll(); + const [eventNumber, setEventNumber] = useState(0); + + useEffect(() => { + if (scrollPosition >= 490) { + setEventNumber(1); + return; + } + setEventNumber(0); + }, [scrollPosition]); + + return ( +
+ {isLoaded && ( +
+

+ λ‚΄ 곡간 관리 +

+ {eventNumber >= 1 && ( + <> + + + + + + )} +
+ )} +
+ ); +}; + +const HostViewSection3 = () => { + const { isLoaded, targetRef: sectionRef } = useLazyLoading(0.3); + + return ( +
+ {isLoaded && ( +
+

+ μ§€κΈˆ λ°”λ‘œ +

+
+ )} +
+ ); +}; + +export default MainPage; diff --git a/frontend/src/layouts/DefaultLayout/MainSection.tsx b/frontend/src/layouts/DefaultLayout/MainSection.tsx new file mode 100644 index 00000000..dde44920 --- /dev/null +++ b/frontend/src/layouts/DefaultLayout/MainSection.tsx @@ -0,0 +1,212 @@ +import { css } from '@emotion/react'; +import React, { useEffect, useRef, useState } from 'react'; +import { IoIosArrowDown } from 'react-icons/io'; + +import useScroll from '@/hooks/useScroll'; + +import homeCover from '@/assets/homeCover.png'; + +import animation from '@/styles/animation'; +import theme from '@/styles/theme'; + +const CIRCLE_SIZE = 80; + +const MainSection: React.FC = () => { + const canvasRef = useRef(null); + + const positionRef = useRef({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); + + const { scrollPosition } = useScroll(); + const scrollPositionInt = scrollPosition + 3; + + const [isFull, setIsFull] = useState(false); + + const onMouseMove = (e: React.MouseEvent, scrollPositionInt: number) => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext('2d'); + if (!canvas || !ctx) return; + + const target = e.target as HTMLInputElement; + const localName = target.localName; + + if (localName !== 'canvas') return; + + positionRef.current = { + x: e.nativeEvent.offsetX, + y: e.nativeEvent.offsetY, + }; + + const scale = window.devicePixelRatio; + canvas.width = Math.floor(window.innerWidth * scale); + canvas.height = Math.floor(window.innerHeight * scale); + ctx.scale(scale, scale); + + const x = e.nativeEvent.offsetX; + const y = e.nativeEvent.offsetY; + + const radius = CIRCLE_SIZE * scrollPositionInt; + + ctx.beginPath(); + let circlePath = new Path2D(); + circlePath.arc(x, y, radius, 0, Math.PI * 2); + circlePath.rect(0, 0, window.innerWidth, window.innerHeight); + ctx.clip(circlePath, 'evenodd'); + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.closePath(); + }; + + useEffect(() => { + const canvas = canvasRef.current; + const ctx = canvas?.getContext('2d'); + if (!canvas || !ctx) return; + + const scale = window.devicePixelRatio; + + canvas.width = Math.floor(window.innerWidth * scale); + canvas.height = Math.floor(window.innerHeight * scale); + ctx.scale(scale, scale); + + const x = positionRef.current.x; + const y = positionRef.current.y; + + const radius = CIRCLE_SIZE * scrollPositionInt - scrollPositionInt * 2; + + ctx.beginPath(); + let circlePath = new Path2D(); + circlePath.arc(x, y, radius, 0, Math.PI * 2); + circlePath.rect(0, 0, window.innerWidth, window.innerHeight); + ctx.clip(circlePath, 'evenodd'); + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.closePath(); + + if (scrollPositionInt >= 12) { + setIsFull(true); + } else { + setIsFull(false); + } + }, [scrollPositionInt]); + + // useEffect(() => { + // const canvas = canvasRef.current; + // const ctx = canvas?.getContext('2d'); + // if (!canvas || !ctx) return; + + // const scale = window.devicePixelRatio; + + // canvas.width = Math.floor(window.innerWidth * scale); + // canvas.height = Math.floor(window.innerHeight * scale); + // ctx.scale(scale, scale); + + // const x = window.innerWidth / 2; + // const y = window.innerHeight / 2; + + // const radius = CIRCLE_SIZE * scrollPositionInt; + + // ctx.beginPath(); + // let circlePath = new Path2D(); + // circlePath.arc(x, y, radius, 0, Math.PI * 2); + // circlePath.rect(0, 0, window.innerWidth, window.innerHeight); + // ctx.clip(circlePath, 'evenodd'); + // ctx.fillStyle = '#000000'; + // ctx.fillRect(0, 0, canvas.width, canvas.height); + // ctx.closePath(); + // }, []); + + return ( + <> +
onMouseMove(e, scrollPositionInt)} + > +
+
+
+ +
+
+
+ μ•„λž˜λ‘œ λ‚΄λ €μ£Όμ„Έμš”. + +
+ + +
+
+
+ + ); +}; + +export default MainSection; diff --git a/frontend/src/layouts/DefaultLayout/index.tsx b/frontend/src/layouts/DefaultLayout/index.tsx index c8f8963e..1e85f14e 100644 --- a/frontend/src/layouts/DefaultLayout/index.tsx +++ b/frontend/src/layouts/DefaultLayout/index.tsx @@ -1,20 +1,20 @@ +import MainPage from './MainPage'; import { useEffect } from 'react'; import { Outlet } from 'react-router-dom'; import useModal from '@/hooks/useModal'; const DefaultLayout: React.FC = () => { + const isRootPath = location.pathname === '/'; + const { closeModal } = useModal(); useEffect(() => { closeModal(); + scrollTo(0, 0); }, []); - return ( - <> - - - ); + return <>{isRootPath ? : }; }; export default DefaultLayout; diff --git a/frontend/src/pages/host/Home/styles.ts b/frontend/src/layouts/DefaultLayout/styles.ts similarity index 100% rename from frontend/src/pages/host/Home/styles.ts rename to frontend/src/layouts/DefaultLayout/styles.ts diff --git a/frontend/src/layouts/UserLayout/index.tsx b/frontend/src/layouts/UserLayout/index.tsx index fb409b3d..62697bee 100644 --- a/frontend/src/layouts/UserLayout/index.tsx +++ b/frontend/src/layouts/UserLayout/index.tsx @@ -4,16 +4,22 @@ import { Global } from '@emotion/react'; import { Suspense, useEffect } from 'react'; import { Outlet, useNavigate, useParams } from 'react-router-dom'; +import { ID } from '@/types'; + import transitions from '@/styles/transitions'; import styles from './styles'; const UserLayout: React.FC = () => { const navigate = useNavigate(); - const { hostId } = useParams(); + + const { hostId } = useParams() as { hostId: ID }; useEffect(() => { - if (!localStorage.getItem('token')) { + const tokenKey = sessionStorage.getItem('tokenKey'); + const token = localStorage.getItem(`${hostId}`); + + if (!tokenKey || tokenKey !== hostId || !token) { navigate(`/enter/${hostId}/pwd`); } }, []); diff --git a/frontend/src/pages/host/AuthCallBack/useAuthCallBack.ts b/frontend/src/pages/host/AuthCallBack/useAuthCallBack.ts index d03dda80..9ccf5ad9 100644 --- a/frontend/src/pages/host/AuthCallBack/useAuthCallBack.ts +++ b/frontend/src/pages/host/AuthCallBack/useAuthCallBack.ts @@ -11,8 +11,7 @@ const useAuthCallBack = () => { const { isSuccessGithubLogin } = useGitHubLogin(); - const { data, refetch: getSpaceData } = useQuery(['spaces'], apis.getSpaces, { - suspense: true, + const { data: spacesData, refetch: getSpaceData } = useQuery(['spaces'], apis.getSpaces, { enabled: false, }); @@ -23,16 +22,16 @@ const useAuthCallBack = () => { }, [isSuccessGithubLogin]); useEffect(() => { - if (data) { - if (data.spaces.length === 0) { - navigate('/host/manage/spaceCreate'); - return; - } - - const space = data.spaces[0]; - navigate(`/host/manage/${space.id}`); + if (!spacesData) return; + + if (spacesData.spaces.length === 0) { + navigate('/host/manage/spaceCreate'); + return; } - }, [data]); + + const firstSpace = spacesData.spaces[0]; + navigate(`/host/manage/${firstSpace.id}`); + }, [spacesData]); }; export default useAuthCallBack; diff --git a/frontend/src/pages/host/DashBoard/index.tsx b/frontend/src/pages/host/DashBoard/index.tsx index 8e64bc62..90ec60bb 100644 --- a/frontend/src/pages/host/DashBoard/index.tsx +++ b/frontend/src/pages/host/DashBoard/index.tsx @@ -2,10 +2,9 @@ import useDashBoard from './useDashBoard'; import { GoLinkExternal } from 'react-icons/go'; import Button from '@/components/common/Button'; -import ImageBox from '@/components/host/ImageBox'; import JobListCard from '@/components/host/JobListCard'; import SpaceDeleteButton from '@/components/host/SpaceDeleteButton'; -import SpaceInfo from '@/components/host/SpaceInfo'; +import SpaceInfoDisplayBox from '@/components/host/SpaceInfoDisplayBox'; import Submissions from '@/components/host/Submissions'; import slackIcon from '@/assets/slackIcon.svg'; @@ -13,15 +12,7 @@ import slackIcon from '@/assets/slackIcon.svg'; import styles from './styles'; const DashBoard: React.FC = () => { - const { - spaceId, - spaceData, - jobsData, - submissionData, - onClickSubmissionsDetail, - onClickSlackButton, - onClickLinkButton, - } = useDashBoard(); + const { spaceId, spaceData, jobsData, submissionData, onClickSlackButton, onClickLinkButton } = useDashBoard(); return (
@@ -35,15 +26,13 @@ const DashBoard: React.FC = () => { μŠ¬λž™ URL νŽΈμ§‘ - +
- - - +
- +
); diff --git a/frontend/src/pages/host/DashBoard/useDashBoard.tsx b/frontend/src/pages/host/DashBoard/useDashBoard.tsx index 5eca0a0c..72d8be26 100644 --- a/frontend/src/pages/host/DashBoard/useDashBoard.tsx +++ b/frontend/src/pages/host/DashBoard/useDashBoard.tsx @@ -1,4 +1,3 @@ -import { clip } from '@/utils/copy'; import { useQuery } from 'react-query'; import { useNavigate, useParams } from 'react-router-dom'; @@ -12,28 +11,27 @@ import apiJobs from '@/apis/job'; import apiSpace from '@/apis/space'; import apiSubmission from '@/apis/submission'; +import { ID } from '@/types'; + const useDashBoard = () => { const navigate = useNavigate(); - const { spaceId } = useParams(); - const { openModal } = useModal(); const { openToast } = useToast(); + const { spaceId } = useParams() as { spaceId: ID }; + const { data: spaceData } = useQuery(['space', spaceId], () => apiSpace.getSpace(spaceId), { - suspense: true, staleTime: 0, cacheTime: 0, }); - const { data: jobsData } = useQuery(['jobs', spaceId], () => apiJobs.getJobs(spaceId), { suspense: true }); - const { data: submissionData } = useQuery(['submissions', spaceId], () => apiSubmission.getSubmission({ spaceId }), { - suspense: true, - }); - const { refetch: getEntranceCode } = useQuery(['entranceCode'], () => apiHost.getEntranceCode(), { - retry: false, + const { data: jobsData } = useQuery(['jobs', spaceId], () => apiJobs.getJobs(spaceId)); + const { data: submissionData } = useQuery(['submissions', spaceId], () => apiSubmission.getSubmission(spaceId)); + const { refetch: copyEntranceLink } = useQuery(['entranceCode'], () => apiHost.getEntranceCode(), { + suspense: false, enabled: false, onSuccess: data => { - clip(`${location.origin}/enter/${data.entranceCode}/pwd`); + navigator.clipboard.writeText(`${location.origin}/enter/${data.entranceCode}/pwd`); openToast('SUCCESS', '곡간 μž…μž₯ 링크가 λ³΅μ‚¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); }, onError: () => { @@ -41,16 +39,12 @@ const useDashBoard = () => { }, }); - const onClickSubmissionsDetail = () => { - navigate('spaceRecord'); - }; - const onClickSlackButton = () => { openModal(); }; const onClickLinkButton = () => { - getEntranceCode(); + copyEntranceLink(); }; return { @@ -58,7 +52,6 @@ const useDashBoard = () => { spaceData, jobsData, submissionData, - onClickSubmissionsDetail, onClickSlackButton, onClickLinkButton, }; diff --git a/frontend/src/pages/host/Home/index.tsx b/frontend/src/pages/host/Home/index.tsx index 710c8fa9..e3e6feca 100644 --- a/frontend/src/pages/host/Home/index.tsx +++ b/frontend/src/pages/host/Home/index.tsx @@ -3,10 +3,13 @@ import { css } from '@emotion/react'; import GitHubLoginButton from '@/components/common/GitHubLoginButton'; import homeCover from '@/assets/homeCover.png'; +import logoTitle from '@/assets/logoTitle.png'; import theme from '@/styles/theme'; const Home: React.FC = () => { + const isMobile = innerWidth < 600; + return ( <>
{ display: flex; align-items: center; width: 100vw; - height: 64px; + height: 84px; padding: 0 48px; - font-size: 32px; - color: ${theme.colors.white}; - background-color: ${theme.colors.primary}; + font-size: 24px; + font-weight: 600; + background-color: ${theme.colors.skyblue200}; + box-shadow: 0px 2px 1px 1px ${theme.colors.shadow20}; + + img { + margin-right: 10px; + } + + span > b { + font-size: 16px; + } `} > - GongCheck + + {!isMobile && ( + + for 곡간 κ΄€λ¦¬μž + + )}
-
+ λͺ¨λ°”일에 μ΅œμ ν™”λ˜μ–΄μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ°μŠ€ν¬νƒ‘μ„ μ΄μš©ν•΄μ£Όμ„Έμš”. +
+ )} + + - -
+ κ΄€λ¦¬μž νŽ˜μ΄μ§€ μ΄μš©μ„ μœ„ν•΄ 둜그인이 ν•„μš”ν•©λ‹ˆλ‹€. +
diff --git a/frontend/src/pages/host/JobCreate/useJobCreate.ts b/frontend/src/pages/host/JobCreate/useJobCreate.ts index e646404d..93ff0df4 100644 --- a/frontend/src/pages/host/JobCreate/useJobCreate.ts +++ b/frontend/src/pages/host/JobCreate/useJobCreate.ts @@ -1,3 +1,4 @@ +import { AxiosError } from 'axios'; import React, { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; import { useNavigate, useParams } from 'react-router-dom'; @@ -7,15 +8,16 @@ import useToast from '@/hooks/useToast'; import apiJobs from '@/apis/job'; -import { SectionType } from '@/types'; -import { ApiError } from '@/types/apis'; +import { ID, SectionType } from '@/types'; -type MutationParams = { spaceId: string | number | undefined; newJobName: string; sections: SectionType[] }; +import errorMessage from '@/constants/errorMessage'; + +type MutationParams = { spaceId: ID; newJobName: string; sections: SectionType[] }; const useJobCreate = () => { const navigate = useNavigate(); - const { spaceId } = useParams(); + const { spaceId } = useParams() as { spaceId: ID }; const [newJobName, setNewJobName] = useState(''); @@ -30,8 +32,8 @@ const useJobCreate = () => { openToast('SUCCESS', '업무가 생성 λ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); navigate(`/host/manage/${spaceId}`); }, - onError: (err: ApiError) => { - openToast('ERROR', `${err.response?.data.message}`); + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); }, } ); @@ -46,7 +48,7 @@ const useJobCreate = () => { }; useEffect(() => { - resetSections(); + return () => resetSections(); }, []); return { sections, createSection, newJobName, onChangeJobName, onClickCreateNewJob }; diff --git a/frontend/src/pages/host/JobUpdate/useJobUpdate.ts b/frontend/src/pages/host/JobUpdate/useJobUpdate.ts index 7278f688..b8939354 100644 --- a/frontend/src/pages/host/JobUpdate/useJobUpdate.ts +++ b/frontend/src/pages/host/JobUpdate/useJobUpdate.ts @@ -1,3 +1,4 @@ +import { AxiosError } from 'axios'; import React, { useEffect, useState } from 'react'; import { useMutation, useQuery } from 'react-query'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; @@ -8,15 +9,17 @@ import useToast from '@/hooks/useToast'; import apiJobs from '@/apis/job'; import apiTask from '@/apis/task'; -import { SectionType } from '@/types'; +import { ID, SectionType } from '@/types'; import { ApiError } from '@/types/apis'; -type MutationParams = { jobId: string | number | undefined; jobName: string; sections: SectionType[] }; +import errorMessage from '@/constants/errorMessage'; + +type MutationParams = { jobId: ID; jobName: string; sections: SectionType[] }; const useJobUpdate = () => { const navigate = useNavigate(); - const { spaceId, jobId } = useParams(); + const { spaceId, jobId } = useParams() as { spaceId: ID; jobId: ID }; const location = useLocation(); const state = location.state as { jobName: string }; @@ -29,13 +32,11 @@ const useJobUpdate = () => { const { refetch: getTaskData } = useQuery(['taskData', jobId], () => apiTask.getTasks(jobId), { enabled: false, - retry: false, - staleTime: Infinity, onSuccess: data => { updateSection(data.sections); }, - onError: (err: ApiError) => { - openToast('ERROR', `${err.response?.data.message}`); + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); }, }); @@ -46,8 +47,8 @@ const useJobUpdate = () => { openToast('SUCCESS', '업무 μˆ˜μ •μ— μ„±κ³΅ν•˜μ˜€μŠ΅λ‹ˆλ‹€.'); navigate(`/host/manage/${spaceId}`); }, - onError: (err: ApiError) => { - openToast('ERROR', `${err.response?.data.message}`); + onError: (err: AxiosError<{ errorCode: keyof typeof errorMessage }>) => { + openToast('ERROR', errorMessage[`${err.response?.data.errorCode!}`]); }, } ); @@ -59,12 +60,12 @@ const useJobUpdate = () => { const onClickUpdateJob = (e: React.FormEvent) => { e.preventDefault(); updateJob({ jobId, jobName, sections }); - resetSections(); }; useEffect(() => { setJobName(state.jobName); getTaskData(); + return () => resetSections(); }, []); return { sections, createSection, jobName, onChangeJobName, onClickUpdateJob }; diff --git a/frontend/src/pages/host/PasswordUpdate/index.tsx b/frontend/src/pages/host/PasswordUpdate/index.tsx index be9665f6..d42fe01c 100644 --- a/frontend/src/pages/host/PasswordUpdate/index.tsx +++ b/frontend/src/pages/host/PasswordUpdate/index.tsx @@ -7,7 +7,7 @@ import Button from '@/components/common/Button'; import styles from './styles'; const PasswordUpdate: React.FC = () => { - const { password, isShowPassword, onChangePassword, onClickFlipShowPassword, onClickChangeButton } = + const { password, isShowPassword, onChangePassword, onClickToggleShowPassword, onClickChangeButton } = usePasswordUpdate(); return ( @@ -23,9 +23,9 @@ const PasswordUpdate: React.FC = () => { onChange={onChangePassword} /> {isShowPassword ? ( - + ) : ( - + )} - - )} + ); }; diff --git a/frontend/src/pages/host/SpaceCreate/useSpaceCreate.ts b/frontend/src/pages/host/SpaceCreate/useSpaceCreate.ts deleted file mode 100644 index ba365c6a..00000000 --- a/frontend/src/pages/host/SpaceCreate/useSpaceCreate.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useState } from 'react'; - -const useSpaceCreate = () => { - const [isCreateSpace, setIsCreateSpace] = useState(true); - - const onCreateSpace = () => { - setIsCreateSpace(true); - }; - - return { isCreateSpace, onCreateSpace }; -}; - -export default useSpaceCreate; diff --git a/frontend/src/pages/host/SpaceRecord/index.tsx b/frontend/src/pages/host/SpaceRecord/index.tsx index 6b2e74bd..beede757 100644 --- a/frontend/src/pages/host/SpaceRecord/index.tsx +++ b/frontend/src/pages/host/SpaceRecord/index.tsx @@ -5,13 +5,14 @@ import Submissions from '@/components/host/Submissions'; import apiSubmission from '@/apis/submission'; +import { ID } from '@/types'; + import styles from './styles'; const SpaceRecord: React.FC = () => { - const { spaceId } = useParams(); - const { data: submissionData } = useQuery(['submissions', spaceId], () => apiSubmission.getSubmission({ spaceId }), { - suspense: true, - }); + const { spaceId } = useParams() as { spaceId: ID }; + + const { data: submissionData } = useQuery(['submissions', spaceId], () => apiSubmission.getSubmission(spaceId)); return (
diff --git a/frontend/src/pages/host/SpaceUpdate/index.tsx b/frontend/src/pages/host/SpaceUpdate/index.tsx index dcc6a7a9..0b1953d5 100644 --- a/frontend/src/pages/host/SpaceUpdate/index.tsx +++ b/frontend/src/pages/host/SpaceUpdate/index.tsx @@ -1,33 +1,22 @@ -import { AxiosError } from 'axios'; -import { useEffect, useState } from 'react'; -import { useMutation, useQuery } from 'react-query'; +import { useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; -import ImageBox from '@/components/host/ImageBox'; -import useImageBox from '@/components/host/ImageBox/useImageBox'; -import SpaceInfo from '@/components/host/SpaceInfo'; - -import useSpaceForm from '@/hooks/useSpaceForm'; +import SpaceInfoUpdateBox from '@/components/host/SpaceInfoUpdateBox'; import apiSpace from '@/apis/space'; +import { ID } from '@/types'; + import styles from './styles'; const SpaceUpdate: React.FC = () => { - const { spaceId } = useParams(); - - const { data } = useQuery(['space', spaceId], () => apiSpace.getSpace(spaceId), { suspense: true }); + const { spaceId } = useParams() as { spaceId: ID }; - const { onSubmitUpdateSpace } = useSpaceForm(); - const { imageUrl, onChangeImg } = useImageBox(data?.imageUrl); + const { data: spaceData } = useQuery(['space', spaceId], () => apiSpace.getSpace(spaceId)); return (
-
onSubmitUpdateSpace(e, imageUrl, spaceId)} encType="multipart/form-data"> - - - -
+
); }; diff --git a/frontend/src/pages/user/JobList/index.tsx b/frontend/src/pages/user/JobList/index.tsx index 0607c6f5..558fa219 100644 --- a/frontend/src/pages/user/JobList/index.tsx +++ b/frontend/src/pages/user/JobList/index.tsx @@ -8,16 +8,14 @@ import styles from './styles'; const JobList: React.FC = () => { const { jobsData, spaceData, goPreviousPage } = useJobList(); - if (!jobsData || !spaceData) return <>; - return (
-
+
- {spaceData.name} + {spaceData?.name}
μ²΄ν¬ν•˜μ‹€ 업무λ₯Ό μ„ νƒν•΄μ£Όμ„Έμš”. - {jobsData.jobs.length === 0 ? ( + {jobsData?.jobs.length === 0 ? (
κ΄€λ¦¬μžκ°€ μƒμ„±ν•œ 업무가 μ—†μ–΄μš”
) : ( jobsData?.jobs.map(job => ) diff --git a/frontend/src/pages/user/JobList/useJobList.ts b/frontend/src/pages/user/JobList/useJobList.ts index ee0fc16f..aa30b087 100644 --- a/frontend/src/pages/user/JobList/useJobList.ts +++ b/frontend/src/pages/user/JobList/useJobList.ts @@ -6,19 +6,15 @@ import useGoPreviousPage from '@/hooks/useGoPreviousPage'; import apiJobs from '@/apis/job'; import apiSpace from '@/apis/space'; +import { ID } from '@/types'; + const useJobList = () => { - const { spaceId } = useParams(); + const { spaceId } = useParams() as { spaceId: ID }; const { goPreviousPage } = useGoPreviousPage(); - const { data: jobsData } = useQuery(['jobs', spaceId], () => apiJobs.getJobs(spaceId), { - suspense: true, - retry: false, - }); - const { data: spaceData } = useQuery(['spaces', spaceId], () => apiSpace.getSpace(spaceId), { - suspense: true, - retry: false, - }); + const { data: jobsData } = useQuery(['jobs', spaceId], () => apiJobs.getJobs(spaceId)); + const { data: spaceData } = useQuery(['spaces', spaceId], () => apiSpace.getSpace(spaceId)); return { jobsData, spaceData, goPreviousPage }; }; diff --git a/frontend/src/pages/user/Password/index.tsx b/frontend/src/pages/user/Password/index.tsx index 5259f300..d2ef3907 100644 --- a/frontend/src/pages/user/Password/index.tsx +++ b/frontend/src/pages/user/Password/index.tsx @@ -23,7 +23,13 @@ const Password: React.FC = () => {

- +
diff --git a/frontend/src/pages/user/Password/usePassword.ts b/frontend/src/pages/user/Password/usePassword.ts index 5cf77864..6c1cf1e3 100644 --- a/frontend/src/pages/user/Password/usePassword.ts +++ b/frontend/src/pages/user/Password/usePassword.ts @@ -5,25 +5,30 @@ import useToast from '@/hooks/useToast'; import apis from '@/apis'; +import { ID } from '@/types'; + const usePassword = () => { const navigate = useNavigate(); const { openToast } = useToast(); + const { hostId } = useParams() as { hostId: ID }; + const [isActiveSubmit, setIsActiveSubmit] = useState(false); - const { hostId } = useParams(); const setToken = async (password: string) => { - const { token } = await apis.postPassword({ hostId, password }); - localStorage.setItem('token', token); + const { token } = await apis.postPassword(hostId, password); + localStorage.setItem(`${hostId}`, token); + sessionStorage.setItem('tokenKey', `${hostId}`); }; const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (!isActiveSubmit) return; const form = e.target as HTMLFormElement; - const { value: password } = form[0] as HTMLInputElement; + const password = form['password'].value; try { await setToken(password).then(() => { @@ -35,10 +40,10 @@ const usePassword = () => { }; const onChangeInput = (e: React.ChangeEvent) => { - const { value } = e.target as HTMLInputElement; + const { value: password } = e.target; - const isExistValue = value.length > 0; - setIsActiveSubmit(isExistValue); + const isTyped = password.length > 0; + setIsActiveSubmit(isTyped); }; return { diff --git a/frontend/src/pages/user/SpaceList/index.tsx b/frontend/src/pages/user/SpaceList/index.tsx index 65bc35fe..d23a4e3c 100644 --- a/frontend/src/pages/user/SpaceList/index.tsx +++ b/frontend/src/pages/user/SpaceList/index.tsx @@ -9,7 +9,7 @@ import logo from '@/assets/logoTitle.png'; import styles from './styles'; const SpaceList: React.FC = () => { - const { data: spaceData } = useQuery(['spaces'], apis.getSpaces, { suspense: true, retry: false }); + const { data: spaceData } = useQuery(['spaces'], apis.getSpaces); return (
diff --git a/frontend/src/pages/user/TaskList/index.tsx b/frontend/src/pages/user/TaskList/index.tsx index 3f7eba97..10efe0f6 100644 --- a/frontend/src/pages/user/TaskList/index.tsx +++ b/frontend/src/pages/user/TaskList/index.tsx @@ -1,9 +1,9 @@ import useTaskList from './useTaskList'; -import { useEffect, useRef } from 'react'; +import React from 'react'; import { IoIosArrowBack } from 'react-icons/io'; import Button from '@/components/common/Button'; -import SectionInfoPreviewBox from '@/components/user/SectionInfoPreviewBox'; +import SectionInfoPreview from '@/components/user/SectionInfoPreview'; import TaskCard from '@/components/user/TaskCard'; import styles from './styles'; @@ -11,18 +11,19 @@ import styles from './styles'; const TaskList: React.FC = () => { const { spaceData, - getSections, - onClickButton, + onSubmit, goPreviousPage, totalCount, - checkCount, + checkedCount, percent, + sectionsAllCheckMap, isAllChecked, locationState, sectionsData, onClickSectionDetail, + onClickSectionAllCheck, progressBarRef, - isSticked, + isActiveSticky, } = useTaskList(); return ( @@ -37,44 +38,37 @@ const TaskList: React.FC = () => {

{locationState?.jobName}

-
+
-
- {`${checkCount}/${totalCount}`} +
+ {`${checkedCount}/${totalCount}`}
-
- {sectionsData?.sections.length === 0 ? ( -
μ²΄ν¬λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€.
- ) : ( - sectionsData?.sections.map(section => ( -
-
-

{section.name}

+ + {sectionsData?.sections.map(section => ( +
+
+

{section.name}

+
+ {!sectionsAllCheckMap.get(`${section.id}`) && ( + + )} {(section.imageUrl || section.description) && ( - - onClickSectionDetail({ - name: section.name, - imageUrl: section.imageUrl, - description: section.description, - }) - } - /> + onClickSectionDetail(section)} /> )}
- -
- )) - )} -
+ +
+ ))} +
diff --git a/frontend/src/pages/user/TaskList/styles.ts b/frontend/src/pages/user/TaskList/styles.ts index 2614743d..5563dff7 100644 --- a/frontend/src/pages/user/TaskList/styles.ts +++ b/frontend/src/pages/user/TaskList/styles.ts @@ -32,6 +32,7 @@ const locationHeader = css` justify-content: space-between; align-items: center; padding: 0 8px; + min-height: 48px; `; const locationName = css` @@ -40,6 +41,11 @@ const locationName = css` margin: 0; `; +const locationHeaderRightItems = css` + display: flex; + gap: 10px; +`; + const header = css` position: relative; width: 100%; @@ -175,12 +181,21 @@ const form = css` align-items: center; `; +const sectionAllCheckButton = css` + width: 40px; + height: 40px; + margin: 0; + padding: 0; + box-shadow: 2px 2px 2px 0px ${theme.colors.shadow30}; +`; + const styles = { layout, contents, location, locationHeader, locationName, + locationHeaderRightItems, arrowBackIconWrapper, header, thumbnail, @@ -190,6 +205,7 @@ const styles = { progressBar, percentText, button, + sectionAllCheckButton, form, }; diff --git a/frontend/src/pages/user/TaskList/useTaskList.tsx b/frontend/src/pages/user/TaskList/useTaskList.tsx index 71334008..811ffccd 100644 --- a/frontend/src/pages/user/TaskList/useTaskList.tsx +++ b/frontend/src/pages/user/TaskList/useTaskList.tsx @@ -1,49 +1,62 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useQuery } from 'react-query'; -import { useLocation, useParams } from 'react-router-dom'; +import { EventSourcePolyfill } from 'event-source-polyfill'; +import { useEffect, useRef, useState } from 'react'; +import { useMutation, useQuery } from 'react-query'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import DetailedInfoCardModal from '@/components/user/DetailedInfoCardModal'; +import DetailInfoModal from '@/components/user/DetailInfoModal'; import NameModal from '@/components/user/NameModal'; import useGoPreviousPage from '@/hooks/useGoPreviousPage'; import useModal from '@/hooks/useModal'; +import useSectionCheck from '@/hooks/useSectionCheck'; +import useToast from '@/hooks/useToast'; import apis from '@/apis'; -const RE_FETCH_INTERVAL_TIME = 100; +import { ID, SectionType } from '@/types'; +import { ApiTaskData } from '@/types/apis'; + +const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080'; const PROGRESS_BAR_DEFAULT_POSITION = 232; const useTaskList = () => { - const { spaceId, jobId, hostId } = useParams(); + const navigate = useNavigate(); + + const { spaceId, jobId, hostId } = useParams() as { spaceId: ID; jobId: ID; hostId: ID }; const location = useLocation(); - const locationState = location.state as { jobName: string } | undefined; + const locationState = location.state as { jobName: string }; const { openModal } = useModal(); - - const progressBarRef = useRef(null); + const { openToast } = useToast(); const { goPreviousPage } = useGoPreviousPage(); - const [isSticked, setIsSticked] = useState(false); - - const { data: sectionsData, refetch: getSections } = useQuery( - ['sections', jobId], - () => apis.getRunningTasks(jobId), - { - suspense: true, - retry: false, - refetchInterval: RE_FETCH_INTERVAL_TIME, - cacheTime: 0, - } - ); + const progressBarRef = useRef(null); - const { data: spaceData } = useQuery(['space', jobId], () => apis.getSpace(spaceId), { - suspense: true, - retry: false, + const [isActiveSticky, setIsActiveSticky] = useState(false); + + const [sectionsData, setSectionsData] = useState({ + sections: [ + { + id: 0, + name: '', + description: '', + imageUrl: '', + tasks: [{ id: 0, name: '', checked: false, description: '', imageUrl: '' }], + }, + ], }); - const onClickButton = (e: React.MouseEvent) => { + const { data: spaceData } = useQuery(['space', jobId], () => apis.getSpace(spaceId)); + + const { mutate: postSectionAllCheck } = useMutation((sectionId: ID) => apis.postSectionAllCheckTask(sectionId)); + + const { sectionsAllCheckMap, totalCount, checkedCount, percent, isAllChecked } = useSectionCheck( + sectionsData?.sections || [] + ); + + const onSubmit = (e: React.FormEvent) => { e.preventDefault(); openModal( { placeholder="이름을 μž…λ ₯ν•΄μ£Όμ„Έμš”." buttonText="확인" jobId={jobId} - hostId={hostId} /> ); }; - const onClickSectionDetail = ({ - name, - imageUrl, - description, - }: { - name: string; - imageUrl: string; - description: string; - }) => { - openModal(); + const onClickSectionDetail = (section: SectionType) => { + openModal(); }; - if (!sectionsData?.sections.length) return { isNotData: true }; - - const { sections } = sectionsData; + const onClickSectionAllCheck = (sectionId: ID) => { + postSectionAllCheck(sectionId); + }; - const tasks = sections.map(section => section.tasks.map(task => task.checked)); - const checkList = tasks.reduce((prev, cur) => { - return prev.concat(...cur); - }); + useEffect(() => { + const isActive = progressBarRef.current?.offsetTop! > PROGRESS_BAR_DEFAULT_POSITION; - const totalCount = useMemo(() => checkList.length, [checkList]); - const checkCount = useMemo(() => checkList.filter(check => check === true).length, [checkList]); - const percent = useMemo(() => Math.ceil((checkCount / totalCount) * 100), [checkCount, totalCount]); - const isAllChecked = totalCount === checkCount; + setIsActiveSticky(isActive); + }, [progressBarRef.current?.offsetTop]); useEffect(() => { - const isStartSticked = progressBarRef.current?.offsetTop! > PROGRESS_BAR_DEFAULT_POSITION; + const tokenKey = sessionStorage.getItem('tokenKey'); + if (!tokenKey) return; - setIsSticked(isStartSticked); - }, [progressBarRef.current?.offsetTop]); + const sse = new EventSourcePolyfill(`${API_URL}/api/jobs/${jobId}/runningTasks/connect`, { + headers: { + Authorization: `Bearer ${localStorage.getItem(tokenKey)}`, + }, + }); + + sse.addEventListener('connect', (e: any) => { + const { data: receivedSections } = e; + + setSectionsData(JSON.parse(receivedSections)); + }); + + sse.addEventListener('flip', (e: any) => { + const { data: receivedSections } = e; + + setSectionsData(JSON.parse(receivedSections)); + }); + + sse.addEventListener('submit', () => { + navigate(`/enter/${hostId}/spaces/${spaceId}`); + openToast('SUCCESS', 'ν•΄λ‹Ή μ²΄ν¬λ¦¬μŠ€νŠΈλŠ” μ œμΆœλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'); + }); + }, []); return { spaceData, - getSections, - onClickButton, + onSubmit, goPreviousPage, totalCount, - checkCount, + checkedCount, percent, + sectionsAllCheckMap, isAllChecked, locationState, sectionsData, onClickSectionDetail, + onClickSectionAllCheck, progressBarRef, - isSticked, + isActiveSticky, }; }; diff --git a/frontend/src/styles/animation.ts b/frontend/src/styles/animation.ts index 6c774b0f..08e8e88b 100644 --- a/frontend/src/styles/animation.ts +++ b/frontend/src/styles/animation.ts @@ -30,6 +30,18 @@ const shake = keyframes` } `; +const littleShake = keyframes` + 0%, 50%{ + transform: rotate(0deg); + } + 5%, 15%, 25%, 35%, 45% { + transform: rotate(5deg); + } + 10%, 20%, 30%, 40% { + transform: rotate(-5deg); + } +`; + const spinnerFace = keyframes` 0% { transform: translate(-50%, 20%); @@ -58,7 +70,29 @@ const spinnerEye = keyframes` const moveUp = keyframes` 0% { opacity: 0; - transform: translateY(25px); + transform: translateY(50px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +`; + +const customMoveUp = (y: string) => keyframes` + 0% { + opacity: 0; + transform: translateY(${y}); + } + 100% { + opacity: 1; + transform: translateY(0); + } +`; + +const moveDown = keyframes` + 0% { + opacity: 0; + transform: translateY(-50px); } 100% { opacity: 1; @@ -66,6 +100,55 @@ const moveUp = keyframes` } `; -const animation = { fadeIn, fadeOut, shake, spinnerFace, spinnerEye, moveUp }; +const moveLeft = keyframes` + 0% { + opacity: 0; + transform: translateX(50px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +`; + +const moveRight = keyframes` + 0% { + opacity: 0; + transform: translateX(-50px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +`; + +const wave = keyframes` + from { + transform: skew(0); + } + 33% { + transform: skew(2deg, 2deg); + } + 66% { + transform: skew(4deg, 4deg); + } + to { + transform: skew(2deg, 2deg); + }`; + +const animation = { + fadeIn, + fadeOut, + shake, + littleShake, + spinnerFace, + spinnerEye, + moveUp, + customMoveUp, + moveDown, + moveRight, + moveLeft, + wave, +}; export default animation; diff --git a/frontend/src/styles/global.ts b/frontend/src/styles/global.ts index 762b75e9..b8aa8afa 100644 --- a/frontend/src/styles/global.ts +++ b/frontend/src/styles/global.ts @@ -26,6 +26,7 @@ const globalStyle = css` justify-content: center; overflow: scroll; overflow-x: hidden; + background-color: ${theme.colors.background}; } button { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index eac4b98e..712cca9f 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -12,7 +12,7 @@ export type JobType = { }; export type SectionType = { - id: ID | undefined; + id: ID; name: string; description: string; imageUrl: string; diff --git a/frontend/src/utils/copy.ts b/frontend/src/utils/copy.ts deleted file mode 100644 index 913426df..00000000 --- a/frontend/src/utils/copy.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const clip = (url: string): void => { - const textarea = document.createElement('textarea'); - document.body.appendChild(textarea); - textarea.value = url; - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); -}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a81aa37b..a12e0411 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -28,6 +28,6 @@ }, "types": ["cypress", "node"] }, - "include": ["src"], - "exclude": ["cypress", "**/*.cy.ts"] + "include": ["src", "cypress"], + "exclude": ["**/*.cy.ts"] } diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index b7d4d67f..439f5361 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -8,7 +8,7 @@ const config = { output: { publicPath: '/', path: path.resolve(__dirname, 'dist'), - filename: 'bundle.js', + filename: 'bundle.[chunkhash].js', }, devServer: { port: 3000, diff --git a/imagestorage/build.gradle b/imagestorage/build.gradle index faf556ff..2be3b82f 100644 --- a/imagestorage/build.gradle +++ b/imagestorage/build.gradle @@ -35,6 +35,10 @@ dependencies { // lombock compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // image resize + implementation 'com.sksamuel.scrimage:scrimage-core:4.0.31' + testImplementation 'com.sksamuel.scrimage:scrimage-webp:4.0.31' } tasks.named('test') { diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/application/ImageService.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/application/ImageService.java index 68aad6ab..c913dcc2 100644 --- a/imagestorage/src/main/java/com/woowacourse/imagestorage/application/ImageService.java +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/application/ImageService.java @@ -1,19 +1,34 @@ package com.woowacourse.imagestorage.application; import com.woowacourse.imagestorage.application.response.ImageResponse; +import com.woowacourse.imagestorage.application.response.ImageSaveResponse; +import com.woowacourse.imagestorage.domain.ChangeWidth; +import com.woowacourse.imagestorage.domain.ImageExtension; import com.woowacourse.imagestorage.domain.ImageFile; +import com.woowacourse.imagestorage.exception.FileIOException; +import com.woowacourse.imagestorage.exception.FileIONotFoundException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Service +@Slf4j public class ImageService { + private static final MediaType IMAGE_WEBP = new MediaType("image", "webp"); + private final Path storageLocation; private final String imagePathPrefix; @@ -23,12 +38,44 @@ public ImageService(@Value("${file.upload-dir}") final String storageLocation, this.imagePathPrefix = imagePathPrefix; } - public ImageResponse storeImage(MultipartFile image) throws IOException { - ImageFile imageFile = ImageFile.from(image); + public ImageSaveResponse storeImage(final MultipartFile image) { + try { + ImageFile imageFile = ImageFile.from(image); + + String imageFileInputName = imageFile.randomName(); + Path fileStorageLocation = resolvePath(imageFileInputName); + Files.copy(imageFile.inputStream(), fileStorageLocation, StandardCopyOption.REPLACE_EXISTING); + return new ImageSaveResponse(imagePathPrefix + imageFileInputName); + } catch (IOException exception) { + throw new FileIOException("이미지 μ €μž₯ μ‹œ μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); + } + } + + public ImageResponse resizeImage(final String imageUrl, final int width, final boolean isWebp) { + try { + Path fileStorageLocation = resolvePath(imageUrl); + File file = fileStorageLocation.toFile(); + ImageExtension imageExtension = ImageExtension.from(FilenameUtils.getExtension(file.getName())); + byte[] originImage = IOUtils.toByteArray(new FileInputStream(file)); + byte[] resizedImage = imageExtension.resizeImage(originImage, new ChangeWidth(width)); + + return imageByRequestToWebp(isWebp, imageExtension, resizedImage); + } catch (FileNotFoundException exception) { + throw new FileIONotFoundException("파일 κ²½λ‘œμ— 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } catch (IOException exception) { + throw new FileIOException("이미지 파일 λ³€ν™˜ μ‹œ μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); + } + } + + private Path resolvePath(final String imageUrl) { + return storageLocation.resolve(imageUrl); + } - String imageFileInputName = imageFile.randomName(); - Path fileStorageLocation = storageLocation.resolve(imageFileInputName); - Files.copy(imageFile.inputStream(), fileStorageLocation, StandardCopyOption.REPLACE_EXISTING); - return new ImageResponse(imagePathPrefix + imageFileInputName); + private ImageResponse imageByRequestToWebp(final boolean isWebp, final ImageExtension imageExtension, + final byte[] resizedImage) { + if (isWebp) { + return ImageResponse.of(imageExtension.convertToWebp(resizedImage), IMAGE_WEBP); + } + return ImageResponse.of(resizedImage, imageExtension.getContentType()); } } diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/application/response/ImageResponse.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/application/response/ImageResponse.java index a1cc1351..b0d6032b 100644 --- a/imagestorage/src/main/java/com/woowacourse/imagestorage/application/response/ImageResponse.java +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/application/response/ImageResponse.java @@ -1,17 +1,23 @@ package com.woowacourse.imagestorage.application.response; +import lombok.Getter; +import org.springframework.http.MediaType; + +@Getter public class ImageResponse { - private String imagePath; + private byte[] bytes; + private MediaType contentType; private ImageResponse() { } - public ImageResponse(final String imagePath) { - this.imagePath = imagePath; + public ImageResponse(final byte[] bytes, final MediaType contentType) { + this.bytes = bytes; + this.contentType = contentType; } - public String getImagePath() { - return imagePath; + public static ImageResponse of(final byte[] bytes, final MediaType contentType) { + return new ImageResponse(bytes, contentType); } } diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/application/response/ImageSaveResponse.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/application/response/ImageSaveResponse.java new file mode 100644 index 00000000..25a604fb --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/application/response/ImageSaveResponse.java @@ -0,0 +1,16 @@ +package com.woowacourse.imagestorage.application.response; + +import lombok.Getter; + +@Getter +public class ImageSaveResponse { + + private String imagePath; + + private ImageSaveResponse() { + } + + public ImageSaveResponse(final String imagePath) { + this.imagePath = imagePath; + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/configuration/EtagConfiguration.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/configuration/EtagConfiguration.java new file mode 100644 index 00000000..b74be59e --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/configuration/EtagConfiguration.java @@ -0,0 +1,14 @@ +package com.woowacourse.imagestorage.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +@Configuration +public class EtagConfiguration { + + @Bean + public ShallowEtagHeaderFilter shallowEtagHeaderFilter() { + return new ShallowEtagHeaderFilter(); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ChangeWidth.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ChangeWidth.java new file mode 100644 index 00000000..86a9efca --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ChangeWidth.java @@ -0,0 +1,41 @@ +package com.woowacourse.imagestorage.domain; + +import com.woowacourse.imagestorage.exception.BusinessException; +import java.util.Objects; +import lombok.Getter; + +@Getter +public class ChangeWidth { + + private static final int MIN_WIDTH = 10; + + private final int value; + + public ChangeWidth(final int value) { + checkAvaliableWidht(value); + this.value = value; + } + + private void checkAvaliableWidht(final int value) { + if (value < MIN_WIDTH) { + throw new BusinessException("λ³€κ²½ν•  κ°€λ‘œ 길이가 μž‘μŠ΅λ‹ˆλ‹€."); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final ChangeWidth that = (ChangeWidth) o; + return value == that.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ImageExtension.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ImageExtension.java new file mode 100644 index 00000000..4d5a0292 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ImageExtension.java @@ -0,0 +1,56 @@ +package com.woowacourse.imagestorage.domain; + +import com.woowacourse.imagestorage.exception.BusinessException; +import com.woowacourse.imagestorage.strategy.convert.Convert2WebpStrategy; +import com.woowacourse.imagestorage.strategy.convert.Gif2WebpStrategy; +import com.woowacourse.imagestorage.strategy.convert.StaticImg2WebpStrategy; +import com.woowacourse.imagestorage.strategy.resize.GifImageResizeStrategy; +import com.woowacourse.imagestorage.strategy.resize.ImageResizeStrategy; +import com.woowacourse.imagestorage.strategy.resize.JpegImageResizeStrategy; +import com.woowacourse.imagestorage.strategy.resize.PngImageResizeStrategy; +import java.util.Arrays; +import lombok.Getter; +import org.springframework.http.MediaType; + +@Getter +public enum ImageExtension { + + PNG("png", MediaType.IMAGE_PNG, new PngImageResizeStrategy(), new StaticImg2WebpStrategy()), + JPEG("jpeg", MediaType.IMAGE_JPEG, new JpegImageResizeStrategy(), new StaticImg2WebpStrategy()), + JPG("jpg", MediaType.IMAGE_JPEG, new JpegImageResizeStrategy(), new StaticImg2WebpStrategy()), + SVG("svg", new MediaType("image", "svg+xml"), new JpegImageResizeStrategy(), new StaticImg2WebpStrategy()), + GIF("gif", MediaType.IMAGE_GIF, new GifImageResizeStrategy(), new Gif2WebpStrategy()), + ; + + private final String extension; + private final MediaType contentType; + private final ImageResizeStrategy imageResizeStrategy; + private final Convert2WebpStrategy convert2WebpStrategy; + + ImageExtension(final String extension, final MediaType contentType, final ImageResizeStrategy imageResizeStrategy, + final Convert2WebpStrategy convert2WebpStrategy) { + this.extension = extension; + this.contentType = contentType; + this.imageResizeStrategy = imageResizeStrategy; + this.convert2WebpStrategy = convert2WebpStrategy; + } + + public static ImageExtension from(final String format) { + return Arrays.stream(values()) + .filter(imageExtension -> imageExtension.containsType(format)) + .findFirst() + .orElseThrow(() -> new BusinessException("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€.")); + } + + public byte[] resizeImage(final byte[] originBytes, final ChangeWidth width) { + return imageResizeStrategy.resize(originBytes, width); + } + + public byte[] convertToWebp(final byte[] originBytes) { + return convert2WebpStrategy.convert(originBytes); + } + + private boolean containsType(final String format) { + return extension.equals(format); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ImageFile.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ImageFile.java index c09c1a3e..9c05df5e 100644 --- a/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ImageFile.java +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/domain/ImageFile.java @@ -8,19 +8,16 @@ import java.io.InputStream; import java.util.Objects; import java.util.UUID; -import java.util.regex.Pattern; import org.springframework.web.multipart.MultipartFile; public class ImageFile { - private static final Pattern IMAGE_FILE_EXTENSION_PATTERN = Pattern.compile("^(png|jpeg|jpg|svg|gif)$"); - private final String originFileName; private final String contentType; - private final String extension; + private final ImageExtension extension; private final byte[] imageBytes; - public ImageFile(final String originFileName, final String contentType, final String extension, + public ImageFile(final String originFileName, final String contentType, final ImageExtension extension, final byte[] imageBytes) { this.originFileName = originFileName; this.contentType = contentType; @@ -28,16 +25,18 @@ public ImageFile(final String originFileName, final String contentType, final St this.imageBytes = imageBytes; } - public static ImageFile from(final MultipartFile multipartFile) { validateNullFile(multipartFile); validateEmptyFile(multipartFile); validateNullFileName(multipartFile); - validateImageExtension(multipartFile); try { - return new ImageFile(multipartFile.getOriginalFilename(), multipartFile.getContentType(), - getFilenameExtension(multipartFile.getOriginalFilename()), multipartFile.getBytes()); + return new ImageFile( + multipartFile.getOriginalFilename(), + multipartFile.getContentType(), + ImageExtension.from(getFilenameExtension(multipartFile.getOriginalFilename())), + multipartFile.getBytes() + ); } catch (IOException exception) { throw new BusinessException("잘λͺ»λœ νŒŒμΌμž…λ‹ˆλ‹€."); } @@ -61,14 +60,6 @@ private static void validateNullFileName(final MultipartFile multipartFile) { } } - private static void validateImageExtension(final MultipartFile multipartFile) { - String fileExtension = getFilenameExtension(multipartFile.getOriginalFilename()); - assert fileExtension != null; - if (!IMAGE_FILE_EXTENSION_PATTERN.matcher(fileExtension).matches()) { - throw new BusinessException("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€."); - } - } - public InputStream inputStream() { return new ByteArrayInputStream(imageBytes); } @@ -78,7 +69,7 @@ public long length() { } public String randomName() { - return UUID.randomUUID().toString() + "." + extension; + return UUID.randomUUID().toString() + "." + extension.getExtension(); } public String contentType() { diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ControllerAdvice.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ControllerAdvice.java index dfd80eca..29d219c5 100644 --- a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ControllerAdvice.java +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ControllerAdvice.java @@ -11,6 +11,35 @@ @Slf4j public class ControllerAdvice { + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleInfrastructureException(final BusinessException e) { + return ResponseEntity.badRequest().body(ErrorResponse.from(e)); + } + + @ExceptionHandler(FileIOException.class) + public ResponseEntity handleInfrastructureException(final FileIOException e) { + log.error(e.getMessage()); + return ResponseEntity.internalServerError().body(ErrorResponse.from(e)); + } + + @ExceptionHandler(FileIONotFoundException.class) + public ResponseEntity handleFileNotFoundException(final FileIONotFoundException e) { + log.warn(e.getMessage()); + return ResponseEntity.notFound().build(); + } + + @ExceptionHandler(FileConvertException.class) + public ResponseEntity handleFileConvertException(final FileConvertException e) { + log.error(e.getMessage()); + return ResponseEntity.internalServerError().build(); + } + + @ExceptionHandler(FileResizeException.class) + public ResponseEntity handleFileResizeException(final FileResizeException e) { + log.error(e.getMessage()); + return ResponseEntity.internalServerError().build(); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleException(final Exception e) { log.error("Stack Trace : {}", extractStackTrace(e)); diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ErrorResponse.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ErrorResponse.java index b9d79f82..b112d344 100644 --- a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ErrorResponse.java +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/ErrorResponse.java @@ -13,4 +13,12 @@ private ErrorResponse() { public ErrorResponse(final String message) { this.message = message; } + + public static ErrorResponse from(final RuntimeException exception) { + return new ErrorResponse(exception.getMessage()); + } + + public static ErrorResponse from(final String message) { + return new ErrorResponse(message); + } } diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileConvertException.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileConvertException.java new file mode 100644 index 00000000..91d6e58c --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileConvertException.java @@ -0,0 +1,8 @@ +package com.woowacourse.imagestorage.exception; + +public class FileConvertException extends RuntimeException { + + public FileConvertException(final String message) { + super(message); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileIOException.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileIOException.java new file mode 100644 index 00000000..41457131 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileIOException.java @@ -0,0 +1,8 @@ +package com.woowacourse.imagestorage.exception; + +public class FileIOException extends RuntimeException { + + public FileIOException(final String message) { + super(message); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileIONotFoundException.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileIONotFoundException.java new file mode 100644 index 00000000..21b924d0 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileIONotFoundException.java @@ -0,0 +1,8 @@ +package com.woowacourse.imagestorage.exception; + +public class FileIONotFoundException extends RuntimeException { + + public FileIONotFoundException(final String message) { + super(message); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileResizeException.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileResizeException.java new file mode 100644 index 00000000..3dde025c --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/exception/FileResizeException.java @@ -0,0 +1,8 @@ +package com.woowacourse.imagestorage.exception; + +public class FileResizeException extends RuntimeException { + + public FileResizeException(final String message) { + super(message); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/presentation/ImageController.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/presentation/ImageController.java index dca62d33..4b9b89d8 100644 --- a/imagestorage/src/main/java/com/woowacourse/imagestorage/presentation/ImageController.java +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/presentation/ImageController.java @@ -2,9 +2,14 @@ import com.woowacourse.imagestorage.application.ImageService; import com.woowacourse.imagestorage.application.response.ImageResponse; -import java.io.IOException; +import com.woowacourse.imagestorage.application.response.ImageSaveResponse; +import java.util.concurrent.TimeUnit; +import org.springframework.http.CacheControl; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -12,16 +17,31 @@ @RestController public class ImageController { + private static final String DEFAULT_RESIZE_WIDTH = "500"; + private static final String DEFAULT_WEBP = "true"; + private static final int CACHE_CONTROL_MAX_AGE = 30; + private final ImageService imageService; public ImageController(final ImageService imageService) { this.imageService = imageService; } - @PostMapping(value = "/api/image-upload") - public ResponseEntity uploadImage(@RequestPart MultipartFile file) throws IOException { + @PostMapping("/api/image-upload") + public ResponseEntity uploadImage(@RequestPart MultipartFile file) { + + ImageSaveResponse imageSaveResponse = imageService.storeImage(file); + return ResponseEntity.ok(imageSaveResponse.getImagePath()); + } - ImageResponse imageResponse = imageService.storeImage(file); - return ResponseEntity.ok(imageResponse.getImagePath()); + @GetMapping("/api/resize/{imageUrl}") + public ResponseEntity getResizeImage(@PathVariable String imageUrl, + @RequestParam(required = false, defaultValue = DEFAULT_RESIZE_WIDTH) int width, + @RequestParam(required = false, defaultValue = DEFAULT_WEBP) boolean webp) { + ImageResponse response = imageService.resizeImage(imageUrl, width, webp); + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(CACHE_CONTROL_MAX_AGE, TimeUnit.DAYS)) + .contentType(response.getContentType()) + .body(response.getBytes()); } } diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/Convert2WebpStrategy.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/Convert2WebpStrategy.java new file mode 100644 index 00000000..fcc93931 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/Convert2WebpStrategy.java @@ -0,0 +1,6 @@ +package com.woowacourse.imagestorage.strategy.convert; + +public interface Convert2WebpStrategy { + + byte[] convert(byte[] originBytes); +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/Gif2WebpStrategy.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/Gif2WebpStrategy.java new file mode 100644 index 00000000..340cd4ce --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/Gif2WebpStrategy.java @@ -0,0 +1,19 @@ +package com.woowacourse.imagestorage.strategy.convert; + +import com.woowacourse.imagestorage.exception.FileConvertException; +import com.woowacourse.imagestorage.strategy.convert.handler.Gif2WebpHandler; +import java.io.IOException; + +public class Gif2WebpStrategy implements Convert2WebpStrategy { + + private static final Gif2WebpHandler DEFAULT = new Gif2WebpHandler(); + + @Override + public byte[] convert(final byte[] originBytes) { + try { + return DEFAULT.convert(originBytes); + } catch (IOException exception) { + throw new FileConvertException("webp λ³€ν™˜ μ‹œ λ¬Έμ œκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/StaticImg2WebpStrategy.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/StaticImg2WebpStrategy.java new file mode 100644 index 00000000..be95a1b0 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/StaticImg2WebpStrategy.java @@ -0,0 +1,19 @@ +package com.woowacourse.imagestorage.strategy.convert; + +import com.woowacourse.imagestorage.exception.FileConvertException; +import com.woowacourse.imagestorage.strategy.convert.handler.CwebpHandler; +import java.io.IOException; + +public class StaticImg2WebpStrategy implements Convert2WebpStrategy { + + private static final CwebpHandler DEFAULT = new CwebpHandler(); + + @Override + public byte[] convert(final byte[] originBytes) { + try { + return DEFAULT.convert(originBytes); + } catch (IOException exception) { + throw new FileConvertException("webp λ³€ν™˜ μ‹œ λ¬Έμ œκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/CwebpHandler.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/CwebpHandler.java new file mode 100644 index 00000000..57d802c3 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/CwebpHandler.java @@ -0,0 +1,62 @@ +package com.woowacourse.imagestorage.strategy.convert.handler; + +import com.woowacourse.imagestorage.exception.FileIOException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class CwebpHandler extends WebpHandler { + + private static final String CWEBP = "cwebp"; + + private static final Path binary; + + static { + try { + binary = createPlaceholder(CWEBP); + installBinary(binary, CWEBP); + } catch (IOException exception) { + throw new FileIOException("cwebp binary νŒŒμΌμ„ 읽을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + } + + public byte[] convert(byte[] bytes) throws IOException { + Path source = Files.createTempFile("input", "gif").toAbsolutePath(); + Path target = Files.createTempFile("to_webp", "webp").toAbsolutePath(); + try { + Files.write(source, bytes, StandardOpenOption.CREATE); + convert(source, target); + return Files.readAllBytes(target); + } finally { + source.toFile() + .delete(); + target.toFile() + .delete(); + } + } + + private void convert(Path source, Path target) throws IOException { + Path stdout = Files.createTempFile("stdout", "webp"); + List commands = new ArrayList<>(); + commands.add(binary.toAbsolutePath().toString()); + commands.add(source.toAbsolutePath().toString()); + commands.add("-o"); + commands.add(target.toAbsolutePath().toString()); + + ProcessBuilder builder = new ProcessBuilder(commands); + builder.redirectErrorStream(true); + builder.redirectOutput(stdout.toFile()); + + Process process = builder.start(); + try { + process.waitFor(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + throw new IOException(e); + } + checkSuccessProcess(stdout, process); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/Gif2WebpHandler.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/Gif2WebpHandler.java new file mode 100644 index 00000000..2fe03006 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/Gif2WebpHandler.java @@ -0,0 +1,62 @@ +package com.woowacourse.imagestorage.strategy.convert.handler; + +import com.woowacourse.imagestorage.exception.FileIOException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class Gif2WebpHandler extends WebpHandler { + + private static final String GIF_TO_WEBP = "gif2webp"; + + private static final Path binary; + + static { + try { + binary = createPlaceholder(GIF_TO_WEBP); + installBinary(binary, GIF_TO_WEBP); + } catch (IOException exception) { + throw new FileIOException("gif to webp binary νŒŒμΌμ„ 읽을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + } + + public byte[] convert(byte[] bytes) throws IOException { + Path source = Files.createTempFile("input", "gif").toAbsolutePath(); + Path target = Files.createTempFile("to_webp", "webp").toAbsolutePath(); + try { + Files.write(source, bytes, StandardOpenOption.CREATE); + convert(source, target); + return Files.readAllBytes(target); + } finally { + source.toFile() + .delete(); + target.toFile() + .delete(); + } + } + + private void convert(Path source, Path target) throws IOException { + Path stdout = Files.createTempFile("stdout", "webp"); + List commands = new ArrayList<>(); + commands.add(binary.toAbsolutePath().toString()); + commands.add(source.toAbsolutePath().toString()); + commands.add("-o"); + commands.add(target.toAbsolutePath().toString()); + + ProcessBuilder builder = new ProcessBuilder(commands); + builder.redirectErrorStream(true); + builder.redirectOutput(stdout.toFile()); + + Process process = builder.start(); + try { + process.waitFor(5, TimeUnit.MINUTES); + } catch (InterruptedException e) { + throw new IOException(e); + } + checkSuccessProcess(stdout, process); + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/WebpHandler.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/WebpHandler.java new file mode 100644 index 00000000..d4085273 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/convert/handler/WebpHandler.java @@ -0,0 +1,59 @@ +package com.woowacourse.imagestorage.strategy.convert.handler; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.SystemUtils; + +public abstract class WebpHandler { + + protected static Path createPlaceholder(String name) throws IOException { + return Files.createTempFile(name, "binary"); + } + + protected static void installBinary(Path output, String source) throws IOException { + InputStream in = WebpHandler.class.getResourceAsStream(getBinaryPath(source)); + Files.copy(in, output, StandardCopyOption.REPLACE_EXISTING); + in.close(); + + if (!SystemUtils.IS_OS_WINDOWS) { + setExecutable(output); + } + } + + protected static void checkSuccessProcess(final Path stdout, final Process process) throws IOException { + int exitStatus = process.exitValue(); + if (exitStatus != 0) { + List error = Files.readAllLines(stdout); + throw new IOException(error.toString()); + } + } + + private static String getBinaryPath(String binaryName) { + if (SystemUtils.IS_OS_WINDOWS) { + return "/dist_webp_binaries/window/" + binaryName + ".exe"; + } + if (SystemUtils.IS_OS_MAC) { + return "/dist_webp_binaries/mac/" + binaryName; + } + String osArch = System.getProperty("os.arch"); + if (osArch.equals("aarch64")) { + return "/dist_webp_binaries/linux_arm/" + binaryName; + } + return "/dist_webp_binaries/linux/" + binaryName; + } + + private static boolean setExecutable(Path output) throws IOException { + try { + return new ProcessBuilder("chmod", "+x", output.toAbsolutePath().toString()) + .start() + .waitFor(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new IOException(e); + } + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/GifImageResizeStrategy.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/GifImageResizeStrategy.java new file mode 100644 index 00000000..6053c089 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/GifImageResizeStrategy.java @@ -0,0 +1,29 @@ +package com.woowacourse.imagestorage.strategy.resize; + +import com.sksamuel.scrimage.ImmutableImage; +import com.sksamuel.scrimage.nio.AnimatedGifReader; +import com.sksamuel.scrimage.nio.GifSequenceWriter; +import com.sksamuel.scrimage.nio.ImageSource; +import com.woowacourse.imagestorage.domain.ChangeWidth; +import com.woowacourse.imagestorage.exception.FileResizeException; +import java.io.IOException; + +public class GifImageResizeStrategy implements ImageResizeStrategy { + + private static final long FRAME_DELAY_MILLIS = 0; + private static final boolean INFINITE_LOOP = true; + + @Override + public byte[] resize(final byte[] originBytes, final ChangeWidth width) { + try { + ImmutableImage[] immutableImages = AnimatedGifReader.read(ImageSource.of(originBytes)) + .getFrames() + .stream() + .map(immutableImage -> immutableImage.scaleToWidth(width.getValue())) + .toArray(ImmutableImage[]::new); + return new GifSequenceWriter(FRAME_DELAY_MILLIS, INFINITE_LOOP).bytes(immutableImages); + } catch (IOException exception) { + throw new FileResizeException("gif μ‚¬μ΄μ¦ˆ λ³€ν™˜ μ‹œ λ¬Έμ œκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/ImageResizeStrategy.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/ImageResizeStrategy.java new file mode 100644 index 00000000..0fee68b5 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/ImageResizeStrategy.java @@ -0,0 +1,8 @@ +package com.woowacourse.imagestorage.strategy.resize; + +import com.woowacourse.imagestorage.domain.ChangeWidth; + +public interface ImageResizeStrategy { + + byte[] resize(byte[] originBytes, ChangeWidth width); +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/JpegImageResizeStrategy.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/JpegImageResizeStrategy.java new file mode 100644 index 00000000..407567fc --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/JpegImageResizeStrategy.java @@ -0,0 +1,22 @@ +package com.woowacourse.imagestorage.strategy.resize; + +import com.sksamuel.scrimage.ImmutableImage; +import com.sksamuel.scrimage.nio.JpegWriter; +import com.woowacourse.imagestorage.domain.ChangeWidth; +import com.woowacourse.imagestorage.exception.FileResizeException; +import java.io.IOException; + +public class JpegImageResizeStrategy implements ImageResizeStrategy { + + @Override + public byte[] resize(final byte[] originBytes, final ChangeWidth width) { + try { + return ImmutableImage.loader() + .fromBytes(originBytes) + .scaleToWidth(width.getValue()) + .bytes(JpegWriter.Default); + } catch (IOException exception) { + throw new FileResizeException("jpeg μ‚¬μ΄μ¦ˆ λ³€ν™˜ μ‹œ λ¬Έμ œκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/PngImageResizeStrategy.java b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/PngImageResizeStrategy.java new file mode 100644 index 00000000..76539fd3 --- /dev/null +++ b/imagestorage/src/main/java/com/woowacourse/imagestorage/strategy/resize/PngImageResizeStrategy.java @@ -0,0 +1,22 @@ +package com.woowacourse.imagestorage.strategy.resize; + +import com.sksamuel.scrimage.ImmutableImage; +import com.sksamuel.scrimage.nio.PngWriter; +import com.woowacourse.imagestorage.domain.ChangeWidth; +import com.woowacourse.imagestorage.exception.FileResizeException; +import java.io.IOException; + +public class PngImageResizeStrategy implements ImageResizeStrategy { + + @Override + public byte[] resize(final byte[] originBytes, final ChangeWidth width) { + try { + return ImmutableImage.loader() + .fromBytes(originBytes) + .scaleToWidth(width.getValue()) + .bytes(PngWriter.NoCompression); + } catch (IOException exception) { + throw new FileResizeException("png μ‚¬μ΄μ¦ˆ λ³€ν™˜ μ‹œ λ¬Έμ œκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/imagestorage/src/main/resources/application-local.yml b/imagestorage/src/main/resources/application-local.yml index 2d93636b..08a51c09 100644 --- a/imagestorage/src/main/resources/application-local.yml +++ b/imagestorage/src/main/resources/application-local.yml @@ -7,7 +7,7 @@ spring: max-request-size: 60MB file: - upload-dir: src/main/resources/static/images + upload-dir: server-path: https://image.gongcheck.shop/images/ server: diff --git a/imagestorage/src/main/resources/dist_webp_binaries/linux/cwebp b/imagestorage/src/main/resources/dist_webp_binaries/linux/cwebp new file mode 100755 index 00000000..80076052 Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/linux/cwebp differ diff --git a/imagestorage/src/main/resources/dist_webp_binaries/linux/gif2webp b/imagestorage/src/main/resources/dist_webp_binaries/linux/gif2webp new file mode 100755 index 00000000..eb1f2daa Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/linux/gif2webp differ diff --git a/imagestorage/src/main/resources/dist_webp_binaries/linux_arm/cwebp b/imagestorage/src/main/resources/dist_webp_binaries/linux_arm/cwebp new file mode 100755 index 00000000..79f1cba1 Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/linux_arm/cwebp differ diff --git a/imagestorage/src/main/resources/dist_webp_binaries/linux_arm/gif2webp b/imagestorage/src/main/resources/dist_webp_binaries/linux_arm/gif2webp new file mode 100755 index 00000000..95d874a8 Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/linux_arm/gif2webp differ diff --git a/imagestorage/src/main/resources/dist_webp_binaries/mac/cwebp b/imagestorage/src/main/resources/dist_webp_binaries/mac/cwebp new file mode 100755 index 00000000..61df9108 Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/mac/cwebp differ diff --git a/imagestorage/src/main/resources/dist_webp_binaries/mac/gif2webp b/imagestorage/src/main/resources/dist_webp_binaries/mac/gif2webp new file mode 100755 index 00000000..e1d0bfb2 Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/mac/gif2webp differ diff --git a/imagestorage/src/main/resources/dist_webp_binaries/window/cwebp.exe b/imagestorage/src/main/resources/dist_webp_binaries/window/cwebp.exe new file mode 100644 index 00000000..68cec74b Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/window/cwebp.exe differ diff --git a/imagestorage/src/main/resources/dist_webp_binaries/window/gif2webp.exe b/imagestorage/src/main/resources/dist_webp_binaries/window/gif2webp.exe new file mode 100644 index 00000000..d1cefbf0 Binary files /dev/null and b/imagestorage/src/main/resources/dist_webp_binaries/window/gif2webp.exe differ diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/ImageFormatDetector.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/ImageFormatDetector.java new file mode 100644 index 00000000..b21889e7 --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/ImageFormatDetector.java @@ -0,0 +1,92 @@ +package com.woowacourse.imagestorage; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class ImageFormatDetector { + + private static final byte[] GIF = new byte[]{'G', 'I', 'F', '8'}; + private static final byte[] PNG = new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}; + private static final byte[] JPEG = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF}; + + private static final byte[] WEBP_START = new byte[]{'R', 'I', 'F', 'F'}; + private static final byte[] WEBP_END = new byte[]{'W', 'E', 'B', 'P'}; + + public static boolean isGif(final byte[] bytes) { + for (int i = 0; i < GIF.length; i++) { + if (isNotSameByte(GIF[i], bytes[i])) { + return false; + } + } + return true; + } + + @Test + void gifνŒŒμΌμ„_νŒλ³„ν• _수_μžˆλ‹€() { + boolean actual = ImageFormatDetector + .isGif(new byte[]{'G', 'I', 'F', '8', 'T', 'E', 'S', 'T'}); + + assertThat(actual).isTrue(); + } + + public static boolean isPng(final byte[] bytes) { + for (int i = 0; i < PNG.length; i++) { + if (isNotSameByte(PNG[i], bytes[i])) { + return false; + } + } + return true; + } + + @Test + void pngνŒŒμΌμ„_νŒλ³„ν• _수_μžˆλ‹€() { + boolean actual = ImageFormatDetector + .isPng(new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 'T', 'E', 'S', 'T'}); + + assertThat(actual).isTrue(); + } + + public static boolean isJpeg(final byte[] bytes) { + for (int i = 0; i < JPEG.length; i++) { + if (isNotSameByte(JPEG[i], bytes[i])) { + return false; + } + } + return true; + } + + @Test + void jpegνŒŒμΌμ„_νŒλ³„ν• _수_μžˆλ‹€() { + boolean actual = ImageFormatDetector + .isJpeg(new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, 'T', 'E', 'S', 'T'}); + + assertThat(actual).isTrue(); + } + + public static boolean isWebp(final byte[] bytes) { + for (int i = 0; i < WEBP_START.length; i++) { + if (isNotSameByte(WEBP_START[i], bytes[i])) { + return false; + } + } + for (int i = 0; i < WEBP_END.length; i++) { + if (isNotSameByte(WEBP_END[i], bytes[i + 8])) { + return false; + } + } + return true; + } + + @Test + void webpνŒŒμΌμ„_νŒλ³„ν• _수_μžˆλ‹€() { + boolean actual = ImageFormatDetector + .isWebp(new byte[]{'R', 'I', 'F', 'F', 'T', 'E', 'S', 'T', 'W', 'E', 'B', 'P'}); + + assertThat(actual).isTrue(); + } + + private static boolean isNotSameByte(final byte origin, final byte compare) { + return origin != compare; + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/ImageTypeTransfer.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/ImageTypeTransfer.java new file mode 100644 index 00000000..cca50d9d --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/ImageTypeTransfer.java @@ -0,0 +1,19 @@ +package com.woowacourse.imagestorage; + +import com.woowacourse.imagestorage.exception.FileIOException; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.imageio.ImageIO; + +public class ImageTypeTransfer { + + public static BufferedImage toBufferedImage(final byte[] bytes) { + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + return ImageIO.read(inputStream); + } catch (IOException exception) { + throw new FileIOException("BufferedImage λ³€ν™˜ μ‹œ λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/application/ImageServiceTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/application/ImageServiceTest.java index a2be05c9..20a712b0 100644 --- a/imagestorage/src/test/java/com/woowacourse/imagestorage/application/ImageServiceTest.java +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/application/ImageServiceTest.java @@ -1,15 +1,25 @@ package com.woowacourse.imagestorage.application; +import static java.awt.image.BufferedImage.TYPE_INT_RGB; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import com.sksamuel.scrimage.ImmutableImage; +import com.woowacourse.imagestorage.ImageTypeTransfer; import com.woowacourse.imagestorage.application.response.ImageResponse; +import com.woowacourse.imagestorage.application.response.ImageSaveResponse; +import com.woowacourse.imagestorage.exception.FileIONotFoundException; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.charset.StandardCharsets; +import javax.imageio.ImageIO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; @@ -19,6 +29,12 @@ class ImageServiceTest { @Autowired private ImageService imageService; + private byte[] toByteArray(final BufferedImage image) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "jpg", byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } + @Nested class storeImage_λ©”μ†Œλ“œλŠ” { @@ -28,19 +44,76 @@ class μ €μž₯ν•˜κ³ μžν•˜λŠ”_이미지_파일이_μž…λ ₯된_경우 { private MultipartFile image; @BeforeEach - void setUp() { + void setUp() throws IOException { image = new MockMultipartFile("image", "jamsil.jpg", "image/jpg", - "123".getBytes(StandardCharsets.UTF_8)); + toByteArray(new BufferedImage(500, 500, TYPE_INT_RGB))); } @Test - void 이미지λ₯Ό_μ €μž₯ν•˜κ³ _이미지_경둜λ₯Ό_λ°˜ν™˜ν•œλ‹€() throws IOException { - ImageResponse actual = imageService.storeImage(image); + void 이미지λ₯Ό_μ €μž₯ν•˜κ³ _이미지_경둜λ₯Ό_λ°˜ν™˜ν•œλ‹€() { + ImageSaveResponse actual = imageService.storeImage(image); assertThat(actual.getImagePath()).isNotNull(); } } } + + @Nested + class getResizeImage_λ©”μ†Œλ“œλŠ” { + + @Nested + class μ‘΄μž¬ν•˜μ§€μ•ŠλŠ”_μ΄λ―Έμ§€μ˜_경둜λ₯Ό_μž…λ ₯받은_경우 { + + private static final String NOT_FOUND_IMAGE_URL = "notfound.jpeg"; + private static final int WIDTH = 500; + + @Test + void μ˜ˆμ™Έλ₯Ό_λ°œμƒμ‹œν‚¨λ‹€() { + assertThatThrownBy(() -> imageService.resizeImage(NOT_FOUND_IMAGE_URL, WIDTH, true)) + .isInstanceOf(FileIONotFoundException.class) + .hasMessage("파일 κ²½λ‘œμ— 파일이 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } + } + + @Nested + class κ²½λ‘œμ™€_λ³€κ²½ν• _길이와_webpλ³€ν™˜ν™•μΈμ„_μž…λ ₯받은_경우 { + + private static final String IMAGE_URL = "test-image.jpeg"; + private static final int WIDTH = 500; + + @Test + void λ¦¬μ‚¬μ΄μ§•λœ_webp_이미지λ₯Ό_λ°˜ν™˜ν•œλ‹€() throws IOException { + ImageResponse actual = imageService.resizeImage(IMAGE_URL, WIDTH, true); + int actualWidth = ImmutableImage.loader() + .fromBytes(actual.getBytes()) + .width; + + assertAll( + () -> assertThat(actualWidth).isEqualTo(WIDTH), + () -> assertThat(actual.getContentType()).isEqualTo(new MediaType("image", "webp")) + ); + } + } + + @Nested + class κ²½λ‘œμ™€_λ³€κ²½ν• _길이와_webpλ³€ν™˜λΆˆκ°€λ₯Ό_μž…λ ₯받은_경우 { + + private static final String IMAGE_URL = "test-image.jpeg"; + private static final int WIDTH = 500; + + @Test + void λ¦¬μ‚¬μ΄μ§•λœ_κΈ°μ‘΄_νƒ€μž…_이미지λ₯Ό_λ°˜ν™˜ν•œλ‹€() { + ImageResponse actual = imageService.resizeImage(IMAGE_URL, WIDTH, false); + int actualWidth = ImageTypeTransfer.toBufferedImage(actual.getBytes()) + .getWidth(); + + assertAll( + () -> assertThat(actualWidth).isEqualTo(WIDTH), + () -> assertThat(actual.getContentType()).isEqualTo(MediaType.IMAGE_JPEG) + ); + } + } + } } diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ChangeWidthTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ChangeWidthTest.java new file mode 100644 index 00000000..91dc2622 --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ChangeWidthTest.java @@ -0,0 +1,18 @@ +package com.woowacourse.imagestorage.domain; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.imagestorage.exception.BusinessException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ChangeWidthTest { + + @ParameterizedTest + @ValueSource(ints = {-1, 0, 9}) + void μž…λ ₯된_widthκ°€_μ΅œμ†ŒκΈΈμ΄λ³΄λ‹€_μž‘μœΌλ©΄_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€(final int input) { + assertThatThrownBy(() -> new ChangeWidth(input)) + .isInstanceOf(BusinessException.class) + .hasMessage("λ³€κ²½ν•  κ°€λ‘œ 길이가 μž‘μŠ΅λ‹ˆλ‹€."); + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ImageExtensionTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ImageExtensionTest.java new file mode 100644 index 00000000..11d51952 --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ImageExtensionTest.java @@ -0,0 +1,26 @@ +package com.woowacourse.imagestorage.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.imagestorage.exception.BusinessException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ImageExtensionTest { + + @Test + void 이미지확μž₯μžκ°€_μ•„λ‹Œ_ν™•μž₯μžκ°€_μž…λ ₯된_경우_μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { + assertThatThrownBy(() -> ImageExtension.from("txt")) + .isInstanceOf(BusinessException.class) + .hasMessage("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€."); + } + + @ParameterizedTest + @CsvSource(value = {"png,PNG", "gif,GIF", "jpeg,JPEG"}) + void 이미지확μž₯μžκ°€_μž…λ ₯된_경우_ImageFormat_이_λ°˜ν™˜λœλ‹€(final String input, final ImageExtension expected) { + ImageExtension actual = ImageExtension.from(input); + assertThat(actual).isEqualTo(expected); + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ImageFileTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ImageFileTest.java index 50b52882..86a536c2 100644 --- a/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ImageFileTest.java +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/domain/ImageFileTest.java @@ -4,7 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.woowacourse.imagestorage.exception.BusinessException; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import javax.imageio.ImageIO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -12,6 +16,13 @@ import org.springframework.web.multipart.MultipartFile; class ImageFileTest { + + private byte[] toByteArray(final BufferedImage image) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(image, "jpg", byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } + @Nested class from_λ©”μ†Œλ“œλŠ” { @@ -68,27 +79,6 @@ void setUp() { } } - @Nested - class 이미지_ν™•μž₯μžκ°€_μ•„λ‹Œ_파일이_μž…λ ₯된_경우 { - - private MultipartFile textFile; - - @BeforeEach - void setUp() { - textFile = new MockMultipartFile("image", - "jamsil.text", - "image/jpg", - "123".getBytes(StandardCharsets.UTF_8)); - } - - @Test - void μ˜ˆμ™Έκ°€_λ°œμƒν•œλ‹€() { - assertThatThrownBy(() -> ImageFile.from(textFile)) - .isInstanceOf(BusinessException.class) - .hasMessage("이미지 파일 ν™•μž₯자만 λ“€μ–΄μ˜¬ 수 μžˆμŠ΅λ‹ˆλ‹€."); - } - } - @Nested class 정상적인_multipartFile이_μž…λ ₯된_경우 { diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/convert/Gif2WebpStrategyTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/convert/Gif2WebpStrategyTest.java new file mode 100644 index 00000000..4139d8ea --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/convert/Gif2WebpStrategyTest.java @@ -0,0 +1,43 @@ +package com.woowacourse.imagestorage.strategy.convert; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.woowacourse.imagestorage.ImageFormatDetector; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Paths; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class Gif2WebpStrategyTest { + + private final Gif2WebpStrategy gif2WebpStrategy = new Gif2WebpStrategy(); + + @Nested + class convert_λ©”μ†Œλ“œλŠ” { + + @Nested + class byteκ°’λ₯Ό_λ°›λŠ”_경우 { + + private byte[] image; + + @BeforeEach + void setUp() throws IOException { + File imageFile = Paths.get("src/test/resources/static/images/") + .resolve("test-image.gif") + .toFile(); + image = IOUtils.toByteArray(new FileInputStream(imageFile)); + } + + @Test + void convertν•œ_Webp이미지데이터λ₯Ό_λ°˜ν™˜ν•œλ‹€() { + byte[] actual = gif2WebpStrategy.convert(image); + + assertThat(ImageFormatDetector.isWebp(actual)).isTrue(); + } + } + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/convert/StaticImg2WebpStrategyTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/convert/StaticImg2WebpStrategyTest.java new file mode 100644 index 00000000..26275725 --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/convert/StaticImg2WebpStrategyTest.java @@ -0,0 +1,43 @@ +package com.woowacourse.imagestorage.strategy.convert; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.woowacourse.imagestorage.ImageFormatDetector; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Paths; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class StaticImg2WebpStrategyTest { + + private final StaticImg2WebpStrategy staticImg2WebpStrategy = new StaticImg2WebpStrategy(); + + @Nested + class convert_λ©”μ†Œλ“œλŠ” { + + @Nested + class byteκ°’λ₯Ό_λ°›λŠ”_경우 { + + private byte[] image; + + @BeforeEach + void setUp() throws IOException { + File imageFile = Paths.get("src/test/resources/static/images/") + .resolve("test-image.jpeg") + .toFile(); + image = IOUtils.toByteArray(new FileInputStream(imageFile)); + } + + @Test + void convertν•œ_Webp이미지데이터λ₯Ό_λ°˜ν™˜ν•œλ‹€() { + byte[] actual = staticImg2WebpStrategy.convert(image); + + assertThat(ImageFormatDetector.isWebp(actual)).isTrue(); + } + } + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/GifImageResizeStrategyTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/GifImageResizeStrategyTest.java new file mode 100644 index 00000000..3488114e --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/GifImageResizeStrategyTest.java @@ -0,0 +1,52 @@ +package com.woowacourse.imagestorage.strategy.resize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.imagestorage.ImageFormatDetector; +import com.woowacourse.imagestorage.ImageTypeTransfer; +import com.woowacourse.imagestorage.domain.ChangeWidth; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Paths; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class GifImageResizeStrategyTest { + + private final GifImageResizeStrategy gifImageResizeStrategy = new GifImageResizeStrategy(); + + @Nested + class resize_λ©”μ†Œλ“œλŠ” { + + @Nested + class byteκ°’κ³Ό_λ³€κ²½ν• _widthλ₯Ό_λ°›λŠ”_경우 { + + private static final int RESIZE_WIDTH = 500; + private byte[] image; + + @BeforeEach + void setUp() throws IOException { + File imageFile = Paths.get("src/test/resources/static/images/") + .resolve("test-image.gif") + .toFile(); + image = IOUtils.toByteArray(new FileInputStream(imageFile)); + } + + @Test + void width의_λΉ„μœ¨λ§ŒνΌ_resizeν•œ_Gif이미지데이터λ₯Ό_λ°˜ν™˜ν•œλ‹€() { + byte[] actual = gifImageResizeStrategy.resize(image, new ChangeWidth(RESIZE_WIDTH)); + int actualWidth = ImageTypeTransfer.toBufferedImage(actual) + .getWidth(); + + assertAll( + () -> assertThat(actualWidth).isEqualTo(RESIZE_WIDTH), + () -> assertThat(ImageFormatDetector.isGif(actual)).isTrue() + ); + } + } + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/JpegImageResizeStrategyTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/JpegImageResizeStrategyTest.java new file mode 100644 index 00000000..8cde583e --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/JpegImageResizeStrategyTest.java @@ -0,0 +1,52 @@ +package com.woowacourse.imagestorage.strategy.resize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.imagestorage.ImageFormatDetector; +import com.woowacourse.imagestorage.ImageTypeTransfer; +import com.woowacourse.imagestorage.domain.ChangeWidth; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Paths; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class JpegImageResizeStrategyTest { + + private final JpegImageResizeStrategy jpegImageResizeStrategy = new JpegImageResizeStrategy(); + + @Nested + class resize_λ©”μ†Œλ“œλŠ” { + + @Nested + class byteκ°’κ³Ό_λ³€κ²½ν• _widthλ₯Ό_λ°›λŠ”_경우 { + + private static final int RESIZE_WIDTH = 500; + private byte[] image; + + @BeforeEach + void setUp() throws IOException { + File imageFile = Paths.get("src/test/resources/static/images/") + .resolve("test-image.jpeg") + .toFile(); + image = IOUtils.toByteArray(new FileInputStream(imageFile)); + } + + @Test + void width의_λΉ„μœ¨λ§ŒνΌ_resizeν•œ_Jpeg이미지데이터λ₯Ό_λ°˜ν™˜ν•œλ‹€() { + byte[] actual = jpegImageResizeStrategy.resize(image, new ChangeWidth(RESIZE_WIDTH)); + int actualWidth = ImageTypeTransfer.toBufferedImage(actual) + .getWidth(); + + assertAll( + () -> assertThat(actualWidth).isEqualTo(RESIZE_WIDTH), + () -> assertThat(ImageFormatDetector.isJpeg(actual)).isTrue() + ); + } + } + } +} diff --git a/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/PngImageResizeStrategyTest.java b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/PngImageResizeStrategyTest.java new file mode 100644 index 00000000..1c6df2e2 --- /dev/null +++ b/imagestorage/src/test/java/com/woowacourse/imagestorage/strategy/resize/PngImageResizeStrategyTest.java @@ -0,0 +1,52 @@ +package com.woowacourse.imagestorage.strategy.resize; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.imagestorage.ImageFormatDetector; +import com.woowacourse.imagestorage.ImageTypeTransfer; +import com.woowacourse.imagestorage.domain.ChangeWidth; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Paths; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class PngImageResizeStrategyTest { + + private final PngImageResizeStrategy pngImageResizeStrategy = new PngImageResizeStrategy(); + + @Nested + class resize_λ©”μ†Œλ“œλŠ” { + + @Nested + class byteκ°’κ³Ό_λ³€κ²½ν• _widthλ₯Ό_λ°›λŠ”_경우 { + + private static final int RESIZE_WIDTH = 500; + private byte[] image; + + @BeforeEach + void setUp() throws IOException { + File imageFile = Paths.get("src/test/resources/static/images/") + .resolve("test-image.jpeg") + .toFile(); + image = IOUtils.toByteArray(new FileInputStream(imageFile)); + } + + @Test + void width의_λΉ„μœ¨λ§ŒνΌ_resizeν•œ_Png이미지데이터λ₯Ό_λ°˜ν™˜ν•œλ‹€() { + byte[] actual = pngImageResizeStrategy.resize(image, new ChangeWidth(RESIZE_WIDTH)); + int actualWidth = ImageTypeTransfer.toBufferedImage(actual) + .getWidth(); + + assertAll( + () -> assertThat(actualWidth).isEqualTo(RESIZE_WIDTH), + () -> assertThat(ImageFormatDetector.isPng(actual)).isTrue() + ); + } + } + } +} diff --git a/imagestorage/src/test/resources/application.yml b/imagestorage/src/test/resources/application.yml index 038257cc..5c8b0234 100644 --- a/imagestorage/src/test/resources/application.yml +++ b/imagestorage/src/test/resources/application.yml @@ -7,7 +7,7 @@ spring: max-request-size: 60MB file: - upload-dir: src/test/resources/static/images + upload-dir: src/test/resources/static/images/ server-path: http://localhost/images/ server: diff --git a/imagestorage/src/test/resources/static/images/test-image.gif b/imagestorage/src/test/resources/static/images/test-image.gif new file mode 100644 index 00000000..4f8463e6 Binary files /dev/null and b/imagestorage/src/test/resources/static/images/test-image.gif differ diff --git a/imagestorage/src/test/resources/static/images/test-image.jpeg b/imagestorage/src/test/resources/static/images/test-image.jpeg new file mode 100644 index 00000000..2f1252e6 Binary files /dev/null and b/imagestorage/src/test/resources/static/images/test-image.jpeg differ