diff --git a/.gitignore b/.gitignore index 241d961b..9d44fbf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +.env.* .DS_Store .gradle @@ -34,3 +35,5 @@ out/ /.nb-gradle/ ### VS Code ### .vscode/ +### logs ### +/logs/ \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle index 5d45cb8f..9ce513b6 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,7 +1,7 @@ plugins { - id 'org.springframework.boot' version '2.7.1' + id 'org.springframework.boot' version '2.6.9' id 'io.spring.dependency-management' version '1.0.11.RELEASE' - id 'org.asciidoctor.convert' version '1.5.8' + id 'org.asciidoctor.jvm.convert' version '3.3.2' id 'java' } @@ -10,9 +10,13 @@ version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { + asciidoctorExtensions compileOnly { extendsFrom annotationProcessor } + all { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging' + } } repositories { @@ -25,10 +29,13 @@ ext { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'io.netty:netty-resolver-dns-native-macos:4.1.75.Final:osx-aarch_64' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.flywaydb:flyway-core:6.4.2' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.h2database:h2:1.4.200' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' @@ -37,6 +44,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.springframework.boot:spring-boot-starter-log4j2' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml' + asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' } tasks.named('test') { @@ -45,6 +55,25 @@ tasks.named('test') { } tasks.named('asciidoctor') { + configurations 'asciidoctorExtensions' + baseDirFollowsSourceFile() inputs.dir snippetsDir dependsOn test } + +asciidoctor.doFirst { + delete file('src/main/resources/static/docs') +} + +task createDocument(type: Copy) { + dependsOn asciidoctor + from file("build/docs/asciidoc") + into file("src/main/resources/static") +} + +bootJar { + dependsOn createDocument + from("${asciidoctor.outputDir}") { + into 'static/docs' + } +} diff --git a/backend/src/docs/asciidoc/auth.adoc b/backend/src/docs/asciidoc/auth.adoc new file mode 100644 index 00000000..dc98418f --- /dev/null +++ b/backend/src/docs/asciidoc/auth.adoc @@ -0,0 +1,4 @@ +== 로그인 + +=== 카카오 Oauth +operation::auth-controller-test/kakao-login[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/errorCodes.adoc b/backend/src/docs/asciidoc/errorCodes.adoc new file mode 100644 index 00000000..09bbc508 --- /dev/null +++ b/backend/src/docs/asciidoc/errorCodes.adoc @@ -0,0 +1,179 @@ + +== 에러코드 +=== 롤링페이퍼 에러 + +|=== +| error code | error message | show message | status code + +| `1001` +| 롤링페이퍼 제목은 20자를 초과할 수 없습니다. title={%s} +| 롤링페이퍼 제목은 20자를 초과할 수 없습니다. +| `400 Bad Request` + +| `1002` +| 롤링페이퍼 제목은 공백일 수 없습니다. +| 롤링페이퍼 제목은 공백일 수 없습니다. +| `400 Bad Request` +|=== + +=== 메시지 에러 + +|=== +| error code | error message | show message | status code + +| `2001` +| 메시지는 공백일 수 없습니다. +| 메시지는 공백일 수 없습니다. +| `400 Bad Request` + +| `2002` +| 메시지 내용 사이즈 초과입니다 message={%s} +| 메시지 내용은 500자 까지만 가능합니다. +| `400 Bad Request` + +| `2003` +| 해당 회원은 작성자가 아닙니다. id={%d} +| 해당 글의 작성자가 아닙니다. +| `403 Forbidden.` +|=== + +=== 유저 에러 + +|=== +| error code | error message | show message | status code + +| `3001` +| 이메일은 공백일 수 없습니다. +| 이메일은 공백일 수 없습니다. +| `400 Bad Request` + +| `3002` +| 이메일 정규식에 위반되는 이메일입니다. email = {%s} +| 유효하지 않은 이메일입니다. +| `400 Bad Request` + +| `3003` +| 이미 이메일이 중복되는 회원이 존재합니다. +| 이미 가입된 이메일입니다. +| `403 Forbidden.` + +| `3004` +| 유저네임은 공백일 수 없습니다 +| 유저네임은 공백일 수 없습니다 +| `400 Bad Request` + +| `3005` +| 유저네임은 2자 이상 20자 이하여야 합니다. username={%s} +| 유저네임은 2자 이상 20자 이하여야 합니다. +| `400 Bad Request` + +| `3006` +| 올바르지 않은 username입니다. username={%s} +| 유저네임은 영어, 한국어, 숫자로 구성해야 합니다. +| `400 Bad Request` + +| `3007` +| 비밀번호는 공백일 수 없습니다. +| 비밀번호는 공백일 수 없습니다. +| `400 Bad Request` + +| `3008` +| 비밀번호는 8자 이상 20자 이하여야 합니다. password={%s} +| 해당 글의 작성자가 아닙니다. +| `400 Bad Request` + +| `3009` +| 비밀번호는 최소 하나 이상의 알파벳과 숫자로 구성해야 합니다. password={%s} +| 비밀번호는 최소 하나 이상의 알파벳과 숫자로 구성해야 합니다. +| `400 Bad Request` + +| `3010` +| 이메일 정규식에 위반되는 이메일입니다. email = {%s} +| 이메일 또는 비밀번호가 일치하지 않습니다. +| `401 Unauthorized` + +| `3011` +| 올바르지 않은 토큰입니다. +| 올바르지 않은 토큰입니다. +| `401 Unauthorized` + +| `3012` +| 토큰의 유효기간이 만료됐습니다. +| 토큰의 유효기간이 만료됐습니다. +| `401 Unauthorized` + +| `3013` +| 토큰의 secret key가 변조됐습니다. 해킹의 우려가 존재합니다. token={%s} +| 토큰의 secret key가 변조됐습니다. 해킹의 우려가 존재합니다. +| `401 Unauthorized` +|=== + +=== 모임 에러 +|=== +| error code | error message | show message | status code + +| `4001` +| 모임 이름은 공백일 수 없습니다. +| 모임 이름은 공백일 수 없습니다. +| `400 Bad Request` + +| `4002` +| 모임 이름은 20자 이하여야 합니다. name={%s} +| 모임 이름은 20자 이하여야 합니다. +| `400 Bad Request` + +| `4003` +| 모임 설명은 공백일 수 없습니다. +| 모임 설명은 공백일 수 없습니다. +| `400 Bad Request` + +| `4004` +| 모임 설명은 100자 이하여야 합니다. description={%s} +| 모임 설명은 100자 이하여야 합니다. +| `400 Bad Request` + +| `4005` +| 이모지가 선택되지 않았습니다. +| 이모지가 선택되지 않았습니다. +| `400 Bad Request` + +| `4006` +| 색상이 선택되지 않았습니다. +| 색상이 선택되지 않았습니다. +| `400 Bad Request` + +| `4007` +| 이미 가입된 모임입니다. teamId={%d}, memberId={%d} +| 이미 가입한 모임입니다. +| `400 Bad Request` + +| `4008` +| 이미 가입돼있는 회원입니다. +| 이미 가입돼있는 회원입니다. +| `400 Bad Request` + +| `4009` +| 닉네임은 공백일 수 없습니다. +| 닉네임은 공백일 수 없습니다. +| `400 Bad Request` + +| `4010` +| 닉네임 글자 수 초과 nickname={%s} +| 닉네임은 20자 이하여야 합니다. +| `400 Bad Request` + +| `4011` +| 이미 존재하는 모임 이름입니다. teamName={%s} +| 이미 존재하는 모임 이름입니다. +| `400 Bad Request` + +| `4012` +| 해당 모임에 가입되지 않은 회원입니다. memberId={%d} +| 해당 모임에 가입되지 않은 회원입니다. +| `404 Not Found` + +| `4013` +| 해당 모임에 가입해야 가능한 작업입니다. teamId={%d}, memberId={%d} +| 해당 모임에 가입해야 가능한 작업입니다. +| `403 Forbidden` +|=== diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc new file mode 100644 index 00000000..234ce79b --- /dev/null +++ b/backend/src/docs/asciidoc/index.adoc @@ -0,0 +1,14 @@ += NAEPYEON(내편) Application API Document +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 2 +:sectlinks: + +include::auth.adoc[] +include::members.adoc[] +include::teams.adoc[] +include::rollingpapers.adoc[] +include::messages.adoc[] +include::errorCodes.adoc[] diff --git a/backend/src/docs/asciidoc/members.adoc b/backend/src/docs/asciidoc/members.adoc new file mode 100644 index 00000000..ea4bc351 --- /dev/null +++ b/backend/src/docs/asciidoc/members.adoc @@ -0,0 +1,16 @@ +== 유저 + +=== 내 정보 조회 +operation::member-controller-test/find-member[snippets='http-request,http-response'] + +=== 내가 받은 롤링페이퍼 목록 +operation::member-controller-test/find-received-rollingpapers[snippets='http-request,http-response'] + +=== 내가 작성한 메시지 목록 +operation::member-controller-test/find-written-messages[snippets='http-request,http-response'] + +=== 내 정보 수정 +operation::member-controller-test/update-member[snippets='http-request,http-response'] + +=== 회원탈퇴 +operation::member-controller-test/delete-member[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/messages.adoc b/backend/src/docs/asciidoc/messages.adoc new file mode 100644 index 00000000..3cedbfb4 --- /dev/null +++ b/backend/src/docs/asciidoc/messages.adoc @@ -0,0 +1,13 @@ +== 메시지 + +=== 메시지 생성 +operation::message-controller-test/create-message[snippets='http-request,http-response'] + +=== 메시지 상세 조회 +operation::message-controller-test/find-message[snippets='http-request,http-response'] + +=== 메시지 수정 +operation::message-controller-test/update-message[snippets='http-request,http-response'] + +=== 메시지 삭제 +operation::message-controller-test/delete-message[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/rollingpapers.adoc b/backend/src/docs/asciidoc/rollingpapers.adoc new file mode 100644 index 00000000..774be93f --- /dev/null +++ b/backend/src/docs/asciidoc/rollingpapers.adoc @@ -0,0 +1,16 @@ +== 롤링페이퍼 + +=== 롤링페이퍼 생성 +operation::rollingpaper-controller-test/create-rollingpaper[snippets='http-request,http-response'] + +=== 롤링페이퍼 단건 조회 +operation::rollingpaper-controller-test/find-rollingpaper-by-id[snippets='http-request,http-response'] + +=== 모임 내 롤링페이퍼 목록 조회 +operation::rollingpaper-controller-test/find-rollingpapers-by-team-id[snippets='http-request,http-response'] + +=== 모임에서 받은 내 롤링페이퍼들 조회 +operation::rollingpaper-controller-test/find-rollingpapers-by-member-id[snippets='http-request,http-response'] + +=== 롤링페이퍼 수정 +operation::rollingpaper-controller-test/update-rollingpaper[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/teams.adoc b/backend/src/docs/asciidoc/teams.adoc new file mode 100644 index 00000000..4d936d57 --- /dev/null +++ b/backend/src/docs/asciidoc/teams.adoc @@ -0,0 +1,31 @@ +== 모임 + +=== 모임 단건 조회 +operation::team-controller-test/get-team[snippets='http-request,http-response'] + +=== 참여중인 모임들 조회 +operation::team-controller-test/get-joined-teams[snippets='http-request,http-response'] + +=== 모든 모임 조회 +operation::team-controller-test/get-all-teams[snippets='http-request,http-response'] + +=== 키워드에 맞는 모든 모임 조회 +operation::team-controller-test/get-all-teams-by-keyword[snippets='http-request,http-response'] + +=== 모임에 참여중인 회원정보 +operation::team-controller-test/get-joined-members[snippets='http-request,http-response'] + +=== 모임 생성 +operation::team-controller-test/create-team/[snippets='http-request,http-response'] + +=== 모임 정보 수정 +operation::team-controller-test/update-team[snippets='http-request,http-response'] + +=== 모임에 회원 참가 +operation::team-controller-test/join-member[snippets='http-request,http-response'] + +=== 모임에 내 정보 조회 +operation::team-controller-test/get-my-info-in-team[snippets='http-request,http-response'] + +=== 모임에서 내 정보 수정 +operation::team-controller-test/update-my-info[snippets='http-request,http-response'] diff --git a/backend/src/main/java/com/woowacourse/naepyeon/config/AuthenticationPrincipalConfig.java b/backend/src/main/java/com/woowacourse/naepyeon/config/AuthenticationPrincipalConfig.java index d422e669..ef0c8e2b 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/config/AuthenticationPrincipalConfig.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/config/AuthenticationPrincipalConfig.java @@ -20,7 +20,7 @@ public class AuthenticationPrincipalConfig implements WebMvcConfigurer { public void addInterceptors(final InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor(jwtTokenProvider)) .addPathPatterns("/api/v1/**") - .excludePathPatterns("/api/v1/login") + .excludePathPatterns("/api/v1/oauth/**") .excludePathPatterns("/api/v1/members"); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/config/JpaAuditingConfig.java b/backend/src/main/java/com/woowacourse/naepyeon/config/JpaAuditingConfig.java new file mode 100644 index 00000000..bc0875ad --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package com.woowacourse.naepyeon.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/MemberController.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/MemberController.java index fd8391fe..dc2557d2 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/MemberController.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/MemberController.java @@ -2,20 +2,22 @@ import com.woowacourse.naepyeon.controller.auth.AuthenticationPrincipal; import com.woowacourse.naepyeon.controller.dto.LoginMemberRequest; -import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; import com.woowacourse.naepyeon.controller.dto.MemberUpdateRequest; import com.woowacourse.naepyeon.service.MemberService; +import com.woowacourse.naepyeon.service.MessageService; +import com.woowacourse.naepyeon.service.RollingpaperService; import com.woowacourse.naepyeon.service.dto.MemberResponseDto; -import java.net.URI; +import com.woowacourse.naepyeon.service.dto.ReceivedRollingpapersResponseDto; +import com.woowacourse.naepyeon.service.dto.WrittenMessagesResponseDto; import javax.validation.Valid; import lombok.RequiredArgsConstructor; 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.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.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -24,14 +26,8 @@ public class MemberController { private final MemberService memberService; - - @PostMapping - public ResponseEntity createMember( - @RequestBody @Valid final MemberRegisterRequest memberRegisterRequest) { - memberService.save(memberRegisterRequest.getUsername(), memberRegisterRequest.getEmail(), - memberRegisterRequest.getPassword()); - return ResponseEntity.created(URI.create("/api/v1/members/me")).build(); - } + private final RollingpaperService rollingpaperService; + private final MessageService messageService; @GetMapping("/me") public ResponseEntity findMember( @@ -40,6 +36,28 @@ public ResponseEntity findMember( return ResponseEntity.ok().body(memberResponseDto); } + @GetMapping("/me/rollingpapers/received") + public ResponseEntity findReceivedRollingpapers( + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @RequestParam("page") final Integer page, + @RequestParam("count") final int count + ) { + return ResponseEntity.ok( + rollingpaperService.findReceivedRollingpapers(loginMemberRequest.getId(), page, count) + ); + } + + @GetMapping("/me/messages/written") + public ResponseEntity findWrittenMessages( + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @RequestParam("page") final Integer page, + @RequestParam("count") final int count + ) { + return ResponseEntity.ok( + messageService.findWrittenMessages(loginMemberRequest.getId(), page, count) + ); + } + @PutMapping("/me") public ResponseEntity updateMember( @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java index 53194f50..b243c44b 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/MessageController.java @@ -31,8 +31,9 @@ public class MessageController { public ResponseEntity createMessage(@AuthenticationPrincipal LoginMemberRequest loginMemberRequest, @RequestBody @Valid final MessageRequest messageRequest, @PathVariable final Long rollingpaperId) { - final Long messageId = - messageService.saveMessage(messageRequest.getContent(), loginMemberRequest.getId(), rollingpaperId); + final Long messageId = messageService.saveMessage( + messageRequest.getContent(), messageRequest.getColor(), rollingpaperId, loginMemberRequest.getId() + ); return ResponseEntity.created( URI.create("/api/v1/rollingpapers/" + rollingpaperId + "/messages/" + messageId) ).body(new CreateResponse(messageId)); @@ -48,12 +49,17 @@ public ResponseEntity findMessage( } @PutMapping("/{messageId}") - public ResponseEntity updateMessageContent( + public ResponseEntity updateMessage( @AuthenticationPrincipal LoginMemberRequest loginMemberRequest, @RequestBody @Valid final MessageUpdateContentRequest messageUpdateContentRequest, @PathVariable final Long rollingpaperId, @PathVariable final Long messageId) { - messageService.updateContent(messageId, messageUpdateContentRequest.getContent(), loginMemberRequest.getId()); + messageService.updateMessage( + messageId, + messageUpdateContentRequest.getContent(), + messageUpdateContentRequest.getColor(), + loginMemberRequest.getId() + ); return ResponseEntity.noContent().build(); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java index 898b147e..7f28f3a8 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/TeamController.java @@ -5,9 +5,11 @@ import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; import com.woowacourse.naepyeon.controller.dto.LoginMemberRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; +import com.woowacourse.naepyeon.controller.dto.UpdateTeamParticipantRequest; import com.woowacourse.naepyeon.exception.UncertificationTeamMemberException; import com.woowacourse.naepyeon.service.TeamService; import com.woowacourse.naepyeon.service.dto.JoinedMembersResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; import java.net.URI; @@ -21,6 +23,7 @@ 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.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -39,14 +42,20 @@ public ResponseEntity getTeam( @GetMapping("/me") public ResponseEntity getJoinedTeams( - @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest) { - return ResponseEntity.ok(teamService.findByJoinedMemberId(loginMemberRequest.getId())); + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @RequestParam("page") final Integer page, + @RequestParam("count") final int count) { + return ResponseEntity.ok(teamService.findByJoinedMemberId(loginMemberRequest.getId(), page, count)); } @GetMapping - public ResponseEntity getAllTeams( - @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest) { - return ResponseEntity.ok(teamService.findAll(loginMemberRequest.getId())); + public ResponseEntity getAllTeamsByKeyword( + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @RequestParam("keyword") final String keyword, @RequestParam("page") final Integer page, + @RequestParam("count") final int count) { + return ResponseEntity.ok( + teamService.findTeamsByContainingTeamName(keyword, loginMemberRequest.getId(), page, count) + ); } @GetMapping("/{teamId}/members") @@ -96,4 +105,21 @@ public ResponseEntity joinMember(@AuthenticationPrincipal @Valid final Log teamService.joinMember(teamId, loginMemberRequest.getId(), joinTeamMemberRequest.getNickname()); return ResponseEntity.noContent().build(); } + + @GetMapping("/{teamId}/me") + public ResponseEntity getMyInfoInTeam( + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @PathVariable final Long teamId) { + final TeamMemberResponseDto responseDto = teamService.findMyInfoInTeam(teamId, loginMemberRequest.getId()); + return ResponseEntity.ok(responseDto); + } + + @PutMapping("/{teamId}/me") + public ResponseEntity updateMyInfo( + @AuthenticationPrincipal @Valid final LoginMemberRequest loginMemberRequest, + @PathVariable final Long teamId, + @RequestBody final UpdateTeamParticipantRequest updateTeamParticipantRequest) { + teamService.updateMyInfo(teamId, loginMemberRequest.getId(), updateTeamParticipantRequest.getNickname()); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/auth/AuthController.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/auth/AuthController.java index 37065997..1022e781 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/auth/AuthController.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/auth/AuthController.java @@ -13,14 +13,15 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/login") +@RequestMapping("/api/v1/oauth") public class AuthController { private final AuthService authService; - @PostMapping - public ResponseEntity login(@RequestBody @Valid final TokenRequest tokenRequest) { - final TokenResponseDto tokenResponseDto = authService.createToken(tokenRequest.toServiceRequest()); + @PostMapping("/kakao") + public ResponseEntity kakaoLogin(@RequestBody @Valid final TokenRequest tokenRequest) { + final TokenResponseDto tokenResponseDto = + authService.createTokenWithKakaoOauth(tokenRequest.toServiceRequest()); return ResponseEntity.ok(tokenResponseDto); } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MemberRegisterRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MemberRegisterRequest.java deleted file mode 100644 index 8d63a9dd..00000000 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MemberRegisterRequest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.woowacourse.naepyeon.controller.dto; - -import javax.validation.constraints.NotBlank; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Getter -public class MemberRegisterRequest { - - @NotBlank(message = "3004:유저네임은 공백일 수 없습니다.") - private String username; - - @NotBlank(message = "3001:이메일은 공백일 수 없습니다.") - private String email; - - @NotBlank(message = "3007:비밀번호는 공백일 수 없습니다.") - private String password; -} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java index bd038400..ca449a61 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageRequest.java @@ -13,4 +13,6 @@ public class MessageRequest { @NotBlank(message = "2001:메시지는 공백일 수 없습니다.") private String content; + + private String color; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java index 934b39db..5d95e855 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/MessageUpdateContentRequest.java @@ -13,4 +13,6 @@ public class MessageUpdateContentRequest { @NotBlank(message = "2001:메시지는 공백일 수 없습니다.") private String content; + + private String color; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/TokenRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/TokenRequest.java index 3c745e7c..77f3b49f 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/TokenRequest.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/TokenRequest.java @@ -11,10 +11,10 @@ @Getter public class TokenRequest { - private String email; - private String password; + private String authorizationCode; + private String redirectUri; public TokenRequestDto toServiceRequest() { - return new TokenRequestDto(email, password); + return new TokenRequestDto(authorizationCode, redirectUri); } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/UpdateTeamParticipantRequest.java b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/UpdateTeamParticipantRequest.java new file mode 100644 index 00000000..20d7be4b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/controller/dto/UpdateTeamParticipantRequest.java @@ -0,0 +1,16 @@ +package com.woowacourse.naepyeon.controller.dto; + +import javax.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class UpdateTeamParticipantRequest { + + @NotBlank(message = "4009:닉네임은 공백일 수 없습니다.") + private String nickname; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java new file mode 100644 index 00000000..4b9a1b4c --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/BaseEntity.java @@ -0,0 +1,24 @@ +package com.woowacourse.naepyeon.domain; + +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +@Getter +abstract public class BaseEntity { + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + @Column(name = "last_modified_at", nullable = false) + private LocalDateTime lastModifiedDate; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java index 302930c5..c2a52b61 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Member.java @@ -1,55 +1,56 @@ package com.woowacourse.naepyeon.domain; -import com.woowacourse.naepyeon.exception.ExceedMemberPasswordLengthException; import com.woowacourse.naepyeon.exception.ExceedMemberUsernameLengthException; import com.woowacourse.naepyeon.exception.InvalidMemberEmailException; -import com.woowacourse.naepyeon.exception.InvalidMemberPasswordException; -import com.woowacourse.naepyeon.exception.InvalidMemberUsernameException; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter +@Table(name = "member") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Member { +public class Member extends BaseEntity { - public static final int MIN_USERNAME_LENGTH = 2; - public static final int MAX_USERNAME_LENGTH = 20; - public static final int MIN_PASSWORD_LENGTH = 8; - public static final int MAX_PASSWORD_LENGTH = 20; + public static final int MIN_USERNAME_LENGTH = 1; + public static final int MAX_USERNAME_LENGTH = 64; - private static final Pattern USER_PATTERN = Pattern.compile("^[가-힣a-zA-Z0-9]+$"); private static final Pattern EMAIL_PATTERN = Pattern.compile("^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\\w+\\.)+\\w+$"); - private static final Pattern PASSWORD_PATTERN = Pattern.compile( - "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d~!@#$%^&*()+|=]*$"); @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "member_id") private Long id; - @Column(length = 20, nullable = false) + @Column(name = "username", length = 255, nullable = false) private String username; - @Column(length = 255, nullable = false, unique = true) + @Column(name = "email", length = 255, nullable = false) private String email; - @Column(length = 255, nullable = false) - private String password; + @Enumerated(EnumType.STRING) + @Column(name = "platform", nullable = false) + private Platform platform; - public Member(final String username, final String email, final String password) { - validateMember(username, email, password); + @Column(name = "platform_id", nullable = false) + private String platformId; + + public Member(final String username, final String email, final Platform platform, final String platformId) { + validateMember(username, email); this.username = username; this.email = email; - this.password = password; + this.platform = platform; + this.platformId = platformId; } public void changeUsername(final String username) { @@ -57,24 +58,13 @@ public void changeUsername(final String username) { this.username = username; } - public void changePassword(final String newPassword) { - validatePassword(newPassword); - this.password = newPassword; - } - - public boolean checkPassword(final String password) { - return this.password.equals(password); - } - - private void validateMember(final String username, final String email, final String password) { + private void validateMember(final String username, final String email) { validateUsername(username); validateEmail(email); - validatePassword(password); } private void validateUsername(final String username) { validateUsernameSize(username); - validateUsernameRegex(username); } private void validateUsernameSize(final String username) { @@ -83,13 +73,6 @@ private void validateUsernameSize(final String username) { } } - private void validateUsernameRegex(final String username) { - final Matcher matcher = USER_PATTERN.matcher(username); - if (!matcher.matches()) { - throw new InvalidMemberUsernameException(username); - } - } - private void validateEmail(final String email) { final Matcher matcher = EMAIL_PATTERN.matcher(email); if (!matcher.matches()) { @@ -97,24 +80,6 @@ private void validateEmail(final String email) { } } - private void validatePassword(final String password) { - validatePasswordSize(password); - validatePasswordRegex(password); - } - - private void validatePasswordSize(final String password) { - if (password.length() < MIN_PASSWORD_LENGTH || password.length() > MAX_PASSWORD_LENGTH) { - throw new ExceedMemberPasswordLengthException(password); - } - } - - private void validatePasswordRegex(final String password) { - final Matcher matcher = PASSWORD_PATTERN.matcher(password); - if (!matcher.matches()) { - throw new InvalidMemberPasswordException(password); - } - } - public boolean isSameMember(final Long memberId) { return this.id.equals(memberId); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java index c44925fb..a639525b 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Message.java @@ -9,14 +9,16 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter +@Table(name = "message") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Message { +public class Message extends BaseEntity { public static final int MAX_CONTENT_LENGTH = 500; @@ -25,20 +27,24 @@ public class Message { @Column(name = "message_id") private Long id; - @Column(length = 500, nullable = false) + @Column(name = "content", length = 500, nullable = false) private String content; + @Column(name = "color") + private String color; + @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member author; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "rollingpaper_id") + @JoinColumn(name = "rollingpaper_id", nullable = false) private Rollingpaper rollingpaper; - public Message(final String content, final Member author, final Rollingpaper rollingpaper) { + public Message(final String content, final String color, final Member author, final Rollingpaper rollingpaper) { validateContentLength(content); this.content = content; + this.color = color; this.author = author; this.rollingpaper = rollingpaper; } @@ -48,6 +54,10 @@ public void changeContent(final String newContent) { this.content = newContent; } + public void changeColor(final String newColor) { + this.color = newColor; + } + private void validateContentLength(final String content) { if (content.length() > MAX_CONTENT_LENGTH) { throw new ExceedMessageContentLengthException(content); diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Platform.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Platform.java new file mode 100644 index 00000000..43aab003 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Platform.java @@ -0,0 +1,7 @@ +package com.woowacourse.naepyeon.domain; + +public enum Platform { + KAKAO, + NAVER, + GOOGLE +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java index 437bb42b..a898a34a 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Rollingpaper.java @@ -18,7 +18,7 @@ @Getter @Table(name = "rollingpaper") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Rollingpaper { +public class Rollingpaper extends BaseEntity { public static final int MAX_TITLE_LENGTH = 20; @@ -27,7 +27,7 @@ public class Rollingpaper { @Column(name = "rollingpaper_id") private Long id; - @Column(length = 20, nullable = false) + @Column(name = "title", length = 20, nullable = false) private String title; @ManyToOne(fetch = FetchType.LAZY) @@ -35,7 +35,7 @@ public class Rollingpaper { private Team team; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member member; public Rollingpaper(final String title, final Team team, final Member member) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java index 7463c00d..4183ffc9 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/Team.java @@ -6,14 +6,16 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter +@Table(name = "team") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Team { +public class Team extends BaseEntity { public static final int MAX_TEAMNAME_LENGTH = 20; @@ -25,13 +27,13 @@ public class Team { @Column(name = "team_name", length = 20, nullable = false, unique = true) private String name; - @Column(length = 100, nullable = false) + @Column(name = "description", length = 100, nullable = false) private String description; - @Column(nullable = false) + @Column(name = "emoji", nullable = false) private String emoji; - @Column(length = 15, nullable = false) + @Column(name = "color", length = 15, nullable = false) private String color; public Team(final String name, final String description, final String emoji, final String color) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java b/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java index ea992a26..8f802151 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/domain/TeamParticipation.java @@ -27,7 +27,7 @@ ) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class TeamParticipation { +public class TeamParticipation extends BaseEntity { public static final int MAX_NICKNAME_LENGTH = 20; @@ -37,14 +37,14 @@ public class TeamParticipation { private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "team_id") + @JoinColumn(name = "team_id", nullable = false) private Team team; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id") + @JoinColumn(name = "member_id", nullable = false) private Member member; - @Column(length = 20, nullable = false) + @Column(name = "nickname", length = 20, nullable = false) private String nickname; public TeamParticipation(final Team team, final Member member, final String nickname) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/exception/DuplicateNicknameException.java b/backend/src/main/java/com/woowacourse/naepyeon/exception/DuplicateNicknameException.java new file mode 100644 index 00000000..5d3b5446 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/exception/DuplicateNicknameException.java @@ -0,0 +1,15 @@ +package com.woowacourse.naepyeon.exception; + +import org.springframework.http.HttpStatus; + +public final class DuplicateNicknameException extends NaePyeonException { + + public DuplicateNicknameException(final String nickname) { + super( + String.format("이미 존재하는 닉네임입니다. teamName={%s}", nickname), + "이미 존재하는 닉네임입니다.", + HttpStatus.BAD_REQUEST, + "4014" + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/exception/ExceedMemberPasswordLengthException.java b/backend/src/main/java/com/woowacourse/naepyeon/exception/ExceedMemberPasswordLengthException.java deleted file mode 100644 index 0f003320..00000000 --- a/backend/src/main/java/com/woowacourse/naepyeon/exception/ExceedMemberPasswordLengthException.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.woowacourse.naepyeon.exception; - -import com.woowacourse.naepyeon.domain.Member; -import org.springframework.http.HttpStatus; - -public final class ExceedMemberPasswordLengthException extends NaePyeonException { - - public ExceedMemberPasswordLengthException(final String password) { - super( - String.format( - "비밀번호는 %d자 이상 %d자 이하여야 합니다. password={%s}", - Member.MIN_PASSWORD_LENGTH, - Member.MAX_USERNAME_LENGTH, - password - ), - String.format( - "비밀번호는 %d자 이상 %d자 이하여야 합니다.", - Member.MIN_PASSWORD_LENGTH, - Member.MAX_USERNAME_LENGTH - ), - HttpStatus.BAD_REQUEST, - "3008" - ); - } -} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/exception/InvalidLoginException.java b/backend/src/main/java/com/woowacourse/naepyeon/exception/InvalidLoginException.java index 316ba9c7..90b14224 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/exception/InvalidLoginException.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/exception/InvalidLoginException.java @@ -4,10 +4,14 @@ public final class InvalidLoginException extends NaePyeonException { - public InvalidLoginException(final String email) { + public InvalidLoginException(final String platformType, final Long platformId) { super( - String.format("이메일 정규식에 위반되는 이메일입니다. email = {%s}", email), - "이메일 또는 비밀번호가 일치하지 않습니다.", + String.format( + "존재하지 않는 유저의 토큰입니다. 다시 로그인해주세요. platformType = {%s}, platformId = {%d}", + platformType, + platformId + ), + "존재하지 않는 유저의 토큰입니다. 다시 로그인해주세요.", HttpStatus.UNAUTHORIZED, "3010" ); diff --git a/backend/src/main/java/com/woowacourse/naepyeon/exception/InvalidMemberPasswordException.java b/backend/src/main/java/com/woowacourse/naepyeon/exception/InvalidMemberPasswordException.java deleted file mode 100644 index 17f6294b..00000000 --- a/backend/src/main/java/com/woowacourse/naepyeon/exception/InvalidMemberPasswordException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.woowacourse.naepyeon.exception; - -import org.springframework.http.HttpStatus; - -public final class InvalidMemberPasswordException extends NaePyeonException { - - public InvalidMemberPasswordException(final String password) { - super( - String.format("비밀번호는 최소 하나 이상의 알파벳과 숫자로 구성해야 합니다. password={%s}", password), - "비밀번호는 최소 하나 이상의 알파벳과 숫자로 구성해야 합니다.", - HttpStatus.BAD_REQUEST, - "3009" - ); - } -} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/exception/KakaoAuthorizationException.java b/backend/src/main/java/com/woowacourse/naepyeon/exception/KakaoAuthorizationException.java new file mode 100644 index 00000000..78c8e29e --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/exception/KakaoAuthorizationException.java @@ -0,0 +1,18 @@ +package com.woowacourse.naepyeon.exception; + +import org.springframework.http.HttpStatus; + +public final class KakaoAuthorizationException extends NaePyeonException { + + public KakaoAuthorizationException(final RuntimeException e) { + super( + String.format( + "전달받은 authorizationCode나 redirectUri가 잘못 되었거나 카카오 서버 에러입니다. errorMessage={%s}", + e.getMessage() + ), + "카카오 로그인에 실패했습니다. 관리자에게 문의하세요.", + HttpStatus.BAD_REQUEST, + "3014" + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/exception/KakaoResourceException.java b/backend/src/main/java/com/woowacourse/naepyeon/exception/KakaoResourceException.java new file mode 100644 index 00000000..ed163d0e --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/exception/KakaoResourceException.java @@ -0,0 +1,19 @@ +package com.woowacourse.naepyeon.exception; + +import org.springframework.http.HttpStatus; + +public final class KakaoResourceException extends NaePyeonException { + + public KakaoResourceException(final RuntimeException e, final String accessToken) { + super( + String.format( + "카카오 리소스 서버 요청시 에러 발생 accessToken={%s}\nerrorMessage={%s}", + accessToken, + e.getMessage() + ), + "카카오 로그인에 실패했습니다. 관리자에게 문의하세요.", + HttpStatus.INTERNAL_SERVER_ERROR, + "3015" + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMemberRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMemberRepository.java index 64222e3e..e389b0a3 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMemberRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMemberRepository.java @@ -1,6 +1,7 @@ package com.woowacourse.naepyeon.repository; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; import com.woowacourse.naepyeon.repository.jpa.MemberJpaDao; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -31,4 +32,10 @@ public Optional findByEmail(final String email) { public void delete(final Long memberId) { memberJpaDao.deleteById(memberId); } + + @Override + public Optional findMemberIdByPlatformAndPlatformId(final Platform platform, final String platformId) { + return memberJpaDao.findByPlatformAndPlatformId(platform, platformId) + .map(Member::getId); + } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java index 04eb15bc..7e68137d 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaMessageRepository.java @@ -3,9 +3,12 @@ import com.woowacourse.naepyeon.domain.Message; import com.woowacourse.naepyeon.exception.NotFoundMessageException; import com.woowacourse.naepyeon.repository.jpa.MessageJpaDao; +import com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository @@ -31,10 +34,16 @@ public List findAllByRollingpaperId(final Long rollingpaperId) { } @Override - public void update(final Long id, final String newContent) { + public Page findAllByAuthorId(final Long authorId, final Pageable pageRequest) { + return messageJpaDao.findAllByAuthorId(authorId, pageRequest); + } + + @Override + public void update(final Long id, final String newColor, final String newContent) { final Message message = findById(id) .orElseThrow(() -> new NotFoundMessageException(id)); message.changeContent(newContent); + message.changeColor(newColor); } @Override diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaRollingpaperRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaRollingpaperRepository.java index 547ea527..2e016089 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaRollingpaperRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaRollingpaperRepository.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository @@ -30,6 +32,11 @@ public List findByMemberId(final Long memberId) { return rollingpaperJpaDao.findByMemberId(memberId); } + @Override + public Page findByMemberId(final Long memberId, final Pageable pageRequest) { + return rollingpaperJpaDao.findByMemberId(memberId, pageRequest); + } + @Override public List findByTeamId(final Long teamId) { return rollingpaperJpaDao.findByTeamId(teamId); diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java index 3219b057..0977df96 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamParticipationRepository.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.repository; +import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import com.woowacourse.naepyeon.exception.DuplicateTeamPaticipateException; @@ -8,6 +9,8 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository @@ -34,6 +37,12 @@ public Optional findById(final Long id) { return teamParticipationJpaDao.findById(id); } + @Override + public Optional findMemberByMemberIdAndTeamId(final Long memberId, final Long teamId) { + return teamParticipationJpaDao.findMemberByMemberIdAndTeamId(memberId, teamId); + + } + @Override public List findByTeamId(final Long teamId) { return teamParticipationJpaDao.findByTeamId(teamId); @@ -45,8 +54,18 @@ public List findTeamsByMemberId(final Long memberId) { } @Override - public String findNicknameByMemberId(final Long addresseeId, final Long teamId) { - return teamParticipationJpaDao.findNicknameByMemberIdAndTeamId(addresseeId, teamId); + public Page findTeamsByMemberIdAndPageRequest(final Long memberId, final Pageable pageRequest) { + return teamParticipationJpaDao.findTeamsByMemberIdAndPageRequest(memberId, pageRequest); + } + + @Override + public String findNicknameByMemberIdAndTeamId(final Long memberId, final Long teamId) { + return teamParticipationJpaDao.findNicknameByMemberIdAndTeamId(memberId, teamId); + } + + @Override + public List findAllNicknamesByTeamId(final Long teamId) { + return teamParticipationJpaDao.findNicknamesByTeamId(teamId); } @Override @@ -55,4 +74,9 @@ public boolean isJoinedMember(final Long memberId, final Long teamId) { return teams.stream() .anyMatch(team -> team.getId().equals(teamId)); } + + @Override + public void updateNickname(final String newNickname, final Long memberId, final Long teamId) { + teamParticipationJpaDao.updateNickname(newNickname, memberId, teamId); + } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamRepository.java index a50f2449..f551b1e4 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/JpaTeamRepository.java @@ -8,6 +8,8 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository @@ -30,6 +32,16 @@ public Optional findById(Long teamId) { return teamJpaDao.findById(teamId); } + @Override + public Page findTeamsByContainingTeamName(final String keyword, final Pageable pageRequest) { + return teamJpaDao.findByNameContaining(keyword, pageRequest); + } + + @Override + public List findAll() { + return teamJpaDao.findAll(); + } + @Override public void delete(Long teamId) { final int affectedRow = teamJpaDao.deleteByIdAndGetAffectedRow(teamId); @@ -38,9 +50,4 @@ public void delete(Long teamId) { throw new NotFoundTeamException(teamId); } } - - @Override - public List findAll() { - return teamJpaDao.findAll(); - } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/MemberRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/MemberRepository.java index 8aef7db6..88111f52 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/MemberRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/MemberRepository.java @@ -1,6 +1,7 @@ package com.woowacourse.naepyeon.repository; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; import java.util.Optional; public interface MemberRepository { @@ -12,4 +13,6 @@ public interface MemberRepository { Optional findByEmail(final String email); void delete(final Long memberId); + + Optional findMemberIdByPlatformAndPlatformId(final Platform platform, final String platformId); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java index 8f678ae9..b8aacc1b 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/MessageRepository.java @@ -1,8 +1,11 @@ package com.woowacourse.naepyeon.repository; import com.woowacourse.naepyeon.domain.Message; +import com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface MessageRepository { @@ -12,7 +15,9 @@ public interface MessageRepository { List findAllByRollingpaperId(final Long rollingpaperId); - void update(final Long id, final String newContent); + Page findAllByAuthorId(final Long memberId, final Pageable pageRequest); + + void update(final Long id, final String newColor, final String newContent); void delete(final Long id); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/RollingpaperRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/RollingpaperRepository.java index ab2a9955..5b4c04b1 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/RollingpaperRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/RollingpaperRepository.java @@ -3,6 +3,8 @@ import com.woowacourse.naepyeon.domain.Rollingpaper; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface RollingpaperRepository { @@ -12,6 +14,8 @@ public interface RollingpaperRepository { List findByMemberId(final Long memberId); + Page findByMemberId(final Long memberId, final Pageable pageRequest); + List findByTeamId(final Long teamId); void update(final Long rollingpaperId, final String newTitle); diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java index 25d037e3..357e2a6e 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamParticipationRepository.java @@ -1,9 +1,12 @@ package com.woowacourse.naepyeon.repository; +import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface TeamParticipationRepository { @@ -11,11 +14,19 @@ public interface TeamParticipationRepository { Optional findById(final Long id); + Optional findMemberByMemberIdAndTeamId(final Long memberId, final Long teamId); + List findByTeamId(final Long teamId); List findTeamsByMemberId(final Long memberId); - String findNicknameByMemberId(final Long addresseeId, final Long teamId); + Page findTeamsByMemberIdAndPageRequest(final Long memberId, final Pageable pageRequest); + + String findNicknameByMemberIdAndTeamId(final Long memberId, final Long teamId); + + List findAllNicknamesByTeamId(final Long teamId); boolean isJoinedMember(final Long memberId, final Long teamId); + + void updateNickname(final String newNickname, final Long memberId, final Long teamId); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamRepository.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamRepository.java index 02a8fb92..e3a6460e 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamRepository.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/TeamRepository.java @@ -3,6 +3,8 @@ import com.woowacourse.naepyeon.domain.Team; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface TeamRepository { @@ -10,7 +12,9 @@ public interface TeamRepository { Optional findById(final Long teamId); - void delete(final Long teamId); + Page findTeamsByContainingTeamName(final String keyword, final Pageable pageRequest); List findAll(); + + void delete(final Long teamId); } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MemberJpaDao.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MemberJpaDao.java index 3fa63b44..199b030f 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MemberJpaDao.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MemberJpaDao.java @@ -1,10 +1,13 @@ package com.woowacourse.naepyeon.repository.jpa; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface MemberJpaDao extends JpaRepository { Optional findByEmail(final String email); + + Optional findByPlatformAndPlatformId(final Platform platform, final String platformId); } \ No newline at end of file diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MessageJpaDao.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MessageJpaDao.java index 8b540e37..d9feeafe 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MessageJpaDao.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/MessageJpaDao.java @@ -1,10 +1,29 @@ package com.woowacourse.naepyeon.repository.jpa; import com.woowacourse.naepyeon.domain.Message; +import com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MessageJpaDao extends JpaRepository { List findByRollingpaperId(final Long rollingpaperId); + + @Query(value = "select new com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto" + + "(m.id, r.id, r.title, t.id, t.name, p.nickname, m.content, m.color) " + + "from Message m" + + ", Rollingpaper r" + + ", Team t" + + ", TeamParticipation p " + + "where m.rollingpaper.id = r.id " + + "and r.team.id = t.id " + + "and p.team.id = t.id " + + "and m.author.id = :authorId " + + "and p.member.id = r.member.id") + Page findAllByAuthorId( + @Param("authorId") final Long authorId, final Pageable pageRequest); } \ No newline at end of file diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/RollingpaperJpaDao.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/RollingpaperJpaDao.java index 47d85979..127b7708 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/RollingpaperJpaDao.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/RollingpaperJpaDao.java @@ -2,6 +2,8 @@ import com.woowacourse.naepyeon.domain.Rollingpaper; import java.util.List; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface RollingpaperJpaDao extends JpaRepository { @@ -9,4 +11,6 @@ public interface RollingpaperJpaDao extends JpaRepository { List findByTeamId(final Long teamId); List findByMemberId(final Long memberId); + + Page findByMemberId(final Long memberId, final Pageable pageRequest); } \ No newline at end of file diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamJpaDao.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamJpaDao.java index 5ffeda58..f089bbc3 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamJpaDao.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamJpaDao.java @@ -1,6 +1,8 @@ package com.woowacourse.naepyeon.repository.jpa; import com.woowacourse.naepyeon.domain.Team; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -11,4 +13,6 @@ public interface TeamJpaDao extends JpaRepository { @Query("delete from Team t where t.id = :id") @Modifying(clearAutomatically = true) int deleteByIdAndGetAffectedRow(@Param("id") final Long id); + + Page findByNameContaining(final String keyword, final Pageable pageRequest); } \ No newline at end of file diff --git a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java index 0bee6b47..d845aa77 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/repository/jpa/TeamParticipationJpaDao.java @@ -1,9 +1,14 @@ package com.woowacourse.naepyeon.repository.jpa; +import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -11,13 +16,39 @@ public interface TeamParticipationJpaDao extends JpaRepository findByTeamId(final Long teamId); - @Query("select p.team " + @Query("select t " + "from TeamParticipation p " + + "join p.team t " + "where p.member.id = :memberId") List findTeamsByMemberId(@Param("memberId") final Long memberId); + @Query("select t " + + "from TeamParticipation p " + + "join p.team t " + + "where p.member.id = :memberId") + Page findTeamsByMemberIdAndPageRequest(@Param("memberId") final Long memberId, final Pageable pageRequest); + + @Query("select p.member " + + "from TeamParticipation p " + + "join Member m on m.id = :memberId " + + "where p.team.id = :teamId") + Optional findMemberByMemberIdAndTeamId(@Param("memberId") final Long memberId, + @Param("teamId") final Long teamId); + @Query("select p.nickname " + "from TeamParticipation p " + "where p.member.id = :memberId and p.team.id = :teamId") String findNicknameByMemberIdAndTeamId(@Param("memberId") final Long memberId, @Param("teamId") final Long teamId); + + @Query("select p.nickname " + + "from TeamParticipation p " + + "where p.team.id = :teamId") + List findNicknamesByTeamId(@Param("teamId") final Long teamId); + + @Query("update TeamParticipation p " + + "set p.nickname = :newNickname " + + "where p.member.id = :memberId and p.team.id = :teamId") + @Modifying(clearAutomatically = true) + void updateNickname(@Param("newNickname") final String newNickname, + @Param("memberId") final Long memberId, @Param("teamId") final Long teamId); } \ No newline at end of file diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/AuthService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/AuthService.java index 21aeab54..7d220cd5 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/AuthService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/AuthService.java @@ -1,11 +1,10 @@ package com.woowacourse.naepyeon.service; -import com.woowacourse.naepyeon.domain.Member; -import com.woowacourse.naepyeon.exception.InvalidLoginException; -import com.woowacourse.naepyeon.repository.MemberRepository; +import com.woowacourse.naepyeon.service.dto.PlatformUserDto; import com.woowacourse.naepyeon.service.dto.TokenRequestDto; import com.woowacourse.naepyeon.service.dto.TokenResponseDto; import com.woowacourse.naepyeon.support.JwtTokenProvider; +import com.woowacourse.naepyeon.support.oauth.kakao.KakaoPlatformUserProvider; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,16 +14,34 @@ @Transactional public class AuthService { - private final MemberRepository memberRepository; + private final MemberService memberService; private final JwtTokenProvider jwtTokenProvider; + private final KakaoPlatformUserProvider kakaoPlatformUserProvider; - public TokenResponseDto createToken(final TokenRequestDto tokenRequestDto) { - final Member member = memberRepository.findByEmail(tokenRequestDto.getEmail()) - .orElseThrow(() -> new InvalidLoginException(tokenRequestDto.getEmail())); - if (!member.checkPassword(tokenRequestDto.getPassword())) { - throw new InvalidLoginException(tokenRequestDto.getEmail()); - } - final String accessToken = jwtTokenProvider.createToken(String.valueOf(member.getId())); - return new TokenResponseDto(accessToken); + public TokenResponseDto createTokenWithKakaoOauth(final TokenRequestDto tokenRequestDto) { + final PlatformUserDto platformUser = kakaoPlatformUserProvider.getPlatformUser( + tokenRequestDto.getAuthorizationCode(), + tokenRequestDto.getRedirectUri() + ); + + final Long memberId = createOrFindMemberId(platformUser); + final String accessToken = jwtTokenProvider.createToken(String.valueOf(memberId)); + return new TokenResponseDto(accessToken, memberId); + } + + private Long createOrFindMemberId(final PlatformUserDto platformUser) { + return memberService.findMemberIdByPlatformAndPlatformId( + platformUser.getPlatform(), + platformUser.getPlatformId() + ).orElseGet(() -> saveMember(platformUser)); + } + + private Long saveMember(final PlatformUserDto platformUser) { + return memberService.save( + platformUser.getUsername(), + platformUser.getEmail(), + platformUser.getPlatform(), + platformUser.getPlatformId() + ); } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/MemberService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/MemberService.java index 38223b27..087ebbe4 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/MemberService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/MemberService.java @@ -1,10 +1,11 @@ package com.woowacourse.naepyeon.service; import com.woowacourse.naepyeon.domain.Member; -import com.woowacourse.naepyeon.exception.DuplicateMemberEmailException; +import com.woowacourse.naepyeon.domain.Platform; import com.woowacourse.naepyeon.exception.NotFoundMemberException; import com.woowacourse.naepyeon.repository.MemberRepository; import com.woowacourse.naepyeon.service.dto.MemberResponseDto; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,12 +17,8 @@ public class MemberService { private final MemberRepository memberRepository; - public Long save(final String username, final String email, final String password) { - memberRepository.findByEmail(email) - .ifPresent(param -> { - throw new DuplicateMemberEmailException(); - }); - final Member member = new Member(username, email, password); + public Long save(final String username, final String email, final String platformType, final String platformId) { + final Member member = new Member(username, email, Platform.valueOf(platformType), platformId); return memberRepository.save(member); } @@ -30,6 +27,7 @@ public MemberResponseDto findById(final Long memberId) { final Member member = memberRepository.findById(memberId) .orElseThrow(() -> new NotFoundMemberException(memberId)); return new MemberResponseDto( + member.getId(), member.getUsername(), member.getEmail() ); @@ -46,4 +44,8 @@ public void delete(final Long memberId) { .orElseThrow(() -> new NotFoundMemberException(memberId)); memberRepository.delete(memberId); } + + public Optional findMemberIdByPlatformAndPlatformId(final String platform, final String platformId) { + return memberRepository.findMemberIdByPlatformAndPlatformId(Platform.valueOf(platform), platformId); + } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java index b2482559..9da3b0c9 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/MessageService.java @@ -13,9 +13,14 @@ import com.woowacourse.naepyeon.repository.RollingpaperRepository; import com.woowacourse.naepyeon.repository.TeamParticipationRepository; import com.woowacourse.naepyeon.service.dto.MessageResponseDto; +import com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto; +import com.woowacourse.naepyeon.service.dto.WrittenMessagesResponseDto; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,12 +34,12 @@ public class MessageService { private final MemberRepository memberRepository; private final TeamParticipationRepository teamParticipationRepository; - public Long saveMessage(final String content, final Long authorId, final Long rollingpaperId) { + public Long saveMessage(final String content, final String color, final Long rollingpaperId, final Long authorId) { final Rollingpaper rollingpaper = rollingpaperRepository.findById(rollingpaperId) .orElseThrow(() -> new NotFoundRollingpaperException(rollingpaperId)); final Member author = memberRepository.findById(authorId) .orElseThrow(() -> new NotFoundMemberException(authorId)); - final Message message = new Message(content, author, rollingpaper); + final Message message = new Message(content, color, author, rollingpaper); return messageRepository.save(message); } @@ -47,6 +52,7 @@ public List findMessages(final Long rollingpaperId, final Lo return new MessageResponseDto( message.getId(), message.getContent(), + message.getColor(), findMessageWriterNickname(teamId, message), author.getId() ); @@ -54,9 +60,22 @@ public List findMessages(final Long rollingpaperId, final Lo .collect(Collectors.toUnmodifiableList()); } + @Transactional(readOnly = true) + public WrittenMessagesResponseDto findWrittenMessages( + final Long loginMemberId, final Integer page, final int count) { + final Pageable pageRequest = PageRequest.of(page, count); + final Page writtenMessages = + messageRepository.findAllByAuthorId(loginMemberId, pageRequest); + return new WrittenMessagesResponseDto( + writtenMessages.getTotalElements(), + writtenMessages.getNumber(), + writtenMessages.getContent() + ); + } + private String findMessageWriterNickname(final Long teamId, final Message message) { final Member author = message.getAuthor(); - return teamParticipationRepository.findNicknameByMemberId(author.getId(), teamId); + return teamParticipationRepository.findNicknameByMemberIdAndTeamId(author.getId(), teamId); } @Transactional(readOnly = true) @@ -68,14 +87,15 @@ public MessageResponseDto findMessage(final Long messageId, final Long rollingpa final Team team = rollingpaper.getTeam(); final Member author = message.getAuthor(); final String nickname = findMessageWriterNickname(team.getId(), message); - return new MessageResponseDto(messageId, message.getContent(), nickname, author.getId()); + return new MessageResponseDto(messageId, message.getContent(), message.getColor(), nickname, author.getId()); } - public void updateContent(final Long messageId, final String newContent, final Long memberId) { + public void updateMessage(final Long messageId, final String newContent, final String newColor, + final Long memberId) { final Message message = messageRepository.findById(messageId) .orElseThrow(() -> new NotFoundMessageException(messageId)); validateAuthor(memberId, message); - messageRepository.update(messageId, newContent); + messageRepository.update(messageId, newColor, newContent); } public void deleteMessage(final Long messageId, final Long memberId) { diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java index 0fe81a1b..f239f44f 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/RollingpaperService.java @@ -12,12 +12,17 @@ import com.woowacourse.naepyeon.repository.RollingpaperRepository; import com.woowacourse.naepyeon.repository.TeamParticipationRepository; import com.woowacourse.naepyeon.repository.TeamRepository; +import com.woowacourse.naepyeon.service.dto.ReceivedRollingpaperResponseDto; +import com.woowacourse.naepyeon.service.dto.ReceivedRollingpapersResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpaperPreviewResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpaperResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpapersResponseDto; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -92,7 +97,22 @@ rollingpaper, findRollingpaperAddresseeNickname(rollingpaper, teamId)) } private String findRollingpaperAddresseeNickname(final Rollingpaper rollingpaper, final Long teamId) { - return teamParticipationRepository.findNicknameByMemberId(rollingpaper.getAddresseeId(), teamId); + return teamParticipationRepository.findNicknameByMemberIdAndTeamId(rollingpaper.getAddresseeId(), teamId); + } + + @Transactional(readOnly = true) + public ReceivedRollingpapersResponseDto findReceivedRollingpapers( + final Long loginMemberId, final Integer page, final int count) { + final Pageable pageRequest = PageRequest.of(page, count); + final Page rollingpapers = rollingpaperRepository.findByMemberId(loginMemberId, pageRequest); + final List receivedRollingpaperResponseDtos = rollingpapers.stream() + .map(it -> ReceivedRollingpaperResponseDto.of(it.getId(), it.getTitle(), it.getTeam())) + .collect(Collectors.toUnmodifiableList()); + return new ReceivedRollingpapersResponseDto( + rollingpapers.getTotalElements(), + rollingpapers.getNumber(), + receivedRollingpaperResponseDtos + ); } public void updateTitle(final Long rollingpaperId, final String newTitle, final Long teamId, diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java b/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java index 8ba14f45..2c583f6d 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/TeamService.java @@ -4,18 +4,25 @@ import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; +import com.woowacourse.naepyeon.exception.DuplicateNicknameException; import com.woowacourse.naepyeon.exception.NotFoundMemberException; import com.woowacourse.naepyeon.exception.NotFoundTeamException; +import com.woowacourse.naepyeon.exception.UncertificationTeamMemberException; import com.woowacourse.naepyeon.repository.MemberRepository; import com.woowacourse.naepyeon.repository.TeamParticipationRepository; import com.woowacourse.naepyeon.repository.TeamRepository; +import com.woowacourse.naepyeon.service.dto.AllTeamsResponseDto; import com.woowacourse.naepyeon.service.dto.JoinedMemberResponseDto; import com.woowacourse.naepyeon.service.dto.JoinedMembersResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -71,7 +78,24 @@ public void delete(final Long teamId) { teamRepository.delete(teamId); } - public TeamsResponseDto findAll(final Long memberId) { + public TeamsResponseDto findTeamsByContainingTeamName(final String keyword, final Long memberId, + final Integer page, final int count) { + final Pageable pageRequest = PageRequest.of(page, count); + final Page teams = teamRepository.findTeamsByContainingTeamName(keyword, pageRequest); + final List joinedTeams = teamParticipationRepository.findTeamsByMemberId(memberId); + + final List teamResponseDtos = teams.stream() + .map(team -> TeamResponseDto.of(team, joinedTeams.contains(team))) + .collect(Collectors.toList()); + + return new TeamsResponseDto( + teams.getTotalElements(), + teams.getNumber(), + teamResponseDtos + ); + } + + public AllTeamsResponseDto findAll(final Long memberId) { final List teams = teamRepository.findAll(); final List joinedTeams = teamParticipationRepository.findTeamsByMemberId(memberId); @@ -79,16 +103,21 @@ public TeamsResponseDto findAll(final Long memberId) { .map(team -> TeamResponseDto.of(team, joinedTeams.contains(team))) .collect(Collectors.toList()); - return new TeamsResponseDto(teamResponseDtos); + return new AllTeamsResponseDto(teamResponseDtos); } - public TeamsResponseDto findByJoinedMemberId(final Long memberId) { - final List teams = teamParticipationRepository.findTeamsByMemberId(memberId); + public TeamsResponseDto findByJoinedMemberId(final Long memberId, final Integer page, final int count) { + final Pageable pageRequest = PageRequest.of(page, count); + final Page teams = teamParticipationRepository.findTeamsByMemberIdAndPageRequest(memberId, pageRequest); final List teamResponseDtos = teams.stream() .map(team -> TeamResponseDto.of(team, true)) .collect(Collectors.toList()); - return new TeamsResponseDto(teamResponseDtos); + return new TeamsResponseDto( + teams.getTotalElements(), + teams.getNumber(), + teamResponseDtos + ); } public JoinedMembersResponseDto findJoinedMembers(final Long teamId) { @@ -105,6 +134,27 @@ public JoinedMembersResponseDto findJoinedMembers(final Long teamId) { return new JoinedMembersResponseDto(joinedMembers); } + public TeamMemberResponseDto findMyInfoInTeam(final Long teamId, final Long memberId) { + checkMemberNotIncludedTeam(teamId, memberId); + final String nickname = teamParticipationRepository.findNicknameByMemberIdAndTeamId(memberId, teamId); + return new TeamMemberResponseDto(nickname); + } + + @Transactional + public void updateMyInfo(final Long teamId, final Long memberId, final String newNickname) { + checkMemberNotIncludedTeam(teamId, memberId); + if (teamParticipationRepository.findAllNicknamesByTeamId(teamId).contains(newNickname)) { + throw new DuplicateNicknameException(newNickname); + } + teamParticipationRepository.updateNickname(newNickname, memberId, teamId); + } + + private void checkMemberNotIncludedTeam(final Long teamId, final Long memberId) { + if (!isJoinedMember(memberId, teamId)) { + throw new UncertificationTeamMemberException(teamId, memberId); + } + } + public boolean isJoinedMember(final Long memberId, final Long teamId) { if (teamRepository.findById(teamId).isEmpty()) { throw new NotFoundTeamException(teamId); diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/AllTeamsResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/AllTeamsResponseDto.java new file mode 100644 index 00000000..75a81ac8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/AllTeamsResponseDto.java @@ -0,0 +1,15 @@ +package com.woowacourse.naepyeon.service.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class AllTeamsResponseDto { + + private List teams; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MemberResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MemberResponseDto.java index c95849f3..4cfc966c 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MemberResponseDto.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MemberResponseDto.java @@ -10,6 +10,7 @@ @AllArgsConstructor public class MemberResponseDto { + private Long id; private String username; private String email; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java index bc5c391d..aa4903c4 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/MessageResponseDto.java @@ -12,6 +12,7 @@ public class MessageResponseDto { private Long id; private String content; + private String color; private String from; private Long authorId; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/PlatformUserDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/PlatformUserDto.java new file mode 100644 index 00000000..bf8089fc --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/PlatformUserDto.java @@ -0,0 +1,17 @@ +package com.woowacourse.naepyeon.service.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class PlatformUserDto { + + private String username; + private String email; + private String platform; + private String platformId; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/ReceivedRollingpaperResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/ReceivedRollingpaperResponseDto.java new file mode 100644 index 00000000..c07b35e2 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/ReceivedRollingpaperResponseDto.java @@ -0,0 +1,22 @@ +package com.woowacourse.naepyeon.service.dto; + +import com.woowacourse.naepyeon.domain.Team; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ReceivedRollingpaperResponseDto { + + private Long id; + private String title; + private Long teamId; + private String teamName; + + public static ReceivedRollingpaperResponseDto of(final Long id, final String title, final Team team) { + return new ReceivedRollingpaperResponseDto(id, title, team.getId(), team.getName()); + } +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/ReceivedRollingpapersResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/ReceivedRollingpapersResponseDto.java new file mode 100644 index 00000000..4dd3bb0c --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/ReceivedRollingpapersResponseDto.java @@ -0,0 +1,17 @@ +package com.woowacourse.naepyeon.service.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class ReceivedRollingpapersResponseDto { + + private Long totalCount; + private int currentPage; + private List rollingpapers; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamMemberResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamMemberResponseDto.java new file mode 100644 index 00000000..dbff9565 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamMemberResponseDto.java @@ -0,0 +1,14 @@ +package com.woowacourse.naepyeon.service.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class TeamMemberResponseDto { + + private String nickname; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamResponseDto.java index 86fea98d..76ca6ffe 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamResponseDto.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamResponseDto.java @@ -1,5 +1,6 @@ package com.woowacourse.naepyeon.service.dto; +import com.woowacourse.naepyeon.controller.dto.TeamRequest; import com.woowacourse.naepyeon.domain.Team; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -28,4 +29,15 @@ public static TeamResponseDto of(final Team team, final boolean joined) { joined ); } + + public static TeamResponseDto byRequest(final Long teamId, final TeamRequest teamRequest, final boolean joined) { + return new TeamResponseDto( + teamId, + teamRequest.getName(), + teamRequest.getDescription(), + teamRequest.getEmoji(), + teamRequest.getColor(), + joined + ); + } } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamsResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamsResponseDto.java index c872900e..119ea769 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamsResponseDto.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TeamsResponseDto.java @@ -11,5 +11,7 @@ @AllArgsConstructor public class TeamsResponseDto { + private Long totalCount; + private int currentPage; private List teams; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenRequestDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenRequestDto.java index f37b9f99..01e54be3 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenRequestDto.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenRequestDto.java @@ -10,6 +10,6 @@ @Getter public class TokenRequestDto { - private String email; - private String password; + private String authorizationCode; + private String redirectUri; } \ No newline at end of file diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenResponseDto.java index 3adecdfe..7a154d5b 100644 --- a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenResponseDto.java +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/TokenResponseDto.java @@ -11,4 +11,5 @@ public class TokenResponseDto { private String accessToken; + private Long id; } diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/WrittenMessageResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/WrittenMessageResponseDto.java new file mode 100644 index 00000000..f20a387b --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/WrittenMessageResponseDto.java @@ -0,0 +1,40 @@ +package com.woowacourse.naepyeon.service.dto; + +import com.woowacourse.naepyeon.domain.Message; +import com.woowacourse.naepyeon.domain.Rollingpaper; +import com.woowacourse.naepyeon.domain.Team; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class WrittenMessageResponseDto { + + private Long id; + private Long rollingpaperId; + private String rollingpaperTitle; + private Long teamId; + private String teamName; + private String to; + private String content; + private String color; + + public static WrittenMessageResponseDto of( + final Rollingpaper rollingpaper, final Team team, + final String addresseeNickName, final Message message + ) { + return new WrittenMessageResponseDto( + message.getId(), + rollingpaper.getId(), + rollingpaper.getTitle(), + team.getId(), + team.getName(), + addresseeNickName, + message.getContent(), + message.getColor() + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/service/dto/WrittenMessagesResponseDto.java b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/WrittenMessagesResponseDto.java new file mode 100644 index 00000000..160feaea --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/service/dto/WrittenMessagesResponseDto.java @@ -0,0 +1,17 @@ +package com.woowacourse.naepyeon.service.dto; + +import java.util.List; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Getter +public class WrittenMessagesResponseDto { + + private Long totalCount; + private int currentPage; + private List messages; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/KakaoPlatformUserProvider.java b/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/KakaoPlatformUserProvider.java new file mode 100644 index 00000000..53a24fd9 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/KakaoPlatformUserProvider.java @@ -0,0 +1,91 @@ +package com.woowacourse.naepyeon.support.oauth.kakao; + +import com.woowacourse.naepyeon.domain.Platform; +import com.woowacourse.naepyeon.exception.KakaoAuthorizationException; +import com.woowacourse.naepyeon.exception.KakaoResourceException; +import com.woowacourse.naepyeon.service.dto.PlatformUserDto; +import com.woowacourse.naepyeon.support.oauth.kakao.dto.AccessTokenResponse; +import com.woowacourse.naepyeon.support.oauth.kakao.dto.KakaoUserResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +@Component +public class KakaoPlatformUserProvider { + + private static final String AUTHORIZATION_SERVER_BASE_URL = "https://kauth.kakao.com"; + private static final String RESOURCE_SERVER_BASE_URL = "https://kapi.kakao.com"; + private static final String ACCESS_TOKEN_URI = "/oauth/token"; + private static final String USER_INFO_URI = "/v2/user/me"; + + private final WebClient authorizationWebClient = WebClient.builder() + .baseUrl(AUTHORIZATION_SERVER_BASE_URL) + .build(); + + private final WebClient resourceWebClient = WebClient.builder() + .baseUrl(RESOURCE_SERVER_BASE_URL) + .build(); + + private final String kakaoAdminKey; + private final String kakaoClientId; + private final String kakaoClientSecret; + + public KakaoPlatformUserProvider( + @Value("${kakao.admin-key}") final String kakaoAdminKey, + @Value("${kakao.client-id}") final String kakaoClientId, + @Value("${kakao.client-secret}") final String kakaoClientSecret + ) { + this.kakaoAdminKey = kakaoAdminKey; + this.kakaoClientId = kakaoClientId; + this.kakaoClientSecret = kakaoClientSecret; + } + + public PlatformUserDto getPlatformUser(final String authorizationCode, final String redirectUri) { + final AccessTokenResponse accessTokenResponse = requestAccessToken(authorizationCode, redirectUri); + final KakaoUserResponse kakaoUserResponse = requestPlatformUser(accessTokenResponse.getAccess_token()); + return new PlatformUserDto( + kakaoUserResponse.getNickname(), + kakaoUserResponse.getEmail(), + Platform.KAKAO.name(), + String.valueOf(kakaoUserResponse.getId()) + ); + } + + private AccessTokenResponse requestAccessToken(final String authorizationCode, final String redirectUri) { + try { + return authorizationWebClient.post() + .uri(uriBuilder -> uriBuilder.path(ACCESS_TOKEN_URI) + .queryParam("grant_type", "authorization_code") + .queryParam("client_id", kakaoClientId) + .queryParam("redirect_uri", redirectUri) + .queryParam("code", authorizationCode) + .queryParam("client_secret", kakaoClientSecret) + .build() + ).contentType(MediaType.APPLICATION_FORM_URLENCODED) + .retrieve() + .bodyToMono(AccessTokenResponse.class) + .block(); + } catch (final RuntimeException e) { + throw new KakaoAuthorizationException(e); + } + } + + private KakaoUserResponse requestPlatformUser(final String accessToken) { + try { + return resourceWebClient.post() + .uri(uriBuilder -> uriBuilder.path(USER_INFO_URI) + .queryParam("secure_resource", "true") + .queryParam("property_keys", "[\"kakao_account.profile\",\"kakao_account.email\"]") + .build() + ).header(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken)) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .retrieve() + .bodyToMono(KakaoUserResponse.class) + .block(); + } catch (final RuntimeException e) { + throw new KakaoResourceException(e, accessToken); + } + } +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/dto/AccessTokenResponse.java b/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/dto/AccessTokenResponse.java new file mode 100644 index 00000000..ddbd0316 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/dto/AccessTokenResponse.java @@ -0,0 +1,14 @@ +package com.woowacourse.naepyeon.support.oauth.kakao.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class AccessTokenResponse { + + private String access_token; +} diff --git a/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/dto/KakaoUserResponse.java b/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/dto/KakaoUserResponse.java new file mode 100644 index 00000000..ede67ae1 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/naepyeon/support/oauth/kakao/dto/KakaoUserResponse.java @@ -0,0 +1,42 @@ +package com.woowacourse.naepyeon.support.oauth.kakao.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class KakaoUserResponse { + + private Long id; + private KakaoAccount kakao_account; + + public String getNickname() { + return kakao_account.getProfile() + .getNickname(); + } + + public String getEmail() { + return kakao_account.getEmail(); + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + private static class KakaoAccount { + + private String email; + private KakaoProfile profile; + } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + private static class KakaoProfile { + + private String nickname; + private String profile_image_url; + } +} diff --git a/backend/src/main/resources/application-deploy.yml b/backend/src/main/resources/application-deploy.yml index 76475a8a..1a80bd15 100644 --- a/backend/src/main/resources/application-deploy.yml +++ b/backend/src/main/resources/application-deploy.yml @@ -1,28 +1,35 @@ spring: + datasource: + url: ${MYSQL_URL} + password: ${MYSQL_PASSWORD} + username: ${MYSQL_USERNAME} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: validate properties: hibernate: - default_batch_fetch_size: '1000' + format_sql: true + default_batch_fetch_size: 1000 + jdbc.lob.non_contextual_creation: true dialect: org.hibernate.dialect.MySQL5Dialect - format_sql: 'true' - hibernate: - ddl-auto: validate - open-in-view: 'false' - datasource: - password: ${MYSQL_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver - username: ${MYSQL_USERNAME} - url: ${MYSQL_URL} - h2: - console: - enabled: 'true' + open-in-view: false + generate-ddl: false + flyway: + enabled: true + baseline-on-migrate: true + security: jwt: token: expire-length: ${JWT_EXPIRE_LENGTH} secret-key: ${JWT_SECRET_KEY} + +kakao: + admin-key: ${KAKAO_ADMIN_KEY} + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + logging: - level: - org: - hibernate: - SQL: info + config: classpath:log4j2-develop.yml \ No newline at end of file diff --git a/backend/src/main/resources/application-flytest.yml b/backend/src/main/resources/application-flytest.yml new file mode 100644 index 00000000..bc2c1adb --- /dev/null +++ b/backend/src/main/resources/application-flytest.yml @@ -0,0 +1,34 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;MODE=MYSQL + username: sa + password: + + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 #최적화 옵션 + jdbc.lob.non_contextual_creation: true + dialect: org.hibernate.dialect.MySQL5Dialect + open-in-view: false + generate-ddl: false + flyway: + enabled: true + baseline-on-migrate: true + +logging.level: + org.hibernate.SQL: debug + +security: + jwt: + token: + secret-key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno + expire-length: 3600000 + +kakao: + admin-key: ${KAKAO_ADMIN_KEY} + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 7dca6554..930ab455 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -2,11 +2,8 @@ spring: h2: console: enabled: true - profiles: - active: local datasource: - # url: jdbc:h2:tc가p://localhost/~/naepyeon - url: jdbc:h2:mem:testdb + url: jdbc:h2:mem:testdb;MODE=MYSQL username: sa password: driver-class-name: org.h2.Driver @@ -16,17 +13,24 @@ spring: ddl-auto: create properties: hibernate: - # show_sql: true format_sql: true default_batch_fetch_size: 1000 #최적화 옵션 + dialect: org.hibernate.dialect.MySQL5Dialect open-in-view: false - -logging.level: - org.hibernate.SQL: debug -# org.hibernate.type: trace + flyway: + enabled: false + baseline-on-migrate: true security: jwt: token: secret-key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno - expire-length: 3600000 \ No newline at end of file + expire-length: 3600000 + +kakao: + admin-key: ${KAKAO_ADMIN_KEY} + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + +logging: + config: classpath:log4j2.yml \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 00000000..0e4380d6 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,89 @@ +CREATE TABLE IF NOT EXISTS message +( + message_id bigint not null auto_increment, + content varchar(500) not null, + member_id bigint, + rollingpaper_id bigint, + primary key (message_id) +) engine=InnoDB + default charset utf8mb4; + +CREATE TABLE IF NOT EXISTS rollingpaper +( + rollingpaper_id bigint not null auto_increment, + title varchar(20) not null, + member_id bigint, + team_id bigint not null, + primary key (rollingpaper_id) +) engine=InnoDB + default charset utf8mb4; + +CREATE TABLE IF NOT EXISTS team_member +( + team_member_id bigint not null auto_increment, + nickname varchar(20) not null, + member_id bigint, + team_id bigint, + primary key (team_member_id) +) engine=InnoDB + default charset utf8mb4; + +CREATE TABLE IF NOT EXISTS team +( + team_id bigint not null auto_increment, + color varchar(15) not null, + description varchar(100) not null, + emoji varchar(255) not null, + team_name varchar(20) not null, + primary key (team_id) + ) engine=InnoDB + default charset utf8mb4; + +CREATE TABLE IF NOT EXISTS member +( + member_id bigint not null auto_increment, + email varchar(255) not null, + password varchar(255) not null, + username varchar(20) not null, + primary key (member_id) + ) engine=InnoDB + default charset utf8mb4; + +alter table member + add constraint uk_member_email unique (email); + +alter table team + add constraint uk_team_name unique (team_name); + +alter table team_member + add constraint uk_participate_duplicate unique (team_id, member_id); + +alter table message + add constraint fk_message_member_id + foreign key (member_id) + references member (member_id); + +alter table message + add constraint fk_message_rollingpaper_id + foreign key (rollingpaper_id) + references rollingpaper (rollingpaper_id); + +alter table rollingpaper + add constraint fk_rollingpaper_member_id + foreign key (member_id) + references member (member_id); + +alter table rollingpaper + add constraint fk_rollingpaper_team_id + foreign key (team_id) + references team (team_id); + +alter table team_member + add constraint fk_team_member_member_id + foreign key (member_id) + references member (member_id); + +alter table team_member + add constraint fk_team_member_team_id + foreign key (team_id) + references team (team_id); \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V2__add_baseEntity.sql b/backend/src/main/resources/db/migration/V2__add_baseEntity.sql new file mode 100644 index 00000000..6211131b --- /dev/null +++ b/backend/src/main/resources/db/migration/V2__add_baseEntity.sql @@ -0,0 +1,10 @@ +ALTER TABLE member ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE member ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE message ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE message ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE rollingpaper ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE rollingpaper ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE team ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE team ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; +ALTER TABLE team_member ADD COLUMN created_at datetime not null DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE team_member ADD COLUMN last_modified_at datetime not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V3__add_message_color.sql b/backend/src/main/resources/db/migration/V3__add_message_color.sql new file mode 100644 index 00000000..6912bc0f --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__add_message_color.sql @@ -0,0 +1 @@ +ALTER TABLE message ADD COLUMN color varchar(255) DEFAULT '#fff598'; \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V4__update_member.sql b/backend/src/main/resources/db/migration/V4__update_member.sql new file mode 100644 index 00000000..538a9221 --- /dev/null +++ b/backend/src/main/resources/db/migration/V4__update_member.sql @@ -0,0 +1,5 @@ +ALTER TABLE member ADD COLUMN platform varchar(255) not null; +ALTER TABLE member ADD COLUMN platform_id varchar(255) not null; +ALTER TABLE member modify COLUMN username varchar(255) not null; +ALTER TABLE member DROP COLUMN password; +ALTER TABLE member DROP CONSTRAINT uk_member_email; \ No newline at end of file diff --git a/backend/src/main/resources/log4j2-develop.yml b/backend/src/main/resources/log4j2-develop.yml new file mode 100644 index 00000000..1191e700 --- /dev/null +++ b/backend/src/main/resources/log4j2-develop.yml @@ -0,0 +1,51 @@ +Configutation: + name: Default + status: warn + + Properties: + Property: + name: log-path + value: "logs" + + Appenders: + Console: + name: Console_Appender + target: SYSTEM_OUT + PatternLayout: + pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t][%F] %c{1} - %msg%n" + File: + name: File_Appender + fileName: ${log-path}/logfile.log + PatternLayout: + pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t][%F] %c{1} - %msg%n" + RollingFile: + - name: RollingFile_Appender + fileName: ${log-path}/rollingfile.log + filePattern: "${log-path}/archive/rollingfile.log_%d{yyyy-MM-dd}.gz" + PatternLayout: + pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t][%F] %c{1} - %msg%n" + Policies: + TimeBasedTriggeringPolicy: + Interval: 1 + modulate: true + DefaultRollOverStrategy: + Delete: + basePath: "${log-path}/archive" + maxDepth: "1" + IfAccumulatedFileCount: + exceeds: 31 + Loggers: + Root: + level: info + AppenderRef: + - ref: Console_Appender + - ref: File_Appender + - ref: RollingFile_Appender + Logger: + - name: com.woowacourse.naepyeon + additivity: false + level: info + AppenderRef: + - ref: Console_Appender + - ref: File_Appender + - ref: RollingFile_Appender \ No newline at end of file diff --git a/backend/src/main/resources/log4j2.yml b/backend/src/main/resources/log4j2.yml new file mode 100644 index 00000000..a848c8ba --- /dev/null +++ b/backend/src/main/resources/log4j2.yml @@ -0,0 +1,51 @@ +Configutation: + name: Default + status: debug + + Properties: + Property: + name: log-path + value: "logs" + + Appenders: + Console: + name: Console_Appender + target: SYSTEM_OUT + PatternLayout: + pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t][%F] %c{1} - %msg%n" + File: + name: File_Appender + fileName: ${log-path}/logfile.log + PatternLayout: + pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t][%F] %c{1} - %msg%n" + RollingFile: + - name: RollingFile_Appender + fileName: ${log-path}/rollingfile.log + filePattern: "${log-path}/archive/rollingfile.log_%d{yyyy-MM-dd}.gz" + PatternLayout: + pattern: "[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t][%F] %c{1} - %msg%n" + Policies: + TimeBasedTriggeringPolicy: + Interval: 1 + modulate: true + DefaultRollOverStrategy: + Delete: + basePath: "${log-path}/archive" + maxDepth: "1" + IfAccumulatedFileCount: + exceeds: 31 + Loggers: + Root: + level: debug + AppenderRef: + - ref: Console_Appender + - ref: File_Appender + - ref: RollingFile_Appender + Logger: + - name: com.woowacourse.naepyeon + additivity: false + level: debug + AppenderRef: + - ref: Console_Appender + - ref: File_Appender + - ref: RollingFile_Appender \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java index 2ddfbb3e..96188ea9 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceFixture.java @@ -2,7 +2,6 @@ import com.woowacourse.naepyeon.controller.dto.CreateResponse; import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; -import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; import com.woowacourse.naepyeon.controller.dto.MemberUpdateRequest; import com.woowacourse.naepyeon.controller.dto.MessageRequest; import com.woowacourse.naepyeon.controller.dto.MessageUpdateContentRequest; @@ -10,6 +9,7 @@ import com.woowacourse.naepyeon.controller.dto.RollingpaperUpdateRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; import com.woowacourse.naepyeon.controller.dto.TokenRequest; +import com.woowacourse.naepyeon.controller.dto.UpdateTeamParticipantRequest; import com.woowacourse.naepyeon.service.dto.TokenResponseDto; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; @@ -48,6 +48,20 @@ public static ExtractableResponse get(final TokenResponseDto tokenResp .extract(); } + public static ExtractableResponse get_search( + final TokenResponseDto tokenResponseDto, final String uri, + final String keyword, final int page, final int count) { + return RestAssured.given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .auth().oauth2(tokenResponseDto.getAccessToken()) + .queryParam("keyword", keyword) + .queryParam("page", page) + .queryParam("count", count) + .when().get(uri) + .then().log().all() + .extract(); + } + public static ExtractableResponse put(final TokenResponseDto tokenResponseDto, final Object body, final String uri) { return RestAssured.given().log().all() @@ -68,9 +82,8 @@ public static ExtractableResponse delete(final TokenResponseDto tokenR .extract(); } - public static TokenResponseDto 회원가입_후_로그인(final MemberRegisterRequest member) { - 회원_추가(member); - return 로그인_응답(new TokenRequest(member.getEmail(), member.getPassword())) + public static TokenResponseDto 회원가입_후_로그인(final TokenRequest tokenRequest) { + return 로그인_응답(tokenRequest) .as(TokenResponseDto.class); } @@ -86,10 +99,6 @@ public static ExtractableResponse delete(final TokenResponseDto tokenR .getId(); } - public static ExtractableResponse 회원_추가(final MemberRegisterRequest member) { - return member_post(member, "/api/v1/members"); - } - public static ExtractableResponse 회원_조회(final TokenResponseDto tokenResponseDto) { return get(tokenResponseDto, "/api/v1/members/me"); } @@ -132,27 +141,45 @@ public static ExtractableResponse delete(final TokenResponseDto tokenR return get(tokenResponseDto, "/api/v1/teams/" + teamId); } - public static ExtractableResponse 모든_모임_조회(final TokenResponseDto tokenResponseDto) { - return get(tokenResponseDto, "/api/v1/teams"); + public static ExtractableResponse 키워드로_모든_모임_조회( + final TokenResponseDto tokenResponseDto, final String keyword, final int page, final int count) { + return get_search(tokenResponseDto, "/api/v1/teams", keyword, page, count); } - public static ExtractableResponse 가입한_모임_조회(final TokenResponseDto tokenResponseDto) { - return get(tokenResponseDto, "/api/v1/teams/me"); + public static ExtractableResponse 가입한_모임_조회( + final TokenResponseDto tokenResponseDto, final int page, final int count) { + return get_search(tokenResponseDto, "/api/v1/teams/me", "", page, count); } - public static ExtractableResponse 모임에_가입한_회원_조회(final TokenResponseDto tokenResponseDto, - final Long teamId) { + public static ExtractableResponse 모임에_가입한_회원_목록_조회(final TokenResponseDto tokenResponseDto, + final Long teamId) { return get(tokenResponseDto, "/api/v1/teams/" + teamId + "/members"); } + public static ExtractableResponse 모임_가입_정보_조회(final TokenResponseDto tokenResponseDto, + final Long teamId) { + return get(tokenResponseDto, "/api/v1/teams/" + teamId + "/me"); + } + + public static ExtractableResponse 모임_내_닉네임_변경(final TokenResponseDto tokenResponseDto, + final Long teamId, + final UpdateTeamParticipantRequest updateTeamParticipantRequest) { + return put(tokenResponseDto, updateTeamParticipantRequest, "/api/v1/teams/" + teamId + "/me"); + } + public static ExtractableResponse 회원_롤링페이퍼_생성(final TokenResponseDto tokenResponseDto, final Long teamId, final RollingpaperCreateRequest rollingpaperCreateRequest) { return post(tokenResponseDto, rollingpaperCreateRequest, "/api/v1/teams/" + teamId + "/rollingpapers"); } - public static ExtractableResponse 나의_롤링페이퍼_조회(final TokenResponseDto tokenResponseDto, - final Long teamId) { + public static ExtractableResponse 나의_롤링페이퍼_조회( + final TokenResponseDto tokenResponseDto, final int page, final int count) { + return get_search(tokenResponseDto, "/api/v1/members/me/rollingpapers/received", "", page, count); + } + + public static ExtractableResponse 회원의_롤링페이퍼_조회(final TokenResponseDto tokenResponseDto, + final Long teamId) { return get(tokenResponseDto, "/api/v1/teams/" + teamId + "/rollingpapers/me"); } @@ -193,8 +220,13 @@ public static ExtractableResponse delete(final TokenResponseDto tokenR } public static ExtractableResponse 메시지_조회(final TokenResponseDto tokenResponseDto, - final Long rollingpaperId, - final Long messageId) { + final Long rollingpaperId, + final Long messageId) { return get(tokenResponseDto, "/api/v1/rollingpapers/" + rollingpaperId + "/messages/" + messageId); } + + public static ExtractableResponse 나의_메시지_조회( + final TokenResponseDto tokenResponseDto, final int page, final int count) { + return get_search(tokenResponseDto, "/api/v1/members/me/messages/written", "", page, count); + } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java index 21666ec8..efa9fef4 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/AcceptanceTest.java @@ -1,10 +1,23 @@ package com.woowacourse.naepyeon.acceptance; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; + +import com.woowacourse.naepyeon.repository.MemberRepository; +import com.woowacourse.naepyeon.service.AuthService; +import com.woowacourse.naepyeon.service.MemberService; +import com.woowacourse.naepyeon.service.dto.PlatformUserDto; +import com.woowacourse.naepyeon.service.dto.TokenRequestDto; +import com.woowacourse.naepyeon.service.dto.TokenResponseDto; +import com.woowacourse.naepyeon.support.JwtTokenProvider; +import com.woowacourse.naepyeon.support.oauth.kakao.KakaoPlatformUserProvider; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.web.server.LocalServerPort; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; @@ -13,11 +26,46 @@ @AutoConfigureTestDatabase public class AcceptanceTest { + @MockBean + protected KakaoPlatformUserProvider kakaoPlatformUserProvider; + @Autowired + protected MemberRepository memberRepository; + @Autowired + protected JwtTokenProvider jwtTokenProvider; + @Autowired + protected MemberService memberService; + protected AuthService authService; @LocalServerPort int port; + protected TokenResponseDto alex; + protected TokenResponseDto kei; + protected TokenResponseDto seungpang; + protected TokenResponseDto zero; + @BeforeEach public void setUp() { RestAssured.port = port; + authService = new AuthService(memberService, jwtTokenProvider, kakaoPlatformUserProvider); + + final String alexName = "alex"; + when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())) + .thenReturn(new PlatformUserDto(alexName, "email1@email.com", "KAKAO", "1")); + alex = authService.createTokenWithKakaoOauth(new TokenRequestDto(alexName, "https://...")); + + final String keiName = "kei"; + when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())) + .thenReturn(new PlatformUserDto(keiName, "email2@email.com", "KAKAO", "2")); + kei = authService.createTokenWithKakaoOauth(new TokenRequestDto(keiName, "https://...")); + + final String seungpangName = "seungpang"; + when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())) + .thenReturn(new PlatformUserDto(seungpangName, "email3@email.com", "KAKAO", "3")); + seungpang = authService.createTokenWithKakaoOauth(new TokenRequestDto(seungpangName, "https://...")); + + final String zeroName = "zero"; + when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())) + .thenReturn(new PlatformUserDto(zeroName, "email4@email.com", "KAKAO", "4")); + zero = authService.createTokenWithKakaoOauth(new TokenRequestDto(zeroName, "https://...")); } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/LoginAcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/LoginAcceptanceTest.java deleted file mode 100644 index 3c97be0c..00000000 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/LoginAcceptanceTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.woowacourse.naepyeon.acceptance; - -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.로그인_응답; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_조회; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_추가; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원가입_후_로그인; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; -import com.woowacourse.naepyeon.controller.dto.TokenRequest; -import com.woowacourse.naepyeon.service.dto.MemberResponseDto; -import com.woowacourse.naepyeon.service.dto.TokenResponseDto; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; - -public class LoginAcceptanceTest extends AcceptanceTest { - - @Test - @DisplayName("로그인에 성공하면 토큰을 발급한다.") - void loginSuccess() { - // 로그인 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); - - // 회원조회 - final MemberResponseDto actual = 회원_조회(tokenResponseDto) - .as(MemberResponseDto.class); - - assertAll( - () -> assertThat(actual.getUsername()).isEqualTo(member.getUsername()), - () -> assertThat(actual.getEmail()).isEqualTo(member.getEmail()) - ); - } - - @Test - @DisplayName("이메일이 올바르지 않으면 예외를 발생시킨다.") - void loginFailureWithWrongEmail() { - // 회원 가입 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - 회원_추가(member); - - // 로그인 - final TokenRequest tokenRequest = new TokenRequest("kth990303@naver.com", member.getPassword()); - final ExtractableResponse response = 로그인_응답(tokenRequest); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); - } - - @Test - @DisplayName("비밀번호가 올바르지 않으면 예외를 발생시킨다.") - void loginFailureWithWrongPassword() { - // 회원 가입 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - 회원_추가(member); - - // 로그인 - final TokenRequest tokenRequest = new TokenRequest(member.getEmail(), "aA!12345678"); - final ExtractableResponse response = 로그인_응답(tokenRequest); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); - } -} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MemberAcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MemberAcceptanceTest.java index f5ee8193..f9ecb935 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MemberAcceptanceTest.java @@ -1,97 +1,48 @@ package com.woowacourse.naepyeon.acceptance; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.로그인_응답; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.나의_롤링페이퍼_조회; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.나의_메시지_조회; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.메시지_작성; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_가입; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_생성; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_롤링페이퍼_생성; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_삭제; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_유저네임_수정; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_조회; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_추가; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원가입_후_로그인; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; -import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; +import com.woowacourse.naepyeon.controller.dto.CreateResponse; +import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; import com.woowacourse.naepyeon.controller.dto.MemberUpdateRequest; -import com.woowacourse.naepyeon.controller.dto.TokenRequest; +import com.woowacourse.naepyeon.controller.dto.MessageRequest; +import com.woowacourse.naepyeon.controller.dto.RollingpaperCreateRequest; import com.woowacourse.naepyeon.service.dto.MemberResponseDto; +import com.woowacourse.naepyeon.service.dto.ReceivedRollingpaperResponseDto; +import com.woowacourse.naepyeon.service.dto.ReceivedRollingpapersResponseDto; import com.woowacourse.naepyeon.service.dto.TokenResponseDto; +import com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto; +import com.woowacourse.naepyeon.service.dto.WrittenMessagesResponseDto; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpStatus; class MemberAcceptanceTest extends AcceptanceTest { @Test - @DisplayName("회원을 추가하고 검증한다.") - void addMember() { - //회원 정보 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - - //회원 추가 및 토큰 - final ExtractableResponse response = 회원_추가(member); - final TokenResponseDto token = 로그인_응답(new TokenRequest("email@email.com", "12345678aA!")) - .as(TokenResponseDto.class); - - //회원추가 검증 및 회원검증 - 회원이_추가됨(response, token, member); - } - - @Test - @DisplayName("회원가입 시, 이미 가입된 회원이 추가되는 경우 예외가 발생한다.") - void addDuplicationMember() { - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - 회원_추가(member); - - final ExtractableResponse response = 회원_추가(member); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - @ParameterizedTest - @DisplayName("회원가입 시, 회원이름이 2 ~ 20자 및 한글, 영어, 숫자가 아닌 경우 예외가 발생한다.") - @ValueSource(strings = {"0", "012345678901234567891", "+특수문자+"}) - void failSignUpUsername(final String failUsername) { - final MemberRegisterRequest member = - new MemberRegisterRequest(failUsername, "email@email.com", "12345678aA!"); - - final ExtractableResponse response = 회원_추가(member); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - @ParameterizedTest - @DisplayName("회원가입 시, 이메일이 기본 이메일 형식이 아닌 경우 예외가 발생한다.") - @ValueSource(strings = {"fail", "fail@email"}) - void failSignUpEmail(final String failEmail) { - final MemberRegisterRequest member = - new MemberRegisterRequest("zero", failEmail, "12345678aA!"); - - final ExtractableResponse response = 회원_추가(member); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - - @ParameterizedTest - @DisplayName("회원가입 시, 비밀번호가 8 ~ 20자 및 영어, 숫자가 포함되지 않는 경우 예외가 발생한다.") - @ValueSource(strings = {"password", "pass123", "passwordPassword12345"}) - void failSignUpPassword(final String failPassword) { - final MemberRegisterRequest member = - new MemberRegisterRequest("zero", "email@email.com", failPassword); - - final ExtractableResponse response = 회원_추가(member); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + @DisplayName("회원을 조회한다.") + void findMember() { + final MemberResponseDto findMember = 회원_조회(alex) + .as(MemberResponseDto.class); + assertThat(findMember.getId()).isEqualTo(alex.getId()); } @Test @DisplayName("올바르지 않은 토큰으로 마이페이지를 조회할 경우 예외를 발생시킨다.") void loginInvalidToken() { - final ExtractableResponse response = 회원_조회(new TokenResponseDto("invalidToken")); + final ExtractableResponse response = 회원_조회(new TokenResponseDto("invalidToken", 9999L)); assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); } @@ -99,30 +50,116 @@ void loginInvalidToken() { @Test @DisplayName("존재하지 않는 id로 회원 조회를 하려 하는 경우 예외를 발생시킨다.") void loginInvalidId() { - //회원 정보 - final MemberRegisterRequest member = - new MemberRegisterRequest("zero", "email@email.com", "12345678aA!"); - - //없는 회원 조회 - final TokenResponseDto token = 회원가입_후_로그인(member); - 회원_삭제(token); - final ExtractableResponse response = 회원_조회(token); + 회원_삭제(alex); + final ExtractableResponse response = 회원_조회(alex); //회원조회 실패 assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } + @Test + @DisplayName("내가 받은 롤링페이퍼 목록을 조회한다.") + void findReceivedRollingpapers() { + // 모임 생성 및 가입시키기 + final Long teamId = 모임_생성(alex); + 모임_가입(zero, teamId, new JoinTeamMemberRequest("알고리즘이좋아요")); + + final String rollingpaperTitle1 = "알렉스가좋아요"; + final Long rollingpaperId1 = 회원_롤링페이퍼_생성(zero, teamId, + new RollingpaperCreateRequest(rollingpaperTitle1, alex.getId())) + .as(CreateResponse.class) + .getId(); + final String rollingpaperTitle2 = "영환이형도좋아요"; + final Long rollingpaperId2 = 회원_롤링페이퍼_생성(zero, teamId, + new RollingpaperCreateRequest(rollingpaperTitle2, alex.getId())) + .as(CreateResponse.class) + .getId(); + + //나의 롤링페이퍼 조회 + final ReceivedRollingpapersResponseDto receivedRollingpapersResponseDto = 나의_롤링페이퍼_조회(alex, 0, 5) + .as(ReceivedRollingpapersResponseDto.class); + final List actual = receivedRollingpapersResponseDto.getRollingpapers(); + final String teamName = "woowacourse-4th"; + final List expected = List.of( + new ReceivedRollingpaperResponseDto(rollingpaperId1, rollingpaperTitle1, teamId, teamName), + new ReceivedRollingpaperResponseDto(rollingpaperId2, rollingpaperTitle2, teamId, teamName) + ); + + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + @DisplayName("내가 작성한 메시지 목록을 조회한다.") + void findWrittenMessages() { + // 모임 생성 및 가입시키기 + final Long teamId = 모임_생성(seungpang); + 모임_가입(kei, teamId, new JoinTeamMemberRequest("알고리즘이좋아요")); + + final String rollingpaperTitle1 = "알렉스가좋아요"; + final Long rollingpaperId1 = 회원_롤링페이퍼_생성(kei, teamId, + new RollingpaperCreateRequest(rollingpaperTitle1, seungpang.getId())) + .as(CreateResponse.class) + .getId(); + final Long rollingpaperId2 = 회원_롤링페이퍼_생성(kei, teamId, + new RollingpaperCreateRequest("영환이형도좋아요", seungpang.getId())) + .as(CreateResponse.class) + .getId(); + + final String messageContent1 = "좋아"; + final String messageContent2 = "ㅋㅋ"; + final String messageContent3 = "테스트"; + final String messageColor1 = "green"; + final String messageColor2 = "red"; + final Long messageId1 = 메시지_작성(kei, rollingpaperId1, new MessageRequest(messageContent1, messageColor1)) + .as(CreateResponse.class) + .getId(); + final Long messageId2 = 메시지_작성(kei, rollingpaperId1, new MessageRequest(messageContent2, messageColor2)) + .as(CreateResponse.class) + .getId(); + 메시지_작성(kei, rollingpaperId2, new MessageRequest(messageContent3, "yellow")) + .as(CreateResponse.class) + .getId(); + + //내가 작성한 메시지를 2개만 조회 + final WrittenMessagesResponseDto writtenMessagesResponseDto = 나의_메시지_조회(kei, 0, 2) + .as(WrittenMessagesResponseDto.class); + final List actual = writtenMessagesResponseDto.getMessages(); + final List expected = List.of( + new WrittenMessageResponseDto( + messageId1, + rollingpaperId1, + rollingpaperTitle1, + teamId, + "woowacourse-4th", + "나는야모임장", + messageContent1, + messageColor1 + ), + new WrittenMessageResponseDto( + messageId2, + rollingpaperId1, + rollingpaperTitle1, + teamId, + "woowacourse-4th", + "나는야모임장", + messageContent2, + messageColor2 + ) + ); + + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); + } + @Test @DisplayName("회원정보를 수정할 수 있다.") void updateMember() { - //회원 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto token = 회원가입_후_로그인(member); - //회원정보 수정 - final MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest("zero"); - final ExtractableResponse response = 회원_유저네임_수정(token, memberUpdateRequest); + final MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest("kingGodZero"); + final ExtractableResponse response = 회원_유저네임_수정(zero, memberUpdateRequest); //회원정보가 수정됨 회원정보가_수정됨(response); @@ -131,47 +168,20 @@ void updateMember() { @Test @DisplayName("올바르지 않은 토큰으로 수정할 경우 예외가 발생한다.") void updateInvalidToken() { - //회원생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("zero", "email@email.com", "12345678aA!"); - 회원가입_후_로그인(member); - //회원정보 수정 - final MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest("kei"); - final ExtractableResponse response = 회원_유저네임_수정(new TokenResponseDto("invalidToken"), - memberUpdateRequest); + final MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest("kingGodKei"); + final ExtractableResponse response = + 회원_유저네임_수정(new TokenResponseDto("invalidToken", 9999L), memberUpdateRequest); //회원정보 수정 실패 assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); } - @ParameterizedTest - @DisplayName("회원수정 시, 회원이름이 2 ~ 20자 및 한글, 영어, 숫자가 아닌 경우 예외가 발생한다.") - @ValueSource(strings = {"0", "012345678901234567891", "+특수문자+"}) - void failUpdateUsername(String failUsername) { - //회원생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("zero", "email@email.com", "12345678aA!"); - final TokenResponseDto token = 회원가입_후_로그인(member); - - //회원정보 수정 - final MemberUpdateRequest memberUpdateRequest = new MemberUpdateRequest(failUsername); - final ExtractableResponse response = 회원_유저네임_수정(token, memberUpdateRequest); - - //회원정보 수정 실패 - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); - } - @Test @DisplayName("회원을 삭제할 수 있다.") void deleteMember() { - //회원 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto token = 회원가입_후_로그인(member); - //회원 삭제 - final ExtractableResponse response = 회원_삭제(token); + final ExtractableResponse response = 회원_삭제(alex); //회원이 삭제됨 회원_삭제됨(response); @@ -180,13 +190,8 @@ void deleteMember() { @Test @DisplayName("올바르지 않은 토큰으로 삭제할 경우 예외가 발생한다.") void deleteInvalidToken() { - //회원 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - 회원가입_후_로그인(member); - //회원 삭제 - final ExtractableResponse response = 회원_삭제(new TokenResponseDto("invalidToken")); + final ExtractableResponse response = 회원_삭제(new TokenResponseDto("invalidToken", 999L)); //회원정보 삭제 실패 assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); @@ -195,34 +200,15 @@ void deleteInvalidToken() { @Test @DisplayName("존재하지 않는 회원을 삭제하려 하는 경우 예외가 발생한다.") void failDeleteMember() { - //회원 생성 및 삭제 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto token = 회원가입_후_로그인(member); - 회원_삭제(token); + 회원_삭제(alex); //없는 회원 삭제 - final ExtractableResponse response = 회원_삭제(token); + final ExtractableResponse response = 회원_삭제(alex); //회원 삭제 실패 assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } - private void 회원이_추가됨(final ExtractableResponse response, final TokenResponseDto token, - final MemberRegisterRequest member) { - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); - assertThat(response.header("Location")).isNotBlank(); - 회원조회_확인(token, member); - } - - private void 회원조회_확인(final TokenResponseDto token, final MemberRegisterRequest member) { - final MemberResponseDto findMember = 회원_조회(token).as(MemberResponseDto.class); - assertAll( - () -> assertThat(findMember.getUsername()).isEqualTo(member.getUsername()), - () -> assertThat(findMember.getEmail()).isEqualTo(member.getEmail()) - ); - } - private void 회원정보가_수정됨(final ExtractableResponse response) { assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java index 289e94b3..3ef45fde 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/MessageAcceptanceTest.java @@ -8,20 +8,17 @@ import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_가입; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_추가; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_롤링페이퍼_생성; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원가입_후_로그인; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import com.woowacourse.naepyeon.controller.dto.CreateResponse; import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; -import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; import com.woowacourse.naepyeon.controller.dto.MessageRequest; import com.woowacourse.naepyeon.controller.dto.MessageUpdateContentRequest; import com.woowacourse.naepyeon.controller.dto.RollingpaperCreateRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; import com.woowacourse.naepyeon.service.dto.MessageResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpaperResponseDto; -import com.woowacourse.naepyeon.service.dto.TokenResponseDto; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.junit.jupiter.api.DisplayName; @@ -33,28 +30,26 @@ class MessageAcceptanceTest extends AcceptanceTest { private final TeamRequest teamRequest = new TeamRequest( "woowacourse", "테스트 모임입니다.", "testEmoji", "#123456", "마스터다" ); - private final MemberRegisterRequest member1 = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - private final MemberRegisterRequest member2 = - new MemberRegisterRequest("yxxnghwan", "yxxnghwan@email.com", "12345678aA!"); @Test @DisplayName("특정 롤링페이퍼에서 메시지를 작성한다.") void createMessageToRollingpaper() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(zero, teamRequest) + .as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) - .as(CreateResponse.class) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(zero, teamId, rollingpaperCreateRequest).as(CreateResponse.class) .getId(); - final ExtractableResponse response = 메시지_작성(tokenResponseDto1, rollingpaperId, - new MessageRequest("환영해 알렉스!!!")); + final ExtractableResponse response = 메시지_작성( + zero, + rollingpaperId, + new MessageRequest("환영해 알렉스!!!🤗", "green") + ); assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); } @@ -62,74 +57,78 @@ void createMessageToRollingpaper() { @Test @DisplayName("특정 롤링페이퍼 내에서 동일한 사람이 동일한 메시지를 여러 개 생성할 수 있다.") void createMessagesToRollingpaperWithSameMember() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(kei, teamRequest).as(CreateResponse.class) .getId(); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(kei, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!")); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("알렉스 점심 뭐 먹어?")); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("생일축하해!")); + 메시지_작성(kei, rollingpaperId, new MessageRequest("환영해 알렉스!!!", "green")); + 메시지_작성(kei, rollingpaperId, new MessageRequest("알렉스 점심 뭐 먹어?", "green")); + 메시지_작성(kei, rollingpaperId, new MessageRequest("생일축하해!", "green")); - final RollingpaperResponseDto response = 롤링페이퍼_특정_조회(tokenResponseDto2, teamId, rollingpaperId) + final RollingpaperResponseDto response = 롤링페이퍼_특정_조회(alex, teamId, rollingpaperId) .as(RollingpaperResponseDto.class); assertThat(response.getMessages()).hasSize(3); } @Test - @DisplayName("작성한 메시지의 내용을 수정한다.") + @DisplayName("작성한 메시지의 내용과 색상을 수정한다.") void updateMessageContent() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!")) + final Long messageId = 메시지_작성(seungpang, rollingpaperId, new MessageRequest("환영해 알렉스!!!", "green")) .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 메시지_수정(tokenResponseDto1, rollingpaperId, messageId, - new MessageUpdateContentRequest("오늘 뭐해??")); + final ExtractableResponse response = 메시지_수정(seungpang, rollingpaperId, messageId, + new MessageUpdateContentRequest("오늘 뭐해??", "red")); - assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + final MessageResponseDto actual = 메시지_조회(seungpang, rollingpaperId, messageId) + .as(MessageResponseDto.class); + final MessageResponseDto expected = + new MessageResponseDto(actual.getId(), "오늘 뭐해??", "red", actual.getFrom(), actual.getAuthorId()); + + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()), + () -> assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected) + ); } @Test @DisplayName("작성한 메시지를 수정할 때 500자를 초과할 경우 예외 발생") void updateMessageContentWithExceedContentLength() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(zero, teamRequest).as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) - .as(CreateResponse.class) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(zero, teamId, rollingpaperCreateRequest).as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("환영해 알렉스!!!")) + final Long messageId = 메시지_작성(zero, rollingpaperId, new MessageRequest("환영해 알렉스!!!", "green")) .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 메시지_수정(tokenResponseDto1, rollingpaperId, messageId, - new MessageUpdateContentRequest("a".repeat(501))); + final ExtractableResponse response = + 메시지_수정(zero, rollingpaperId, messageId, new MessageUpdateContentRequest("a".repeat(501), "green")); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @@ -137,24 +136,22 @@ void updateMessageContentWithExceedContentLength() { @Test @DisplayName("롤링페이퍼에 본인이 작성하지 않은 메시지를 수정할 경우 예외 발생") void updateMessageFromOthersMessage() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest("테스트 메시지2")) + final Long messageId = 메시지_작성(alex, rollingpaperId, new MessageRequest("테스트 메시지2", "green")) .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 메시지_수정(tokenResponseDto1, rollingpaperId, messageId, - new MessageUpdateContentRequest("수정할 때 예외 발생")); + final ExtractableResponse response = 메시지_수정(seungpang, rollingpaperId, messageId, + new MessageUpdateContentRequest("수정할 때 예외 발생", "green")); assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); } @@ -162,13 +159,11 @@ void updateMessageFromOthersMessage() { @Test @DisplayName("존재하지 않는 롤링페이퍼에 메시지를 작성할 경우 예외 발생") void createMessageWithNRollingpaperNotExist() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) - .getId(); + 모임_추가(zero, teamRequest).as(CreateResponse.class); final Long invalidMessageId = 9999L; - final ExtractableResponse response = 메시지_작성(tokenResponseDto1, invalidMessageId, - new MessageRequest("환영해 알렉스!!!")); + final ExtractableResponse response = + 메시지_작성(zero, invalidMessageId, new MessageRequest("환영해 알렉스!!!", "green")); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @@ -176,23 +171,21 @@ void createMessageWithNRollingpaperNotExist() { @Test @DisplayName("롤링페이퍼에 본인이 작성한 메시지를 삭제한다.") void deleteMessage() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(kei, teamRequest).as(CreateResponse.class) .getId(); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(kei, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); - final Long messageId = 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("곧 삭제될 메시지")) + final Long messageId = 메시지_작성(kei, rollingpaperId, new MessageRequest("곧 삭제될 메시지", "green")) .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 메시지_삭제(tokenResponseDto1, rollingpaperId, messageId); + final ExtractableResponse response = 메시지_삭제(kei, rollingpaperId, messageId); assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); } @@ -200,22 +193,20 @@ void deleteMessage() { @Test @DisplayName("롤링페이퍼에서 존재하지 않는 메시지를 삭제할 경우 예외 발생") void deleteMessageWithRollingpaperNotExist() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("테스트 메시지")); + 메시지_작성(seungpang, rollingpaperId, new MessageRequest("테스트 메시지", "green")); final Long invalidMessageId = 9999L; - final ExtractableResponse response = 메시지_삭제(tokenResponseDto1, rollingpaperId, invalidMessageId); + final ExtractableResponse response = 메시지_삭제(seungpang, rollingpaperId, invalidMessageId); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @@ -223,24 +214,22 @@ void deleteMessageWithRollingpaperNotExist() { @Test @DisplayName("롤링페이퍼에 본인이 작성하지 않은 메시지를 삭제할 경우 예외 발생") void deleteMessageFromOthersMessage() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(kei, teamRequest).as(CreateResponse.class) .getId(); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("알렉스당")); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("알렉스당")); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(kei, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); - 메시지_작성(tokenResponseDto1, rollingpaperId, new MessageRequest("테스트 메시지1")); - final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest("테스트 메시지2")) + 메시지_작성(kei, rollingpaperId, new MessageRequest("테스트 메시지1", "green")); + final Long messageId = 메시지_작성(alex, rollingpaperId, new MessageRequest("테스트 메시지2", "green")) .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 메시지_삭제(tokenResponseDto1, rollingpaperId, messageId); + final ExtractableResponse response = 메시지_삭제(kei, rollingpaperId, messageId); assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); } @@ -248,32 +237,32 @@ void deleteMessageFromOthersMessage() { @Test @DisplayName("롤링페이퍼에 작성된 메시지를 상세 조회한다.") void findDetailMessageWithRollingpaper() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); final String nickname = "알렉스당"; - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest(nickname)); + 모임_가입(alex, teamId, new JoinTeamMemberRequest(nickname)); - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이 승팡", 1L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto2, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이 승팡", seungpang.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(alex, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); final String content = "상세조회용 메시지 입니다."; - final Long messageId = 메시지_작성(tokenResponseDto2, rollingpaperId, new MessageRequest(content)) + final String color = "green"; + final Long messageId = 메시지_작성(alex, rollingpaperId, new MessageRequest(content, color)) .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 메시지_조회(tokenResponseDto2, rollingpaperId, messageId); + final ExtractableResponse response = 메시지_조회(alex, rollingpaperId, messageId); final MessageResponseDto messageResponseDto = response.as(MessageResponseDto.class); assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(messageResponseDto) - .extracting("id", "content", "from", "authorId") - .containsExactly(messageId, content, nickname, 2L) + .extracting("id", "content", "color", "from", "authorId") + .containsExactly(messageId, content, color, nickname, alex.getId()) ); } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/RollingpaperAcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/RollingpaperAcceptanceTest.java index 7650cde6..8b8b9e7b 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/RollingpaperAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/RollingpaperAcceptanceTest.java @@ -1,25 +1,22 @@ package com.woowacourse.naepyeon.acceptance; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.나의_롤링페이퍼_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.롤링페이퍼_제목_수정; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.롤링페이퍼_특정_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_가입; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_모든_회원들_롤링페이퍼_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_추가; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원_롤링페이퍼_생성; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원가입_후_로그인; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원의_롤링페이퍼_조회; import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.naepyeon.controller.dto.CreateResponse; import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; -import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; import com.woowacourse.naepyeon.controller.dto.RollingpaperCreateRequest; import com.woowacourse.naepyeon.controller.dto.RollingpaperUpdateRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; import com.woowacourse.naepyeon.service.dto.RollingpaperPreviewResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpaperResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpapersResponseDto; -import com.woowacourse.naepyeon.service.dto.TokenResponseDto; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import java.util.List; @@ -33,31 +30,23 @@ class RollingpaperAcceptanceTest extends AcceptanceTest { private final TeamRequest teamRequest = new TeamRequest( "woowacourse", "테스트 모임입니다.", "testEmoji", "#123456", "나는야모임장" ); - private final MemberRegisterRequest member1 = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - private final MemberRegisterRequest member2 = - new MemberRegisterRequest("yxxnghwan", "yxxnghwan@email.com", "12345678aA!"); - private final MemberRegisterRequest member3 = - new MemberRegisterRequest("kth990303", "kth990303@email.com", "12345678aA!"); @Test @DisplayName("특정 회원에게 롤링페이퍼를 생성하고 조회한다.") void createRollingpaperToMember() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(alex, teamRequest).as(CreateResponse.class) .getId(); - - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); + 모임_가입(seungpang, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); // when: seungpang이 yxxnghwan에게 롤링페이퍼 작성 - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); // then: yxxnghwan이 받은 롤링페이퍼 조회 - final List actual = 나의_롤링페이퍼_조회(tokenResponseDto2, teamId).as(RollingpapersResponseDto.class) + final List actual = 회원의_롤링페이퍼_조회(alex, teamId).as(RollingpapersResponseDto.class) .getRollingpapers() .stream() .map(RollingpaperPreviewResponseDto::getId) @@ -69,25 +58,23 @@ void createRollingpaperToMember() { @Test @DisplayName("특정 회원에게 롤링페이퍼를 여러 번 생성할 수 있다.") void createRollingpapersToMember() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(alex, teamRequest).as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); + 모임_가입(seungpang, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); - // when: seungpang이 yxxnghwan에게 롤링페이퍼 2개 작성 - final RollingpaperCreateRequest rollingpaperCreateRequest1 = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId1 = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest1) + final RollingpaperCreateRequest rollingpaperCreateRequest1 = + new RollingpaperCreateRequest("하이알렉스", seungpang.getId()); + final Long rollingpaperId1 = 회원_롤링페이퍼_생성(alex, teamId, rollingpaperCreateRequest1) .as(CreateResponse.class) .getId(); - final RollingpaperCreateRequest rollingpaperCreateRequest2 = new RollingpaperCreateRequest("반가워", 2L); - final Long rollingpaperId2 = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest2) + final RollingpaperCreateRequest rollingpaperCreateRequest2 = + new RollingpaperCreateRequest("반가워", seungpang.getId()); + final Long rollingpaperId2 = 회원_롤링페이퍼_생성(alex, teamId, rollingpaperCreateRequest2) .as(CreateResponse.class) .getId(); - // then: yxxnghwan이 받은 롤링페이퍼 조회 - final List actual = 나의_롤링페이퍼_조회(tokenResponseDto2, teamId).as(RollingpapersResponseDto.class) + final List actual = 회원의_롤링페이퍼_조회(seungpang, teamId).as(RollingpapersResponseDto.class) .getRollingpapers() .stream() .map(RollingpaperPreviewResponseDto::getId) @@ -99,13 +86,12 @@ void createRollingpapersToMember() { @Test @DisplayName("존재하지 않는 회원에게 롤링페이퍼를 생성하는 경우 예외를 발생시킨다.") void createRollingpaperWithNotFoundMember() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(kei, teamRequest).as(CreateResponse.class) .getId(); - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final ExtractableResponse response = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, - rollingpaperCreateRequest); + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final ExtractableResponse response = 회원_롤링페이퍼_생성(kei, teamId, rollingpaperCreateRequest); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @@ -113,16 +99,12 @@ void createRollingpaperWithNotFoundMember() { @Test @DisplayName("모임에 속하지 않은 사람이 해당 모임에서 롤링페이퍼를 생성하는 경우 예외를 발생시킨다.") void createRollingpaperWithAnonymous() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(alex, teamRequest).as(CreateResponse.class) .getId(); - // 모임에 가입하지는 않음 - final TokenResponseDto tokenResponseDto3 = 회원가입_후_로그인(member3); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이승팡", 1L); - final ExtractableResponse response = 회원_롤링페이퍼_생성(tokenResponseDto3, teamId, - rollingpaperCreateRequest); + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이승팡", alex.getId()); + final ExtractableResponse response = 회원_롤링페이퍼_생성(zero, teamId, rollingpaperCreateRequest); assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); } @@ -130,28 +112,26 @@ void createRollingpaperWithAnonymous() { @Test @DisplayName("특정 모임의 회원들 전체가 받은 롤링페이퍼 목록을 조회한다.") void findRollingpapersWithTeam() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(alex, teamRequest).as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); - - final TokenResponseDto tokenResponseDto3 = 회원가입_후_로그인(member3); - 모임_가입(tokenResponseDto3, teamId, new JoinTeamMemberRequest("알고리즘이좋아요")); + 모임_가입(seungpang, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); + 모임_가입(kei, teamId, new JoinTeamMemberRequest("알고리즘이좋아요")); // when: seungpang이 yxxnghwan, kth990303에게 각각 롤링페이퍼 작성 - final RollingpaperCreateRequest rollingpaperCreateRequest1 = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId1 = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest1) + final RollingpaperCreateRequest rollingpaperCreateRequest1 = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId1 = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest1) .as(CreateResponse.class) .getId(); - final RollingpaperCreateRequest rollingpaperCreateRequest2 = new RollingpaperCreateRequest("알고리즘좀그만해!", 3L); - final Long rollingpaperId2 = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest2) + final RollingpaperCreateRequest rollingpaperCreateRequest2 = + new RollingpaperCreateRequest("알고리즘좀그만해!", kei.getId()); + final Long rollingpaperId2 = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest2) .as(CreateResponse.class) .getId(); // then - final List actual = 모임_모든_회원들_롤링페이퍼_조회(tokenResponseDto2, teamId).as(RollingpapersResponseDto.class) + final List actual = 모임_모든_회원들_롤링페이퍼_조회(seungpang, teamId).as(RollingpapersResponseDto.class) .getRollingpapers() .stream() .map(RollingpaperPreviewResponseDto::getId) @@ -163,22 +143,18 @@ void findRollingpapersWithTeam() { @Test @DisplayName("모임에 속하지 않은 회원이 롤링페이퍼를 조회할 경우 예외를 발생시킨다.") void findRollingpaperWithAnonymous() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); + 모임_가입(kei, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); - // 모임에 가입하지는 않음 - final TokenResponseDto tokenResponseDto3 = 회원가입_후_로그인(member3); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이케이", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이케이", kei.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 롤링페이퍼_특정_조회(tokenResponseDto3, teamId, rollingpaperId); + final ExtractableResponse response = 롤링페이퍼_특정_조회(alex, teamId, rollingpaperId); assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); } @@ -186,23 +162,22 @@ void findRollingpaperWithAnonymous() { @Test @DisplayName("롤링페이퍼 이름을 수정한다.") void updateRollingpaperTitle() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); final String expected = "알고리즘좀그만해!"; final RollingpaperUpdateRequest rollingpaperUpdateRequest = new RollingpaperUpdateRequest("알고리즘좀그만해!"); - 롤링페이퍼_제목_수정(tokenResponseDto1, teamId, rollingpaperId, rollingpaperUpdateRequest); + 롤링페이퍼_제목_수정(seungpang, teamId, rollingpaperId, rollingpaperUpdateRequest); - final String actual = 롤링페이퍼_특정_조회(tokenResponseDto1, teamId, rollingpaperId).as(RollingpaperResponseDto.class) + final String actual = 롤링페이퍼_특정_조회(seungpang, teamId, rollingpaperId).as(RollingpaperResponseDto.class) .getTitle(); assertThat(actual).isEqualTo(expected); } @@ -210,22 +185,21 @@ void updateRollingpaperTitle() { @Test @DisplayName("롤링페이퍼 이름을 20자 초과하여 수정할 경우 예외를 발생시킨다.") void updateRollingpaperTitleWithExceedTitleLength() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); final RollingpaperUpdateRequest rollingpaperUpdateRequest = new RollingpaperUpdateRequest("알렉스. 사실은 나도 케이만큼 알고리즘 좋아하는데 숨기는 거였어..."); - final ExtractableResponse response = 롤링페이퍼_제목_수정(tokenResponseDto1, teamId, rollingpaperId, - rollingpaperUpdateRequest); + final ExtractableResponse response = + 롤링페이퍼_제목_수정(seungpang, teamId, rollingpaperId, rollingpaperUpdateRequest); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @@ -233,23 +207,18 @@ void updateRollingpaperTitleWithExceedTitleLength() { @Test @DisplayName("모임에 속하지 않은 회원이 롤링페이퍼를 이름을 수정할 경우 예외를 발생시킨다.") void updateRollingpaperTitleWithAnonymous() { - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final Long teamId = 모임_추가(tokenResponseDto1, teamRequest).as(CreateResponse.class) + final Long teamId = 모임_추가(seungpang, teamRequest).as(CreateResponse.class) .getId(); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); - 모임_가입(tokenResponseDto2, teamId, new JoinTeamMemberRequest("영환이형도좋아요")); - - // 모임에 가입하지는 않음 - final TokenResponseDto tokenResponseDto3 = 회원가입_후_로그인(member3); - - final RollingpaperCreateRequest rollingpaperCreateRequest = new RollingpaperCreateRequest("하이알렉스", 2L); - final Long rollingpaperId = 회원_롤링페이퍼_생성(tokenResponseDto1, teamId, rollingpaperCreateRequest) + final RollingpaperCreateRequest rollingpaperCreateRequest = + new RollingpaperCreateRequest("하이알렉스", alex.getId()); + final Long rollingpaperId = 회원_롤링페이퍼_생성(seungpang, teamId, rollingpaperCreateRequest) .as(CreateResponse.class) .getId(); final RollingpaperUpdateRequest rollingpaperUpdateRequest = new RollingpaperUpdateRequest("알고리즘좀그만해!"); - final ExtractableResponse response = 롤링페이퍼_제목_수정(tokenResponseDto3, teamId, rollingpaperId, + final ExtractableResponse response = 롤링페이퍼_제목_수정(zero, teamId, rollingpaperId, rollingpaperUpdateRequest); assertThat(response.statusCode()).isEqualTo(HttpStatus.FORBIDDEN.value()); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java index c9b81c1b..39d938b3 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/acceptance/TeamAcceptanceTest.java @@ -1,27 +1,28 @@ package com.woowacourse.naepyeon.acceptance; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.가입한_모임_조회; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모든_모임_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_가입; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_가입_정보_조회; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_내_닉네임_변경; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_단건_조회; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_삭제; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_생성; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_이름_수정; import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임_추가; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임에_가입한_회원_조회; -import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.회원가입_후_로그인; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.모임에_가입한_회원_목록_조회; +import static com.woowacourse.naepyeon.acceptance.AcceptanceFixture.키워드로_모든_모임_조회; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import com.woowacourse.naepyeon.controller.dto.CreateResponse; import com.woowacourse.naepyeon.controller.dto.JoinTeamMemberRequest; -import com.woowacourse.naepyeon.controller.dto.MemberRegisterRequest; import com.woowacourse.naepyeon.controller.dto.TeamRequest; +import com.woowacourse.naepyeon.controller.dto.UpdateTeamParticipantRequest; import com.woowacourse.naepyeon.service.dto.JoinedMemberResponseDto; import com.woowacourse.naepyeon.service.dto.JoinedMembersResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; -import com.woowacourse.naepyeon.service.dto.TokenResponseDto; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import java.util.List; @@ -35,10 +36,6 @@ class TeamAcceptanceTest extends AcceptanceTest { @Test @DisplayName("모임 추가") void addTeam() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); final TeamRequest teamRequest = new TeamRequest( "woowacourse", "테스트 모임입니다.", @@ -46,7 +43,7 @@ void addTeam() { "#123456", "나는야모임장" ); - final ExtractableResponse response = 모임_추가(tokenResponseDto, teamRequest); + final ExtractableResponse response = 모임_추가(alex, teamRequest); //모임이 추가됨 모임이_추가됨(response); @@ -55,13 +52,9 @@ void addTeam() { @Test @DisplayName("모임을 생성시 생성한 유저가 자동으로 모임에 가입된다.") void createTeamAndParticipateTeam() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); - final Long teamId = 모임_생성(tokenResponseDto); + final Long teamId = 모임_생성(alex); - final List joinedTeamIds = 가입한_모임_조회(tokenResponseDto).body() + final List joinedTeamIds = 가입한_모임_조회(alex, 0, 5).body() .as(TeamsResponseDto.class) .getTeams() .stream() @@ -74,25 +67,17 @@ void createTeamAndParticipateTeam() { @Test @DisplayName("모임을 생성하고 조회한다.") void addTeamAndGet() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); final String teamName = "woowacourse"; final String teamDescription = "테스트 모임입니다."; final String teamEmoji = "testEmoji"; final String teamColor = "#123456"; - final TeamRequest teamRequest = new TeamRequest( - teamName, - teamDescription, - teamEmoji, - teamColor, - "나는야모임장" - ); - final Long teamId = 모임_추가(tokenResponseDto, teamRequest).as(CreateResponse.class) + final TeamRequest teamRequest = + new TeamRequest(teamName, teamDescription, teamEmoji, teamColor, "나는야모임장"); + final Long teamId = 모임_추가(alex, teamRequest) + .as(CreateResponse.class) .getId(); - final ExtractableResponse response = 모임_단건_조회(tokenResponseDto, teamId); + final ExtractableResponse response = 모임_단건_조회(alex, teamId); final TeamResponseDto teamResponse = response.as(TeamResponseDto.class); assertThat(teamResponse).extracting("id", "name", "description", "emoji", "color") @@ -102,10 +87,7 @@ void addTeamAndGet() { @Test @DisplayName("존재하지 않는 id로 모임 조회를 하려 하는 경우 예외를 발생시킨다.") void findByIdWithNotExistId() { - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); - final ExtractableResponse response = 모임_단건_조회(tokenResponseDto, 10000L); + final ExtractableResponse response = 모임_단건_조회(alex, 10000L); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @@ -113,10 +95,6 @@ void findByIdWithNotExistId() { @Test @DisplayName("모임을 중복해서 생성하는 경우 예외를 발생시킨다.") void addTeamDuplicate() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); final TeamRequest teamRequest = new TeamRequest( "woowacourse", "테스트 모임입니다.", @@ -124,9 +102,9 @@ void addTeamDuplicate() { "#123456", "나는야모임장" ); - 모임_추가(tokenResponseDto, teamRequest); + 모임_추가(alex, teamRequest); - final ExtractableResponse response = 모임_추가(tokenResponseDto, teamRequest); + final ExtractableResponse response = 모임_추가(alex, teamRequest); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @@ -134,10 +112,6 @@ void addTeamDuplicate() { @Test @DisplayName("모든 팀을 조회한다.") void getAllTeams() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); final TeamRequest teamRequest1 = new TeamRequest( "woowacourse1", "테스트 모임입니다.", @@ -145,39 +119,33 @@ void getAllTeams() { "#123456", "나는야모임장" ); - final Long team1Id = 모임_추가(tokenResponseDto, teamRequest1).as(CreateResponse.class) + final Long team1Id = 모임_추가(alex, teamRequest1).as(CreateResponse.class) .getId(); //모임 생성 - final TeamRequest teamRequest2 = new TeamRequest( - "woowacourse2", - "테스트 모임입니다.", - "testEmoji", - "#123456", - "나는야모임장" - ); - final Long team2Id = 모임_추가(tokenResponseDto, teamRequest2).as(CreateResponse.class) + final TeamRequest teamRequest2 = + new TeamRequest("내편아니야", ".", "a", "#123456", "테스트"); + final Long team2Id = 모임_추가(alex, teamRequest2).as(CreateResponse.class) .getId(); - final List teamIds = 모든_모임_조회(tokenResponseDto).body() - .as(TeamsResponseDto.class) - .getTeams() - .stream() - .map(TeamResponseDto::getId) - .collect(Collectors.toList()); + //결과 조회 + final ExtractableResponse response = + 키워드로_모든_모임_조회(alex, "", 0, 5); + + final List actual = response.as(TeamsResponseDto.class) + .getTeams(); + final List expected = List.of( + TeamResponseDto.byRequest(team1Id, teamRequest1, true), + TeamResponseDto.byRequest(team2Id, teamRequest2, true) + ); - assertThat(teamIds).contains(team1Id, team2Id); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @DisplayName("모든 모임 조회시 내가 가입한 모임의 joined컬럼이 true로, 가입하지 않은 모임은 false로 나온다.") void checkJoinedColumn() { - //모임 생성 - final MemberRegisterRequest member1 = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final MemberRegisterRequest member2 = - new MemberRegisterRequest("seungpang2", "email2@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); final String teamName1 = "woowacourse1"; final TeamRequest teamRequest1 = new TeamRequest( teamName1, @@ -186,7 +154,7 @@ void checkJoinedColumn() { "#123456", "나는야모임장" ); - final Long team1Id = 모임_추가(tokenResponseDto1, teamRequest1).as(CreateResponse.class) + final Long team1Id = 모임_추가(alex, teamRequest1).as(CreateResponse.class) .getId(); //모임 생성 final String teamName2 = "woowacourse2"; @@ -197,22 +165,23 @@ void checkJoinedColumn() { "#123456", "나는야모임장" ); - final Long team2Id = 모임_추가(tokenResponseDto1, teamRequest2).as(CreateResponse.class) - .getId(); + 모임_추가(alex, teamRequest2).as(CreateResponse.class); - 모임_가입(tokenResponseDto2, team1Id, new JoinTeamMemberRequest("가입자")); + 모임_가입(zero, team1Id, new JoinTeamMemberRequest("가입자")); - final List teams = 모든_모임_조회(tokenResponseDto2) + final List teams = 키워드로_모든_모임_조회(zero, "woowa", 0, 5) .as(TeamsResponseDto.class) .getTeams(); final TeamResponseDto joinedTeam = teams.stream() .filter(TeamResponseDto::isJoined) - .findAny().get(); + .findAny() + .get(); final TeamResponseDto notJoinedTeam = teams.stream() .filter(teamResponseDto -> !teamResponseDto.isJoined()) - .findAny().get(); + .findAny() + .get(); assertAll( () -> assertThat(joinedTeam.getName()).isEqualTo(teamName1), @@ -223,13 +192,6 @@ void checkJoinedColumn() { @Test @DisplayName("팀에 가입한 회원 목록을 조회한다.") void findJoinedMembers() { - //모임 생성 - final MemberRegisterRequest member1 = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto1 = 회원가입_후_로그인(member1); - final MemberRegisterRequest member2 = - new MemberRegisterRequest("seungpang2", "email2@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto2 = 회원가입_후_로그인(member2); final String teamName1 = "woowacourse1"; final String masterNickname = "나는야모임장"; final TeamRequest teamRequest1 = new TeamRequest( @@ -239,13 +201,13 @@ void findJoinedMembers() { "#123456", masterNickname ); - final Long team1Id = 모임_추가(tokenResponseDto1, teamRequest1).as(CreateResponse.class) + final Long team1Id = 모임_추가(alex, teamRequest1).as(CreateResponse.class) .getId(); final String joinNickname = "가입자"; - 모임_가입(tokenResponseDto2, team1Id, new JoinTeamMemberRequest(joinNickname)); + 모임_가입(kei, team1Id, new JoinTeamMemberRequest(joinNickname)); - final JoinedMembersResponseDto joinedMembers = 모임에_가입한_회원_조회(tokenResponseDto1, team1Id) + final JoinedMembersResponseDto joinedMembers = 모임에_가입한_회원_목록_조회(alex, team1Id) .as(JoinedMembersResponseDto.class); final List joinedMemberNickNames = joinedMembers.getMembers() @@ -259,11 +221,7 @@ void findJoinedMembers() { @Test @DisplayName("모임 이름 수정") void updateTeam() { - // 모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); - final Long teamId = 모임_생성(tokenResponseDto); + final Long teamId = 모임_생성(alex); // 모임 이름 수정 final TeamRequest changeTeamRequest = new TeamRequest( @@ -273,7 +231,7 @@ void updateTeam() { "#123456", "나는야모임장" ); - final ExtractableResponse response = 모임_이름_수정(tokenResponseDto, teamId, changeTeamRequest); + final ExtractableResponse response = 모임_이름_수정(alex, teamId, changeTeamRequest); 모임이름이_수정됨(response); } @@ -297,17 +255,10 @@ void updateTeam() { @Test @DisplayName("모임에 회원을 가입시킨다.") void joinMember() { - //모임 생성 - final MemberRegisterRequest owner = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto ownerTokenResponseDto = 회원가입_후_로그인(owner); - final Long teamId = 모임_생성(ownerTokenResponseDto); - final MemberRegisterRequest member = - new MemberRegisterRequest("alex", "alex@alex.com", "12345678aA!"); - final TokenResponseDto memberTokenResponseDto = 회원가입_후_로그인(member); + final Long teamId = 모임_생성(alex); final ExtractableResponse response = - 모임_가입(memberTokenResponseDto, teamId, new JoinTeamMemberRequest("모임닉네임")); + 모임_가입(kei, teamId, new JoinTeamMemberRequest("모임닉네임")); assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); } @@ -315,14 +266,10 @@ void joinMember() { @Test @DisplayName("모임에 이미 가입된 회원을 가입시키려 하는 경우 예외를 발생시킨다.") void joinMemberDuplicate() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); - final Long teamId = 모임_생성(tokenResponseDto); + final Long teamId = 모임_생성(alex); final ExtractableResponse response = - 모임_가입(tokenResponseDto, teamId, new JoinTeamMemberRequest("모임닉네임")); + 모임_가입(alex, teamId, new JoinTeamMemberRequest("모임닉네임")); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @@ -330,10 +277,6 @@ void joinMemberDuplicate() { @Test @DisplayName("회원이 가입한 모임을 조회한다.") void getJoinedTeams() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); final TeamRequest teamRequest1 = new TeamRequest( "woowacourse1", "테스트 모임입니다.", @@ -341,7 +284,7 @@ void getJoinedTeams() { "#123456", "나는야모임장" ); - final Long team1Id = 모임_추가(tokenResponseDto, teamRequest1).as(CreateResponse.class) + final Long team1Id = 모임_추가(alex, teamRequest1).as(CreateResponse.class) .getId(); final TeamRequest teamRequest2 = new TeamRequest( "woowacourse2", @@ -350,8 +293,7 @@ void getJoinedTeams() { "#123456", "나는야모임장" ); - final Long team2Id = 모임_추가(tokenResponseDto, teamRequest2).as(CreateResponse.class) - .getId(); + 모임_추가(alex, teamRequest2).as(CreateResponse.class); final TeamRequest teamRequest3 = new TeamRequest( "woowacourse3", "테스트 모임입니다.", @@ -359,12 +301,12 @@ void getJoinedTeams() { "#123456", "나는야모임장" ); - final Long team3Id = 모임_추가(tokenResponseDto, teamRequest3).as(CreateResponse.class) + final Long team3Id = 모임_추가(alex, teamRequest3).as(CreateResponse.class) .getId(); - 모임_가입(tokenResponseDto, team1Id, new JoinTeamMemberRequest("닉네임1")); - 모임_가입(tokenResponseDto, team3Id, new JoinTeamMemberRequest("닉네임3")); + 모임_가입(zero, team1Id, new JoinTeamMemberRequest("닉네임1")); + 모임_가입(zero, team3Id, new JoinTeamMemberRequest("닉네임3")); - final List joinedTeamIds = 가입한_모임_조회(tokenResponseDto).body() + final List joinedTeamIds = 가입한_모임_조회(zero, 0, 5).body() .as(TeamsResponseDto.class) .getTeams() .stream() @@ -377,11 +319,7 @@ void getJoinedTeams() { @Test @DisplayName("모임 이름을 수정할 때, 20자를 초과하는 이름으로 수정하는 경우 예외를 발생시킨다.") void changeTeamNameWithExceedLength() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); - final Long teamId = 모임_생성(tokenResponseDto); + final Long teamId = 모임_생성(alex); // 모임 이름 수정 final TeamRequest changeTeamRequest = new TeamRequest( @@ -391,7 +329,7 @@ void changeTeamNameWithExceedLength() { "#123456", "나는야모임장" ); - final ExtractableResponse response = 모임_이름_수정(tokenResponseDto, teamId, changeTeamRequest); + final ExtractableResponse response = 모임_이름_수정(alex, teamId, changeTeamRequest); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @@ -399,18 +337,52 @@ void changeTeamNameWithExceedLength() { @Test @DisplayName("존재하지 않는 모임을 삭제하려는 경우 예외를 발생시킨다.") void deleteNotExistTeam() { - //모임 생성 - final MemberRegisterRequest member = - new MemberRegisterRequest("seungpang", "email@email.com", "12345678aA!"); - final TokenResponseDto tokenResponseDto = 회원가입_후_로그인(member); - final Long teamId = 모임_생성(tokenResponseDto); - - //모임 삭제 - final ExtractableResponse response = 모임_삭제(tokenResponseDto, 10000L); + final ExtractableResponse response = 모임_삭제(alex, 10000L); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } + @Test + @DisplayName("모임에서의 내 정보를 조회한다.") + void findMyInfoInTeam() { + final String expected = "나는야모임장"; + + final Long teamId = 모임_생성(alex); + + final String actual = 모임_가입_정보_조회(alex, teamId) + .as(TeamMemberResponseDto.class) + .getNickname(); + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("모임의 닉네임을 수정한다.") + void updateNickname() { + final String expected = "나모임장안해"; + final Long teamId = 모임_생성(alex); + + final UpdateTeamParticipantRequest updateTeamParticipantRequest = new UpdateTeamParticipantRequest(expected); + 모임_내_닉네임_변경(alex, teamId, updateTeamParticipantRequest); + + final String actual = 모임_가입_정보_조회(alex, teamId) + .as(TeamMemberResponseDto.class) + .getNickname(); + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("이미 팀에 존재하는 닉네임으로 수정할 경우 예외를 발생시킨다.") + void updateDuplicatedNickname() { + final Long teamId = 모임_생성(alex); + final JoinTeamMemberRequest request = new JoinTeamMemberRequest("애플"); + 모임_가입(seungpang, teamId, request); + + final UpdateTeamParticipantRequest updateTeamParticipantRequest = + new UpdateTeamParticipantRequest("나는야모임장"); + final ExtractableResponse response = 모임_내_닉네임_변경(seungpang, teamId, updateTeamParticipantRequest); + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + private void 모임_삭제됨(ExtractableResponse response) { assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/document/AuthControllerTest.java b/backend/src/test/java/com/woowacourse/naepyeon/document/AuthControllerTest.java new file mode 100644 index 00000000..5e92a32e --- /dev/null +++ b/backend/src/test/java/com/woowacourse/naepyeon/document/AuthControllerTest.java @@ -0,0 +1,34 @@ +package com.woowacourse.naepyeon.document; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.woowacourse.naepyeon.controller.dto.TokenRequest; +import com.woowacourse.naepyeon.service.dto.PlatformUserDto; +import com.woowacourse.naepyeon.support.oauth.kakao.KakaoPlatformUserProvider; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; + +class AuthControllerTest extends TestSupport { + + @MockBean + private KakaoPlatformUserProvider kakaoPlatformUserProvider; + + @Test + void kakaoLogin() throws Exception { + when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())) + .thenReturn(new PlatformUserDto("이순신", "email1@email.com", "KAKAO", "1")); + final TokenRequest tokenRequest = new TokenRequest("seungpang", "https://..."); + + mockMvc.perform( + post("/api/v1/oauth/kakao") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(tokenRequest)) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/document/MemberControllerTest.java b/backend/src/test/java/com/woowacourse/naepyeon/document/MemberControllerTest.java new file mode 100644 index 00000000..a8164862 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/naepyeon/document/MemberControllerTest.java @@ -0,0 +1,64 @@ +package com.woowacourse.naepyeon.document; + +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +class MemberControllerTest extends TestSupport { + + @Test + void findMember() throws Exception { + mockMvc.perform( + get("/api/v1/members/me") + .header("Authorization", "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void findReceivedRollingpapers() throws Exception { + mockMvc.perform( + get("/api/v1/members/me/rollingpapers/received?page=0&count=10") + .header("Authorization", "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + + @Test + void findWrittenMessages() throws Exception { + mockMvc.perform( + get("/api/v1/members/me/messages/written?page=0&count=10") + .header("Authorization", "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void updateMember() throws Exception { + mockMvc.perform( + put("/api/v1/members/me") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/members/member-update.json")) + ) + .andExpect(status().isNoContent()) + .andDo(restDocs.document()); + } + + @Test + void deleteMember() throws Exception { + mockMvc.perform( + delete("/api/v1/members/me") + .header("Authorization", "Bearer " + accessToken) + ) + .andExpect(status().isNoContent()); + } +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/document/MessageControllerTest.java b/backend/src/test/java/com/woowacourse/naepyeon/document/MessageControllerTest.java new file mode 100644 index 00000000..3e5e36a7 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/naepyeon/document/MessageControllerTest.java @@ -0,0 +1,57 @@ +package com.woowacourse.naepyeon.document; + +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +class MessageControllerTest extends TestSupport { + + @Test + void createMessage() throws Exception { + mockMvc.perform( + post("/api/v1/rollingpapers/{rollingpaperId}/messages", rollingpaperId1) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/messages/message-create.json")) + ) + .andExpect(status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + void findMessage() throws Exception { + mockMvc.perform( + get("/api/v1/rollingpapers/{rollingpaperId}/messages/{messageId}", rollingpaperId1, messageId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void updateMessage() throws Exception { + mockMvc.perform( + put("/api/v1/rollingpapers/{rollingpaperId}/messages/{messageId}", rollingpaperId1, messageId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/messages/message-update.json")) + ) + .andExpect(status().isNoContent()) + .andDo(restDocs.document()); + } + + @Test + void deleteMessage() throws Exception { + mockMvc.perform( + delete("/api/v1/rollingpapers/{rollingpaperId}/messages/{messageId}", rollingpaperId1, messageId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isNoContent()) + .andDo(restDocs.document()); + } +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/document/RestDocsConfiguration.java b/backend/src/test/java/com/woowacourse/naepyeon/document/RestDocsConfiguration.java new file mode 100644 index 00000000..f366c56b --- /dev/null +++ b/backend/src/test/java/com/woowacourse/naepyeon/document/RestDocsConfiguration.java @@ -0,0 +1,20 @@ +package com.woowacourse.naepyeon.document; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.operation.preprocess.Preprocessors; + +@TestConfiguration +public class RestDocsConfiguration { + + @Bean + public RestDocumentationResultHandler write() { + return MockMvcRestDocumentation.document( + "{class-name}/{method-name}", + Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), + Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) + ); + } +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/document/RollingpaperControllerTest.java b/backend/src/test/java/com/woowacourse/naepyeon/document/RollingpaperControllerTest.java new file mode 100644 index 00000000..53f75fb5 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/naepyeon/document/RollingpaperControllerTest.java @@ -0,0 +1,67 @@ +package com.woowacourse.naepyeon.document; + +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.woowacourse.naepyeon.controller.dto.RollingpaperCreateRequest; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +class RollingpaperControllerTest extends TestSupport { + + @Test + void createRollingpaper() throws Exception { + mockMvc.perform( + post("/api/v1/teams/{teamId}/rollingpapers", teamId) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(new RollingpaperCreateRequest("어서오세요", memberId2))) + ) + .andExpect(status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + void findRollingpaperById() throws Exception { + mockMvc.perform( + get("/api/v1/teams/{teamId}/rollingpapers/{rollingpaperId}", teamId, rollingpaperId1) + .header("Authorization", "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void findRollingpapersByTeamId() throws Exception { + mockMvc.perform( + get("/api/v1/teams/{teamId}/rollingpapers", teamId) + .header("Authorization", "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void findRollingpapersByMemberId() throws Exception { + mockMvc.perform( + get("/api/v1/teams/{teamId}/rollingpapers/me", teamId) + .header("Authorization", "Bearer " + accessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void updateRollingpaper() throws Exception { + mockMvc.perform( + put("/api/v1/teams/{teamId}/rollingpapers/{rollingpaperId}", teamId, rollingpaperId2) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/rollingpapers/rollingpaper-update.json")) + ) + .andExpect(status().isNoContent()) + .andDo(restDocs.document()); + } +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/document/TeamControllerTest.java b/backend/src/test/java/com/woowacourse/naepyeon/document/TeamControllerTest.java new file mode 100644 index 00000000..3260b080 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/naepyeon/document/TeamControllerTest.java @@ -0,0 +1,121 @@ +package com.woowacourse.naepyeon.document; + +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +class TeamControllerTest extends TestSupport { + + @Test + void getTeam() throws Exception { + mockMvc.perform( + get("/api/v1/teams/{teamId}", teamId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void getJoinedTeams() throws Exception { + mockMvc.perform( + get("/api/v1/teams/me?page=0&count=10") + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void getAllTeams() throws Exception { + mockMvc.perform( + get("/api/v1/teams?keyword=&page=0&count=10") + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + + } + + @Test + void getAllTeamsByKeyword() throws Exception { + mockMvc.perform( + get("/api/v1/teams?keyword=우테코&page=0&count=10") + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void getJoinedMembers() throws Exception { + mockMvc.perform( + get("/api/v1/teams/{teamId}/members", teamId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void createTeam() throws Exception { + mockMvc.perform( + post("/api/v1/teams") + .header("Authorization", "Bearer " + joinedMemberAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/teams/team-create.json")) + ) + .andExpect(status().isCreated()) + .andDo(restDocs.document()); + } + + @Test + void updateTeam() throws Exception { + mockMvc.perform( + put("/api/v1/teams/{teamId}", teamId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/teams/team-update.json")) + ) + .andExpect(status().isNoContent()) + .andDo(restDocs.document()); + } + + @Test + void joinMember() throws Exception { + mockMvc.perform( + post("/api/v1/teams/{teamId}", teamId) + .header("Authorization", "Bearer " + notJoinedMemberAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/teams/team-join.json")) + ) + .andExpect(status().isNoContent()) + .andDo(restDocs.document()); + } + + @Test + void getMyInfoInTeam() throws Exception { + mockMvc.perform( + get("/api/v1/teams/{teamId}/me", teamId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + ) + .andExpect(status().isOk()) + .andDo(restDocs.document()); + } + + @Test + void updateMyInfo() throws Exception { + mockMvc.perform( + put("/api/v1/teams/{teamId}/me", teamId) + .header("Authorization", "Bearer " + joinedMemberAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(readJson("/json/teams/update-my-info.json")) + ) + .andExpect(status().isNoContent()) + .andDo(restDocs.document()); + } +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/document/TestSupport.java b/backend/src/test/java/com/woowacourse/naepyeon/document/TestSupport.java new file mode 100644 index 00000000..340049c6 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/naepyeon/document/TestSupport.java @@ -0,0 +1,105 @@ +package com.woowacourse.naepyeon.document; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.woowacourse.naepyeon.controller.dto.TeamRequest; +import com.woowacourse.naepyeon.service.MemberService; +import com.woowacourse.naepyeon.service.MessageService; +import com.woowacourse.naepyeon.service.RollingpaperService; +import com.woowacourse.naepyeon.service.TeamService; +import com.woowacourse.naepyeon.support.JwtTokenProvider; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ExtendWith(RestDocumentationExtension.class) +@Import(RestDocsConfiguration.class) +@AutoConfigureTestDatabase +@Transactional +public abstract class TestSupport { + + @Autowired + protected RestDocumentationResultHandler restDocs; + + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + protected JwtTokenProvider jwtTokenProvider; + + @Autowired + protected MemberService memberService; + + @Autowired + protected TeamService teamService; + + @Autowired + protected RollingpaperService rollingpaperService; + + @Autowired + protected MessageService messageService; + + protected MockMvc mockMvc; + protected String accessToken; + protected String joinedMemberAccessToken; + protected String notJoinedMemberAccessToken; + protected Long memberId1; + protected Long memberId2; + protected Long memberId3; + protected Long teamId; + protected Long rollingpaperId1; + protected Long rollingpaperId2; + protected Long messageId; + + @BeforeEach + void setUp( + final WebApplicationContext context, + final RestDocumentationContextProvider provider + ) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(restDocs) + .build(); + saveBaseData(); + accessToken = jwtTokenProvider.createToken(String.valueOf(memberId1)); + joinedMemberAccessToken = jwtTokenProvider.createToken(String.valueOf(memberId2)); + notJoinedMemberAccessToken = jwtTokenProvider.createToken(String.valueOf(memberId3)); + } + + private void saveBaseData() { + memberId1 = memberService.save("홍길동", "email1@email.com", "KAKAO", "1"); + memberId2 = memberService.save("이순신", "email2@email.com", "KAKAO", "2"); + memberId3 = memberService.save("아이유", "email3@email.com", "KAKAO", "3"); + + teamId = teamService.save(new TeamRequest("우테코 4기", "우테코 4기 크루원들 모임입니다.", "\\uD83D\\uDE00", "#212121", "길동이"), + memberId1); + teamService.joinMember(teamId, memberId2, "순신이"); + + rollingpaperId1 = rollingpaperService.createRollingpaper("이순신 환영해", teamId, memberId1, memberId2); + rollingpaperId2 = rollingpaperService.createRollingpaper("홍길동 반가워", teamId, memberId2, memberId1); + + messageId = messageService.saveMessage("생일축하해!", "#123456", rollingpaperId1, memberId2); + messageService.saveMessage("환영합니다", "#123456", rollingpaperId1, memberId1); + messageService.saveMessage("감사합니다!", "#123456", rollingpaperId2, memberId1); + messageService.saveMessage("많은 가르침 받았습니다.", "#123456", rollingpaperId2, memberId2); + } + + protected String readJson(final String path) throws IOException { + return new String(Files.readAllBytes(Paths.get("src/test/resources", path))); + } +} diff --git a/backend/src/test/java/com/woowacourse/naepyeon/domain/MemberTest.java b/backend/src/test/java/com/woowacourse/naepyeon/domain/MemberTest.java index c5615296..e20bfe28 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/domain/MemberTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/domain/MemberTest.java @@ -4,11 +4,8 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import com.woowacourse.naepyeon.exception.ExceedMemberPasswordLengthException; import com.woowacourse.naepyeon.exception.ExceedMemberUsernameLengthException; import com.woowacourse.naepyeon.exception.InvalidMemberEmailException; -import com.woowacourse.naepyeon.exception.InvalidMemberPasswordException; -import com.woowacourse.naepyeon.exception.InvalidMemberUsernameException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -16,29 +13,20 @@ class MemberTest { - @Test - @DisplayName("이름은 2~20자, 한글, 숫자, 영어만 가능하다.") - void checkUsername() { - final String validUsername = "제로0zero"; + @ParameterizedTest + @ValueSource(strings = {"a", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) + @DisplayName("이름은 1~64자 까지 가능하다.") + void checkUsername(final String name) { - assertThatCode(() -> new Member(validUsername, "email@email.com", "zero1234")) + assertThatCode(() -> new Member(name, "email@email.com", Platform.KAKAO, "1")) .doesNotThrowAnyException(); } - @Test - @DisplayName("한글, 숫자, 영어 외의 문자가 포함된 유저이름으로 생성하면 예외가 발생한다.") - void createWithWrongUsername() { - final String invalidUsername = "제로0zero@"; - - assertThatThrownBy(() -> new Member(invalidUsername, "email@email.com", "zero1234")) - .isInstanceOf(InvalidMemberUsernameException.class); - } - @ParameterizedTest - @DisplayName("이름은 2~20자가 아니라면 예외가 발생한다.") - @ValueSource(strings = {"0", "012345678901234567891"}) + @DisplayName("이름은 1~64자가 아니라면 예외가 발생한다.") + @ValueSource(strings = {"", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) void createSizeWrongUsername(final String invalidUsername) { - assertThatThrownBy(() -> new Member(invalidUsername, "email@email.com", "zero1234")) + assertThatThrownBy(() -> new Member(invalidUsername, "email@email.com", Platform.KAKAO, "1")) .isInstanceOf(ExceedMemberUsernameLengthException.class); } @@ -46,30 +34,14 @@ void createSizeWrongUsername(final String invalidUsername) { @ValueSource(strings = {"email@email", "email.com", "email@email."}) @DisplayName("유효하지 않은 이메일로 유저를 생성하면 예외가 발생한다.") void createWithWrongEmail(final String invalidUserEmail) { - assertThatThrownBy(() -> new Member("zero0제로", invalidUserEmail, "zero1234")) + assertThatThrownBy(() -> new Member("zero0제로", invalidUserEmail, Platform.KAKAO, "1")) .isInstanceOf(InvalidMemberEmailException.class); } - @ParameterizedTest - @ValueSource(strings = {"abc1234", "abcdefghijklmnopqrs12"}) - @DisplayName("유저 비밀번호 길이를 올바르지 않게 생성할 경우 예외를 발생시킨다.") - void createExceedLengthUserPassword(final String wrongPassword) { - assertThatThrownBy(() -> new Member("alex", "alex@naepyeon.com", wrongPassword)) - .isInstanceOf(ExceedMemberPasswordLengthException.class); - } - - @ParameterizedTest - @DisplayName("유저 비밀번호가 알파벳, 숫자 조합이 아닌 경우로 생성할 경우 예외를 발생시킨다.") - @ValueSource(strings = {"abcdefghijklmn", "123456789"}) - void createWithWrongPassword(final String wrongPassword) { - assertThatThrownBy(() -> new Member("alex", "alex@naepyeon.com", wrongPassword)) - .isInstanceOf(InvalidMemberPasswordException.class); - } - @Test @DisplayName("유저이름을 변경할 수 있다.") void changeUsername() { - final Member member = new Member("제로0zeoro", "email@email.com", "zero1234"); + final Member member = new Member("제로0zeoro", "email@email.com", Platform.KAKAO, "1"); final String expected = "zero0제로"; member.changeUsername(expected); @@ -78,53 +50,13 @@ void changeUsername() { } @ParameterizedTest - @DisplayName("유저 이름을 2자 이상 20자 이하로 변경하지 않은 경우 예외를 발생시킨다.") - @ValueSource(strings = {"a", "abcdefghijklmnopqrstu"}) + @DisplayName("유저 이름을 1자 이상 64자 이하로 변경하지 않은 경우 예외를 발생시킨다.") + @ValueSource(strings = {"", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}) void changeExceedLengthUserName(final String updateUsername) { final String validUsername = "제로0zero"; - final Member member = new Member(validUsername, "email@email.com", "zero1234"); + final Member member = new Member(validUsername, "email@email.com", Platform.KAKAO, "1"); assertThatThrownBy(() -> member.changeUsername(updateUsername)) .isInstanceOf(ExceedMemberUsernameLengthException.class); } - - @Test - @DisplayName("한글, 숫자, 영어 외의 문자가 포함된 유저이름으로 변경하면 예외가 발생한다.") - void changeWithWrongUsername() { - final String validUsername = "제로0zero"; - final Member member = new Member(validUsername, "email@email.com", "zero1234"); - final String invalidUsername = "제로0zero@"; - - assertThatThrownBy(() -> member.changeUsername(invalidUsername)) - .isInstanceOf(InvalidMemberUsernameException.class); - } - - @Test - @DisplayName("유저 비밀번호를 변경할 수 있다.") - void changeUserPassword() { - final Member member = new Member("제로0zero", "email@email.com", "zero1234"); - final String expected = "123@zero"; - - member.changePassword(expected); - - assertThat(member.getPassword()).isEqualTo(expected); - } - - @ParameterizedTest - @ValueSource(strings = {"abc1234", "abcdefghijklmnopqrs12"}) - @DisplayName("유저 비밀번호 길이를 올바르지 않게 변경할 경우 예외를 발생시킨다.") - void changeExceedLengthUserPassword(final String wrongPassword) { - final Member member = new Member("alex", "alex@naepyeon.com", "abc12345"); - assertThatThrownBy(() -> member.changePassword(wrongPassword)) - .isInstanceOf(ExceedMemberPasswordLengthException.class); - } - - @ParameterizedTest - @DisplayName("유저 비밀번호가 알파벳, 숫자 조합이 아닌 경우로 변경할 경우 예외를 발생시킨다.") - @ValueSource(strings = {"abcdefghijklmn", "123456789"}) - void changeInvalidUserPassword(final String wrongPassword) { - final Member member = new Member("alex", "alex@naepyeon.com", "abc12345"); - assertThatThrownBy(() -> member.changePassword(wrongPassword)) - .isInstanceOf(InvalidMemberPasswordException.class); - } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java b/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java index 49780c78..97b73080 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/domain/MessageTest.java @@ -18,10 +18,10 @@ void changeContent() { "testEmoji", "#123456" ); - final Member member = new Member("member", "m@hello.com", "abc@@2345"); - final Member author = new Member("author", "a@hello.com", "abc@@2345"); + final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); + final Member author = new Member("author", "a@hello.com", Platform.KAKAO, "2"); final Rollingpaper rollingpaper = new Rollingpaper("alexAndKei", team, member); - final Message message = new Message("헬로우", author, rollingpaper); + final Message message = new Message("헬로우", "green", author, rollingpaper); final String expected = "낫 헬로우"; message.changeContent(expected); @@ -38,10 +38,30 @@ void exceedLength() { "testEmoji", "#123456" ); - final Member member = new Member("member", "m@hello.com", "abc@@1234"); - final Member author = new Member("author", "a@hello.com", "abc@@1234"); + final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); + final Member author = new Member("author", "a@hello.com", Platform.KAKAO, "2"); final Rollingpaper rollingpaper = new Rollingpaper("alexAndKei", team, member); - assertThatThrownBy(() -> new Message("a".repeat(501), author, rollingpaper)) + assertThatThrownBy(() -> new Message("a".repeat(501), "green", author, rollingpaper)) .isInstanceOf(ExceedMessageContentLengthException.class); } + + @Test + @DisplayName("메시지 색상을 변경한다.") + void changeColor() { + final Team team = new Team( + "nae-pyeon", + "테스트 모임입니다.", + "testEmoji", + "#123456" + ); + final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); + final Member author = new Member("author", "a@hello.com", Platform.KAKAO, "2"); + final Rollingpaper rollingpaper = new Rollingpaper("alexAndKei", team, member); + final Message message = new Message("헬로우", "green", author, rollingpaper); + final String expected = "red"; + + message.changeColor(expected); + + assertThat(message.getColor()).isEqualTo(expected); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/domain/RollingpaperTest.java b/backend/src/test/java/com/woowacourse/naepyeon/domain/RollingpaperTest.java index 1ba63150..aeb18936 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/domain/RollingpaperTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/domain/RollingpaperTest.java @@ -18,7 +18,7 @@ void changeTitle() { "testEmoji", "#123456" ); - final Member member = new Member("member", "m@hello.com", "abc@@1234"); + final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); final Rollingpaper rollingpaper = new Rollingpaper("alexAndKei", team, member); final String expected = "kth990303"; @@ -36,7 +36,7 @@ void throwException_invalidChangeTitleLength() { "testEmoji", "#123456" ); - final Member member = new Member("member", "m@hello.com", "abc@@1234"); + final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); final Rollingpaper rollingpaper = new Rollingpaper("seungpang", team, member); final String invalidTitle = "seungapng, happy new year, good luck"; @@ -54,7 +54,7 @@ void throwException_invalidTitleLength() { "testEmoji", "#123456" ); - final Member member = new Member("member", "m@hello.com", "abc@@1234"); + final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); assertThatThrownBy(() -> new Rollingpaper("seungpang seungpang seungpang", team, member)) .isInstanceOf(ExceedRollingpaperNameLengthException.class); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/domain/TeamParticipationTest.java b/backend/src/test/java/com/woowacourse/naepyeon/domain/TeamParticipationTest.java index acd41045..dfd97f4a 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/domain/TeamParticipationTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/domain/TeamParticipationTest.java @@ -18,7 +18,7 @@ void changeNickname() { "testEmoji", "#123456" ); - final Member member = new Member("제로", "email@email.com", "zero1234"); + final Member member = new Member("제로", "email@email.com", Platform.KAKAO, "1"); final TeamParticipation teamParticipation = new TeamParticipation(team, member, "닉네임ㅋㅋ"); final String expected = "바뀌는닉네임ㅋㅋ"; @@ -36,7 +36,7 @@ void changeNicknameWithInvalidNickname() { "testEmoji", "#123456" ); - final Member member = new Member("제로", "email@email.com", "zero1234"); + final Member member = new Member("제로", "email@email.com", Platform.KAKAO, "1"); final TeamParticipation teamParticipation = new TeamParticipation(team, member, "닉네임ㅋㅋ"); assertThatThrownBy(() -> teamParticipation.changeNickname("asdsaddasdasddasdasdasdasdasdsadasdaasdasd")) diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java index d7023d69..86f8b659 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MemberRepositoryTest.java @@ -1,8 +1,12 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; +import java.time.LocalDateTime; +import javax.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -16,11 +20,14 @@ class MemberRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private EntityManager em; + @Test @DisplayName("회원을 id값으로 찾는다.") void findById() { // given - final Member member = new Member("seungpang", "email@email.com", "password123!A"); + final Member member = new Member("seungpang", "email@email.com", Platform.KAKAO, "1"); final Long memberId = memberRepository.save(member); // when @@ -28,15 +35,16 @@ void findById() { // then assertThat(findMember) - .extracting("id", "username", "email", "password") - .containsExactly(memberId, "seungpang", "email@email.com", "password123!A"); + .extracting("id", "username", "email", "platform", "platformId") + .containsExactly(memberId, "seungpang", "email@email.com", member.getPlatform(), + member.getPlatformId()); } @Test @DisplayName("회원을 id값을 통해 제거한다.") void delete() { // given - final Member member = new Member("seungpang", "email@email.com", "password123!A"); + final Member member = new Member("seungpang", "email@email.com", Platform.KAKAO, "1"); final Long memberId = memberRepository.save(member); // when @@ -46,4 +54,30 @@ void delete() { assertThat(memberRepository.findById(memberId)) .isEmpty(); } + + @Test + @DisplayName("회원을 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Member member = new Member("alex", "alex@naepyeon.com", Platform.KAKAO, "1"); + final Long memberId = memberRepository.save(member); + + final Member actual = memberRepository.findById(memberId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("회원 정보를 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() throws InterruptedException { + final Member member = new Member("alex", "alex@naepyeon.com", Platform.KAKAO, "1"); + final Long memberId = memberRepository.save(member); + + sleep(1); + member.changeUsername("kth990303"); + em.flush(); + + final Member actual = memberRepository.findById(memberId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java index 0e38581c..bbf36a9a 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/MessageRepositoryTest.java @@ -1,45 +1,62 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import com.woowacourse.naepyeon.domain.Member; import com.woowacourse.naepyeon.domain.Message; +import com.woowacourse.naepyeon.domain.Platform; import com.woowacourse.naepyeon.domain.Rollingpaper; import com.woowacourse.naepyeon.domain.Team; +import com.woowacourse.naepyeon.domain.TeamParticipation; +import com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto; +import java.time.LocalDateTime; 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.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.transaction.annotation.Transactional; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional class MessageRepositoryTest { - private static final String content = "안녕하세요"; - - private final Team team = new Team( - "nae-pyeon", - "테스트 모임입니다.", - "testEmoji", - "#123456" - ); - private final Member member = new Member("member", "email1@email.com", "password123"); - private final Member author = new Member("author", "email2@email.com", "password123"); - private final Rollingpaper rollingpaper = new Rollingpaper("AlexAndKei", team, member); + private static final String content = "안녕하세요😁"; @Autowired private TeamRepository teamRepository; + @Autowired private MemberRepository memberRepository; + @Autowired private RollingpaperRepository rollingpaperRepository; + @Autowired private MessageRepository messageRepository; + @Autowired + private TeamParticipationRepository teamParticipationRepository; + + @Autowired + private EntityManager em; + + private final Team team = new Team( + "nae-pyeon", + "테스트 모임입니다.", + "testEmoji", + "#123456" + ); + private final Member member = new Member("member", "email1@email.com", Platform.KAKAO, "1"); + private final Member author = new Member("author", "email2@email.com", Platform.KAKAO, "2"); + private final Rollingpaper rollingpaper = new Rollingpaper("AlexAndKei", team, member); + @BeforeEach void setUp() { teamRepository.save(team); @@ -81,17 +98,43 @@ void findAllByRollingpaperId() { assertThat(findMessages.size()).isEqualTo(2); } + @Test + @DisplayName("본인이 작성한 메시지들을 찾는다.") + void findAllByMemberIdAndPageRequest() { + final TeamParticipation teamParticipation1 = new TeamParticipation(team, member, "멤버"); + teamParticipationRepository.save(teamParticipation1); + final TeamParticipation teamParticipation2 = new TeamParticipation(team, author, "작성자"); + teamParticipationRepository.save(teamParticipation2); + + final Message message1 = createMessage(); + messageRepository.save(message1); + final Message message2 = createMessage(); + messageRepository.save(message2); + final Message message3 = createMessage(); + messageRepository.save(message3); + final Message message4 = createMessage(); + messageRepository.save(message4); + final Message message5 = createMessage(); + messageRepository.save(message5); + + final Page writtenMessageResponseDtos = + messageRepository.findAllByAuthorId(author.getId(), PageRequest.of(1, 2)); + final List actual = writtenMessageResponseDtos.getContent(); + + assertThat(actual).hasSize(2); + } @Test - @DisplayName("본인이 작성한 메시지 내용을 변경한다.") + @DisplayName("본인이 작성한 메시지 내용과 색상을 변경한다.") void update() { final Member member = memberRepository.findByEmail(author.getEmail()) .orElseThrow(); - final Message message = new Message(content, member, rollingpaper); + final Message message = new Message(content, "green", member, rollingpaper); final Long messageId = messageRepository.save(message); final String newContent = "알고리즘이 좋아요"; + final String newColor = "red"; - messageRepository.update(messageId, newContent); + messageRepository.update(messageId, newColor, newContent); final Message updateMessage = messageRepository.findById(messageId) .orElseThrow(); @@ -103,7 +146,7 @@ void update() { void delete() { final Member member = memberRepository.findByEmail(author.getEmail()) .orElseThrow(); - final Message message = new Message(content, member, rollingpaper); + final Message message = new Message(content, "green", member, rollingpaper); final Long messageId = messageRepository.save(message); messageRepository.delete(messageId); @@ -112,7 +155,33 @@ void delete() { .isEmpty(); } + @Test + @DisplayName("메시지를 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Message message = createMessage(); + final Long messageId = messageRepository.save(message); + + final Message actual = messageRepository.findById(messageId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("메시지를 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() throws InterruptedException { + final Message message = createMessage(); + final Long messageId = messageRepository.save(message); + + sleep(1); + message.changeContent("updateupdate"); + em.flush(); + + final Message actual = messageRepository.findById(messageId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } + private Message createMessage() { - return new Message(content, author, rollingpaper); + return new Message(content, "green", author, rollingpaper); } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java index afcd75fa..4a893268 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/RollingpaperRepositoryTest.java @@ -1,19 +1,25 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; import com.woowacourse.naepyeon.domain.Rollingpaper; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.repository.jpa.MemberJpaDao; import com.woowacourse.naepyeon.repository.jpa.TeamJpaDao; +import java.time.LocalDateTime; 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.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.transaction.annotation.Transactional; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -22,21 +28,26 @@ class RollingpaperRepositoryTest { private static final String rollingPaperTitle = "AlexAndKei"; - private final Team team = new Team( - "nae-pyeon", - "테스트 모임입니다.", - "testEmoji", - "#123456" - ); - private final Member member = new Member("member", "m@hello.com", "abc@@1234"); - @Autowired private TeamJpaDao teamJpaDao; + @Autowired private MemberJpaDao memberJpaDao; + @Autowired private RollingpaperRepository rollingpaperRepository; + @Autowired + private EntityManager em; + + private final Team team = new Team( + "nae-pyeon", + "테스트 모임입니다.", + "testEmoji", + "#123456" + ); + private final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); + @BeforeEach void setUp() { teamJpaDao.save(team); @@ -87,6 +98,32 @@ void findByMemberId() { assertThat(rollingpapers).hasSize(1); } + @Test + @DisplayName("롤링페이퍼들을 memberId와 PageRequest로 찾는다.") + void findByMemberIdAndPageRequest() { + final Rollingpaper rollingPaper1 = createRollingPaper(); + final Rollingpaper rollingPaper2 = createRollingPaper(); + final Rollingpaper rollingPaper3 = createRollingPaper(); + final Rollingpaper rollingPaper4 = createRollingPaper(); + final Rollingpaper rollingPaper5 = createRollingPaper(); + final Rollingpaper rollingPaper6 = createRollingPaper(); + final Rollingpaper rollingPaper7 = createRollingPaper(); + rollingpaperRepository.save(rollingPaper1); + rollingpaperRepository.save(rollingPaper2); + rollingpaperRepository.save(rollingPaper3); + rollingpaperRepository.save(rollingPaper4); + rollingpaperRepository.save(rollingPaper5); + rollingpaperRepository.save(rollingPaper6); + rollingpaperRepository.save(rollingPaper7); + + final Page actual = rollingpaperRepository.findByMemberId(member.getId(), PageRequest.of(1, 3)); + + assertAll( + () -> assertThat(actual).contains(rollingPaper4, rollingPaper5, rollingPaper6), + () -> assertThat(actual).doesNotContain(rollingPaper1, rollingPaper2, rollingPaper3, rollingPaper7) + ); + } + @Test @DisplayName("롤링페이퍼 타이틀을 변경한다.") void update() { @@ -113,6 +150,32 @@ void delete() { .isEmpty(); } + @Test + @DisplayName("롤링페이퍼를 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Rollingpaper message = createRollingPaper(); + final Long rollingpaperId = rollingpaperRepository.save(message); + + final Rollingpaper actual = rollingpaperRepository.findById(rollingpaperId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("롤링페이퍼를 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() throws InterruptedException { + final Rollingpaper rollingpaper = createRollingPaper(); + final Long rollingpaperId = rollingpaperRepository.save(rollingpaper); + + sleep(1); + rollingpaper.changeTitle("updateupdate"); + em.flush(); + + final Rollingpaper actual = rollingpaperRepository.findById(rollingpaperId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } + private Rollingpaper createRollingPaper() { return new Rollingpaper(rollingPaperTitle, team, member); } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java index 03be44cc..266c953a 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamParticipationRepositoryTest.java @@ -1,19 +1,25 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; 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.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; import com.woowacourse.naepyeon.exception.DuplicateTeamPaticipateException; +import java.time.LocalDateTime; 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.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.transaction.annotation.Transactional; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -29,8 +35,11 @@ class TeamParticipationRepositoryTest { @Autowired private MemberRepository memberRepository; - private final Member member1 = new Member("내편이1", "naePyeon1@test.com", "testtest123"); - private final Member member2 = new Member("내편이2", "naePyeon2@test.com", "testtest123"); + @Autowired + private EntityManager em; + + private final Member member1 = new Member("내편이1", "naePyeon1@test.com", Platform.KAKAO, "1"); + private final Member member2 = new Member("내편이2", "naePyeon2@test.com", Platform.KAKAO, "2"); private final Team team1 = new Team("wooteco1", "테스트 모임입니다.", "testEmoji", "#123456"); private final Team team2 = new Team("wooteco2", "테스트 모임입니다.", "testEmoji", "#123456"); private final Team team3 = new Team("wooteco3", "테스트 모임입니다.", "testEmoji", "#123456"); @@ -73,6 +82,17 @@ void duplicateSaveException() { .isInstanceOf(DuplicateTeamPaticipateException.class); } + @Test + @DisplayName("모임에 가입한 회원을 memberId와 teamId로 찾는다.") + void findMemberByMemberIdAndTeamId() { + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member1, "닉네임1"); + teamParticipationRepository.save(teamParticipation1); + + final Member actual = teamParticipationRepository.findMemberByMemberIdAndTeamId(member1.getId(), team1.getId()) + .orElseThrow(); + assertThat(actual).isEqualTo(member1); + } + @Test @DisplayName("모임에 가입한 회원들을 team id로 조회한다.") void findByTeamId() { @@ -101,9 +121,13 @@ void findTeamsByJoinedMemberId() { teamParticipationRepository.save(teamParticipation1); teamParticipationRepository.save(teamParticipation2); - final List joinedTeams = teamParticipationRepository.findTeamsByMemberId(member1.getId()); + final Page joinedTeams = + teamParticipationRepository.findTeamsByMemberIdAndPageRequest(member1.getId(), PageRequest.of(0, 1)); - assertThat(joinedTeams).contains(team1, team3); + assertAll( + () -> assertThat(joinedTeams).contains(team1), + () -> assertThat(joinedTeams).doesNotContain(team2, team3) + ); } @Test @@ -115,11 +139,25 @@ void findNicknameByAddresseeId() { teamParticipationRepository.save(teamParticipation1); final String actual = - teamParticipationRepository.findNicknameByMemberId(member1.getId(), team1.getId()); + teamParticipationRepository.findNicknameByMemberIdAndTeamId(member1.getId(), team1.getId()); assertThat(actual).isEqualTo(expected); } + @Test + @DisplayName("특정 팀의 모든 닉네임들을 조회한다.") + void findAllNicknamesByTeamId() { + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member1, "닉네임1"); + final TeamParticipation teamParticipation2 = new TeamParticipation(team1, member2, "닉네임2"); + + teamParticipationRepository.save(teamParticipation1); + teamParticipationRepository.save(teamParticipation2); + + final List actual = teamParticipationRepository.findAllNicknamesByTeamId(team1.getId()); + + assertThat(actual).contains("닉네임1", "닉네임2"); + } + @Test @DisplayName("특정 모임에 회원이 가입했는지 여부를 반환한다.") void isJoinedMember() { @@ -135,4 +173,32 @@ void isJoinedMember() { () -> assertThat(teamParticipationRepository.isJoinedMember(member2.getId(), team1.getId())).isFalse() ); } + + @Test + @DisplayName("회원 가입일자가 올바르게 나온다.") + void createMemberWhen() { + final TeamParticipation teamParticipation = new TeamParticipation(team1, member1, "닉네임1"); + final Long teamParticipationId = teamParticipationRepository.save(teamParticipation); + + final TeamParticipation actual = teamParticipationRepository.findById(teamParticipationId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("회원이 특정 팀의 닉네임을 변경한다.") + void updateNickname() throws InterruptedException { + final String expected = "닉네임2"; + final TeamParticipation teamParticipation = new TeamParticipation(team1, member1, "닉네임1"); + final Long teamParticipationId = teamParticipationRepository.save(teamParticipation); + + sleep(1); + teamParticipationRepository.updateNickname(expected, member1.getId(), team1.getId()); + em.flush(); + + final String actual = teamParticipationRepository.findById(teamParticipationId) + .orElseThrow() + .getNickname(); + assertThat(actual).isEqualTo(expected); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java index 53f10e6c..e7b2c7c9 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/repository/TeamRepositoryTest.java @@ -1,16 +1,21 @@ package com.woowacourse.naepyeon.repository; +import static java.lang.Thread.sleep; 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.naepyeon.domain.Team; import com.woowacourse.naepyeon.exception.NotFoundTeamException; +import java.time.LocalDateTime; import java.util.List; +import javax.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.transaction.annotation.Transactional; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -20,6 +25,9 @@ class TeamRepositoryTest { @Autowired private TeamRepository teamRepository; + @Autowired + private EntityManager em; + @Test @DisplayName("모임을 id로 찾는다.") void findById() { @@ -53,15 +61,45 @@ void delete() { } @Test - @DisplayName("존재하지 않는 모임을 삭제할 경우 예외를 발생시킨다.") - void deleteWithNotFoundTeam() { - // given - final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); - final Long teamId = teamRepository.save(team); + @DisplayName("특정 키워드가 포함된 모임 목록을 조회한다.") + void findTeamsByContainingTeamName() { + final Team team1 = new Team("woowacourse", "it's a.", "testEmoji", "#123456"); + final Team team2 = new Team("woowac", "it's b.", "testEmoji", "#123456"); + final Team team3 = new Team("woowc", "it's c.", "testEmoji", "#123456"); + teamRepository.save(team1); + teamRepository.save(team2); + teamRepository.save(team3); - // when // then - assertThatThrownBy(() -> teamRepository.delete(teamId + 1L)) - .isInstanceOf(NotFoundTeamException.class); + final Page teams = teamRepository.findTeamsByContainingTeamName("woowa", PageRequest.of(0, 5)); + assertAll( + () -> assertThat(teams).contains(team1, team2), + () -> assertThat(teams).doesNotContain(team3) + ); + } + + @Test + @DisplayName("특정 키워드가 포함된 모임 목록을 일부 페이지만 조회한다.") + void findTeamsByContainingTeamNameWithSomePages() { + final Team team1 = new Team("woowacourse", "it's a.", "testEmoji", "#123456"); + final Team team2 = new Team("woowa", "it's b.", "testEmoji", "#123456"); + final Team team3 = new Team("woowac", "it's c.", "testEmoji", "#123456"); + final Team team4 = new Team("woowaco", "it's d.", "testEmoji", "#123456"); + final Team team5 = new Team("woowacou", "it's e.", "testEmoji", "#123456"); + final Team team6 = new Team("woowacour", "it's f.", "testEmoji", "#123456"); + final Team team7 = new Team("woowacours", "it's g.", "testEmoji", "#123456"); + teamRepository.save(team1); + teamRepository.save(team2); + teamRepository.save(team3); + teamRepository.save(team4); + teamRepository.save(team5); + teamRepository.save(team6); + teamRepository.save(team7); + + final Page teams = teamRepository.findTeamsByContainingTeamName("woowa", PageRequest.of(1, 5)); + assertAll( + () -> assertThat(teams).contains(team6, team7), + () -> assertThat(teams).doesNotContain(team1, team2, team3, team4, team5) + ); } @Test @@ -79,4 +117,42 @@ void findAll() { () -> assertThat(teams).doesNotContain(team3) ); } + + @Test + @DisplayName("존재하지 않는 모임을 삭제할 경우 예외를 발생시킨다.") + void deleteWithNotFoundTeam() { + // given + final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); + final Long teamId = teamRepository.save(team); + + // when // then + assertThatThrownBy(() -> teamRepository.delete(teamId + 1L)) + .isInstanceOf(NotFoundTeamException.class); + } + + @Test + @DisplayName("모임을 생성할 때 생성일자가 올바르게 나온다.") + void createMemberWhen() { + final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); + final Long teamId = teamRepository.save(team); + + final Team actual = teamRepository.findById(teamId) + .orElseThrow(); + assertThat(actual.getCreatedDate()).isAfter(LocalDateTime.MIN); + } + + @Test + @DisplayName("모임을 수정할 때 수정일자가 올바르게 나온다.") + void updateMemberWhen() throws InterruptedException { + final Team team = new Team("woowacourse", "테스트 모임입니다.", "testEmoji", "#123456"); + final Long teamId = teamRepository.save(team); + + sleep(1); + team.changeName("updateupdate"); + em.flush(); + + final Team actual = teamRepository.findById(teamId) + .orElseThrow(); + assertThat(actual.getLastModifiedDate()).isAfter(actual.getCreatedDate()); + } } \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/service/AuthServiceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/service/AuthServiceTest.java index ff79d87b..e1eceac2 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/service/AuthServiceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/service/AuthServiceTest.java @@ -1,60 +1,81 @@ package com.woowacourse.naepyeon.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; -import com.woowacourse.naepyeon.exception.InvalidLoginException; +import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; +import com.woowacourse.naepyeon.repository.MemberRepository; +import com.woowacourse.naepyeon.service.dto.PlatformUserDto; import com.woowacourse.naepyeon.service.dto.TokenRequestDto; +import com.woowacourse.naepyeon.service.dto.TokenResponseDto; import com.woowacourse.naepyeon.support.JwtTokenProvider; +import com.woowacourse.naepyeon.support.oauth.kakao.KakaoPlatformUserProvider; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.transaction.annotation.Transactional; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Transactional class AuthServiceTest { - @Autowired - private AuthService authService; + @MockBean + private KakaoPlatformUserProvider kakaoPlatformUserProvider; @Autowired - private MemberService memberService; + private MemberRepository memberRepository; @Autowired private JwtTokenProvider jwtTokenProvider; - @Test - @DisplayName("이메일 비밀번호가 일치하는 경우 로그인에 성공한다.") - void successLogin() { - // given - final Long memberId = memberService.save("zero", "email@email.com", "password123!"); - - // when - final String accessToken = authService.createToken(new TokenRequestDto("email@email.com", "password123!")) - .getAccessToken(); - - // then - final String payload = jwtTokenProvider.getPayload(accessToken); - assertThat(String.valueOf(memberId)).isEqualTo(payload); + @Autowired + private MemberService memberService; + + private AuthService authService; + + @BeforeEach + void setUp() { + authService = new AuthService(memberService, jwtTokenProvider, kakaoPlatformUserProvider); } @Test - @DisplayName("이메일이 일치하지 않는 경우 로그인에 실패한다.") - void failEmailLogin() { - assertThatThrownBy(() -> authService.createToken(new TokenRequestDto("email@email.com", "password123!"))) - .isInstanceOf(InvalidLoginException.class); + @DisplayName("사용자 정보를 받아 토큰을 만들어 반환한다.") + void createToken() { + final PlatformUserDto platformUserDto = new PlatformUserDto("alex", "email@email.com", "KAKAO", "1"); + when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())).thenReturn(platformUserDto); + + final TokenRequestDto tokenRequestDto = new TokenRequestDto("authorizationCode", "https://..."); + final TokenResponseDto tokenResponseDto = authService.createTokenWithKakaoOauth(tokenRequestDto); + + assertThatCode(() -> jwtTokenProvider.validateAbleToken(tokenResponseDto.getAccessToken())) + .doesNotThrowAnyException(); } @Test - @DisplayName("비밀번호가 일치하지 않는 경우 로그인에 실패한다.") - void failPasswordLogin() { - // given - memberService.save("zero", "email@email.com", "password123!"); - - // when then - assertThatThrownBy(() -> authService.createToken(new TokenRequestDto("email@email.com", "password1234"))) - .isInstanceOf(InvalidLoginException.class); + @DisplayName("사용자 정보가 DB에 없는 경우 DB에 사용자 정보를 추가하고 토큰을 반환한다.") + void createTokenWithNotExistMember() { + final PlatformUserDto platformUserDto = new PlatformUserDto("alex", "email@email.com", "KAKAO", "1"); + when(kakaoPlatformUserProvider.getPlatformUser(anyString(), anyString())).thenReturn(platformUserDto); + final TokenRequestDto tokenRequestDto = new TokenRequestDto("authorizationCode", "https://..."); + + final TokenResponseDto tokenResponseDto = authService.createTokenWithKakaoOauth(tokenRequestDto); + final Long memberId = tokenResponseDto.getId(); + final Member findMember = memberRepository.findById(memberId) + .get(); + + assertThat(findMember).extracting("id", "username", "email", "platform", "platformId") + .containsExactly( + memberId, + platformUserDto.getUsername(), + platformUserDto.getEmail(), + Platform.valueOf(platformUserDto.getPlatform()), + platformUserDto.getPlatformId() + ); } } \ No newline at end of file diff --git a/backend/src/test/java/com/woowacourse/naepyeon/service/MemberServiceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/service/MemberServiceTest.java index dd096fd3..9ac9d0ab 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/service/MemberServiceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/service/MemberServiceTest.java @@ -22,7 +22,7 @@ class MemberServiceTest { @DisplayName("회원을 id값으로 찾는다.") void findById() { // given - final Long memberId = memberService.save("seungpang", "email@email.com", "password123!A"); + final Long memberId = memberService.save("seungpang", "email@email.com", "KAKAO", "1"); // when final MemberResponseDto findMember = memberService.findById(memberId); @@ -37,7 +37,7 @@ void findById() { @DisplayName("회원의 유저네임을 수정한다.") void update() { // given - final Long memberId = memberService.save("seungpang", "email@email.com", "password123!A"); + final Long memberId = memberService.save("seungpang", "email@email.com", "KAKAO", "2"); // when memberService.updateUsername(memberId, "zero"); @@ -52,7 +52,7 @@ void update() { @DisplayName("회원을 id값을 통해 제거한다.") void delete() { // given - final Long memberId = memberService.save("seungpang", "email@email.com", "password123!A"); + final Long memberId = memberService.save("seungpang", "email@email.com", "KAKAO", "3"); // when memberService.delete(memberId); diff --git a/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java index a6ca088d..edf4912e 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/service/MessageServiceTest.java @@ -5,6 +5,7 @@ import com.woowacourse.naepyeon.controller.dto.MessageRequest; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; import com.woowacourse.naepyeon.domain.Rollingpaper; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; @@ -15,6 +16,9 @@ import com.woowacourse.naepyeon.repository.TeamParticipationRepository; import com.woowacourse.naepyeon.repository.TeamRepository; import com.woowacourse.naepyeon.service.dto.MessageResponseDto; +import com.woowacourse.naepyeon.service.dto.WrittenMessageResponseDto; +import com.woowacourse.naepyeon.service.dto.WrittenMessagesResponseDto; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -31,10 +35,13 @@ class MessageServiceTest { "테스트 모임입니다.", "testEmoji", "#123456"); - private final Member member = new Member("member", "m@hello.com", "abc@@1234"); - private final Member author = new Member("author", "au@hello.com", "abc@@1234"); + private final Member member = new Member("member", "m@hello.com", Platform.KAKAO, "1"); + private final Member author = new Member("author", "au@hello.com", Platform.KAKAO, "2"); + private final Member otherAuthor = new Member("author2", "aut@hello.com", Platform.KAKAO, "3"); private final Rollingpaper rollingpaper = new Rollingpaper("AlexAndKei", team, member); - private final TeamParticipation teamParticipation = new TeamParticipation(team, author, "테스트닉네임"); + private final TeamParticipation teamParticipation1 = new TeamParticipation(team, member, "일케이"); + private final TeamParticipation teamParticipation2 = new TeamParticipation(team, author, "이케이"); + private final TeamParticipation teamParticipation3 = new TeamParticipation(team, otherAuthor, "삼케이"); @Autowired private MessageService messageService; @@ -53,8 +60,11 @@ void setUp() { teamRepository.save(team); memberRepository.save(member); memberRepository.save(author); + memberRepository.save(otherAuthor); rollingpaperRepository.save(rollingpaper); - teamParticipationRepository.save(teamParticipation); + teamParticipationRepository.save(teamParticipation1); + teamParticipationRepository.save(teamParticipation2); + teamParticipationRepository.save(teamParticipation3); } @Test @@ -62,33 +72,77 @@ void setUp() { void saveMessageAndFind() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); - final Team team = rollingpaper.getTeam(); final MessageResponseDto messageResponse = messageService.findMessage(messageId, rollingpaper.getId()); assertThat(messageResponse).extracting("content", "from", "authorId") - .containsExactly(messageRequest.getContent(), "테스트닉네임", author.getId()); + .containsExactly(messageRequest.getContent(), "이케이", author.getId()); } @Test - @DisplayName("메시지의 내용을 수정한다.") + @DisplayName("내가 작성한 메시지 목록을 조회한다.") + void findWrittenMessages() { + final MessageRequest messageRequest = createMessageRequest(); + final Long messageId = messageService.saveMessage( + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() + ); + messageService.saveMessage( + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), otherAuthor.getId() + ); + final Long messageId2 = messageService.saveMessage( + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() + ); + + final WrittenMessagesResponseDto writtenMessagesResponseDto = + messageService.findWrittenMessages(author.getId(), 0, 10); + final List actual = writtenMessagesResponseDto.getMessages(); + final List expected = List.of( + new WrittenMessageResponseDto( + messageId, + rollingpaper.getId(), + rollingpaper.getTitle(), + team.getId(), + team.getName(), + "일케이", + messageRequest.getContent(), + messageRequest.getColor() + ), + new WrittenMessageResponseDto( + messageId2, + rollingpaper.getId(), + rollingpaper.getTitle(), + team.getId(), + team.getName(), + "일케이", + messageRequest.getContent(), + messageRequest.getColor() + ) + ); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + @DisplayName("메시지 내용과 색상을 수정한다.") void updateContent() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); - final String expected = "안녕하지 못합니다."; + final String expectedContent = "안녕하지 못합니다."; + final String expectedColor = "red"; - messageService.updateContent(messageId, expected, author.getId()); + messageService.updateMessage(messageId, expectedContent, expectedColor, author.getId()); - final MessageResponseDto messageResponse = messageService.findMessage(messageId, rollingpaper.getId()); - assertThat(messageResponse.getContent()).isEqualTo(expected); + final MessageResponseDto actual = messageService.findMessage(messageId, rollingpaper.getId()); + final MessageResponseDto expected = + new MessageResponseDto(messageId, expectedContent, expectedColor, "이케이", author.getId()); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -96,13 +150,11 @@ void updateContent() { void updateContentWithNotAuthor() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); final String expected = "안녕하지 못합니다."; - assertThatThrownBy(() -> messageService.updateContent(messageId, expected, 9999L)) + assertThatThrownBy(() -> messageService.updateMessage(messageId, expected, "green", 9999L)) .isInstanceOf(NotAuthorException.class); } @@ -111,9 +163,7 @@ void updateContentWithNotAuthor() { void deleteMessage() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); messageService.deleteMessage(messageId, author.getId()); @@ -127,9 +177,7 @@ void deleteMessage() { void deleteMessageWithNotAuthor() { final MessageRequest messageRequest = createMessageRequest(); final Long messageId = messageService.saveMessage( - messageRequest.getContent(), - author.getId(), - rollingpaper.getId() + messageRequest.getContent(), messageRequest.getColor(), rollingpaper.getId(), author.getId() ); assertThatThrownBy(() -> messageService.deleteMessage(messageId, 9999L)) @@ -137,6 +185,6 @@ void deleteMessageWithNotAuthor() { } private MessageRequest createMessageRequest() { - return new MessageRequest("안녕하세요"); + return new MessageRequest("안녕하세요", "green"); } } diff --git a/backend/src/test/java/com/woowacourse/naepyeon/service/RollingpaperServiceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/service/RollingpaperServiceTest.java index 746135ce..5e90b8ee 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/service/RollingpaperServiceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/service/RollingpaperServiceTest.java @@ -7,6 +7,8 @@ import com.woowacourse.naepyeon.exception.NotFoundRollingpaperException; import com.woowacourse.naepyeon.exception.NotFoundTeamMemberException; import com.woowacourse.naepyeon.exception.UncertificationTeamMemberException; +import com.woowacourse.naepyeon.service.dto.ReceivedRollingpaperResponseDto; +import com.woowacourse.naepyeon.service.dto.ReceivedRollingpapersResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpaperPreviewResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpaperResponseDto; import com.woowacourse.naepyeon.service.dto.RollingpapersResponseDto; @@ -41,9 +43,9 @@ class RollingpaperServiceTest { @BeforeEach void setUp() { - memberId = memberService.save("member", "m@hello.com", "abc@@1234"); - member2Id = memberService.save("writer", "w@hello.com", "abc@@1234"); - member3Id = memberService.save("anonymous", "a@hello.com", "abc@@1234"); + memberId = memberService.save("member", "m@hello.com", "KAKAO", "1"); + member2Id = memberService.save("writer", "w@hello.com", "KAKAO", "2"); + member3Id = memberService.save("anonymous", "a@hello.com", "KAKAO", "3"); teamId = teamService.save(teamRequest, memberId); teamService.joinMember(teamId, member2Id, "안뇽안뇽"); } @@ -130,10 +132,10 @@ void findRollingpapersByMemberId() { // when final RollingpapersResponseDto responseDto = rollingpaperService.findByMemberId(teamId, memberId); - final List rollingpaperPreviewResponseDtos = responseDto.getRollingpapers(); + final List actual = responseDto.getRollingpapers(); // then - assertThat(rollingpaperPreviewResponseDtos) + assertThat(actual) .usingRecursiveComparison() .isEqualTo(expected); } @@ -152,6 +154,30 @@ private List convertPreviewDto(final Long rollin ); } + @Test + @DisplayName("내가 받은 롤링페이퍼 목록을 조회한다.") + void findReceivedRollingpapers() { + // given + final Long rollingpaperId1 = + rollingpaperService.createRollingpaper(rollingPaperTitle, teamId, member2Id, memberId); + final Long rollingpaperId2 = + rollingpaperService.createRollingpaper(rollingPaperTitle, teamId, member2Id, memberId); + + // when + final ReceivedRollingpapersResponseDto receivedRollingpapersResponseDto = + rollingpaperService.findReceivedRollingpapers(memberId, 0, 2); + final List actual = receivedRollingpapersResponseDto.getRollingpapers(); + final List expected = List.of( + new ReceivedRollingpaperResponseDto(rollingpaperId1, rollingPaperTitle, teamId, teamRequest.getName()), + new ReceivedRollingpaperResponseDto(rollingpaperId2, rollingPaperTitle, teamId, teamRequest.getName()) + ); + + // then + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); + } + @Test @DisplayName("롤링페이퍼 타이틀을 수정한다.") void updateTitle() { diff --git a/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java b/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java index e8435e3f..b5b4757c 100644 --- a/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java +++ b/backend/src/test/java/com/woowacourse/naepyeon/service/TeamServiceTest.java @@ -6,14 +6,19 @@ import com.woowacourse.naepyeon.controller.dto.TeamRequest; import com.woowacourse.naepyeon.domain.Member; +import com.woowacourse.naepyeon.domain.Platform; import com.woowacourse.naepyeon.domain.Team; import com.woowacourse.naepyeon.domain.TeamParticipation; +import com.woowacourse.naepyeon.exception.DuplicateNicknameException; import com.woowacourse.naepyeon.exception.NotFoundMemberException; import com.woowacourse.naepyeon.exception.NotFoundTeamException; +import com.woowacourse.naepyeon.exception.UncertificationTeamMemberException; import com.woowacourse.naepyeon.repository.MemberRepository; import com.woowacourse.naepyeon.repository.TeamParticipationRepository; import com.woowacourse.naepyeon.repository.TeamRepository; +import com.woowacourse.naepyeon.service.dto.AllTeamsResponseDto; import com.woowacourse.naepyeon.service.dto.JoinedMemberResponseDto; +import com.woowacourse.naepyeon.service.dto.TeamMemberResponseDto; import com.woowacourse.naepyeon.service.dto.TeamResponseDto; import com.woowacourse.naepyeon.service.dto.TeamsResponseDto; import java.util.List; @@ -29,8 +34,8 @@ @Transactional class TeamServiceTest { - private final Member member = new Member("내편이", "naePyeon@test.com", "testtest123"); - private final Member member2 = new Member("알렉스형", "alex@test.com", "testtest123"); + private final Member member = new Member("내편이", "naePyeon@test.com", Platform.KAKAO, "1"); + private final Member member2 = new Member("알렉스형", "alex@test.com", Platform.KAKAO, "2"); private final Team team1 = new Team("wooteco1", "테스트 모임입니다.", "testEmoji", "#123456"); private final Team team2 = new Team("wooteco2", "테스트 모임입니다.", "testEmoji", "#123456"); private final Team team3 = new Team("wooteco3", "테스트 모임입니다.", "testEmoji", "#123456"); @@ -94,7 +99,7 @@ void createTeamAndParticipateOwner() { ); final Long teamId = teamService.save(teamRequest, member.getId()); - final List joinedTeamIds = teamService.findByJoinedMemberId(member.getId()) + final List joinedTeamIds = teamService.findByJoinedMemberId(member.getId(), 0, 5) .getTeams() .stream() .map(TeamResponseDto::getId) @@ -183,10 +188,22 @@ void joinMemberWithNotFoundTeam() { .isInstanceOf(NotFoundTeamException.class); } + @Test + @DisplayName("이름에 특정 키워드가 포함된 모임들을 조회한다.") + void findTeamsByContainingTeamName() { + final List actual = + teamService.findTeamsByContainingTeamName("wooteco1", member.getId(), 0, 5) + .getTeams(); + final List expected = List.of(TeamResponseDto.of(team1, false)); + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(expected); + } + @Test @DisplayName("모든 모임을 조회한다.") void findAll() { - final TeamsResponseDto teams = teamService.findAll(member.getId()); + final AllTeamsResponseDto teams = teamService.findAll(member.getId()); final List teamNames = teams.getTeams() .stream() .map(TeamResponseDto::getName) @@ -203,7 +220,7 @@ void findByJoinedMemberId() { teamParticipationRepository.save(teamParticipation1); teamParticipationRepository.save(teamParticipation2); - final TeamsResponseDto teams = teamService.findByJoinedMemberId(member.getId()); + final TeamsResponseDto teams = teamService.findByJoinedMemberId(member.getId(), 0, 5); final List teamNames = teams.getTeams() .stream() .map(TeamResponseDto::getName) @@ -252,4 +269,79 @@ void isJoinedMemberWithNotFoundTeam() { assertThatThrownBy(() -> teamService.isJoinedMember(member.getId(), team1.getId() + 1000L)) .isInstanceOf(NotFoundTeamException.class); } -} \ No newline at end of file + + @Test + @DisplayName("모임에서의 마이페이지를 조회한다.") + void findMyInfoInTeam() { + final String expected = "닉네임1"; + final TeamParticipation teamParticipation = new TeamParticipation(team1, member, expected); + teamParticipationRepository.save(teamParticipation); + + final TeamMemberResponseDto actual = teamService.findMyInfoInTeam(team1.getId(), member.getId()); + + assertThat(actual.getNickname()).isEqualTo(expected); + } + + @Test + @DisplayName("모임 내 마이페이지 조회 창에서 가입한 모임이 아닐 경우 예외를 발생시킨다.") + void findMyInfoInOtherTeam() { + final TeamParticipation teamParticipation = new TeamParticipation(team1, member, "해커"); + teamParticipationRepository.save(teamParticipation); + + assertThatThrownBy(() -> teamService.findMyInfoInTeam(team2.getId(), member.getId())) + .isInstanceOf(UncertificationTeamMemberException.class); + } + + @Test + @DisplayName("존재하지 않는 모임에서 마이페이지를 조회할 경우 예외를 발생시킨다.") + void findMyInfoInNotExistTeam() { + assertThatThrownBy(() -> teamService.findMyInfoInTeam(team1.getId() + 10000L, member.getId())) + .isInstanceOf(NotFoundTeamException.class); + } + + @Test + @DisplayName("모임에 가입된 닉네임을 수정한다. 다른 모임에 해당 닉네임이 존재해도 수정에 문제되지 않는다.") + void updateNickname() { + final String expected = "닉네임1"; + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member, "닉네임1"); + final TeamParticipation teamParticipation2 = new TeamParticipation(team2, member2, "닉네임2"); + teamParticipationRepository.save(teamParticipation1); + teamParticipationRepository.save(teamParticipation2); + + teamService.updateMyInfo(team2.getId(), member2.getId(), expected); + + final String actual = teamParticipationRepository.findNicknameByMemberIdAndTeamId(member2.getId(), + team2.getId()); + assertThat(actual).isEqualTo(expected); + } + + @Test + @DisplayName("이미 존재하는 닉네임으로 닉네임을 수정할 경우 예외를 발생시킨다.") + void updateDuplicateNickname() { + final String expected = "닉네임1"; + final TeamParticipation teamParticipation1 = new TeamParticipation(team1, member, "닉네임1"); + final TeamParticipation teamParticipation2 = new TeamParticipation(team1, member2, "닉네임2"); + teamParticipationRepository.save(teamParticipation1); + teamParticipationRepository.save(teamParticipation2); + + assertThatThrownBy(() -> teamService.updateMyInfo(team1.getId(), member2.getId(), expected)) + .isInstanceOf(DuplicateNicknameException.class); + } + + @Test + @DisplayName("내가 가입되지 않은 모임에서 닉네임 변경을 하려 할 경우 예외를 발생시킨다.") + void updateNicknameNotMyTeam() { + final TeamParticipation teamParticipation = new TeamParticipation(team1, member, "해커"); + teamParticipationRepository.save(teamParticipation); + + assertThatThrownBy(() -> teamService.updateMyInfo(team2.getId(), member.getId(), "선량한시민")) + .isInstanceOf(UncertificationTeamMemberException.class); + } + + @Test + @DisplayName("존재하지 않는 모임에서 닉네임 변경을 하려 할 경우 예외를 발생시킨다.") + void updateNicknameNotExistTeam() { + assertThatThrownBy(() -> teamService.updateMyInfo(team1.getId() + 10000L, member.getId(), "해커")) + .isInstanceOf(NotFoundTeamException.class); + } +} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml new file mode 100644 index 00000000..d4f8fe79 --- /dev/null +++ b/backend/src/test/resources/application.yml @@ -0,0 +1,37 @@ +spring: + h2: + console: + enabled: true + profiles: + active: local + datasource: + url: jdbc:h2:mem:testdb + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 1000 #최적화 옵션 + open-in-view: false + + flyway: + enabled: false + +logging.level: + org.hibernate.SQL: debug + +security: + jwt: + token: + secret-key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.ih1aovtQShabQ7l0cINw4k1fagApg3qLWiB8Kt59Lno + expire-length: 3600000 + +kakao: + admin-key: admin_key + client-id: client_id + client-secret: client_secret \ No newline at end of file diff --git a/backend/src/test/resources/json/auth/login.json b/backend/src/test/resources/json/auth/login.json new file mode 100644 index 00000000..98ba8f01 --- /dev/null +++ b/backend/src/test/resources/json/auth/login.json @@ -0,0 +1,4 @@ +{ + "email": "email1@email.com", + "password": "password12!" +} diff --git a/backend/src/test/resources/json/members/member-create.json b/backend/src/test/resources/json/members/member-create.json new file mode 100644 index 00000000..ad3ab0df --- /dev/null +++ b/backend/src/test/resources/json/members/member-create.json @@ -0,0 +1,5 @@ +{ + "username": "seungpang", + "email": "seungpang@gmail.com", + "password": "1234567aA!" +} diff --git a/backend/src/test/resources/json/members/member-update.json b/backend/src/test/resources/json/members/member-update.json new file mode 100644 index 00000000..17af7686 --- /dev/null +++ b/backend/src/test/resources/json/members/member-update.json @@ -0,0 +1,3 @@ +{ + "username": "승팡" +} diff --git a/backend/src/test/resources/json/messages/message-create.json b/backend/src/test/resources/json/messages/message-create.json new file mode 100644 index 00000000..7bd6f8ea --- /dev/null +++ b/backend/src/test/resources/json/messages/message-create.json @@ -0,0 +1,4 @@ +{ + "content": "오늘 하루도 고생했어!\n 내일은 더 좋은 일들이 기다릴거야!", + "color": "#123456" +} diff --git a/backend/src/test/resources/json/messages/message-update.json b/backend/src/test/resources/json/messages/message-update.json new file mode 100644 index 00000000..acc6e71a --- /dev/null +++ b/backend/src/test/resources/json/messages/message-update.json @@ -0,0 +1,4 @@ +{ + "content": "발표 화이팅!", + "color": "#234567" +} diff --git a/backend/src/test/resources/json/rollingpapers/rollingpaper-update.json b/backend/src/test/resources/json/rollingpapers/rollingpaper-update.json new file mode 100644 index 00000000..96f774f1 --- /dev/null +++ b/backend/src/test/resources/json/rollingpapers/rollingpaper-update.json @@ -0,0 +1,3 @@ +{ + "title": "포비짱짱" +} diff --git a/backend/src/test/resources/json/teams/team-create.json b/backend/src/test/resources/json/teams/team-create.json new file mode 100644 index 00000000..46523d80 --- /dev/null +++ b/backend/src/test/resources/json/teams/team-create.json @@ -0,0 +1,7 @@ +{ + "name": "우아한테크코스3기", + "description": "우아한 테크코스 3기 구성원들의 모임입니다.", + "emoji": "\uD83D\uDE00", + "color": "141414", + "nickname": "자바지기" +} diff --git a/backend/src/test/resources/json/teams/team-join.json b/backend/src/test/resources/json/teams/team-join.json new file mode 100644 index 00000000..42f68e00 --- /dev/null +++ b/backend/src/test/resources/json/teams/team-join.json @@ -0,0 +1,3 @@ +{ + "nickname": "승팡" +} diff --git a/backend/src/test/resources/json/teams/team-update.json b/backend/src/test/resources/json/teams/team-update.json new file mode 100644 index 00000000..b417f4a4 --- /dev/null +++ b/backend/src/test/resources/json/teams/team-update.json @@ -0,0 +1,7 @@ +{ + "name": "우아한테크코스5기", + "description": "우아한 테크코스 5기 구성원들의 모임입니다.", + "emoji": "\uD83D\uDE00", + "color": "242424", + "nickname": "포비아님" +} diff --git a/backend/src/test/resources/json/teams/update-my-info.json b/backend/src/test/resources/json/teams/update-my-info.json new file mode 100644 index 00000000..7128fa62 --- /dev/null +++ b/backend/src/test/resources/json/teams/update-my-info.json @@ -0,0 +1,3 @@ +{ + "nickname": "승파팡" +} diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 7e26c1dc..0815a9f0 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -17,6 +17,17 @@ module.exports = { "@components": resolve(__dirname, "../src/components"), }; config.resolve.alias = Object.assign(config.resolve.alias, alias); + + const fileLoaderRule = config.module.rules.find( + (rule) => rule.test && rule.test.test(".svg") + ); + fileLoaderRule.exclude = /\.svg$/; + + config.module.rules.unshift({ + test: /\.svg$/, + use: ["@svgr/webpack"], + }); + return config; }, }; diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index f76babb5..a2ccdc89 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -1,16 +1,25 @@ import React from "react"; import { Global, ThemeProvider } from "@emotion/react"; -import reset from "../src/styles/reset"; +import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; + +import { UserProvider } from "../src/context/UserContext"; +import reset from "../src/styles/reset"; import theme from "../src/styles/theme"; +const queryClient = new QueryClient(); + export const decorators = [ (Story) => ( <> - - - - + + + + + + + + ), ]; diff --git a/frontend/package.json b/frontend/package.json index c2f8e585..34e8b419 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,9 +24,11 @@ "@storybook/manager-webpack5": "^6.5.9", "@storybook/react": "^6.5.9", "@storybook/testing-library": "^0.0.13", + "@svgr/webpack": "^6.3.1", "@typescript-eslint/eslint-plugin": "^5.30.0", "@typescript-eslint/parser": "^5.30.0", "babel-loader": "^8.2.5", + "dotenv": "^16.0.1", "eslint": "^8.18.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", @@ -41,14 +43,13 @@ "dependencies": { "@emotion/react": "^11.9.3", "@emotion/styled": "^11.9.3", + "@tanstack/react-query": "^4.0.10", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", "@types/react-router-dom": "^5.3.3", "axios": "^0.27.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-icons": "^4.4.0", - "react-query": "^3.39.1", "react-router-dom": "6", "storybook": "^6.5.9", "typescript": "^4.7.4" diff --git a/frontend/public/index.html b/frontend/public/index.html index cae9c876..03b7bf4c 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,5 +7,7 @@
+
+ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a185eda2..666ae002 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Global, ThemeProvider } from "@emotion/react"; import { Routes, Route } from "react-router-dom"; -import { QueryClient, QueryClientProvider } from "react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; import reset from "@/styles/reset"; import font from "@/styles/font"; @@ -10,25 +10,27 @@ import theme from "@/styles/theme"; import HeaderLayoutPage from "@/pages/HeaderLayoutPage"; import RollingpaperPage from "@/pages/RollingpaperPage"; import RollingpaperCreationPage from "@/pages/RollingpaperCreationPage"; -import MessageWritePage from "@/pages/MessageWritePage"; -import MessageEditPage from "@/pages/MessageEditPage"; -import MessageDetailPage from "@/pages/MessageDetailPage"; import TeamDetailPage from "@/pages/TeamDetailPage"; -import SignUpPage from "@/pages/SignUpPage"; import TeamCreationPage from "@/pages/TeamCreationPage"; import MainPage from "@/pages/MainPage"; import LoginPage from "@/pages/LoginPage"; import TeamSearch from "@/pages/TeamSearchPage"; import ErrorPage from "@/pages/ErrorPage"; +import KakaoRedirectPage from "@/pages/KakaoRedirectPage"; import RequireLogin from "@/components/RequireLogin"; -import RequireLogout from "./components/RequireLogout"; +import RequireLogout from "@/components/RequireLogout"; import PageContainer from "@/components/PageContainer"; +import MyPage from "@/pages/MyPage"; import { UserProvider } from "@/context/UserContext"; +import { useSnackbar } from "@/context/SnackbarContext"; +import Snackbar from "@/components/Snackbar"; -const queryClient = new QueryClient(); +import { queryClient } from "@/api"; const App = () => { + const { isOpened } = useSnackbar(); + return ( @@ -62,16 +64,16 @@ const App = () => { } /> + + + + } + /> } /> - - - - } - /> { } /> - { } /> - { } /> - - - - } - /> - - - - } - /> - - - - } - /> + } /> + {isOpened && } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index a5df5102..95106c85 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,14 +1,11 @@ import axios from "axios"; -import { API_URL } from "@/constants"; -import { getCookie } from "@/util/cookie"; - -const accessToken = getCookie("accessToken") || ""; +import { QueryClient } from "@tanstack/react-query"; const appClient = axios.create({ - baseURL: API_URL, + baseURL: process.env.API_URL, timeout: 3000, }); -appClient.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`; +const queryClient = new QueryClient(); -export default appClient; +export { appClient, queryClient }; diff --git a/frontend/src/api/kakaoOauth.ts b/frontend/src/api/kakaoOauth.ts new file mode 100644 index 00000000..825008b9 --- /dev/null +++ b/frontend/src/api/kakaoOauth.ts @@ -0,0 +1,16 @@ +import { appClient } from "@/api"; + +import { RequestKakaoOauthBody } from "@/types/oauth"; + +const postKakaoOauth = async ({ + authorizationCode, + redirectUri, +}: RequestKakaoOauthBody) => { + const response = await appClient.post("/oauth/kakao", { + authorizationCode, + redirectUri, + }); + return response.data; +}; + +export { postKakaoOauth }; diff --git a/frontend/src/api/member.ts b/frontend/src/api/member.ts new file mode 100644 index 00000000..ad7f31c6 --- /dev/null +++ b/frontend/src/api/member.ts @@ -0,0 +1,19 @@ +import { appClient } from "@/api"; + +const getMyUserInfo = () => { + return appClient.get("/members/me").then((response) => response.data); +}; + +const getMyReceivedRollingpapers = (page = 0, count = 5) => { + return appClient + .get(`/members/me/rollingpapers/received?page=${page}&count=${count}`) + .then((response) => response.data); +}; + +const getMySentMessage = (page = 0, count = 5) => { + return appClient + .get(`/members/me/messages/written?page=${page}&count=${count}`) + .then((response) => response.data); +}; + +export { getMyUserInfo, getMyReceivedRollingpapers, getMySentMessage }; diff --git a/frontend/src/api/team.ts b/frontend/src/api/team.ts new file mode 100644 index 00000000..2f5059fb --- /dev/null +++ b/frontend/src/api/team.ts @@ -0,0 +1,21 @@ +import { appClient } from "@/api"; + +const getMyTeams = + (teamPageCount = 5) => + async ({ pageParam = 0 }) => { + const data = appClient + .get(`teams/me?page=${pageParam}&count=${teamPageCount}`) + .then((response) => response.data); + return data; + }; + +const getTeamSearchResult = + ({ keyword, count }: { keyword: string; count: number }) => + async ({ pageParam = 0 }) => { + const data = appClient + .get(`teams?keyword=${keyword}&page=${pageParam}&count=${count}`) + .then((response) => response.data); + return data; + }; + +export { getMyTeams, getTeamSearchResult }; diff --git a/frontend/src/assets/icons/bx-check.svg b/frontend/src/assets/icons/bx-check.svg new file mode 100644 index 00000000..b21b4a71 --- /dev/null +++ b/frontend/src/assets/icons/bx-check.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-chevron-left.svg b/frontend/src/assets/icons/bx-chevron-left.svg new file mode 100644 index 00000000..5ce27e41 --- /dev/null +++ b/frontend/src/assets/icons/bx-chevron-left.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-chevron-right.svg b/frontend/src/assets/icons/bx-chevron-right.svg new file mode 100644 index 00000000..f5ee198e --- /dev/null +++ b/frontend/src/assets/icons/bx-chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/bx-dots-horizontal-rounded.svg b/frontend/src/assets/icons/bx-dots-horizontal-rounded.svg new file mode 100644 index 00000000..d36820da --- /dev/null +++ b/frontend/src/assets/icons/bx-dots-horizontal-rounded.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/icons/bx-hide.svg b/frontend/src/assets/icons/bx-hide.svg new file mode 100644 index 00000000..15cdc740 --- /dev/null +++ b/frontend/src/assets/icons/bx-hide.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-pencil.svg b/frontend/src/assets/icons/bx-pencil.svg new file mode 100644 index 00000000..4b296f3a --- /dev/null +++ b/frontend/src/assets/icons/bx-pencil.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-plus.svg b/frontend/src/assets/icons/bx-plus.svg new file mode 100644 index 00000000..65d5e0c3 --- /dev/null +++ b/frontend/src/assets/icons/bx-plus.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-search.svg b/frontend/src/assets/icons/bx-search.svg new file mode 100644 index 00000000..042f09c9 --- /dev/null +++ b/frontend/src/assets/icons/bx-search.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-show.svg b/frontend/src/assets/icons/bx-show.svg new file mode 100644 index 00000000..955ba98d --- /dev/null +++ b/frontend/src/assets/icons/bx-show.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-trash.svg b/frontend/src/assets/icons/bx-trash.svg new file mode 100644 index 00000000..3714577b --- /dev/null +++ b/frontend/src/assets/icons/bx-trash.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-user.svg b/frontend/src/assets/icons/bx-user.svg new file mode 100644 index 00000000..8aaaab30 --- /dev/null +++ b/frontend/src/assets/icons/bx-user.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bx-x.svg b/frontend/src/assets/icons/bx-x.svg new file mode 100644 index 00000000..e44ae00d --- /dev/null +++ b/frontend/src/assets/icons/bx-x.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/icons/bxs-error.svg b/frontend/src/assets/icons/bxs-error.svg new file mode 100644 index 00000000..cf13c1c1 --- /dev/null +++ b/frontend/src/assets/icons/bxs-error.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/images/empty-state.svg b/frontend/src/assets/images/empty-state.svg new file mode 100644 index 00000000..8cbab18f --- /dev/null +++ b/frontend/src/assets/images/empty-state.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/src/assets/images/logo-google.png b/frontend/src/assets/images/logo-google.png new file mode 100644 index 00000000..e5df190e Binary files /dev/null and b/frontend/src/assets/images/logo-google.png differ diff --git a/frontend/src/assets/images/kakao.png b/frontend/src/assets/images/logo-kakao.png similarity index 100% rename from frontend/src/assets/images/kakao.png rename to frontend/src/assets/images/logo-kakao.png diff --git a/frontend/src/assets/images/logo-naver.png b/frontend/src/assets/images/logo-naver.png new file mode 100644 index 00000000..117fb0c3 Binary files /dev/null and b/frontend/src/assets/images/logo-naver.png differ diff --git a/frontend/src/components/AutoCompleteInput.tsx b/frontend/src/components/AutoCompleteInput.tsx index 2a4f306d..4260a10d 100644 --- a/frontend/src/components/AutoCompleteInput.tsx +++ b/frontend/src/components/AutoCompleteInput.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import styled from "@emotion/styled"; -import { BiSearch } from "react-icons/bi"; +import SearchIcon from "@/assets/icons/bx-search.svg"; interface AutoCompleteInputProps extends React.InputHTMLAttributes { @@ -49,7 +49,7 @@ const AutoCompleteInput = ({ {labelText} - + void; +} +interface DropdownProp { + DropdownButton: React.ReactNode; + optionList: optionList[]; +} + +const Dropdown = ({ DropdownButton, optionList }: DropdownProp) => { + const [isOpened, setIsOpened] = useState(false); + + const handleButtonClick = () => { + setIsOpened((prev) => !prev); + }; + + const handleOptionClick = (callback: () => void) => { + setIsOpened(false); + callback(); + }; + + return ( + + + {isOpened && ( + + {optionList.map(({ option, callback }, index) => ( + + handleOptionClick(callback)}> + {option} + + {index < optionList.length - 1 &&
} +
+ ))} +
+ )} +
+ ); +}; + +const StyledDropdown = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: flex-end; +`; + +const fadeInDown = keyframes` + from { + opacity: 0; + margin-top: -10px; + } + + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +`; + +const StyledDropdownList = styled.ul` + position: absolute; + top: 24px; + width: 120px; + padding: 8px; + + z-index: 2; + + text-align: center; + + background-color: ${({ theme }) => theme.colors.WHITE}; + box-shadow: 0 4px 4px rgba(89, 87, 87, 0.25); + border-radius: 8px; + + animation: ${fadeInDown} 0.3s; + + hr { + border: 0.5px solid ${({ theme }) => theme.colors.GRAY_400}; + } +`; + +const StyledOption = styled.li` + font-size: 14px; + + cursor: pointer; +`; + +export default Dropdown; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 9ad06cfa..2431d870 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -2,19 +2,18 @@ import React, { useContext } from "react"; import { Link, useNavigate } from "react-router-dom"; import styled from "@emotion/styled"; -import Logo from "@/assets/images/logo.png"; -import { BiSearch, BiUser } from "react-icons/bi"; -import { deleteCookie } from "@/util/cookie"; import { UserContext } from "@/context/UserContext"; import IconButton from "./IconButton"; +import SearchIcon from "@/assets/icons/bx-search.svg"; +import UserIcon from "@/assets/icons/bx-user.svg"; + const Header = () => { - const { setIsLoggedIn } = useContext(UserContext); + const { logout } = useContext(UserContext); const navigate = useNavigate(); - const handleLogoutClick = () => { - deleteCookie("accessToken"); - setIsLoggedIn(false); + const handleMyPageClick = () => { + navigate("/mypage"); }; const handleSearchClick = () => { @@ -28,10 +27,10 @@ const Header = () => { - + - - + + @@ -39,6 +38,10 @@ const Header = () => { }; const StyledHeader = styled.header` + position: sticky; + top: 0px; + z-index: 9; + display: flex; align-items: center; justify-content: space-between; @@ -47,7 +50,7 @@ const StyledHeader = styled.header` width: 100%; height: 70px; - background-color: white; + background-color: ${({ theme }) => `${theme.colors.WHITE}e8`}; img { width: 30px; diff --git a/frontend/src/components/LabeledInput.tsx b/frontend/src/components/LabeledInput.tsx index 7df5af3a..48915690 100644 --- a/frontend/src/components/LabeledInput.tsx +++ b/frontend/src/components/LabeledInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "@emotion/styled"; -import { BiError } from "react-icons/bi"; +import ErrorIcon from "@/assets/icons/bxs-error.svg"; interface LabeledInputProps extends React.InputHTMLAttributes { @@ -32,7 +32,7 @@ const LabeledInput = ({ }} />
- + {errorMessage}
@@ -82,6 +82,8 @@ const StyledLabel = styled.label` position: relative; top: 2px; margin-right: 4px; + + fill: ${({ theme }) => theme.colors.RED_300}; } } `; diff --git a/frontend/src/components/LetterPaper.tsx b/frontend/src/components/LetterPaper.tsx deleted file mode 100644 index 9d419127..00000000 --- a/frontend/src/components/LetterPaper.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; -import { useNavigate, Link } from "react-router-dom"; - -import IconButton from "@components/IconButton"; -import RollingpaperMessage from "@components/RollingpaperMessage"; -import { Message } from "@/types"; - -import { BiPencil } from "react-icons/bi"; - -interface LetterPaperProp { - to: string; - messageList: Message[]; -} - -const LetterPaper = ({ to, messageList }: LetterPaperProp) => { - const navigate = useNavigate(); - - const handleMessageWriteButtonClick: React.MouseEventHandler< - HTMLButtonElement - > = (e) => { - e.preventDefault(); - navigate(`message/new`); - }; - - return ( - - - To. {to} - - - - - - {messageList.map((message) => ( - - - - ))} - - - ); -}; - -const StyledLetterPaper = styled.div` - width: 100%; - height: 100%; - padding: 20px; - - background: ${({ theme }) => theme.colors.GRAY_100}; - border-radius: 8px; -`; - -const StyledLetterPaperTop = styled.div` - display: flex; - align-items: center; - justify-content: space-between; - - height: 20px; - - margin-bottom: 20px; -`; - -const StyledTo = styled.h3` - font-weight: 600; -`; - -const StyledMessageList = styled.div` - display: grid; - grid-template-columns: repeat(2, 1fr); - grid-row-gap: 20px; - grid-column-gap: 20px; - justify-items: center; - - height: calc(100% - 40px); - - @media only screen and (min-width: 960px) { - grid-template-columns: repeat(3, 1fr); - } - - @media only screen and (min-width: 1280px) { - grid-template-columns: repeat(4, 1fr); - } -`; - -export default LetterPaper; diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index e966f706..e483354a 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -1,8 +1,9 @@ import React, { PropsWithChildren } from "react"; +import ReactDom from "react-dom"; import styled from "@emotion/styled"; import IconButton from "./IconButton"; -import { BiX } from "react-icons/bi"; +import XIcon from "@/assets/icons/bx-x.svg"; interface ModalProps { onClickCloseButton: React.MouseEventHandler; @@ -12,7 +13,7 @@ const Modal = ({ children, onClickCloseButton, }: PropsWithChildren) => { - return ( + return ReactDom.createPortal( <> @@ -21,7 +22,8 @@ const Modal = ({ {children} - + , + document.getElementById("modal__root")! ); }; @@ -30,7 +32,7 @@ const CloseButton = ({ }: React.ButtonHTMLAttributes) => { return ( - + ); }; @@ -45,6 +47,8 @@ const StyledDimmer = styled.div` background-color: ${({ theme }) => `${theme.colors.GRAY_700}66`}; backdrop-filter: blur(1px); + + z-index: 99; `; const StyledModalContainer = styled.div` @@ -60,12 +64,14 @@ const StyledModalContainer = styled.div` border-radius: 8px; background: #ffffff; box-shadow: 0px 4px 4px 2px rgba(147, 147, 147, 0.25); + + z-index: 99; `; const StyledCloseButtonWrapper = styled.div` position: absolute; top: 0; - left: 0; + right: 0; padding: 10px; `; diff --git a/frontend/src/components/PageTitleWithBackButton.tsx b/frontend/src/components/PageTitleWithBackButton.tsx index 0db6a40e..246dd683 100644 --- a/frontend/src/components/PageTitleWithBackButton.tsx +++ b/frontend/src/components/PageTitleWithBackButton.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren } from "react"; import styled from "@emotion/styled"; import { useNavigate } from "react-router-dom"; -import { BiChevronLeft } from "react-icons/bi"; +import ChevronIcon from "@/assets/icons/bx-chevron-left.svg"; import IconButton from "@components/IconButton"; @@ -16,7 +16,7 @@ const PageTitleWithBackButton = ({ children }: PropsWithChildren) => { return ( - +

{children}

diff --git a/frontend/src/components/Paging.tsx b/frontend/src/components/Paging.tsx new file mode 100644 index 00000000..1759f3f7 --- /dev/null +++ b/frontend/src/components/Paging.tsx @@ -0,0 +1,120 @@ +import React, { Dispatch, SetStateAction } from "react"; +import styled from "@emotion/styled"; + +import IconButton from "@/components/IconButton"; + +import LeftIcon from "@/assets/icons/bx-chevron-left.svg"; +import RightIcon from "@/assets/icons/bx-chevron-right.svg"; + +interface PagingProp { + currentPage: number; + maxPage: number; + setCurrentPage: Dispatch>; +} + +type StyledPaging = { + isCurrent: boolean; +}; + +const Paging = ({ currentPage, maxPage, setCurrentPage }: PagingProp) => { + const handleNumberClick = (number: number) => { + setCurrentPage(number); + }; + + const handleMinusClick = () => { + if (currentPage <= 0) { + return; + } + setCurrentPage((prev) => prev - 1); + }; + + const handlePlusClick = () => { + if (currentPage + 1 >= maxPage) { + return; + } + setCurrentPage((prev) => prev + 1); + }; + + return ( + + + + + {currentPage < 3 || maxPage <= 5 ? ( + + {[...Array(maxPage > 5 ? 5 : maxPage).keys()].map((num) => ( + handleNumberClick(num)} + > + {num + 1} + + ))} + + ) : currentPage > maxPage - 3 ? ( + + {[...Array(5).keys()].reverse().map((num) => ( + handleNumberClick(maxPage - num - 1)} + > + {maxPage - num} + + ))} + + ) : ( + + {[...Array(5).keys()].map((num) => ( + handleNumberClick(currentPage - (2 - num))} + > + {currentPage - (2 - num) + 1} + + ))} + + )} + + + + + ); +}; + +const StyledPaging = styled.div` + display: flex; + + svg { + font-size: 24px; + } +`; + +const StyledPageButtons = styled.div` + display: flex; +`; + +const StyledPage = styled.div` + width: 24px; + text-align: center; + line-height: 24px; + margin: 2px; + + color: ${(props) => + props.isCurrent ? props.theme.colors.WHITE : props.theme.colors.BLACK}; + + background-color: ${(props) => + props.isCurrent && props.theme.colors.SKY_BLUE_300}; + border-radius: 50%; + + cursor: pointer; + + &:hover { + background-color: ${(props) => + !props.isCurrent && props.theme.colors.SKY_BLUE_100}; + } +`; + +export default Paging; diff --git a/frontend/src/components/RollingpaperMessage.tsx b/frontend/src/components/RollingpaperMessage.tsx deleted file mode 100644 index b23f237b..00000000 --- a/frontend/src/components/RollingpaperMessage.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; - -interface RollingpaperMessageProp { - content: string; - author: string; -} - -const RollingpaperMessage = ({ content, author }: RollingpaperMessageProp) => { - return ( - - {content} - {author} - - ); -}; - -const StyledMessage = styled.div` - display: flex; - flex-direction: column; - justify-content: space-between; - white-space: pre-line; - - width: 130px; - height: 130px; - padding: 20px 15px 10px; - - background-color: ${({ theme }) => theme.colors.YELLOW_300}; - - @media only screen and (min-width: 600px) { - width: 180px; - height: 180px; - } -`; - -const StyledMessageContent = styled.div` - height: 64px; - overflow: hidden; - - font-size: 14px; - line-height: 16px; - - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 4; - - @media only screen and (min-width: 600px) { - height: 126px; - - font-size: 14px; - line-height: 18px; - - -webkit-line-clamp: 7; - } -`; - -const StyledMessageAuthor = styled.div` - align-self: flex-end; - - font-size: 12px; - color: ${({ theme }) => theme.colors.GRAY_700}; -`; - -export default RollingpaperMessage; diff --git a/frontend/src/components/RollingpaperMessageDetail.tsx b/frontend/src/components/RollingpaperMessageDetail.tsx deleted file mode 100644 index e132c196..00000000 --- a/frontend/src/components/RollingpaperMessageDetail.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; - -import { BiPencil, BiTrash } from "react-icons/bi"; - -import IconButton from "./IconButton"; - -interface RollingpaperMessageDetailProps { - content: string; - author: string; - handleDeleteButtonClick: (e: React.MouseEvent) => void; - handleEditButtonClick: (e: React.MouseEvent) => void; -} - -const RollingpaperMessageDetail = ({ - content, - author, - handleDeleteButtonClick, - handleEditButtonClick, -}: RollingpaperMessageDetailProps) => { - return ( - - {content} - - - - - - - - - - - {author} - - - ); -}; - -const StyledMessage = styled.div` - display: flex; - flex-direction: column; - justify-content: space-between; - - width: 300px; - min-height: 300px; - - padding: 20px; - - border: none; - border-radius: 8px; - background-color: ${({ theme }) => theme.colors.YELLOW_300}; - - @media only screen and (min-width: 960px) { - width: 600px; - min-height: 400px; - } -`; - -const StyledContent = styled.div` - height: 90%; - - white-space: pre-wrap; -`; - -const StyledBottom = styled.div` - height: 10%; - margin-top: 20px; - - display: flex; - align-items: center; - justify-content: space-between; -`; - -const StyledMenu = styled.div` - display: flex; - gap: 10px; -`; - -const StyledAuthor = styled.div``; - -export default RollingpaperMessageDetail; diff --git a/frontend/src/components/SearchInput.tsx b/frontend/src/components/SearchInput.tsx index d19cbba5..89b9a86b 100644 --- a/frontend/src/components/SearchInput.tsx +++ b/frontend/src/components/SearchInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "@emotion/styled"; -import { BiSearch } from "react-icons/bi"; +import SearchIcon from "@/assets/icons/bx-search.svg"; type ButtonAttributes = React.ButtonHTMLAttributes; type InputAttributes = React.InputHTMLAttributes; @@ -14,7 +14,7 @@ const SearchInput = ({ ); @@ -46,7 +46,7 @@ const StyledSearch = styled.form` padding: 4px; margin: auto; - color: ${({ theme }) => theme.colors.WHITE}; + fill: ${({ theme }) => theme.colors.WHITE}; font-size: 40px; } diff --git a/frontend/src/components/Snackbar.tsx b/frontend/src/components/Snackbar.tsx new file mode 100644 index 00000000..5313d2d9 --- /dev/null +++ b/frontend/src/components/Snackbar.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import ReactDom from "react-dom"; +import styled from "@emotion/styled"; +import { keyframes } from "@emotion/react"; + +import { useSnackbar } from "@/context/SnackbarContext"; + +const Snackbar = () => { + const { message } = useSnackbar(); + + return ReactDom.createPortal( + {message}, + document.getElementById("snackbar__root")! + ); +}; + +const fadein = keyframes` + from { + bottom: 0; + opacity: 0; + } + to { + bottom: 20px; + opacity: 1; + } +`; + +const StyledSnackbar = styled.div` + display: flex; + justify-content: center; + align-items: center; + position: fixed; + + bottom: 20px; + left: 50%; + margin-left: -125px; + padding: 8px; + + width: 250px; + + background-color: ${({ theme }) => theme.colors.GRAY_700}; + + color: ${({ theme }) => theme.colors.WHITE}; + + animation: ${fadein} 0.5s; +`; + +export default Snackbar; diff --git a/frontend/src/components/SocialLoginButton.tsx b/frontend/src/components/SocialLoginButton.tsx deleted file mode 100644 index 35a9b05a..00000000 --- a/frontend/src/components/SocialLoginButton.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; - -import kakao from "@/assets/images/kakao.png"; - -const SocialLoginButton = () => { - return ( - - - 카카오 계정으로 로그인 - - ); -}; - -const StyledSocialLoginButton = styled.button` - display: flex; - align-items: center; - - width: 100%; - height: 48px; - - padding: 0 20px; - - background-color: ${({ theme }) => theme.colors.GRAY_100}; - border-radius: 8px; - - font-size: 16px; - font-weight: 600; - - &:hover { - background-color: ${({ theme }) => theme.colors.GRAY_200}; - } - - img { - width: 30px; - height: 30px; - - margin-right: 8px; - } -`; - -export default SocialLoginButton; diff --git a/frontend/src/components/TeamJoinModalForm.tsx b/frontend/src/components/TeamJoinModalForm.tsx deleted file mode 100644 index 4704fc43..00000000 --- a/frontend/src/components/TeamJoinModalForm.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useMutation } from "react-query"; -import styled from "@emotion/styled"; - -import appClient from "@/api"; - -import LineButton from "@/components/LineButton"; -import Modal from "@/components/Modal"; -import UnderlineInput from "@/components/UnderlineInput"; - -import { REGEX } from "@/constants"; - -interface TeamJoinModalFormProp { - onClickCloseButton: React.MouseEventHandler; -} - -interface TeamJoinFormInfo { - nickname: string; -} - -const TeamJoinModalForm = ({ onClickCloseButton }: TeamJoinModalFormProp) => { - const navigate = useNavigate(); - const { teamId } = useParams(); - const [nickname, setNickname] = useState(""); - - const { mutate: joinTeam } = useMutation( - async ({ nickname }: TeamJoinFormInfo) => { - const response = await appClient.post(`/teams/${teamId}`, { nickname }); - return response.data; - }, - { - onSuccess: () => { - alert("가입 성공!"); - navigate(0); - }, - onError: (error) => { - console.log(error); - }, - } - ); - - const handleTeamJoinSubmit: React.FormEventHandler = (e) => { - e.preventDefault(); - - if (!REGEX.USERNAME.test(nickname)) { - alert("닉네임은 한글, 영어, 숫자만 가능합니다."); - return; - } - - joinTeam({ nickname }); - }; - - return ( - - -

모임에서 사용할 닉네임을 입력해주세요. (2 ~ 20자)

- - 모임 가입하기 -
-
- ); -}; - -const StyledJoinForm = styled.form` - display: flex; - flex-direction: column; - align-items: center; - - width: 100%; - height: 100%; - - p { - margin: 20px 0 100px; - } - - button { - margin-top: 8px; - } -`; - -export default TeamJoinModalForm; diff --git a/frontend/src/components/TextArea.tsx b/frontend/src/components/TextArea.tsx deleted file mode 100644 index 8fbef1b0..00000000 --- a/frontend/src/components/TextArea.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; - -type TextAreaAttributes = React.TextareaHTMLAttributes; - -const TextArea = React.forwardRef( - (props, ref) => { - return ; - } -); - -const StyledTextArea = styled.textarea` - width: 300px; - height: 300px; - padding: 20px; - - line-height: 1.4; - font-size: 16px; - - border: none; - border-radius: 8px; - background-color: ${({ theme }) => theme.colors.GRAY_100}; - - resize: none; - - :focus { - outline: none; - } - - @media only screen and (min-width: 960px) { - width: 600px; - height: 400px; - } -`; - -export default TextArea; diff --git a/frontend/src/components/UnderlineInput.tsx b/frontend/src/components/UnderlineInput.tsx index 11c25a1f..c63e2b41 100644 --- a/frontend/src/components/UnderlineInput.tsx +++ b/frontend/src/components/UnderlineInput.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "@emotion/styled"; -import { BiError } from "react-icons/bi"; +import ErrorIcon from "@/assets/icons/bxs-error.svg"; interface UnderlineInputProps extends React.InputHTMLAttributes { @@ -29,7 +29,7 @@ const UnderlineInput = ({ }} />
- + {errorMessage}
@@ -53,6 +53,8 @@ const StyledInputContainer = styled.div` position: relative; top: 2px; margin-right: 4px; + + fill: ${({ theme }) => theme.colors.RED_300}; } } `; diff --git a/frontend/src/stories/AutoCompleteInput.stories.jsx b/frontend/src/components/stories/AutoCompleteInput.stories.jsx similarity index 89% rename from frontend/src/stories/AutoCompleteInput.stories.jsx rename to frontend/src/components/stories/AutoCompleteInput.stories.jsx index ea810e92..a37b373f 100644 --- a/frontend/src/stories/AutoCompleteInput.stories.jsx +++ b/frontend/src/components/stories/AutoCompleteInput.stories.jsx @@ -3,7 +3,7 @@ import AutoCompleteInput from "@/components/AutoCompleteInput"; export default { component: AutoCompleteInput, - title: "AutoCompleteInput", + title: "components/common/AutoCompleteInput", }; const Template = (args) => ; diff --git a/frontend/src/stories/Button.stories.jsx b/frontend/src/components/stories/Button.stories.jsx similarity index 87% rename from frontend/src/stories/Button.stories.jsx rename to frontend/src/components/stories/Button.stories.jsx index 5f47997a..a086b031 100644 --- a/frontend/src/stories/Button.stories.jsx +++ b/frontend/src/components/stories/Button.stories.jsx @@ -3,7 +3,7 @@ import Button from "@/components/Button"; export default { component: Button, - title: "Button", + title: "components/common/Button", }; const Template = (args) => ; diff --git a/frontend/src/stories/Header.stories.jsx b/frontend/src/components/stories/Header.stories.jsx similarity index 89% rename from frontend/src/stories/Header.stories.jsx rename to frontend/src/components/stories/Header.stories.jsx index b6838296..928b70ea 100644 --- a/frontend/src/stories/Header.stories.jsx +++ b/frontend/src/components/stories/Header.stories.jsx @@ -4,7 +4,7 @@ import Header from "@/components/Header"; export default { component: Header, - title: "Header", + title: "components/common/Header", }; const Template = (args) => ( diff --git a/frontend/src/stories/IconButton.stories.jsx b/frontend/src/components/stories/IconButton.stories.jsx similarity index 66% rename from frontend/src/stories/IconButton.stories.jsx rename to frontend/src/components/stories/IconButton.stories.jsx index f4937263..c1f0cc18 100644 --- a/frontend/src/stories/IconButton.stories.jsx +++ b/frontend/src/components/stories/IconButton.stories.jsx @@ -1,15 +1,16 @@ import React from "react"; import IconButton from "@components/IconButton"; -import { BiPencil } from "react-icons/bi"; + +import PencilIcon from "@/assets/icons/bx-pencil.svg"; export default { component: IconButton, - title: "IconButton", + title: "components/common/IconButton", }; const Template = (args) => ; export const Default = Template.bind({}); Default.args = { - children: , + children: , }; diff --git a/frontend/src/stories/LabeledInput.stories.jsx b/frontend/src/components/stories/LabeledInput.stories.jsx similarity index 88% rename from frontend/src/stories/LabeledInput.stories.jsx rename to frontend/src/components/stories/LabeledInput.stories.jsx index ca6e137b..bb793c08 100644 --- a/frontend/src/stories/LabeledInput.stories.jsx +++ b/frontend/src/components/stories/LabeledInput.stories.jsx @@ -3,7 +3,7 @@ import LabeledInput from "@/components/LabeledInput"; export default { component: LabeledInput, - title: "LabeledInput", + title: "components/common/LabeledInput", }; const Template = (args) => ; diff --git a/frontend/src/stories/LabeledRadio.stories.jsx b/frontend/src/components/stories/LabeledRadio.stories.jsx similarity index 94% rename from frontend/src/stories/LabeledRadio.stories.jsx rename to frontend/src/components/stories/LabeledRadio.stories.jsx index f4803719..8becbedd 100644 --- a/frontend/src/stories/LabeledRadio.stories.jsx +++ b/frontend/src/components/stories/LabeledRadio.stories.jsx @@ -3,7 +3,7 @@ import LabeledRadio from "@/components/LabeledRadio"; export default { component: LabeledRadio, - title: "LabeledRadio", + title: "components/common/LabeledRadio", }; const Template = (args) => ; diff --git a/frontend/src/stories/LabeledTextArea.stories.jsx b/frontend/src/components/stories/LabeledTextArea.stories.jsx similarity index 87% rename from frontend/src/stories/LabeledTextArea.stories.jsx rename to frontend/src/components/stories/LabeledTextArea.stories.jsx index 535a1d41..5bb2fc1a 100644 --- a/frontend/src/stories/LabeledTextArea.stories.jsx +++ b/frontend/src/components/stories/LabeledTextArea.stories.jsx @@ -3,7 +3,7 @@ import LabeledTextArea from "@/components/LabeledTextArea"; export default { component: LabeledTextArea, - title: "LabeledTextArea", + title: "components/common/LabeledTextArea", }; const Template = (args) => ; diff --git a/frontend/src/stories/LineButton.stories.jsx b/frontend/src/components/stories/LineButton.stories.jsx similarity index 87% rename from frontend/src/stories/LineButton.stories.jsx rename to frontend/src/components/stories/LineButton.stories.jsx index e5df2722..4cad5ce4 100644 --- a/frontend/src/stories/LineButton.stories.jsx +++ b/frontend/src/components/stories/LineButton.stories.jsx @@ -3,7 +3,7 @@ import LineButton from "@/components/LineButton"; export default { component: LineButton, - title: "LineButton", + title: "components/common/LineButton", }; const Template = (args) => ; diff --git a/frontend/src/stories/Modal.stories.jsx b/frontend/src/components/stories/Modal.stories.jsx similarity index 88% rename from frontend/src/stories/Modal.stories.jsx rename to frontend/src/components/stories/Modal.stories.jsx index 5a5d95a2..7120a31e 100644 --- a/frontend/src/stories/Modal.stories.jsx +++ b/frontend/src/components/stories/Modal.stories.jsx @@ -3,7 +3,7 @@ import Modal from "@/components/Modal"; export default { component: Modal, - title: "Modal", + title: "components/common/Modal", }; const Template = (args) => ; diff --git a/frontend/src/components/stories/MoreDropdown.stories.jsx b/frontend/src/components/stories/MoreDropdown.stories.jsx new file mode 100644 index 00000000..46ea01bc --- /dev/null +++ b/frontend/src/components/stories/MoreDropdown.stories.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import Dropdown from "@/components/Dropdown"; + +export default { + component: Dropdown, + title: "Dropdown", +}; + +const Template = (args) => ; + +const teamOption = [ + { + option: "초대하기", + callback: () => { + console.log("초대하기"); + }, + }, + { + option: "모임 프로필 설정", + callback: () => { + console.log("모임 프로필 설정"); + }, + }, +]; + +export const Default = Template.bind({}); +Default.args = { + optionList: teamOption, +}; diff --git a/frontend/src/stories/PageTitle.stories.jsx b/frontend/src/components/stories/PageTitle.stories.jsx similarity index 87% rename from frontend/src/stories/PageTitle.stories.jsx rename to frontend/src/components/stories/PageTitle.stories.jsx index c7ed744d..316e3967 100644 --- a/frontend/src/stories/PageTitle.stories.jsx +++ b/frontend/src/components/stories/PageTitle.stories.jsx @@ -3,7 +3,7 @@ import PageTitle from "@/components/PageTitle"; export default { component: PageTitle, - title: "PageTitle", + title: "components/common/PageTitle", }; const Template = (args) => ; diff --git a/frontend/src/stories/PageTitleWithBackButton.stories.jsx b/frontend/src/components/stories/PageTitleWithBackButton.stories.jsx similarity index 58% rename from frontend/src/stories/PageTitleWithBackButton.stories.jsx rename to frontend/src/components/stories/PageTitleWithBackButton.stories.jsx index af030c62..46019aec 100644 --- a/frontend/src/stories/PageTitleWithBackButton.stories.jsx +++ b/frontend/src/components/stories/PageTitleWithBackButton.stories.jsx @@ -1,13 +1,17 @@ import React from "react"; +import { MemoryRouter } from "react-router-dom"; + import PageTitleWithBackButton from "@/components/PageTitleWithBackButton"; export default { component: PageTitleWithBackButton, - title: "PageTitleWithBackButton", + title: "components/common/PageTitleWithBackButton", }; const Template = (args) => ( - + + + ); export const Default = Template.bind({}); diff --git a/frontend/src/components/stories/Paging.stories.jsx b/frontend/src/components/stories/Paging.stories.jsx new file mode 100644 index 00000000..1d879d51 --- /dev/null +++ b/frontend/src/components/stories/Paging.stories.jsx @@ -0,0 +1,12 @@ +import React from "react"; +import Paging from "@/components/Paging"; + +export default { + component: Paging, + title: "components/common/Paging", +}; + +const Template = (args) => ; + +export const Default = Template.bind({}); +Default.args = { currentPage: 3, maxPage: 10 }; diff --git a/frontend/src/stories/SearchInput.stories.jsx b/frontend/src/components/stories/SearchInput.stories.jsx similarity index 85% rename from frontend/src/stories/SearchInput.stories.jsx rename to frontend/src/components/stories/SearchInput.stories.jsx index 09b90b92..90a0e187 100644 --- a/frontend/src/stories/SearchInput.stories.jsx +++ b/frontend/src/components/stories/SearchInput.stories.jsx @@ -3,7 +3,7 @@ import SearchInput from "@/components/SearchInput"; export default { component: SearchInput, - title: "SearchInput", + title: "components/common/SearchInput", }; const Template = (args) => ; diff --git a/frontend/src/stories/UnderlineInput.stories.jsx b/frontend/src/components/stories/UnderlineInput.stories.jsx similarity index 85% rename from frontend/src/stories/UnderlineInput.stories.jsx rename to frontend/src/components/stories/UnderlineInput.stories.jsx index fa96b0c7..fdd00039 100644 --- a/frontend/src/stories/UnderlineInput.stories.jsx +++ b/frontend/src/components/stories/UnderlineInput.stories.jsx @@ -3,7 +3,7 @@ import UnderlineInput from "@/components/UnderlineInput"; export default { component: UnderlineInput, - title: "UnderlineInput", + title: "components/common/UnderlineInput", }; const Template = (args) => ; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index a39fa369..ac53d1c2 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -1,13 +1,48 @@ +import { colors } from "@/styles/theme"; + const REGEX = { EMAIL: /^[_a-z0-9-]+(.[_a-z0-9-]+)*@(?:\w+\.)+\w+$/, - USERNAME: /^[가-힣a-zA-Z0-9]{2,20}$/, + USERNAME: /^.{1,64}$/, PASSWORD: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d~!@#$%^&*()+|=]{8,20}$/, TEAM_NAME: /^[가-힣a-zA-Z\d~!@#$%^&*()+_\-\s]{1,20}$/, + TEAM_NICKNAME: /^.{1,20}$/, + ROLLINGPAPER_TITLE: /^.{1,20}$/, +}; + +const COLORS = { + GREEN: colors.LIGHT_GREEN_300, + RED: colors.RED_300, + YELLOW: colors.YELLOW_300, + BLUE: colors.SKY_BLUE_300, + PURPLE: colors.PURPLE_300, + PINK: colors.PINK_300, }; -const API_URL = - process.env.NODE_ENV === "production" - ? "http://54.180.122.233:8080/api/v1" - : "/api/v1"; +const TOTAL_TEAMS_PAGING_COUNT = 5; + +const MYPAGE_ROLLINGPAPER_PAGING_COUNT = 5; +const MYPAGE_MESSAGE_PAGING_COUNT = 5; -export { REGEX, API_URL }; +const SOCIAL_LOGIN_PLATFORM = { + KAKAO: "KAKAO", + NAVER: "NAVER", + GOOGLE: "GOOGLE", +} as const; + +const KAKAO_OAUTH_URL = { + AUTHORIZE_CODE: `https://kauth.kakao.com/oauth/authorize?client_id=${process.env.KAKAO_REST_API_KEY}&redirect_uri=${process.env.KAKAO_REDIRECT_URL}&response_type=code`, + TOKEN: (authorize_code: string) => + `https://kauth.kakao.com/oauth/token?client_id=${process.env.KAKAO_REST_API_KEY}&redirect_uri=${process.env.KAKAO_REDIRECT_URL}&grant_type=authorization_code&client_secret=${process.env.KAKAO_CLIENT_SECRET}&code=${authorize_code}`, + USER_INFO: + 'https://kapi.kakao.com/v2/user/me?secure_resource=true&property_keys=["kakao_account.profile","kakao_account.email"]', +}; + +export { + REGEX, + COLORS, + TOTAL_TEAMS_PAGING_COUNT, + MYPAGE_ROLLINGPAPER_PAGING_COUNT, + MYPAGE_MESSAGE_PAGING_COUNT, + SOCIAL_LOGIN_PLATFORM, + KAKAO_OAUTH_URL, +}; diff --git a/frontend/src/context/SnackbarContext.tsx b/frontend/src/context/SnackbarContext.tsx new file mode 100644 index 00000000..9646e1ee --- /dev/null +++ b/frontend/src/context/SnackbarContext.tsx @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { useState, createContext, PropsWithChildren, useContext } from "react"; + +interface SnackbarContextType { + openSnackbar: (message: string) => void; + closeSnackbar: () => void; + message: string; + isOpened: boolean; +} + +const SnackbarContext = createContext({ + openSnackbar: () => {}, + closeSnackbar: () => {}, + message: "", + isOpened: true, +}); + +const SnackbarProvider = ({ children }: PropsWithChildren) => { + const [isOpened, setIsOpened] = useState(false); + const [message, setMessage] = useState(""); + + let timer: NodeJS.Timeout; + + const openSnackbar = (message: string) => { + setIsOpened(true); + setMessage(message); + + timer = setTimeout(() => { + closeSnackbar(); + }, 3000); + }; + + const closeSnackbar = () => { + clearTimeout(timer); + setIsOpened(false); + }; + + const value = { openSnackbar, closeSnackbar, message, isOpened }; + + return ( + + {children} + + ); +}; + +const useSnackbar = () => { + const context = useContext(SnackbarContext); + if (context === undefined) { + throw new Error("useSnackbar는 SnackbarContext가 필요합니다"); + } + return context; +}; + +export { useSnackbar, SnackbarProvider }; diff --git a/frontend/src/context/UserContext.tsx b/frontend/src/context/UserContext.tsx index 3383112d..420c26b2 100644 --- a/frontend/src/context/UserContext.tsx +++ b/frontend/src/context/UserContext.tsx @@ -1,17 +1,74 @@ -import { getCookie } from "@/util/cookie"; import { useState, createContext, PropsWithChildren } from "react"; +import { useQuery } from "@tanstack/react-query"; + +import { appClient } from "@/api"; +import { deleteCookie, getCookie, setCookie } from "@/util/cookie"; + +const COOKIE_KEY = { + ACCESS_TOKEN: "accessToken", +}; interface UserContextType { isLoggedIn: boolean; - setIsLoggedIn: (value: boolean) => void; + login: (accessToken: string, memberId: number) => void; + logout: () => void; + memberId: number | null; +} + +interface UserInfo { + id: number; + username: string; + email: string; } const UserContext = createContext(null!); const UserProvider = ({ children }: PropsWithChildren) => { - const [isLoggedIn, setIsLoggedIn] = useState(!!getCookie("accessToken")); + const accessTokenCookie = getCookie(COOKIE_KEY.ACCESS_TOKEN); + + const [isLoggedIn, setIsLoggedIn] = useState(!!accessTokenCookie); + const [memberId, setMemberId] = useState(null); + + useQuery( + ["memberId"], + () => + appClient + .get("/members/me", { + headers: { + Authorization: `Bearer ${accessTokenCookie || ""}`, + }, + }) + .then((response) => response.data), + { + enabled: !!accessTokenCookie, + onSuccess: (data) => { + setMemberId(data.id); + setIsLoggedIn(true); + + appClient.defaults.headers.common[ + "Authorization" + ] = `Bearer ${accessTokenCookie}`; + }, + } + ); + + const login = (accessToken: string, memberId: number) => { + appClient.defaults.headers.common[ + "Authorization" + ] = `Bearer ${accessToken}`; + setCookie(COOKIE_KEY.ACCESS_TOKEN, accessToken); + setIsLoggedIn(true); + setMemberId(memberId); + }; + + const logout = () => { + appClient.defaults.headers.common["Authorization"] = `Bearer `; + deleteCookie(COOKIE_KEY.ACCESS_TOKEN); + setIsLoggedIn(false); + setMemberId(null); + }; - const value = { isLoggedIn, setIsLoggedIn }; + const value = { isLoggedIn, login, logout, memberId }; return {children}; }; diff --git a/frontend/src/hooks/useIntersect.tsx b/frontend/src/hooks/useIntersect.tsx new file mode 100644 index 00000000..1b876435 --- /dev/null +++ b/frontend/src/hooks/useIntersect.tsx @@ -0,0 +1,36 @@ +import { useEffect, useRef } from "react"; + +type IntersectHandler = ( + entry: IntersectionObserverEntry, + observer: IntersectionObserver +) => void; + +const useIntersect = ( + onIntersect: IntersectHandler, + options?: IntersectionObserverInit +) => { + const ref = useRef(null); + const callback = ( + entries: IntersectionObserverEntry[], + observer: IntersectionObserver + ) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + onIntersect(entry, observer); + } + }); + }; + + useEffect(() => { + if (!ref.current) { + return; + } + const observer = new IntersectionObserver(callback, options); + observer.observe(ref.current); + return () => observer.disconnect(); + }, [ref, options, callback]); + + return ref; +}; + +export default useIntersect; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 5b7b95f1..ffeb05e4 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; +import { SnackbarProvider } from "@/context/SnackbarContext"; import App from "./App"; if (process.env.NODE_ENV === "development") { @@ -17,7 +18,9 @@ const root = ReactDOM.createRoot( root.render( - + + + ); diff --git a/frontend/src/mocks/dummy/myTeams.json b/frontend/src/mocks/dummy/myTeams.json index bb45b7a8..a576562d 100644 --- a/frontend/src/mocks/dummy/myTeams.json +++ b/frontend/src/mocks/dummy/myTeams.json @@ -127,6 +127,30 @@ "emoji": "😎", "color": "#98DAFF", "joined": true + }, + { + "id": 17, + "name": "우테코 4기", + "description": "우테코 4기 설명입니다", + "emoji": "😎", + "color": "#C5FF98", + "joined": true + }, + { + "id": 18, + "name": "우테코 4기", + "description": "우테코 4기 설명입니다", + "emoji": "😎", + "color": "#FFF598", + "joined": true + }, + { + "id": 19, + "name": "우테코 4기", + "description": "우테코 4기 설명입니다", + "emoji": "😎", + "color": "#98DAFF", + "joined": true } ] } diff --git a/frontend/src/mocks/dummy/receivedRollingpapers.json b/frontend/src/mocks/dummy/receivedRollingpapers.json new file mode 100644 index 00000000..24433612 --- /dev/null +++ b/frontend/src/mocks/dummy/receivedRollingpapers.json @@ -0,0 +1,64 @@ +{ + "rollingpapers": [ + { + "id": 1, + "title": "소피아 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기" + }, + { + "id": 2, + "title": "소피아 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기" + }, + { + "id": 3, + "title": "소피아 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기" + }, + { + "id": 4, + "title": "소피아 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기" + }, + { + "id": 5, + "title": "소피아 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기" + }, + { + "id": 11, + "title": "도리야 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기 프로젝트 내편" + }, + { + "id": 12, + "title": "도리야 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기 프로젝트 내편" + }, + { + "id": 13, + "title": "도리야 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기 프로젝트 내편" + }, + { + "id": 14, + "title": "도리야 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기 프로젝트 내편" + }, + { + "id": 15, + "title": "도리야 생일 축하해", + "teamId": 1, + "teamName": "우테코 4기 프로젝트 내편" + } + ] +} diff --git a/frontend/src/mocks/dummy/rollingpapers.json b/frontend/src/mocks/dummy/rollingpapers.json index 16491ef3..5c46cfef 100644 --- a/frontend/src/mocks/dummy/rollingpapers.json +++ b/frontend/src/mocks/dummy/rollingpapers.json @@ -9,61 +9,71 @@ "id": 1, "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일아", "from": "도리", - "authorId": 123 + "authorId": 123, + "color": "#C5FF98" }, { "id": 2, - "content": "소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생", - "from": "도리", - "authorId": 123 + "content": "소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n소피아\n\n\n생일축하해✨🚀\n소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀\n", + "from": "소피아", + "authorId": 1, + "color": "#FF8181" }, { "id": 3, "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일아", - "from": "도리", - "authorId": 123 + "from": "승팡", + "authorId": 2, + "color": "#FFF598" }, { "id": 4, - "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", - "from": "도리", - "authorId": 123 + "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", + "from": "제로", + "authorId": 3, + "color": "#98DAFF" }, { "id": 5, "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일아", - "from": "도리", - "authorId": 123 + "from": "케이", + "authorId": 4, + "color": "#98A2FF" }, { "id": 6, "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", - "from": "도리", - "authorId": 123 + "from": "알렉스", + "authorId": 5, + "color": "#FF98D0" }, { "id": 7, - "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일아", + "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해 소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해", "from": "도리", - "authorId": 123 + "authorId": 123, + "color": "#98A2FF" }, { "id": 8, - "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", + "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", "from": "도리", - "authorId": 123 + "authorId": 123, + "color": "#98A2FF" }, { "id": 9, - "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일아", + "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해 소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일축하해", "from": "도리", - "authorId": 123 + "authorId": 123, + "color": "#FFF598" }, { "id": 10, "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", "from": "도리", - "authorId": 123 + "authorId": 123, + "color": "#C5FF98" } ] }, @@ -76,13 +86,15 @@ "id": 1, "content": "소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀소피아생일축하해✨🚀", "from": "도리", - "authorId": 123 + "authorId": 123, + "color": "#C5FF98" }, { "id": 2, - "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피", + "content": "소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아 생일 축하해 아아아아아아아아소피아", "from": "도리", - "authorId": 123 + "authorId": 123, + "color": "#C5FF98" } ] } diff --git a/frontend/src/mocks/dummy/sentMessages.json b/frontend/src/mocks/dummy/sentMessages.json new file mode 100644 index 00000000..05a8a417 --- /dev/null +++ b/frontend/src/mocks/dummy/sentMessages.json @@ -0,0 +1,104 @@ +{ + "messages": [ + { + "id": 1, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#FFF598" + }, + { + "id": 2, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#C5FF98" + }, + { + "id": 3, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 FE", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#FFF598" + }, + { + "id": 4, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#FF8181" + }, + { + "id": 5, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#FFF598" + }, + { + "id": 6, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#98DAFF" + }, + { + "id": 7, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#98A2FF" + }, + { + "id": 8, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#FF98D0" + }, + { + "id": 9, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#FFF598" + }, + { + "id": 10, + "rollingpaperId": 1, + "rollingpaperTitle": "소피아의 생일을 축하해", + "teamId": 1, + "teamName": "우테코 4기", + "to": "소피아", + "content": "소피아야 생일 축하해~ 축카추카추 소피아야 생일 축하해~ 축카추카추", + "color": "#FFF598" + } + ] +} diff --git a/frontend/src/mocks/dummy/totalTeams.json b/frontend/src/mocks/dummy/totalTeams.json index 142517f7..c7d517e0 100644 --- a/frontend/src/mocks/dummy/totalTeams.json +++ b/frontend/src/mocks/dummy/totalTeams.json @@ -74,7 +74,7 @@ }, { "id": 10, - "name": "우테코 4기", + "name": "테스트", "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", "emoji": "😀", "color": "#C5FF98", @@ -82,7 +82,7 @@ }, { "id": 11, - "name": "우테코 4기", + "name": "테스트2", "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", "emoji": "😀", "color": "#C5FF98", @@ -90,6 +90,78 @@ }, { "id": 12, + "name": "테테테테테스트", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 13, + "name": "텟스트", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 14, + "name": "테스스스슷트", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 15, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 16, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 17, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 18, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 19, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 20, + "name": "우테코 4기", + "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", + "emoji": "😀", + "color": "#C5FF98", + "joined": true + }, + { + "id": 21, "name": "우테코 4기", "description": "우아한테크코스 4기 프론트앤드와 백엔드 모임입니다.", "emoji": "😀", diff --git a/frontend/src/mocks/handlers/memberHandlers.js b/frontend/src/mocks/handlers/memberHandlers.js index 0d3d29a1..0cd20e2e 100644 --- a/frontend/src/mocks/handlers/memberHandlers.js +++ b/frontend/src/mocks/handlers/memberHandlers.js @@ -1,21 +1,88 @@ import { rest } from "msw"; +import receivedRollinpapersDummy from "../dummy/receivedRollingpapers.json"; +import sentMessagesDummy from "../dummy/sentMessages.json"; const memberHandlers = [ - // 회원 가입 요쳥 - rest.post("/api/v1/members", (req, res, ctx) => { - const { username, email, password } = req.body; + // 로그인 + rest.post("/api/v1/login", (req, res, ctx) => { + const { platformType, platformId, email, username, profileImageUrl } = + req.body; - return res(ctx.status(201)); + const result = { accessToken: "accessToken2", id: 1 }; + + return res(ctx.status(200), ctx.json(result)); }), - // 로그인 - rest.post("/api/v1/login", (req, res, ctx) => { - const { email, password } = req.body; + // 카카오 OAuth + rest.post("/api/v1/oauth/kakao", (req, res, ctx) => { + const { authorizationCode, redirectUri } = req.body; - const result = { accessToken: "accessToken2" }; + const result = { accessToken: "accessToken2", id: 1 }; return res(ctx.status(200), ctx.json(result)); }), + + // 내 정보 조회 + rest.get("/api/v1/members/me", (req, res, ctx) => { + const accessToken = req.headers.headers.authorization.split(" ")[1]; + + if (!accessToken) { + return res(ctx.status(400)); + } + + const result = { + id: 123, + username: "우아한", + email: "woowa@woowa.com", + }; + + return res(ctx.json(result)); + }), + + // 내 정보 수정 + rest.put("/api/v1/members/me", (req, res, ctx) => { + const accessToken = req.headers.headers.authorization.split(" ")[1]; + + if (!accessToken) { + return res(ctx.status(400)); + } + + return res(ctx.status(204)); + }), + + // 내가 받은 롤링페이퍼 목록 조회 + rest.get("/api/v1/members/me/rollingpapers/received", (req, res, ctx) => { + const page = req.url.searchParams.get("page"); + const count = req.url.searchParams.get("count"); + + const begin = page % 2 === 1 ? 0 : 5; + const end = page % 2 === 1 ? 5 : 10; + + const result = { + totalCount: 160, + currentPage: Number(page), + rollingpapers: receivedRollinpapersDummy.rollingpapers.slice(0, 5), + }; + + return res(ctx.json(result)); + }), + + // 내가 쓴 메시지 목록 조회 + rest.get("/api/v1/members/me/messages/written", (req, res, ctx) => { + const page = req.url.searchParams.get("page"); + const count = req.url.searchParams.get("count"); + + const begin = page % 2 === 1 ? 0 : 5; + const end = page % 2 === 1 ? 5 : 10; + + const result = { + totalCount: 160, + currentPage: Number(page), + messages: sentMessagesDummy.messages.slice(0, 5), + }; + + return res(ctx.json(result)); + }), ]; export default memberHandlers; diff --git a/frontend/src/mocks/handlers/teamHandlers.js b/frontend/src/mocks/handlers/teamHandlers.js index c17686d6..3d4cb95c 100644 --- a/frontend/src/mocks/handlers/teamHandlers.js +++ b/frontend/src/mocks/handlers/teamHandlers.js @@ -1,3 +1,4 @@ +import { raw } from "@storybook/react"; import { rest } from "msw"; import myTeamsDummy from "../dummy/myTeams.json"; @@ -20,9 +21,13 @@ const teamHandlers = [ // 내가 가입한 모임 조회 rest.get("/api/v1/teams/me", (req, res, ctx) => { const accessToken = req.headers.headers.authorization; + const page = +req.url.searchParams.get("page"); + const count = +req.url.searchParams.get("count"); const result = { - teams: myTeams, + totalCount: myTeams.length, + currentPage: Number(page), + teams: myTeams.slice(page * count, page * count + count), }; return res(ctx.json(result)); @@ -31,9 +36,18 @@ const teamHandlers = [ // 전체 모임 조회 rest.get("/api/v1/teams", (req, res, ctx) => { const accessToken = req.headers.headers.authorization; + const keyword = req.url.searchParams.get("keyword"); + const page = +req.url.searchParams.get("page"); + const count = +req.url.searchParams.get("count"); + + const keywordTeam = totalTeams.filter((team) => + team.name.includes(keyword) + ); const result = { - teams: totalTeams, + totalCount: keywordTeam.length, + currentPage: Number(page), + teams: keywordTeam.slice(page * count, page * count + count), }; return res(ctx.json(result)); @@ -56,6 +70,11 @@ const teamHandlers = [ return res(ctx.status(204)); }), + // 모임 닉네임 수정 + rest.put("/api/v1/teams/:teamId/me", (req, res, ctx) => { + return res(ctx.status(204)); + }), + // 모임에 속한 회원 리스트 rest.get("/api/v1/teams/:teamId/members", (req, res, ctx) => { const result = { diff --git a/frontend/src/pages/ErrorPage.tsx b/frontend/src/pages/ErrorPage/index.tsx similarity index 100% rename from frontend/src/pages/ErrorPage.tsx rename to frontend/src/pages/ErrorPage/index.tsx diff --git a/frontend/src/pages/HeaderLayoutPage.tsx b/frontend/src/pages/HeaderLayoutPage/index.tsx similarity index 100% rename from frontend/src/pages/HeaderLayoutPage.tsx rename to frontend/src/pages/HeaderLayoutPage/index.tsx diff --git a/frontend/src/pages/KakaoRedirectPage/index.tsx b/frontend/src/pages/KakaoRedirectPage/index.tsx new file mode 100644 index 00000000..5cea49a6 --- /dev/null +++ b/frontend/src/pages/KakaoRedirectPage/index.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useContext } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { useMutation } from "@tanstack/react-query"; +import axios from "axios"; + +import { UserContext } from "@/context/UserContext"; + +import { postKakaoOauth } from "@/api/kakaoOauth"; + +import { CustomError } from "@/types"; +import { RequestKakaoOauthBody } from "@/types/oauth"; + +const KakaoRedirectPage = () => { + const navigate = useNavigate(); + const { login } = useContext(UserContext); + const params = new URLSearchParams(useLocation().search); + const authorizationCode = params.get("code"); + + const { mutate: kakaoOauthLogin } = useMutation( + ({ authorizationCode, redirectUri }: RequestKakaoOauthBody) => + postKakaoOauth({ + authorizationCode, + redirectUri, + }), + { + onSuccess: (data) => { + login(data.accessToken, data.id); + navigate(`/`, { replace: true }); + }, + onError: (error) => { + if (axios.isAxiosError(error) && error.response) { + const customError = error.response.data as CustomError; + console.error(customError.message); + } + }, + } + ); + + useEffect(() => { + const redirectUri = process.env.KAKAO_REDIRECT_URL; + if (!authorizationCode || !redirectUri) { + return; + } + + kakaoOauthLogin({ + authorizationCode, + redirectUri, + }); + }, []); + + return
KakaoRedirectPage
; +}; + +export default KakaoRedirectPage; diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx deleted file mode 100644 index 6ecce032..00000000 --- a/frontend/src/pages/LoginPage.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React, { useState, useContext } from "react"; -import { Link, useNavigate } from "react-router-dom"; -import { useMutation } from "react-query"; -import axios from "axios"; -import styled from "@emotion/styled"; - -import Logo from "@/components/Logo"; -import LabeledInput from "@/components/LabeledInput"; -import PasswordInput from "@/components/PasswordInput"; -import Button from "@/components/Button"; -import SocialLoginButton from "@/components/SocialLoginButton"; - -import { UserContext } from "@/context/UserContext"; - -import appClient from "@/api"; -import { setCookie } from "@/util/cookie"; -import { CustomError } from "@/types"; - -const LoginPage = () => { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [emailError, setEmailError] = useState(false); - const [passwordError, setPasswordError] = useState(false); - const { setIsLoggedIn } = useContext(UserContext); - - const navigate = useNavigate(); - - const { mutate: login } = useMutation( - () => { - return appClient - .post(`/login`, { - email, - password, - }) - .then((response) => response.data); - }, - { - onSuccess: (data) => { - appClient.defaults.headers.common[ - "Authorization" - ] = `Bearer ${data.accessToken}`; - setCookie("accessToken", data.accessToken); - - setIsLoggedIn(true); - - navigate(`/`, { replace: true }); - }, - onError: (error) => { - if (axios.isAxiosError(error) && error.response) { - const customError = error.response.data as CustomError; - alert(customError.message); - } - }, - } - ); - - const handleLoginSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!email) { - setEmailError(true); - return; - } - - if (!password) { - setPasswordError(true); - return; - } - - login(); - }; - - return ( - <> - - -
내 마음을 편지로
-
- -
- - - - {(emailError && "이메일을 입력해주세요") || - (passwordError && "비밀번호를 입력해주세요")} - - - -
- - - 아직 내편의 회원이 아닌가요? 회원가입 - -
- - ); -}; - -const StyledTitle = styled.div` - display: flex; - flex-direction: column; - - padding: 15% 0; - - align-items: center; - - color: ${({ theme }) => theme.colors.SKY_BLUE_300}; -`; - -const StyledMain = styled.div` - display: flex; - flex-direction: column; - align-items: center; - - form { - width: 100%; - - button { - margin-top: 24px; - } - } - - hr { - width: 100%; - margin: 40px 0; - - background-color: ${({ theme }) => theme.colors.GRAY_300}; - height: 2px; - border: none; - } -`; - -const StyledGuideText = styled.div` - margin: 20px 0; - - a { - color: ${({ theme }) => theme.colors.SKY_BLUE_300}; - - &:hover { - color: ${({ theme }) => theme.colors.SKY_BLUE_400}; - } - } -`; - -const StyledErrorMessage = styled.div` - margin-top: 8px; - font-size: 14px; - color: ${({ theme }) => theme.colors.RED_300}; -`; - -export default LoginPage; diff --git a/frontend/src/components/Logo.tsx b/frontend/src/pages/LoginPage/components/Logo.tsx similarity index 69% rename from frontend/src/components/Logo.tsx rename to frontend/src/pages/LoginPage/components/Logo.tsx index e04158c7..7a10249f 100644 --- a/frontend/src/components/Logo.tsx +++ b/frontend/src/pages/LoginPage/components/Logo.tsx @@ -22,11 +22,22 @@ const StyledLogo = styled.div` width: 56px; height: 60px; margin-right: 10px; + + @media only screen and (min-width: 960px) { + width: 84px; + height: 90px; + margin-right: 15px; + } } h1 { margin-top: 8px; font-size: 56px; + + @media only screen and (min-width: 960px) { + margin-top: 12px; + font-size: 84px; + } } `; diff --git a/frontend/src/stories/PasswordInput.stories.jsx b/frontend/src/pages/LoginPage/components/PasswordInput.stories.jsx similarity index 63% rename from frontend/src/stories/PasswordInput.stories.jsx rename to frontend/src/pages/LoginPage/components/PasswordInput.stories.jsx index ccd8f486..26f3a27b 100644 --- a/frontend/src/stories/PasswordInput.stories.jsx +++ b/frontend/src/pages/LoginPage/components/PasswordInput.stories.jsx @@ -1,9 +1,9 @@ import React from "react"; -import PasswordInput from "@/components/PasswordInput"; +import PasswordInput from "@/pages/LoginPage/components/PasswordInput"; export default { component: PasswordInput, - title: "PasswordInput", + title: "components/LoginPage/PasswordInput", }; const Template = (args) => ; diff --git a/frontend/src/components/PasswordInput.tsx b/frontend/src/pages/LoginPage/components/PasswordInput.tsx similarity index 81% rename from frontend/src/components/PasswordInput.tsx rename to frontend/src/pages/LoginPage/components/PasswordInput.tsx index ff010ca7..7880b620 100644 --- a/frontend/src/components/PasswordInput.tsx +++ b/frontend/src/pages/LoginPage/components/PasswordInput.tsx @@ -1,7 +1,8 @@ import React, { useState } from "react"; import styled from "@emotion/styled"; -import { BiShow, BiHide } from "react-icons/bi"; +import ShowIcon from "@/assets/icons/bx-show.svg"; +import HideIcon from "@/assets/icons/bx-hide.svg"; interface PasswordInput extends React.InputHTMLAttributes { labelText: string; @@ -24,8 +25,8 @@ const PasswordInput = ({ labelText, value, setValue }: PasswordInput) => { value={value} onChange={(e) => setValue(e.target.value)} /> - {showPassword && } - {!showPassword && } + {showPassword && } + {!showPassword && } ); @@ -36,7 +37,6 @@ const StyledLabel = styled.label` flex-direction: column; font-size: 14px; - color: ${({ theme }) => theme.colors.GRAY_600}; `; const StyledPasswordInput = styled.div` @@ -52,6 +52,10 @@ const StyledPasswordInput = styled.div` svg { margin-top: 8px; font-size: 24px; + + fill: ${({ theme }) => theme.colors.GRAY_600}; + + cursor: pointer; } input { diff --git a/frontend/src/stories/SocialLoginButton.stories.jsx b/frontend/src/pages/LoginPage/components/SocialLoginButton.stories.jsx similarity index 62% rename from frontend/src/stories/SocialLoginButton.stories.jsx rename to frontend/src/pages/LoginPage/components/SocialLoginButton.stories.jsx index bc9d959b..114ed4ec 100644 --- a/frontend/src/stories/SocialLoginButton.stories.jsx +++ b/frontend/src/pages/LoginPage/components/SocialLoginButton.stories.jsx @@ -1,9 +1,9 @@ import React from "react"; -import SocialLoginButton from "@/components/SocialLoginButton"; +import SocialLoginButton from "@/pages/LoginPage/components/SocialLoginButton"; export default { component: SocialLoginButton, - title: "SocialLoginButton", + title: "components/LoginPage/SocialLoginButton", }; const Template = (args) => ; diff --git a/frontend/src/pages/LoginPage/components/SocialLoginButton.tsx b/frontend/src/pages/LoginPage/components/SocialLoginButton.tsx new file mode 100644 index 00000000..57a8311b --- /dev/null +++ b/frontend/src/pages/LoginPage/components/SocialLoginButton.tsx @@ -0,0 +1,63 @@ +import React, { HTMLAttributes } from "react"; +import styled from "@emotion/styled"; + +import { ValueOf } from "@/types"; +import { SOCIAL_LOGIN_PLATFORM } from "@/constants"; + +import GoogleLogo from "@/assets/images/logo-google.png"; +import KakaoLogo from "@/assets/images/logo-kakao.png"; +import NaverLogo from "@/assets/images/logo-naver.png"; + +const LOGO_ICON = { + GOOGLE: GoogleLogo, + KAKAO: KakaoLogo, + NAVER: NaverLogo, +}; + +const BUTTON_TEXT = { + GOOGLE: "구글로 시작하기", + KAKAO: "카카오로 시작하기", + NAVER: "네이버로 시작하기", +}; + +type SocialLoginButtonProps = HTMLAttributes & { + platform: ValueOf; +}; + +const SocialLoginButton = ({ onClick, platform }: SocialLoginButtonProps) => { + return ( + + + {BUTTON_TEXT[platform]} + + ); +}; + +const StyledSocialLoginButton = styled.button` + display: flex; + align-items: center; + + width: 100%; + height: 48px; + + padding: 0 20px; + + background-color: ${({ theme }) => theme.colors.GRAY_100}; + border-radius: 8px; + + font-size: 16px; + font-weight: 600; + + &:hover { + background-color: ${({ theme }) => theme.colors.GRAY_200}; + } + + img { + width: 30px; + height: 30px; + + margin-right: 12px; + } +`; + +export default SocialLoginButton; diff --git a/frontend/src/pages/LoginPage/index.tsx b/frontend/src/pages/LoginPage/index.tsx new file mode 100644 index 00000000..30a55fa6 --- /dev/null +++ b/frontend/src/pages/LoginPage/index.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import styled from "@emotion/styled"; + +import { useSnackbar } from "@/context/SnackbarContext"; + +import Logo from "@/pages/LoginPage/components/Logo"; +import SocialLoginButton from "@/pages/LoginPage/components/SocialLoginButton"; + +import { KAKAO_OAUTH_URL, SOCIAL_LOGIN_PLATFORM } from "@/constants"; + +const LoginPage = () => { + const { openSnackbar } = useSnackbar(); + + const handleKakaoLoginButtonClick: React.MouseEventHandler< + HTMLButtonElement + > = () => { + location.href = KAKAO_OAUTH_URL.AUTHORIZE_CODE; + }; + + const handleNaverLoginButtonClick: React.MouseEventHandler< + HTMLButtonElement + > = () => { + openSnackbar("준비 중! 🤗"); + }; + + const handleGoogleLoginButtonClick: React.MouseEventHandler< + HTMLButtonElement + > = () => { + openSnackbar("준비 중! 🤗"); + }; + + return ( + + + +
내 마음을 편지로
+
+ + + + + +
+ ); +}; + +const StyledMain = styled.main` + display: flex; + flex-direction: column; + justify-content: space-around; + + height: 100vh; +`; + +const StyledTitle = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + color: ${({ theme }) => theme.colors.SKY_BLUE_300}; + + @media only screen and (min-width: 960px) { + font-size: 24px; + } +`; + +const StyledSocialLoginButtonContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + + button { + width: 100%; + + @media only screen and (min-width: 960px) { + width: 500px; + } + } +`; + +export default LoginPage; diff --git a/frontend/src/pages/MainPage/components/EmptyMyTeamList.tsx b/frontend/src/pages/MainPage/components/EmptyMyTeamList.tsx new file mode 100644 index 00000000..cc2206c9 --- /dev/null +++ b/frontend/src/pages/MainPage/components/EmptyMyTeamList.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import styled from "@emotion/styled"; + +import EmptyStateImg from "@/assets/images/empty-state.svg"; +import LineButton from "@/components/LineButton"; + +const EmptyState = () => { + const navigate = useNavigate(); + + const handleJoinTeamButtonClick = () => { + navigate("/search"); + }; + + const handleCreateTeamButtonClick = () => { + navigate("/team/new"); + }; + + return ( + <> + + 아직 참여한 모임이 없어요! + + 참가할 모임 찾기 + + + 새 모임 만들기 + + + ); +}; + +const StyledMessage = styled.div` + font-size: 24px; + font-weight: 600; + color: ${({ theme }) => theme.colors.GRAY_400}; + + margin-bottom: 20px; +`; + +export default EmptyState; diff --git a/frontend/src/pages/MainPage/components/MyTeamCard.stories.jsx b/frontend/src/pages/MainPage/components/MyTeamCard.stories.jsx new file mode 100644 index 00000000..0a3701a4 --- /dev/null +++ b/frontend/src/pages/MainPage/components/MyTeamCard.stories.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import { MemoryRouter } from "react-router-dom"; + +import MyTeamCard from "@/pages/MainPage/components/MyTeamCard"; + +export default { + component: MyTeamCard, + title: "components/MainPage/MyTeamCard", +}; + +const Template = (args) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = { + name: "우테코 4기", + description: "우테코 4기 설명입니다", + emoji: "😎", + color: "#FF8181", +}; diff --git a/frontend/src/components/MyTeamCard.tsx b/frontend/src/pages/MainPage/components/MyTeamCard.tsx similarity index 92% rename from frontend/src/components/MyTeamCard.tsx rename to frontend/src/pages/MainPage/components/MyTeamCard.tsx index b6e977ec..8454815b 100644 --- a/frontend/src/components/MyTeamCard.tsx +++ b/frontend/src/pages/MainPage/components/MyTeamCard.tsx @@ -36,13 +36,13 @@ const StyledMyTeamCard = styled.div` display: flex; flex-direction: column; - width: 160px; - height: 210px; + width: 140px; + height: 190px; - padding: 20px 14px; - border-radius: 8px; + padding: 18px 14px; background-color: ${(props) => `${props.color}AB`}; + border-radius: 8px; transition-duration: 0.3s; @@ -53,6 +53,13 @@ const StyledMyTeamCard = styled.div` transition: transform 0.3s ease; } + @media only screen and (min-width: 400px) { + width: 160px; + height: 210px; + + padding: 20px 14px; + } + @media only screen and (min-width: 600px) { width: 200px; height: 250px; diff --git a/frontend/src/pages/MainPage/components/TeamCreateButton.stories.jsx b/frontend/src/pages/MainPage/components/TeamCreateButton.stories.jsx new file mode 100644 index 00000000..f4d15f33 --- /dev/null +++ b/frontend/src/pages/MainPage/components/TeamCreateButton.stories.jsx @@ -0,0 +1,18 @@ +import React from "react"; +import { MemoryRouter } from "react-router-dom"; + +import TeamCreateButton from "@/pages/MainPage/components/TeamCreateButton"; + +export default { + component: TeamCreateButton, + title: "components/MainPage/TeamCreateButton", +}; + +const Template = (args) => ( + + + +); + +export const Default = Template.bind({}); +Default.args = {}; diff --git a/frontend/src/components/TeamCreateButton.tsx b/frontend/src/pages/MainPage/components/TeamCreateButton.tsx similarity index 86% rename from frontend/src/components/TeamCreateButton.tsx rename to frontend/src/pages/MainPage/components/TeamCreateButton.tsx index 865598b8..0b409cce 100644 --- a/frontend/src/components/TeamCreateButton.tsx +++ b/frontend/src/pages/MainPage/components/TeamCreateButton.tsx @@ -2,14 +2,14 @@ import React from "react"; import { useNavigate } from "react-router-dom"; import styled from "@emotion/styled"; -import { BiPlus } from "react-icons/bi"; +import PlusIcon from "@/assets/icons/bx-plus.svg"; const TeamCreateButton = () => { const navigate = useNavigate(); return ( - { navigate("team/new"); }} @@ -29,7 +29,6 @@ const StyledTeamCreateButton = styled.button` border-radius: 4px; background-color: ${({ theme }) => theme.colors.SKY_BLUE_300}; - color: ${({ theme }) => theme.colors.WHITE}; font-size: 30px; @@ -38,6 +37,10 @@ const StyledTeamCreateButton = styled.button` &:hover { background-color: ${({ theme }) => theme.colors.SKY_BLUE_400}; } + + svg { + fill: ${({ theme }) => theme.colors.WHITE}; + } `; export default TeamCreateButton; diff --git a/frontend/src/pages/MainPage.tsx b/frontend/src/pages/MainPage/index.tsx similarity index 50% rename from frontend/src/pages/MainPage.tsx rename to frontend/src/pages/MainPage/index.tsx index 5c877492..0b5214be 100644 --- a/frontend/src/pages/MainPage.tsx +++ b/frontend/src/pages/MainPage/index.tsx @@ -1,18 +1,23 @@ import React from "react"; -import { useQuery } from "react-query"; +import { useInfiniteQuery } from "@tanstack/react-query"; import axios from "axios"; import styled from "@emotion/styled"; -import MyTeamCard from "@/components/MyTeamCard"; -import TeamCreateButton from "@/components/TeamCreateButton"; +import MyTeamCard from "@/pages/MainPage/components/MyTeamCard"; +import TeamCreateButton from "@/pages/MainPage/components/TeamCreateButton"; +import EmptyMyTeamList from "@/pages/MainPage/components/EmptyMyTeamList"; -import appClient from "@/api"; +import { getMyTeams } from "@/api/team"; import { CustomError } from "@/types"; +import useIntersect from "@/hooks/useIntersect"; + +const TEAM_PAGING_COUNT = 10; interface MyTeamListResponse { teams: Team[]; + currentPage: number; + totalCount: number; } - interface Team { id: number; name: string; @@ -22,13 +27,34 @@ interface Team { } const MainPage = () => { + const ref = useIntersect( + async (entry, observer) => { + observer.unobserve(entry.target); + if (hasNextPage && !isFetching) { + fetchNextPage(); + } + }, + { rootMargin: "10px", threshold: 1.0 } + ); + const { - isLoading, - isError, - error: getMyTeamListError, data: myTeamListResponse, - } = useQuery(["my-teams"], () => - appClient.get(`/teams/me`).then((response) => response.data) + error: getMyTeamListError, + fetchNextPage, + hasNextPage, + isFetching, + isError, + isLoading, + } = useInfiniteQuery( + ["my-teams"], + getMyTeams(TEAM_PAGING_COUNT), + { + getNextPageParam: (lastPage) => { + if (lastPage.currentPage * TEAM_PAGING_COUNT < lastPage.totalCount) { + return lastPage.currentPage + 1; + } + }, + } ); if (isLoading) { @@ -47,11 +73,19 @@ const MainPage = () => { return
에러
; } + if (myTeamListResponse.pages[0].teams.length === 0) { + return ( + + + + ); + } + return ( - + - {myTeamListResponse.teams.map( - ({ id, name, description, emoji, color }) => ( + {myTeamListResponse.pages.map((page) => + page.teams.map(({ id, name, description, emoji, color }: Team) => ( { emoji={emoji} color={color} /> - ) + )) )} +
- + ); }; -const StyleMain = styled.div` +const StyledEmptyMain = styled.main` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + gap: 12px; + + height: calc(100vh - 150px); + + button { + width: 152px; + } + svg { + font-size: 200px; + } +`; + +const StyledMain = styled.div` button { position: sticky; bottom: 30px; diff --git a/frontend/src/pages/MessageDetailPage.tsx b/frontend/src/pages/MessageDetailPage.tsx deleted file mode 100644 index 9844dfeb..00000000 --- a/frontend/src/pages/MessageDetailPage.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from "react"; -import { useQuery, useMutation } from "react-query"; -import { useParams, useNavigate } from "react-router-dom"; -import axios from "axios"; -import styled from "@emotion/styled"; - -import appClient from "@/api"; -import RollingpaperMessageDetail from "@/components/RollingpaperMessageDetail"; -import PageTitleWithBackButton from "@/components/PageTitleWithBackButton"; - -import { CustomError, Message } from "@/types"; - -const MessageDetailPage = () => { - const { teamId, rollingpaperId, messageId } = useParams(); - const navigate = useNavigate(); - - const { - isLoading: isLoadingGetMessage, - isError: isErrorGetMessage, - error: getMessageError, - data: message, - } = useQuery(["message"], () => - appClient - .get(`/rollingpapers/${rollingpaperId}/messages/${messageId}`) - .then((response) => response.data) - ); - - const { mutate: deleteMessage } = useMutation( - () => { - return appClient - .delete(`/rollingpapers/${rollingpaperId}/messages/${messageId}`) - .then((response) => response.data); - }, - { - onSuccess: () => { - alert("메시지 삭제 완료"); - navigate(`/team/${teamId}/rollingpaper/${rollingpaperId}`, { - replace: true, - }); - }, - onError: (error) => { - if (axios.isAxiosError(error) && error.response) { - const customError = error.response.data as CustomError; - alert(customError.message); - } - }, - } - ); - - const handleEditButtonClick = (e: React.MouseEvent) => { - navigate("edit"); - }; - - const handleDeleteButtonClick = (e: React.MouseEvent) => { - deleteMessage(); - }; - - if (isLoadingGetMessage) { - return
로딩중
; - } - - if (isErrorGetMessage) { - if (axios.isAxiosError(getMessageError) && getMessageError.response) { - const customError = getMessageError.response.data as CustomError; - return
{customError.message}
; - } - return
에러
; - } - - if (!message) { - return
에러
; - } - - return ( - <> - - - - - - ); -}; - -const StyledMain = styled.main` - display: flex; - flex-direction: column; - align-items: center; - gap: 40px; - - button { - align-self: flex-end; - } -`; - -export default MessageDetailPage; diff --git a/frontend/src/pages/MessageEditPage.tsx b/frontend/src/pages/MessageEditPage.tsx deleted file mode 100644 index b4f78d86..00000000 --- a/frontend/src/pages/MessageEditPage.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React, { useRef } from "react"; -import { useParams, useNavigate } from "react-router-dom"; -import { useQuery, useMutation } from "react-query"; -import axios from "axios"; -import styled from "@emotion/styled"; - -import appClient from "@/api"; -import Button from "@/components/Button"; -import TextArea from "@/components/TextArea"; -import PageTitleWithBackButton from "@/components/PageTitleWithBackButton"; - -import { CustomError, Message } from "@/types"; - -const MessageEditPage = () => { - const { teamId, rollingpaperId, messageId } = useParams(); - const navigate = useNavigate(); - - const contentRef = useRef(null); - - const { - isLoading: isLoadingGetMessage, - isError: isErrorGetMessage, - error: getMessageError, - data: initialMessage, - } = useQuery(["message"], () => - appClient - .get(`/rollingpapers/${rollingpaperId}/messages/${messageId}`) - .then((response) => response.data) - ); - - const { mutate: updateMessage } = useMutation( - ({ content }: Pick) => { - return appClient - .put(`/rollingpapers/${rollingpaperId}/messages/${messageId}`, { - content, - }) - .then((response) => response.data); - }, - { - onSuccess: () => { - alert("메시지 수정 완료"); - navigate(`/team/${teamId}/rollingpaper/${rollingpaperId}`, { - replace: true, - }); - }, - onError: (error) => { - if (axios.isAxiosError(error) && error.response) { - const customError = error.response.data as CustomError; - alert(customError.message); - } - }, - } - ); - - const handleMessageFormSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!contentRef.current) { - console.error("ERROR :: No contentRef.current"); - return; - } - - if (!contentRef.current.value) { - alert("메시지를 작성해주세요"); - return; - } - - updateMessage({ content: contentRef.current.value }); - }; - - if (isLoadingGetMessage) { - return
로딩중
; - } - - if (isErrorGetMessage) { - if (axios.isAxiosError(getMessageError) && getMessageError.response) { - const customError = getMessageError.response.data as CustomError; - return
{customError.message}
; - } - return
에러
; - } - - if (!initialMessage) { - return
에러
; - } - - return ( - <> - - - {initialMessage && ( - ; - -export const Default = Template.bind({}); -Default.args = {}; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index bc769ef0..5032fd22 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -3,6 +3,7 @@ export interface Message { content: string; from: string; authorId: number; + color: string; } export interface Rollingpaper { @@ -12,7 +13,45 @@ export interface Rollingpaper { messages: Message[]; } +export interface UserInfo { + id: number; + username: string; + email: string; +} + +export interface ReceivedRollingpaper { + id: number; + title: string; + teamId: number; + teamName: string; +} + +export interface SentMessage { + id: number; + rollingpaperId: number; + rollingpaperTitle: string; + teamId: number; + teamName: string; + to: string; + content: string; + color: string; +} + +export interface ResponseReceivedRollingpapers { + totalCount: number; + currentPage: number; + rollingpapers: ReceivedRollingpaper[]; +} + +export interface ResponseSentMessages { + totalCount: number; + currentPage: number; + messages: SentMessage[]; +} + export type CustomError = { errorCode: number; message: string; }; + +export type ValueOf = T[keyof T]; diff --git a/frontend/src/types/oauth.ts b/frontend/src/types/oauth.ts new file mode 100644 index 00000000..f1333669 --- /dev/null +++ b/frontend/src/types/oauth.ts @@ -0,0 +1,4 @@ +export type RequestKakaoOauthBody = { + authorizationCode: string; + redirectUri: string; +}; diff --git a/frontend/src/util/cookie.ts b/frontend/src/util/cookie.ts index b1603fdc..f95d75f3 100644 --- a/frontend/src/util/cookie.ts +++ b/frontend/src/util/cookie.ts @@ -1,5 +1,7 @@ const setCookie = (name: string, value: string) => { - document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; + document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent( + value + )}; max-age=3600; path=/ `; }; const getCookie = (name: string) => { diff --git a/frontend/src/util/index.ts b/frontend/src/util/index.ts new file mode 100644 index 00000000..8e5fb9fe --- /dev/null +++ b/frontend/src/util/index.ts @@ -0,0 +1,11 @@ +const divideArrayByIndexRemainder = (array: any[], divisor: number) => { + const result: any[][] = Array.from({ length: divisor }, () => []); + + array.forEach((element, index) => { + result[index % divisor].push(element); + }); + + return result; +}; + +export { divideArrayByIndexRemainder }; diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 6db82bc7..37726eae 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -30,9 +30,19 @@ module.exports = { use: ["babel-loader", "ts-loader"], }, { - test: /\.(png|jpe?g|gif|svg)$/i, + test: /\.(png|jpe?g|gif)$/i, type: "asset/resource", }, + { + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + use: [ + { + loader: "@svgr/webpack", + options: { icon: true }, + }, + ], + }, ], }, output: { diff --git a/frontend/webpack.dev.js b/frontend/webpack.dev.js index 82207f84..ab32c6c9 100644 --- a/frontend/webpack.dev.js +++ b/frontend/webpack.dev.js @@ -1,5 +1,11 @@ const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); +const path = require("path"); +const webpack = require("webpack"); + +require("dotenv").config({ + path: path.join(__dirname, "./.env.development"), +}); module.exports = merge(common, { mode: "development", @@ -9,4 +15,9 @@ module.exports = merge(common, { port: 3000, hot: true, }, + plugins: [ + new webpack.DefinePlugin({ + "process.env": JSON.stringify(process.env), + }), + ], }); diff --git a/frontend/webpack.prod.js b/frontend/webpack.prod.js index 015ba8e8..70fd3e8c 100644 --- a/frontend/webpack.prod.js +++ b/frontend/webpack.prod.js @@ -1,7 +1,16 @@ const { merge } = require("webpack-merge"); const common = require("./webpack.common.js"); +const path = require("path"); +const webpack = require("webpack"); + +require("dotenv").config({ path: path.join(__dirname, "./.env.production") }); module.exports = merge(common, { mode: "production", devtool: "source-map", + plugins: [ + new webpack.DefinePlugin({ + "process.env": JSON.stringify(process.env), + }), + ], }); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 41189663..86580c6f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -22,6 +22,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.6.tgz#8b37d24e88e8e21c499d4328db80577d8882fa53" integrity sha512-tzulrgDT0QD6U7BJ4TKVk2SDDg7wlP39P9yAx1RfLy7vP/7rsDRlWVfbWxElslu56+r7QOhB2NSDsabYYruoZQ== +"@babel/compat-data@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" + integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== + "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" @@ -65,6 +70,27 @@ json5 "^2.2.1" semver "^6.3.0" +"@babel/core@^7.18.5": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.9.tgz#805461f967c77ff46c74ca0460ccf4fe933ddd59" + integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.9" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helpers" "^7.18.9" + "@babel/parser" "^7.18.9" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + "@babel/generator@^7.12.11", "@babel/generator@^7.12.5", "@babel/generator@^7.18.6": version "7.18.7" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.7.tgz#2aa78da3c05aadfc82dbac16c99552fc802284bd" @@ -74,6 +100,15 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.9.tgz#68337e9ea8044d6ddc690fb29acae39359cca0a5" + integrity sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug== + dependencies: + "@babel/types" "^7.18.9" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" @@ -99,6 +134,16 @@ browserslist "^4.20.2" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz#69e64f57b524cde3e5ff6cc5a9f4a387ee5563bf" + integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== + dependencies: + "@babel/compat-data" "^7.18.8" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.20.2" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.18.6.tgz#6f15f8459f3b523b39e00a99982e2c040871ed72" @@ -153,6 +198,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7" integrity sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q== +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + "@babel/helper-explode-assignable-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" @@ -168,6 +218,14 @@ "@babel/template" "^7.18.6" "@babel/types" "^7.18.6" +"@babel/helper-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz#940e6084a55dee867d33b4e487da2676365e86b0" + integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A== + dependencies: + "@babel/template" "^7.18.6" + "@babel/types" "^7.18.9" + "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -182,6 +240,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-member-expression-to-functions@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" + integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg== + dependencies: + "@babel/types" "^7.18.9" + "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" @@ -203,6 +268,20 @@ "@babel/traverse" "^7.18.6" "@babel/types" "^7.18.6" +"@babel/helper-module-transforms@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz#5a1079c005135ed627442df31a42887e80fcb712" + integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + "@babel/helper-optimise-call-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" @@ -220,6 +299,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz#9448974dd4fb1d80fefe72e8a0af37809cd30d6d" integrity sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg== +"@babel/helper-plugin-utils@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" + integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== + "@babel/helper-remap-async-to-generator@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.6.tgz#fa1f81acd19daee9d73de297c0308783cd3cfc23" @@ -241,6 +325,17 @@ "@babel/traverse" "^7.18.6" "@babel/types" "^7.18.6" +"@babel/helper-replace-supers@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.18.9.tgz#1092e002feca980fbbb0bd4d51b74a65c6a500e6" + integrity sha512-dNsWibVI4lNT6HiuOIBr1oyxo40HvIVmbwPUm3XZ7wMh4k2WxrxTqZwSqw/eEmXDS9np0ey5M2bz9tBmO9c+YQ== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + "@babel/helper-simple-access@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea" @@ -255,6 +350,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-skip-transparent-expression-wrappers@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz#778d87b3a758d90b471e7b9918f34a9a02eb5818" + integrity sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw== + dependencies: + "@babel/types" "^7.18.9" + "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" @@ -291,6 +393,15 @@ "@babel/traverse" "^7.18.6" "@babel/types" "^7.18.6" +"@babel/helpers@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.9.tgz#4bef3b893f253a1eced04516824ede94dcfe7ff9" + integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + "@babel/highlight@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" @@ -305,6 +416,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.6.tgz#845338edecad65ebffef058d3be851f1d28a63bc" integrity sha512-uQVSa9jJUe/G/304lXspfWVpKpK4euFLgGiMQFOCpM/bgcAdeoHwi/OQz23O9GK2osz26ZiXRRV9aV+Yl1O8tw== +"@babel/parser@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539" + integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" @@ -321,6 +437,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.18.6" "@babel/plugin-proposal-optional-chaining" "^7.18.6" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz#a11af19aa373d68d561f08e0a57242350ed0ec50" + integrity sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.18.6.tgz#aedac81e6fc12bb643374656dd5f2605bf743d17" @@ -383,6 +508,14 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-proposal-export-namespace-from@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-proposal-json-strings@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" @@ -399,6 +532,14 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" +"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz#8148cbb350483bf6220af06fa6db3690e14b2e23" + integrity sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1", "@babel/plugin-proposal-nullish-coalescing-operator@^7.13.8", "@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" @@ -435,6 +576,17 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.18.6" +"@babel/plugin-proposal-object-rest-spread@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz#f9434f6beb2c8cae9dfcf97d2a5941bbbf9ad4e7" + integrity sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q== + dependencies: + "@babel/compat-data" "^7.18.8" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.18.8" + "@babel/plugin-proposal-optional-catch-binding@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" @@ -452,6 +604,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.18.6" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +"@babel/plugin-proposal-optional-chaining@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz#e8e8fe0723f2563960e4bf5e9690933691915993" + integrity sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-proposal-private-methods@^7.12.1", "@babel/plugin-proposal-private-methods@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" @@ -655,6 +816,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-block-scoping@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.18.9.tgz#f9b7e018ac3f373c81452d6ada8bd5a18928926d" + integrity sha512-5sDIJRV1KtQVEbt/EIBwGy4T01uYIo4KRB3VUqzkhrAIOGx7AoctL9+Ux88btY0zXdDyPJ9mW+bg+v+XEkGmtw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.6.tgz#3501a8f3f4c7d5697c27a3eedbee71d68312669f" @@ -669,6 +837,20 @@ "@babel/helper-split-export-declaration" "^7.18.6" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.18.9.tgz#90818efc5b9746879b869d5ce83eb2aa48bbc3da" + integrity sha512-EkRQxsxoytpTlKJmSPYrsOMjCILacAjtSVkd4gChEe2kXjFCun3yohhW5I7plXJhCemM0gKsaGMcO8tinvCA5g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-replace-supers" "^7.18.9" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + "@babel/plugin-transform-computed-properties@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.6.tgz#5d15eb90e22e69604f3348344c91165c5395d032" @@ -676,6 +858,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-computed-properties@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz#2357a8224d402dad623caf6259b611e56aec746e" + integrity sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-destructuring@^7.12.1", "@babel/plugin-transform-destructuring@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.6.tgz#a98b0e42c7ffbf5eefcbcf33280430f230895c6f" @@ -683,6 +872,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-destructuring@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.18.9.tgz#68906549c021cb231bee1db21d3b5b095f8ee292" + integrity sha512-p5VCYNddPLkZTq4XymQIaIfZNJwT9YsjkPOhkVEqt6QIpQFZVM9IltqqYpOEkJoN1DPznmxUDyZ5CTZs/ZCuHA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" @@ -698,6 +894,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-duplicate-keys@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" + integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" @@ -721,6 +924,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-for-of@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" + integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-function-name@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.6.tgz#6a7e4ae2893d336fd1b8f64c9f92276391d0f1b4" @@ -730,6 +940,15 @@ "@babel/helper-function-name" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" + integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== + dependencies: + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-literals@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.6.tgz#9d6af353b5209df72960baf4492722d56f39a205" @@ -737,6 +956,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" + integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-member-expression-literals@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" @@ -774,6 +1000,17 @@ "@babel/helper-validator-identifier" "^7.18.6" babel-plugin-dynamic-import-node "^2.3.3" +"@babel/plugin-transform-modules-systemjs@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.18.9.tgz#545df284a7ac6a05125e3e405e536c5853099a06" + integrity sha512-zY/VSIbbqtoRoJKo2cDTewL364jSlZGvn0LKOf9ntbfxOvjfmyrdtEEOAdswOswhZEb8UH3jDkCKHd1sPgsS0A== + dependencies: + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-validator-identifier" "^7.18.6" + babel-plugin-dynamic-import-node "^2.3.3" + "@babel/plugin-transform-modules-umd@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" @@ -812,6 +1049,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-parameters@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.18.8.tgz#ee9f1a0ce6d78af58d0956a9378ea3427cccb48a" + integrity sha512-ivfbE3X2Ss+Fj8nnXvKJS6sjRG4gzwPMsP+taZC+ZzEGjAYlvENixmt1sZ5Ca6tWls+BlKSGKPJ6OOXvXCbkFg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-transform-property-literals@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" @@ -819,6 +1063,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-react-constant-elements@^7.17.12": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.18.9.tgz#ff6aeedd38f57ba6b41dcf824fcc8bcedb3e783f" + integrity sha512-IrTYh1I3YCEL1trjknnlLKTp5JggjzhKl/d3ibzPc97JhpFcDTr38Jdek/oX4cFbS6By0bXJcOkpRvJ5ZHK2wQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-react-display-name@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415" @@ -882,6 +1133,14 @@ "@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-skip-transparent-expression-wrappers" "^7.18.6" +"@babel/plugin-transform-spread@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.18.9.tgz#6ea7a6297740f381c540ac56caf75b05b74fb664" + integrity sha512-39Q814wyoOPtIB/qGopNIL9xDChOE1pNU0ZY5dO0owhiVt/5kFm4li+/bBtwc7QotG0u5EPzqhZdjMtmqBqyQA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-transform-sticky-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" @@ -896,6 +1155,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-template-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" + integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-typeof-symbol@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.6.tgz#486bb39d5a18047358e0d04dc0d2f322f0b92e92" @@ -903,6 +1169,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" +"@babel/plugin-transform-typeof-symbol@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" + integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-transform-typescript@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.18.6.tgz#8f4ade1a9cf253e5cf7c7c20173082c2c08a50a7" @@ -1008,6 +1281,87 @@ core-js-compat "^3.22.1" semver "^6.3.0" +"@babel/preset-env@^7.18.2": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.18.9.tgz#9b3425140d724fbe590322017466580844c7eaff" + integrity sha512-75pt/q95cMIHWssYtyfjVlvI+QEZQThQbKvR9xH+F/Agtw/s4Wfc2V9Bwd/P39VtixB7oWxGdH4GteTTwYJWMg== + dependencies: + "@babel/compat-data" "^7.18.8" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions" "^7.18.6" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.18.9" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.18.9" + "@babel/plugin-transform-classes" "^7.18.9" + "@babel/plugin-transform-computed-properties" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.18.9" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-function-name" "^7.18.9" + "@babel/plugin-transform-literals" "^7.18.9" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.18.6" + "@babel/plugin-transform-modules-commonjs" "^7.18.6" + "@babel/plugin-transform-modules-systemjs" "^7.18.9" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.18.6" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.18.8" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.18.9" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.9" + "@babel/plugin-transform-typeof-symbol" "^7.18.9" + "@babel/plugin-transform-unicode-escapes" "^7.18.6" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.18.9" + babel-plugin-polyfill-corejs2 "^0.3.1" + babel-plugin-polyfill-corejs3 "^0.5.2" + babel-plugin-polyfill-regenerator "^0.3.1" + core-js-compat "^3.22.1" + semver "^6.3.0" + "@babel/preset-flow@^7.12.1", "@babel/preset-flow@^7.13.13": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.18.6.tgz#83f7602ba566e72a9918beefafef8ef16d2810cb" @@ -1028,7 +1382,7 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/preset-react@^7.12.10", "@babel/preset-react@^7.18.6": +"@babel/preset-react@^7.12.10", "@babel/preset-react@^7.17.12", "@babel/preset-react@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d" integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== @@ -1040,7 +1394,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.18.6" "@babel/plugin-transform-react-pure-annotations" "^7.18.6" -"@babel/preset-typescript@^7.12.7", "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.18.6": +"@babel/preset-typescript@^7.12.7", "@babel/preset-typescript@^7.13.0", "@babel/preset-typescript@^7.17.12", "@babel/preset-typescript@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.18.6.tgz#ce64be3e63eddc44240c6358daefac17b3186399" integrity sha512-s9ik86kXBAnD760aybBucdpnLsAt0jK1xqJn2juOn9lkOvSHV60os5hxoVJsPzMQxvnUJFAlkont2DvvaYEBtQ== @@ -1106,6 +1460,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98" + integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.9" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.9" + "@babel/types" "^7.18.9" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.12.11", "@babel/types@^7.12.7", "@babel/types@^7.18.6", "@babel/types@^7.18.7", "@babel/types@^7.2.0", "@babel/types@^7.4.4": version "7.18.7" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.7.tgz#a4a2c910c15040ea52cdd1ddb1614a65c8041726" @@ -1114,6 +1484,14 @@ "@babel/helper-validator-identifier" "^7.18.6" to-fast-properties "^2.0.0" +"@babel/types@^7.18.4", "@babel/types@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.9.tgz#7148d64ba133d8d73a41b3172ac4b83a1452205f" + integrity sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" @@ -2559,6 +2937,110 @@ regenerator-runtime "^0.13.7" resolve-from "^5.0.0" +"@svgr/babel-plugin-add-jsx-attribute@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.3.1.tgz#b9a5d84902be75a05ede92e70b338d28ab63fa74" + integrity sha512-jDBKArXYO1u0B1dmd2Nf8Oy6aTF5vLDfLoO9Oon/GLkqZ/NiggYWZA+a2HpUMH4ITwNqS3z43k8LWApB8S583w== + +"@svgr/babel-plugin-remove-jsx-attribute@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.3.1.tgz#4877995452efc997b36777abe1fde9705ef78e8b" + integrity sha512-dQzyJ4prwjcFd929T43Z8vSYiTlTu8eafV40Z2gO7zy/SV5GT+ogxRJRBIKWomPBOiaVXFg3jY4S5hyEN3IBjQ== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.3.1.tgz#2d67a0e92904c9be149a5b22d3a3797ce4d7b514" + integrity sha512-HBOUc1XwSU67fU26V5Sfb8MQsT0HvUyxru7d0oBJ4rA2s4HW3PhyAPC7fV/mdsSGpAvOdd8Wpvkjsr0fWPUO7A== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.3.1.tgz#306f5247139c53af70d1778f2719647c747998ee" + integrity sha512-C12e6aN4BXAolRrI601gPn5MDFCRHO7C4TM8Kks+rDtl8eEq+NN1sak0eAzJu363x3TmHXdZn7+Efd2nr9I5dA== + +"@svgr/babel-plugin-svg-dynamic-title@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.3.1.tgz#6ce26d34cbc93eb81737ef528528907c292e7aa2" + integrity sha512-6NU55Mmh3M5u2CfCCt6TX29/pPneutrkJnnDCHbKZnjukZmmgUAZLtZ2g6ZoSPdarowaQmAiBRgAHqHmG0vuqA== + +"@svgr/babel-plugin-svg-em-dimensions@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.3.1.tgz#5ade2a724b290873c30529d1d8cd23523856287a" + integrity sha512-HV1NGHYTTe1vCNKlBgq/gKuCSfaRlKcHIADn7P8w8U3Zvujdw1rmusutghJ1pZJV7pDt3Gt8ws+SVrqHnBO/Qw== + +"@svgr/babel-plugin-transform-react-native-svg@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.3.1.tgz#d654f509d692c3a09dfb475757a44bd9f6ad7ddf" + integrity sha512-2wZhSHvTolFNeKDAN/ZmIeSz2O9JSw72XD+o2bNp2QAaWqa8KGpn5Yk5WHso6xqfSAiRzAE+GXlsrBO4UP9LLw== + +"@svgr/babel-plugin-transform-svg-component@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.3.1.tgz#21a285dbffdce9567c437ebf0d081bf9210807e6" + integrity sha512-cZ8Tr6ZAWNUFfDeCKn/pGi976iWSkS8ijmEYKosP+6ktdZ7lW9HVLHojyusPw3w0j8PI4VBeWAXAmi/2G7owxw== + +"@svgr/babel-preset@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.3.1.tgz#8bd1ead79637d395e9362b01dd37cfd59702e152" + integrity sha512-tQtWtzuMMQ3opH7je+MpwfuRA1Hf3cKdSgTtAYwOBDfmhabP7rcTfBi3E7V3MuwJNy/Y02/7/RutvwS1W4Qv9g== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^6.3.1" + "@svgr/babel-plugin-remove-jsx-attribute" "^6.3.1" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^6.3.1" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^6.3.1" + "@svgr/babel-plugin-svg-dynamic-title" "^6.3.1" + "@svgr/babel-plugin-svg-em-dimensions" "^6.3.1" + "@svgr/babel-plugin-transform-react-native-svg" "^6.3.1" + "@svgr/babel-plugin-transform-svg-component" "^6.3.1" + +"@svgr/core@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.3.1.tgz#752adf49d8d5473b15d76ca741961de093f715bd" + integrity sha512-Sm3/7OdXbQreemf9aO25keerZSbnKMpGEfmH90EyYpj1e8wMD4TuwJIb3THDSgRMWk1kYJfSRulELBy4gVgZUA== + dependencies: + "@svgr/plugin-jsx" "^6.3.1" + camelcase "^6.2.0" + cosmiconfig "^7.0.1" + +"@svgr/hast-util-to-babel-ast@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.3.1.tgz#59614e24d2a4a28010e02089213b3448d905769d" + integrity sha512-NgyCbiTQIwe3wHe/VWOUjyxmpUmsrBjdoIxKpXt3Nqc3TN30BpJG22OxBvVzsAh9jqep0w0/h8Ywvdk3D9niNQ== + dependencies: + "@babel/types" "^7.18.4" + entities "^4.3.0" + +"@svgr/plugin-jsx@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.3.1.tgz#de7b2de824296b836d6b874d498377896e367f50" + integrity sha512-r9+0mYG3hD4nNtUgsTXWGYJomv/bNd7kC16zvsM70I/bGeoCi/3lhTmYqeN6ChWX317OtQCSZZbH4wq9WwoXbw== + dependencies: + "@babel/core" "^7.18.5" + "@svgr/babel-preset" "^6.3.1" + "@svgr/hast-util-to-babel-ast" "^6.3.1" + svg-parser "^2.0.4" + +"@svgr/plugin-svgo@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.3.1.tgz#3c1ff2efaed10e5c5d35a6cae7bacaedc18b5d4a" + integrity sha512-yJIjTDKPYqzFVjmsbH5EdIwEsmKxjxdXSGJVLeUgwZOZPAkNQmD1v7LDbOdOKbR44FG8465Du+zWPdbYGnbMbw== + dependencies: + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + svgo "^2.8.0" + +"@svgr/webpack@^6.3.1": + version "6.3.1" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-6.3.1.tgz#001d03236ebb03bf47c0a4b92d5423e05095ebe6" + integrity sha512-eODxwIUShLxSMaRjzJtrj9wg89D75JLczvWg9SaB5W+OtVTkiC1vdGd8+t+pf5fTlBOy4RRXAq7x1E3DUl3D0A== + dependencies: + "@babel/core" "^7.18.5" + "@babel/plugin-transform-react-constant-elements" "^7.17.12" + "@babel/preset-env" "^7.18.2" + "@babel/preset-react" "^7.17.12" + "@babel/preset-typescript" "^7.17.12" + "@svgr/core" "^6.3.1" + "@svgr/plugin-jsx" "^6.3.1" + "@svgr/plugin-svgo" "^6.3.1" + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -2566,6 +3048,20 @@ dependencies: defer-to-connect "^1.0.1" +"@tanstack/query-core@^4.0.0-beta.1": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-4.0.10.tgz#cae6f818006616dc72c95c863592f5f68b47548a" + integrity sha512-9LsABpZXkWZHi4P1ozRETEDXQocLAxVzQaIhganxbNuz/uA3PsCAJxJTiQrknG5htLMzOF5MqM9G10e6DCxV1A== + +"@tanstack/react-query@^4.0.10": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-4.0.10.tgz#92c71a2632c06450d848d4964959bd216cde03c0" + integrity sha512-Wn5QhZUE5wvr6rGClV7KeQIUsdTmYR9mgmMZen7DSRWauHW2UTynFg3Kkf6pw+XlxxOLsyLWwz/Q6q1lSpM3TQ== + dependencies: + "@tanstack/query-core" "^4.0.0-beta.1" + "@types/use-sync-external-store" "^0.0.3" + use-sync-external-store "^1.2.0" + "@testing-library/dom@^8.3.0": version "8.14.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.14.0.tgz#c9830a21006d87b9ef6e1aae306cf49b0283e28e" @@ -2587,6 +3083,11 @@ dependencies: "@babel/runtime" "^7.12.5" +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@types/aria-query@^4.2.0": version "4.2.2" resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" @@ -2941,6 +3442,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/webpack-env@^1.16.0": version "1.17.0" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.17.0.tgz#f99ce359f1bfd87da90cc4a57cab0a18f34a48d0" @@ -4682,7 +5188,7 @@ commander@^6.2.1: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commander@^7.0.0: +commander@^7.0.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== @@ -4859,7 +5365,7 @@ cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -cosmiconfig@^7.0.0: +cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== @@ -5014,6 +5520,14 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + css-what@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" @@ -5024,6 +5538,13 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + csstype@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2" @@ -5343,6 +5864,11 @@ dotenv-expand@^5.1.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== +dotenv@^16.0.1: + version "16.0.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d" + integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ== + dotenv@^8.0.0: version "8.6.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" @@ -5439,6 +5965,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4" + integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg== + envinfo@^7.7.3: version "7.8.1" resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" @@ -8168,6 +8699,11 @@ mdast-util-to-string@^1.0.0: resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527" integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A== +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + mdurl@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" @@ -9768,11 +10304,6 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" -react-icons@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.4.0.tgz#a13a8a20c254854e1ec9aecef28a95cdf24ef703" - integrity sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg== - react-inspector@^5.1.0: version "5.1.1" resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-5.1.1.tgz#58476c78fde05d5055646ed8ec02030af42953c8" @@ -11058,6 +11589,24 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + symbol.prototype.description@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/symbol.prototype.description/-/symbol.prototype.description-1.0.5.tgz#d30e01263b6020fbbd2d2884a6276ce4d49ab568" @@ -11666,6 +12215,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-sync-external-store@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"