diff --git a/backend/emm-sale/src/docs/asciidoc/index.adoc b/backend/emm-sale/src/docs/asciidoc/index.adoc index 814b1b304..52942a868 100644 --- a/backend/emm-sale/src/docs/asciidoc/index.adoc +++ b/backend/emm-sale/src/docs/asciidoc/index.adoc @@ -1,7 +1,9 @@ = EMM-SALE API Docs :doctype: book +:icons: font :source-highlighter: highlightjs :toc: left +:toclevels: 2 :sectlinks: == Product @@ -87,3 +89,68 @@ include::{snippets}/wish-with-no-authentication-products/request-headers.adoc[] .HTTP response include::{snippets}/wish-with-no-authentication-products/http-response.adoc[] + +== Career +=== `POST`: 커리어 등록 API + +.HTTP request 설명 +include::{snippets}/add-career/request-fields.adoc[] + +.HTTP request +include::{snippets}/add-career/http-request.adoc[] + +.HTTP response +include::{snippets}/add-career/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/add-career/response-fields.adoc[] + + +=== `DELETE`: 커리어 삭제 API + +.HTTP request 설명 +include::{snippets}/delete-career/request-fields.adoc[] + +.HTTP request +include::{snippets}/delete-career/http-request.adoc[] + +.HTTP response +include::{snippets}/delete-career/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/delete-career/response-fields.adoc[] + +=== `GET`: 존재하는 커리어 전체 조회 + +.HTTP request +include::{snippets}/findAll-career/http-request.adoc[] + +.HTTP response +include::{snippets}/findAll-career/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/findAll-career/response-fields.adoc[] + +=== `GET`: 사용자의 커리어 조회 + +.HTTP request +include::{snippets}/find-career/http-request.adoc[] + +.HTTP response +include::{snippets}/find-career/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/find-career/response-fields.adoc[] + +== Member + +=== `POST`: 초기 사용자 등록 + +.HTTP request 설명 +include::{snippets}/initial-register-member/request-fields.adoc[] + +.HTTP request +include::{snippets}/initial-register-member/http-request.adoc[] + +.HTTP response +include::{snippets}/initial-register-member/http-response.adoc[] diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/api/MemberApi.java b/backend/emm-sale/src/main/java/com/emmsale/member/api/MemberApi.java index 538ae4bba..c929cfc0e 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/member/api/MemberApi.java +++ b/backend/emm-sale/src/main/java/com/emmsale/member/api/MemberApi.java @@ -1,10 +1,17 @@ package com.emmsale.member.api; import com.emmsale.member.application.MemberCareerService; +import com.emmsale.member.application.dto.MemberCareerDeleteRequest; import com.emmsale.member.application.dto.MemberCareerInitialRequest; +import com.emmsale.member.application.dto.MemberCareerAddRequest; +import com.emmsale.member.application.dto.MemberCareerResponse; import com.emmsale.member.domain.Member; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; 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.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -23,4 +30,26 @@ public ResponseEntity register( memberCareerService.registerCareer(member, memberCareerInitialRequest); return ResponseEntity.noContent().build(); } + + @PostMapping("/members/careers") + public ResponseEntity> addCareer( + final Member member, + @RequestBody final MemberCareerAddRequest memberCareerAddRequest + ) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(memberCareerService.addCareer(member, memberCareerAddRequest)); + } + + @DeleteMapping("/members/careers") + public ResponseEntity> deleteCareer( + final Member member, + @RequestBody final MemberCareerDeleteRequest memberCareerDeleteRequest + ) { + return ResponseEntity.ok(memberCareerService.deleteCareer(member, memberCareerDeleteRequest)); + } + + @GetMapping("/members/careers") + public ResponseEntity> findCareer(final Member member) { + return ResponseEntity.ok(memberCareerService.findCareers(member)); + } } diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/application/MemberCareerService.java b/backend/emm-sale/src/main/java/com/emmsale/member/application/MemberCareerService.java index 17f037aac..0256343c5 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/member/application/MemberCareerService.java +++ b/backend/emm-sale/src/main/java/com/emmsale/member/application/MemberCareerService.java @@ -1,12 +1,18 @@ package com.emmsale.member.application; +import static java.util.stream.Collectors.toList; + import com.emmsale.career.domain.CareerRepository; +import com.emmsale.member.application.dto.MemberCareerAddRequest; +import com.emmsale.member.application.dto.MemberCareerDeleteRequest; import com.emmsale.member.application.dto.MemberCareerInitialRequest; +import com.emmsale.member.application.dto.MemberCareerResponse; import com.emmsale.member.domain.Member; import com.emmsale.member.domain.MemberCareer; import com.emmsale.member.domain.MemberCareerRepository; +import com.emmsale.member.exception.MemberException; +import com.emmsale.member.exception.MemberExceptionType; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -19,17 +25,66 @@ public class MemberCareerService { private final MemberCareerRepository memberCareerRepository; private final CareerRepository careerRepository; - public void registerCareer(final Member member, final MemberCareerInitialRequest memberCareerInitialRequest) { + public void registerCareer( + final Member member, + final MemberCareerInitialRequest memberCareerInitialRequest + ) { final List careerIds = memberCareerInitialRequest.getCareerIds(); + saveMemberCareers(member, careerIds); + member.updateName(memberCareerInitialRequest.getName()); + } + + private void saveMemberCareers(final Member member, final List careerIds) { final List memberCareers = careerRepository.findAllById(careerIds) .stream() .map(it -> new MemberCareer(it, member)) - .collect(Collectors.toList()); + .collect(toList()); - member.updateName(memberCareerInitialRequest.getName()); + validateAllCareerIdsExist(careerIds, memberCareers); memberCareerRepository.saveAll(memberCareers); } + + private void validateAllCareerIdsExist( + final List careerIds, + final List memberCareers + ) { + if (memberCareers.size() != careerIds.size()) { + throw new MemberException(MemberExceptionType.INVALID_CAREER_IDS); + } + } + + public List addCareer( + final Member member, + final MemberCareerAddRequest memberCareerAddRequest + ) { + final List careerIds = memberCareerAddRequest.getCareerIds(); + saveMemberCareers(member, careerIds); + + return MemberCareerResponse.from(memberCareerRepository.findAllByMember(member)); + } + + public List deleteCareer( + final Member member, + final MemberCareerDeleteRequest memberCareerDeleteRequest + ) { + final List deleteCareerIds = memberCareerDeleteRequest.getCareerIds(); + + final List savedMemberCareerIds = + memberCareerRepository.findAllByMemberAndCareerIds(member, deleteCareerIds) + .stream() + .map(MemberCareer::getId) + .collect(toList()); + + memberCareerRepository.deleteAllByIdInBatch(savedMemberCareerIds); + + return MemberCareerResponse.from(memberCareerRepository.findAllByMember(member)); + } + + @Transactional(readOnly = true) + public List findCareers(final Member member) { + return MemberCareerResponse.from(memberCareerRepository.findAllByMember(member)); + } } diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberActivityResponse.java b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberActivityResponse.java new file mode 100644 index 000000000..a5c59715b --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberActivityResponse.java @@ -0,0 +1,12 @@ +package com.emmsale.member.application.dto; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class MemberActivityResponse { + + private final Long id; + private final String name; +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerAddRequest.java b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerAddRequest.java new file mode 100644 index 000000000..b4ae598d2 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerAddRequest.java @@ -0,0 +1,16 @@ +package com.emmsale.member.application.dto; + +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class MemberCareerAddRequest { + + private final List careerIds; + + private MemberCareerAddRequest() { + this(null); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerDeleteRequest.java b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerDeleteRequest.java new file mode 100644 index 000000000..865966a80 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerDeleteRequest.java @@ -0,0 +1,16 @@ +package com.emmsale.member.application.dto; + +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class MemberCareerDeleteRequest { + + private final List careerIds; + + private MemberCareerDeleteRequest() { + this(null); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerResponse.java b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerResponse.java new file mode 100644 index 000000000..66e3b8c35 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/MemberCareerResponse.java @@ -0,0 +1,60 @@ +package com.emmsale.member.application.dto; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; + +import com.emmsale.career.domain.ActivityType; +import com.emmsale.career.domain.Career; +import com.emmsale.member.domain.MemberCareer; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map.Entry; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class MemberCareerResponse { + + private final String activityName; + private final List memberActivityResponses; + + public static List from(final List memberCareers) { + final EnumMap> groupByActivityType = + groupingByActivityTypeAndSortedByCareerName(memberCareers); + + final List responses = new ArrayList<>(); + + for (final Entry> entry : groupByActivityType.entrySet()) { + final List activityResponse = + mapToMemberActivityResponses(entry); + + responses.add(new MemberCareerResponse(entry.getKey().getValue(), activityResponse)); + } + + return responses; + } + + private static List mapToMemberActivityResponses( + final Entry> entry + ) { + return entry.getValue() + .stream() + .map(it -> new MemberActivityResponse(it.getId(), it.getName())) + .collect(toList()); + } + + private static EnumMap> groupingByActivityTypeAndSortedByCareerName( + final List memberCareers + ) { + return memberCareers + .stream() + .map(MemberCareer::getCareer) + .sorted(comparing(career -> career.getName().toLowerCase())) + .collect( + groupingBy(Career::getActivityType, () -> new EnumMap<>(ActivityType.class), toList()) + ); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/domain/MemberCareerRepository.java b/backend/emm-sale/src/main/java/com/emmsale/member/domain/MemberCareerRepository.java index 1c7c1a207..31312fae1 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/member/domain/MemberCareerRepository.java +++ b/backend/emm-sale/src/main/java/com/emmsale/member/domain/MemberCareerRepository.java @@ -1,7 +1,20 @@ package com.emmsale.member.domain; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface MemberCareerRepository extends JpaRepository { + @Query("select mc from MemberCareer mc where mc.member = :member") + List findAllByMember(@Param("member") final Member member); + + @Query("select mc from MemberCareer mc " + + "where mc.member = :member " + + "and mc.career.id in :deleteCareerIds") + List findAllByMemberAndCareerIds( + @Param("member") final Member member, + @Param("deleteCareerIds") final List deleteCareerIds + ); } diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/exception/MemberExceptionType.java b/backend/emm-sale/src/main/java/com/emmsale/member/exception/MemberExceptionType.java index 997d670a2..678a12b94 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/member/exception/MemberExceptionType.java +++ b/backend/emm-sale/src/main/java/com/emmsale/member/exception/MemberExceptionType.java @@ -8,6 +8,11 @@ public enum MemberExceptionType implements BaseExceptionType { NOT_FOUND_MEMBER( HttpStatus.NOT_FOUND, "해당 멤버는 존재하지 않습니다." + ), + + INVALID_CAREER_IDS( + HttpStatus.BAD_REQUEST, + "요청한 career id들 중에 유효하지 않은 값이 존재합니다" ); private final HttpStatus httpStatus; diff --git a/backend/emm-sale/src/main/resources/http/member.http b/backend/emm-sale/src/main/resources/http/member.http index abe3862aa..a14fa4e8c 100644 --- a/backend/emm-sale/src/main/resources/http/member.http +++ b/backend/emm-sale/src/main/resources/http/member.http @@ -7,3 +7,19 @@ Content-Type: application/json "name": "우르", "careerIds": [1, 2] } + +### 사용자 커리어 추가 +POST http://localhost:8080/members/careers +Content-Type: application/json + +{ + "careerIds": [4, 5, 6] +} + +### 사용자 커리어 제거 +DELETE http://localhost:8080/members/careers +Content-Type: application/json + +{ + "careerIds": [1, 2] +} diff --git a/backend/emm-sale/src/main/resources/static/docs/index.html b/backend/emm-sale/src/main/resources/static/docs/index.html index 9a21de3ab..9e34a7d45 100644 --- a/backend/emm-sale/src/main/resources/static/docs/index.html +++ b/backend/emm-sale/src/main/resources/static/docs/index.html @@ -435,6 +435,7 @@ #footer-text{color:rgba(0,0,0,.6);font-size:.9em}} @media amzn-kf8{#header,#content,#footnotes,#footer{padding:0}} + @@ -466,7 +480,7 @@

HTTP request
-
GET /products/top-price-drop?page=0&size=4 HTTP/1.1
+
GET /test/restdocs/products/top-price-drop?page=0&size=4 HTTP/1.1
 Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
 Host: localhost:8080
@@ -622,7 +636,7 @@

HTTP request
-
GET /products/search?page=0&size=2&keyword=keyword HTTP/1.1
+
GET /test/restdocs/products/search?page=0&size=2&keyword=keyword HTTP/1.1
 Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
 Host: localhost:8080
@@ -764,7 +778,7 @@

HTTP request
-
GET /products/wish?notificationFilter=false&page=0&size=2 HTTP/1.1
+
GET /test/restdocs/products/wish?notificationFilter=false&page=0&size=2 HTTP/1.1
 Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
 Host: localhost:8080
@@ -906,7 +920,7 @@

HTTP request
-
GET /products/wish?notificationFilter=true&page=0&size=2 HTTP/1.1
+
GET /test/restdocs/products/wish?notificationFilter=true&page=0&size=2 HTTP/1.1
 Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
 Host: localhost:8080
@@ -1048,7 +1062,7 @@

HTTP request
-
GET /products/wish?notificationFilter=true&notificationFilter=false&page=0&size=2 HTTP/1.1
+
GET /test/restdocs/products/wish?notificationFilter=true&notificationFilter=false&page=0&size=2 HTTP/1.1
 Host: localhost:8080
@@ -1107,11 +1121,432 @@

+
+

Career

+
+
+

POST: 커리어 등록 API

+ + +++++ + + + + + + + + + + + + + + +
Table 15. HTTP request 설명
PathTypeDescription

careerIds

Array

활동 id들

+
+
HTTP request
+
+
POST /members/careers HTTP/1.1
+Content-Type: application/json
+Content-Length: 31
+Host: localhost:8080
+
+{
+  "careerIds" : [ 4, 5, 6 ]
+}
+
+
+
+
HTTP response
+
+
HTTP/1.1 201 Created
+Content-Type: application/json
+Content-Length: 552
+
+[ {
+  "activityName" : "동아리",
+  "memberActivityResponses" : [ {
+    "id" : 1,
+    "name" : "YAPP"
+  }, {
+    "id" : 2,
+    "name" : "DND"
+  }, {
+    "id" : 3,
+    "name" : "nexters"
+  } ]
+}, {
+  "activityName" : "컨퍼런스",
+  "memberActivityResponses" : [ {
+    "id" : 4,
+    "name" : "인프콘"
+  } ]
+}, {
+  "activityName" : "교육",
+  "memberActivityResponses" : [ {
+    "id" : 5,
+    "name" : "우아한테크코스"
+  } ]
+}, {
+  "activityName" : "직무",
+  "memberActivityResponses" : [ {
+    "id" : 6,
+    "name" : "Backend"
+  } ]
+} ]
+
+
+ + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
Table 16. HTTP response 설명
PathTypeDescription

[].activityName

String

career 분류

[].memberActivityResponses[].id

Number

career id

[].memberActivityResponses[].name

String

career 이름

+
+
+

DELETE: 커리어 삭제 API

+ + +++++ + + + + + + + + + + + + + + +
Table 17. HTTP request 설명
PathTypeDescription

careerIds

Array

활동 id들

+
+
HTTP request
+
+
DELETE /members/careers HTTP/1.1
+Content-Type: application/json
+Content-Length: 28
+Host: localhost:8080
+
+{
+  "careerIds" : [ 1, 2 ]
+}
+
+
+
+
HTTP response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 116
+
+[ {
+  "activityName" : "동아리",
+  "memberActivityResponses" : [ {
+    "id" : 3,
+    "name" : "nexters"
+  } ]
+} ]
+
+
+ + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
Table 18. HTTP response 설명
PathTypeDescription

[].activityName

String

career 분류

[].memberActivityResponses[].id

Number

career id

[].memberActivityResponses[].name

String

career 이름

+
+
+

GET: 존재하는 커리어 전체 조회

+
+
HTTP request
+
+
GET /careers HTTP/1.1
+Host: localhost:8080
+
+
+
+
HTTP response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 528
+
+[ {
+  "activityName" : "동아리",
+  "activityResponses" : [ {
+    "id" : 1,
+    "name" : "YAPP"
+  }, {
+    "id" : 2,
+    "name" : "DND"
+  }, {
+    "id" : 3,
+    "name" : "nexters"
+  } ]
+}, {
+  "activityName" : "컨퍼런스",
+  "activityResponses" : [ {
+    "id" : 4,
+    "name" : "인프콘"
+  } ]
+}, {
+  "activityName" : "교육",
+  "activityResponses" : [ {
+    "id" : 5,
+    "name" : "우아한테크코스"
+  } ]
+}, {
+  "activityName" : "직무",
+  "activityResponses" : [ {
+    "id" : 6,
+    "name" : "Backend"
+  } ]
+} ]
+
+
+ + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
Table 19. HTTP response 설명
PathTypeDescription

[].activityName

String

career 분류

[].activityResponses[].id

Number

career id

[].activityResponses[].name

String

career 이름

+
+
+

GET: 사용자의 커리어 조회

+
+
HTTP request
+
+
GET /members/careers HTTP/1.1
+Content-Type: application/json
+Host: localhost:8080
+
+
+
+
HTTP response
+
+
HTTP/1.1 200 OK
+Content-Type: application/json
+Content-Length: 552
+
+[ {
+  "activityName" : "동아리",
+  "memberActivityResponses" : [ {
+    "id" : 1,
+    "name" : "YAPP"
+  }, {
+    "id" : 2,
+    "name" : "DND"
+  }, {
+    "id" : 3,
+    "name" : "nexters"
+  } ]
+}, {
+  "activityName" : "컨퍼런스",
+  "memberActivityResponses" : [ {
+    "id" : 4,
+    "name" : "인프콘"
+  } ]
+}, {
+  "activityName" : "교육",
+  "memberActivityResponses" : [ {
+    "id" : 5,
+    "name" : "우아한테크코스"
+  } ]
+}, {
+  "activityName" : "직무",
+  "memberActivityResponses" : [ {
+    "id" : 6,
+    "name" : "Backend"
+  } ]
+} ]
+
+
+ + +++++ + + + + + + + + + + + + + + + + + + + + + + + + +
Table 20. HTTP response 설명
PathTypeDescription

[].activityName

String

career 분류

[].memberActivityResponses[].id

Number

career id

[].memberActivityResponses[].name

String

career 이름

+
+
+
+
+

Member

+
+
+

POST: 초기 사용자 등록

+ + +++++ + + + + + + + + + + + + + + + + + + + +
Table 21. HTTP request 설명
PathTypeDescription

careerIds

Array

활동 id들

name

String

사용자 이름

+
+
HTTP request
+
+
POST /members HTTP/1.1
+Content-Type: application/json
+Content-Length: 49
+Host: localhost:8080
+
+{
+  "name" : "우르",
+  "careerIds" : [ 1, 2 ]
+}
+
+
+
+
HTTP response
+
+
HTTP/1.1 204 No Content
+
+
+
+
+
diff --git a/backend/emm-sale/src/test/java/com/emmsale/career/api/CareerApiTest.java b/backend/emm-sale/src/test/java/com/emmsale/career/api/CareerApiTest.java index d72598353..e253473fc 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/career/api/CareerApiTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/career/api/CareerApiTest.java @@ -1,14 +1,23 @@ package com.emmsale.career.api; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.emmsale.career.application.CareerService; +import com.emmsale.career.application.dto.ActivityResponse; +import com.emmsale.career.application.dto.CareerResponse; import com.emmsale.helper.MockMvcTestHelper; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; @WebMvcTest(CareerApi.class) class CareerApiTest extends MockMvcTestHelper { @@ -19,8 +28,42 @@ class CareerApiTest extends MockMvcTestHelper { @Test @DisplayName("커리어를 전제 조회할 수 있으면 200 OK를 반환한다.") void findAll() throws Exception { + // given + final ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[].activityName").type(JsonFieldType.STRING).description("career 분류"), + fieldWithPath("[].activityResponses[].id").type(JsonFieldType.NUMBER) + .description("career id"), + fieldWithPath("[].activityResponses[].name").type(JsonFieldType.STRING) + .description("career 이름") + ); + + final List careerResponses = List.of( + new CareerResponse("동아리", + List.of( + new ActivityResponse(1L, "YAPP"), + new ActivityResponse(2L, "DND"), + new ActivityResponse(3L, "nexters") + )), + new CareerResponse("컨퍼런스", + List.of( + new ActivityResponse(4L, "인프콘") + )), + new CareerResponse("교육", + List.of( + new ActivityResponse(5L, "우아한테크코스") + )), + new CareerResponse("직무", + List.of( + new ActivityResponse(6L, "Backend") + )) + ); + + when(careerService.findAll()).thenReturn(careerResponses); + // when & then + mockMvc.perform(get("/careers")) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andDo(document("findAll-career", responseFields)); } } diff --git a/backend/emm-sale/src/test/java/com/emmsale/career/application/CareerServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/career/application/CareerServiceTest.java index c640160ef..89184c4cc 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/career/application/CareerServiceTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/career/application/CareerServiceTest.java @@ -53,6 +53,4 @@ void findAll() throws Exception { .containsExactlyInAnyOrderElementsOf(actualCareerNames) ); } - - } diff --git a/backend/emm-sale/src/test/java/com/emmsale/member/api/MemberApiTest.java b/backend/emm-sale/src/test/java/com/emmsale/member/api/MemberApiTest.java index c417b995c..84a48cddc 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/member/api/MemberApiTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/member/api/MemberApiTest.java @@ -1,18 +1,33 @@ package com.emmsale.member.api; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.emmsale.helper.MockMvcTestHelper; import com.emmsale.member.application.MemberCareerService; +import com.emmsale.member.application.dto.MemberActivityResponse; +import com.emmsale.member.application.dto.MemberCareerAddRequest; +import com.emmsale.member.application.dto.MemberCareerDeleteRequest; import com.emmsale.member.application.dto.MemberCareerInitialRequest; +import com.emmsale.member.application.dto.MemberCareerResponse; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; @WebMvcTest(MemberApi.class) class MemberApiTest extends MockMvcTestHelper { @@ -20,8 +35,20 @@ class MemberApiTest extends MockMvcTestHelper { @MockBean private MemberCareerService memberCareerService; + private static final ResponseFieldsSnippet RESPONSE_FIELDS = responseFields( + + fieldWithPath("[].activityName").type(JsonFieldType.STRING).description("career 분류"), + fieldWithPath("[].memberActivityResponses[].id").type(JsonFieldType.NUMBER) + .description("career id"), + fieldWithPath("[].memberActivityResponses[].name").type(JsonFieldType.STRING) + .description("career 이름") + ); + + private static final RequestFieldsSnippet REQUEST_FIELDS = requestFields( + fieldWithPath("careerIds").description("활동 id들")); + @Test - @DisplayName("사용자 정보를 잘 저장하면, 204 no Content를 반환해준다.") + @DisplayName("사용자 정보를 잘 저장하면, 204 no Content를 반환해줄 수 있다.") void register() throws Exception { //given final List careerIds = List.of(1L, 2L); @@ -29,11 +56,119 @@ void register() throws Exception { final MemberCareerInitialRequest request = new MemberCareerInitialRequest(name, careerIds); + final RequestFieldsSnippet REQUEST_FIELDS = requestFields( + fieldWithPath("careerIds").description("활동 id들"), + fieldWithPath("name").description("사용자 이름")); + //when & then mockMvc.perform(post("/members") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isNoContent()) - .andDo(print()); + .andDo(print()) + .andDo(document("initial-register-member", REQUEST_FIELDS)); + } + + @Test + @DisplayName("내 명함에서 활동이력들을 성공적으로 추가하면, 201 Created를 반환해줄 수 있다.") + void addCareer() throws Exception { + //given + final List careerIds = List.of(4L, 5L, 6L); + final MemberCareerAddRequest request = new MemberCareerAddRequest(careerIds); + + final List memberCareerResponses = List.of( + new MemberCareerResponse("동아리", + List.of( + new MemberActivityResponse(1L, "YAPP"), + new MemberActivityResponse(2L, "DND"), + new MemberActivityResponse(3L, "nexters") + )), + new MemberCareerResponse("컨퍼런스", + List.of( + new MemberActivityResponse(4L, "인프콘") + )), + new MemberCareerResponse("교육", + List.of( + new MemberActivityResponse(5L, "우아한테크코스") + )), + new MemberCareerResponse("직무", + List.of( + new MemberActivityResponse(6L, "Backend") + )) + ); + + when(memberCareerService.addCareer(any(), any())) + .thenReturn(memberCareerResponses); + + //when & then + mockMvc.perform(post("/members/careers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isCreated()) + .andDo(print()) + .andDo(document("add-career", REQUEST_FIELDS, RESPONSE_FIELDS)); + } + + @Test + @DisplayName("내 명함에서 활동이력들을 성공적으로 삭제하면, 200 OK를 반환해줄 수 있다.") + void test_deleteCareer() throws Exception { + //given + final List careerIds = List.of(1L, 2L); + final MemberCareerDeleteRequest request = new MemberCareerDeleteRequest(careerIds); + + final List memberCareerResponses = List.of( + new MemberCareerResponse("동아리", + List.of( + new MemberActivityResponse(3L, "nexters") + )) + ); + + when(memberCareerService.deleteCareer(any(), any())) + .thenReturn(memberCareerResponses); + + //when & then + mockMvc.perform(delete("/members/careers") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document("delete-career", REQUEST_FIELDS, RESPONSE_FIELDS)); + } + + @Test + @DisplayName("내 활동들을 조회할 수 있다.") + void test_findCareer() throws Exception { + //given + final List memberCareerResponses = List.of( + new MemberCareerResponse("동아리", + List.of( + new MemberActivityResponse(1L, "YAPP"), + new MemberActivityResponse(2L, "DND"), + new MemberActivityResponse(3L, "nexters") + )), + new MemberCareerResponse("컨퍼런스", + List.of( + new MemberActivityResponse(4L, "인프콘") + )), + new MemberCareerResponse("교육", + List.of( + new MemberActivityResponse(5L, "우아한테크코스") + )), + new MemberCareerResponse("직무", + List.of( + new MemberActivityResponse(6L, "Backend") + )) + ); + + //when + when(memberCareerService.findCareers(any())) + .thenReturn(memberCareerResponses); + + //then + mockMvc.perform(get("/members/careers") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document("find-career", RESPONSE_FIELDS)); } } diff --git a/backend/emm-sale/src/test/java/com/emmsale/member/application/MemberCareerServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/member/application/MemberCareerServiceTest.java index 26743a5d7..f2156af7a 100644 --- a/backend/emm-sale/src/test/java/com/emmsale/member/application/MemberCareerServiceTest.java +++ b/backend/emm-sale/src/test/java/com/emmsale/member/application/MemberCareerServiceTest.java @@ -1,13 +1,21 @@ package com.emmsale.member.application; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import com.emmsale.helper.ServiceIntegrationTestHelper; +import com.emmsale.member.application.dto.MemberActivityResponse; +import com.emmsale.member.application.dto.MemberCareerAddRequest; +import com.emmsale.member.application.dto.MemberCareerDeleteRequest; import com.emmsale.member.application.dto.MemberCareerInitialRequest; +import com.emmsale.member.application.dto.MemberCareerResponse; import com.emmsale.member.domain.Member; import com.emmsale.member.domain.MemberRepository; +import com.emmsale.member.exception.MemberException; +import com.emmsale.member.exception.MemberExceptionType; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,7 +30,7 @@ class MemberCareerServiceTest extends ServiceIntegrationTestHelper { private MemberRepository memberRepository; @Test - @DisplayName("커리어의 id를 통해서, 사용자의 커리어를 등록할 수 있다.") + @DisplayName("커리어의 id를 통해서, 사용자의 커리어를 등록하고 사용자의 이름을 수정할 수 있다.") void registerCareer() throws Exception { //given final List careerIds = List.of(1L, 2L, 3L, 4L); @@ -40,4 +48,86 @@ void registerCareer() throws Exception { () -> assertEquals(updateName, member.getName()) ); } + + @Test + @DisplayName("커리어의 id를 통해서, 사용자의 커리어에 추가할 수 있다.") + void addCareer() throws Exception { + //given + final List careerIds = List.of(4L, 5L, 6L); + final long savedMemberId = 1L; + + final Member member = memberRepository.findById(savedMemberId).get(); + final MemberCareerAddRequest request = new MemberCareerAddRequest(careerIds); + + final List expected = List.of( + new MemberCareerResponse("동아리", + List.of( + new MemberActivityResponse(1L, "YAPP"), + new MemberActivityResponse(2L, "DND"), + new MemberActivityResponse(3L, "nexters") + )), + new MemberCareerResponse("컨퍼런스", + List.of( + new MemberActivityResponse(4L, "인프콘") + )), + new MemberCareerResponse("교육", + List.of( + new MemberActivityResponse(5L, "우아한테크코스") + )), + new MemberCareerResponse("직무", + List.of( + new MemberActivityResponse(6L, "Backend") + )) + ); + + //when + final List actual = memberCareerService.addCareer(member, request); + + //then + assertThat(expected) + .usingRecursiveComparison() + .ignoringCollectionOrder() + .isEqualTo(actual); + } + + @Test + @DisplayName("addCareer() : 유효하지 않은 careerId들이 있으면 invalid_career_ids Exception이 발생합니다.") + void test_addCareer_invalid_career_ids_Exception() throws Exception { + //given + final Member savedMember = memberRepository.findById(1L).get(); + final List careerIds = List.of(1L, 2L, 7L); + final MemberCareerAddRequest request = new MemberCareerAddRequest(careerIds); + + //when & then + assertThatThrownBy(() -> memberCareerService.addCareer(savedMember, request)) + .isInstanceOf(MemberException.class) + .hasMessage(MemberExceptionType.INVALID_CAREER_IDS.errorMessage()); + } + + @Test + @DisplayName("deleteCareer() : member Id와 삭제할 career Id를 통해서 사용자의 커리어를 삭제할 수 있다.") + void test_deleteCareer() throws Exception { + //given + final List deleteCareerIds = List.of(1L, 2L); + final long savedMemberId = 1L; + + final Member member = memberRepository.findById(savedMemberId).get(); + final MemberCareerDeleteRequest request = new MemberCareerDeleteRequest(deleteCareerIds); + + final List expected = List.of( + new MemberCareerResponse("동아리", + List.of( + new MemberActivityResponse(3L, "nexters") + )) + ); + + //when + final List actual = memberCareerService.deleteCareer(member, request); + + //then + assertThat(expected) + .usingRecursiveComparison() + .ignoringCollectionOrder() + .isEqualTo(actual); + } } diff --git a/backend/emm-sale/src/test/java/com/emmsale/member/domain/MemberCareerRepositoryTest.java b/backend/emm-sale/src/test/java/com/emmsale/member/domain/MemberCareerRepositoryTest.java new file mode 100644 index 000000000..8e989642b --- /dev/null +++ b/backend/emm-sale/src/test/java/com/emmsale/member/domain/MemberCareerRepositoryTest.java @@ -0,0 +1,62 @@ +package com.emmsale.member.domain; + +import static java.util.stream.Collectors.toUnmodifiableList; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = Replace.NONE) +class MemberCareerRepositoryTest { + + @Autowired + private MemberCareerRepository memberCareerRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("사용자를 통해 사용자의 커리어들을 모두 조회할 수 있다.") + void findAllByMemberId() throws Exception { + //given + final Long memberId = 1L; + final Member member = memberRepository.findById(memberId).get(); + + //when + final List memberCareers = memberCareerRepository.findAllByMember(member); + + //then + final List memberCareerIds = memberCareers.stream() + .map(MemberCareer::getId) + .collect(toUnmodifiableList()); + + assertThat(memberCareerIds).containsExactlyInAnyOrder(1L, 2L, 3L); + } + + @Test + @DisplayName("사용자와 career의 id를 통해서 사용자의 커리어들을 모두 조회할 수 있다.") + void test_findAllByMemberAndCareerIds() throws Exception { + //given + final Long memberId = 1L; + final List careerIds = List.of(1L, 2L); + + final Member member = memberRepository.findById(memberId).get(); + + //when + final List memberCareers = + memberCareerRepository.findAllByMemberAndCareerIds(member, careerIds); + + //then + final List memberCareerIds = memberCareers.stream() + .map(MemberCareer::getId) + .collect(toUnmodifiableList()); + + assertThat(memberCareerIds).containsExactlyInAnyOrder(1L, 2L); + } +}