diff --git a/backend/emm-sale/src/docs/asciidoc/index.adoc b/backend/emm-sale/src/docs/asciidoc/index.adoc index 1f647c67e..bb416625e 100644 --- a/backend/emm-sale/src/docs/asciidoc/index.adoc +++ b/backend/emm-sale/src/docs/asciidoc/index.adoc @@ -155,6 +155,17 @@ include::{snippets}/initial-register-member/http-request.adoc[] .HTTP response include::{snippets}/initial-register-member/http-response.adoc[] +=== `PUT`: 사용자의 Open Profile URL 업데이트 + +.HTTP request 설명 +include::{snippets}/update-open-profile-url/request-fields.adoc[] + +.HTTP request +include::{snippets}/update-open-profile-url/http-request.adoc[] + +.HTTP response +include::{snippets}/update-open-profile-url/http-response.adoc[] + == Event === `GET` : 행사 상세정보 조회 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 768915ecf..18249002b 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,18 +1,22 @@ package com.emmsale.member.api; import com.emmsale.member.application.MemberActivityService; +import com.emmsale.member.application.MemberUpdateService; +import com.emmsale.member.application.dto.MemberActivityAddRequest; import com.emmsale.member.application.dto.MemberActivityDeleteRequest; import com.emmsale.member.application.dto.MemberActivityInitialRequest; -import com.emmsale.member.application.dto.MemberActivityAddRequest; import com.emmsale.member.application.dto.MemberActivityResponses; +import com.emmsale.member.application.dto.OpenProfileUrlRequest; import com.emmsale.member.domain.Member; import java.util.List; +import javax.validation.Valid; 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -21,6 +25,7 @@ public class MemberApi { private final MemberActivityService memberActivityService; + private final MemberUpdateService memberUpdateService; @PostMapping("/members") public ResponseEntity register( @@ -45,11 +50,21 @@ public ResponseEntity> deleteCareer( final Member member, @RequestBody final MemberActivityDeleteRequest memberActivityDeleteRequest ) { - return ResponseEntity.ok(memberActivityService.deleteCareer(member, memberActivityDeleteRequest)); + return ResponseEntity.ok( + memberActivityService.deleteCareer(member, memberActivityDeleteRequest)); } @GetMapping("/members/activities") public ResponseEntity> findCareer(final Member member) { return ResponseEntity.ok(memberActivityService.findCareers(member)); } + + @PutMapping("/members/open-profile-url") + public ResponseEntity updateOpenProfileUrl( + final Member member, + @RequestBody @Valid final OpenProfileUrlRequest openProfileUrlRequest + ) { + memberUpdateService.updateOpenProfileUrl(member, openProfileUrlRequest); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/application/MemberUpdateService.java b/backend/emm-sale/src/main/java/com/emmsale/member/application/MemberUpdateService.java new file mode 100644 index 000000000..9b19faadb --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/member/application/MemberUpdateService.java @@ -0,0 +1,26 @@ +package com.emmsale.member.application; + +import com.emmsale.member.application.dto.OpenProfileUrlRequest; +import com.emmsale.member.domain.Member; +import com.emmsale.member.domain.MemberRepository; +import javax.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@RequiredArgsConstructor +public class MemberUpdateService { + + private final MemberRepository memberRepository; + + public void updateOpenProfileUrl( + final Member member, + final OpenProfileUrlRequest openProfileUrlRequest + ) { + final Member persistMember = memberRepository.findById(member.getId()).get(); + final String openProfileUrl = openProfileUrlRequest.getOpenProfileUrl(); + + persistMember.updateOpenProfileUrl(openProfileUrl); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/OpenProfileUrlRequest.java b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/OpenProfileUrlRequest.java new file mode 100644 index 000000000..b8f248ef4 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/member/application/dto/OpenProfileUrlRequest.java @@ -0,0 +1,18 @@ +package com.emmsale.member.application.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.Pattern; +import lombok.Getter; + +@Getter +public class OpenProfileUrlRequest { + + @Pattern(regexp = "(https://open.kakao.com/).*") + private final String openProfileUrl; + + @JsonCreator + public OpenProfileUrlRequest(@JsonProperty final String openProfileUrl) { + this.openProfileUrl = openProfileUrl; + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java b/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java index bec75a051..c9fb9c69c 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java +++ b/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java @@ -46,6 +46,10 @@ public void updateName(final String name) { this.name = name; } + public void updateOpenProfileUrl(final String openProfileUrl) { + this.openProfileUrl = openProfileUrl; + } + public boolean isNotMe(final Member member) { if (member.getId().equals(id)) { return false; diff --git a/backend/emm-sale/src/main/resources/http/member.http b/backend/emm-sale/src/main/resources/http/member.http index a14fa4e8c..db5287de2 100644 --- a/backend/emm-sale/src/main/resources/http/member.http +++ b/backend/emm-sale/src/main/resources/http/member.http @@ -5,7 +5,10 @@ Content-Type: application/json { "name": "우르", - "careerIds": [1, 2] + "careerIds": [ + 1, + 2 + ] } ### 사용자 커리어 추가 @@ -13,7 +16,11 @@ POST http://localhost:8080/members/careers Content-Type: application/json { - "careerIds": [4, 5, 6] + "careerIds": [ + 4, + 5, + 6 + ] } ### 사용자 커리어 제거 @@ -21,5 +28,16 @@ DELETE http://localhost:8080/members/careers Content-Type: application/json { - "careerIds": [1, 2] + "careerIds": [ + 1, + 2 + ] +} + +### 사용자의 오픈 프로필 URL 업데이트 +PUT http://localhost:8080/members/open-profile-url +Content-Type: application/json + +{ + "openProfileUrl": "https://open.kakao.com/profile" } 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 7d3ce56ff..84d1746b5 100644 --- a/backend/emm-sale/src/main/resources/static/docs/index.html +++ b/backend/emm-sale/src/main/resources/static/docs/index.html @@ -466,14 +466,7 @@

EMM-SALE API Docs

  • Member -
  • -
  • Comment -
  • @@ -1548,102 +1541,11 @@

    -

    Comment

    -
    -

    GET : 댓글 모두 조회

    +

    PUT: 사용자의 Open Profile URL 업데이트

    --- - - - - - - - - - - - - -
    Table 22. HTTP request 설명
    ParameterDescription

    eventId

    댓글을 볼 행사 id

    -
    -
    HTTP request
    -
    -
    GET /comments?eventId=1 HTTP/1.1
    -Host: localhost:8080
    -
    -
    -
    -
    HTTP response
    -
    -
    HTTP/1.1 200 OK
    -Content-Type: application/json
    -Content-Length: 1313
    -
    -[ {
    -  "parentComment" : {
    -    "content" : "부모댓글2",
    -    "commentId" : 4,
    -    "parentId" : null,
    -    "eventId" : 1,
    -    "createdAt" : "2023:07:26:14:24:19",
    -    "updatedAt" : "2023:07:26:14:24:19",
    -    "memberId" : 1,
    -    "memberImageUrl" : "이미지",
    -    "memberName" : "이름1",
    -    "deleted" : false
    -  },
    -  "childComments" : [ ]
    -}, {
    -  "parentComment" : {
    -    "content" : "부모댓글1",
    -    "commentId" : 5,
    -    "parentId" : null,
    -    "eventId" : 1,
    -    "createdAt" : "2023:07:26:14:24:19",
    -    "updatedAt" : "2023:07:26:14:24:19",
    -    "memberId" : 1,
    -    "memberImageUrl" : "이미지",
    -    "memberName" : "이름1",
    -    "deleted" : false
    -  },
    -  "childComments" : [ {
    -    "content" : "부모댓글1에 대한 자식댓글1",
    -    "commentId" : 2,
    -    "parentId" : 1,
    -    "eventId" : 1,
    -    "createdAt" : "2023:07:26:14:24:19",
    -    "updatedAt" : "2023:07:26:14:24:19",
    -    "memberId" : 1,
    -    "memberImageUrl" : "이미지",
    -    "memberName" : "이름1",
    -    "deleted" : false
    -  }, {
    -    "content" : "부모댓글1에 대한 자식댓글2",
    -    "commentId" : 3,
    -    "parentId" : 1,
    -    "eventId" : 1,
    -    "createdAt" : "2023:07:26:14:24:19",
    -    "updatedAt" : "2023:07:26:14:24:19",
    -    "memberId" : 1,
    -    "memberImageUrl" : "이미지",
    -    "memberName" : "이름1",
    -    "deleted" : false
    -  } ]
    -} ]
    -
    -
    - - -@@ -1657,398 +1559,31 @@

    [].parentComment.content

    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Table 23. HTTP response 설명

    String

    댓글 내용

    [].parentComment.commentId

    Number

    댓글 ID

    [].parentComment.parentId

    Null

    부모 댓글 ID

    [].parentComment.eventId

    Number

    이벤트 ID

    [].parentComment.deleted

    Boolean

    댓글 삭제 여부

    [].parentComment.createdAt

    String

    댓글 생성 날짜

    [].parentComment.updatedAt

    String

    댓글 수정 날짜

    [].parentComment.memberId

    Number

    댓글 작성자 ID

    [].parentComment.memberImageUrl

    String

    댓글 작성자 이미지 Url

    [].parentComment.memberName

    String

    댓글 작성자 이름

    [].childComments[]

    Array

    자식 댓글 목록

    [].childComments[].content

    String

    댓글 내용

    [].childComments[].commentId

    Number

    댓글 ID

    [].childComments[].parentId

    Number

    부모 댓글 ID

    [].childComments[].eventId

    Number

    이벤트 ID

    [].childComments[].deleted

    Boolean

    댓글 삭제 여부

    [].childComments[].createdAt

    String

    댓글 생성 날짜

    [].childComments[].updatedAt

    String

    댓글 수정 날짜

    [].childComments[].memberId

    Number

    댓글 작성자 ID

    [].childComments[].memberImageUrl

    String

    댓글 작성자 이미지 Url

    [].childComments[].memberName

    String

    댓글 작성자 이름

    -
    -
    -

    POST : 댓글 및 대댓글 생성

    -
    -
    HTTP request
    -
    -
    POST /comments HTTP/1.1
    -Content-Type: application/json
    -Content-Length: 64
    -Host: localhost:8080
    -
    -{
    -  "content" : "내용",
    -  "eventId" : 1,
    -  "parentId" : null
    -}
    -
    -
    -
    -
    HTTP response
    -
    -
    HTTP/1.1 200 OK
    -Content-Type: application/json
    -Content-Length: 259
    -
    -{
    -  "content" : "내용",
    -  "commentId" : 2,
    -  "parentId" : 1,
    -  "eventId" : 1,
    -  "createdAt" : "2023:07:26:14:24:19",
    -  "updatedAt" : "2023:07:26:14:24:19",
    -  "memberId" : 1,
    -  "memberImageUrl" : "이미지",
    -  "memberName" : "이름1",
    -  "deleted" : false
    -}
    -
    -
    - - ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - -
    Table 24. HTTP response 설명
    PathTypeDescription

    content

    String

    저장된 댓글 내용

    commentId

    Number

    저장된 댓글 id

    parentId

    Number

    대댓글일 경우 부모 댓글 id

    eventId

    Number

    행사 id

    createdAt

    String

    댓글 생성 시간

    updatedAt

    String

    댓글 최근 수정 시간

    deleted

    Boolean

    댓글 삭제 여부

    memberId

    Number

    댓글 작성자 ID

    memberImageUrl

    openProfileUrl

    String

    댓글 작성자 이미지 Url

    memberName

    String

    댓글 작성자 이름

    -
    -
    -

    DELETE : 댓글 삭제

    -
    -
    HTTP request
    -
    -
    DELETE /comments/1 HTTP/1.1
    -Host: localhost:8080
    -
    -
    - - ---- - - - - - - - - - - +
    Table 25. /comments/{comment-id}
    ParameterDescription

    comment-id

    삭제할 댓글의 ID

    오픈 채팅 url

    -
    HTTP response
    -
    -
    HTTP/1.1 204 No Content
    -
    -
    -
    -
    -

    PATCH : 댓글 수정

    -
    HTTP request
    -
    PATCH /comments/1 HTTP/1.1
    +
    PUT /members/openprofile HTTP/1.1
     Content-Type: application/json
    -Content-Length: 36
    +Content-Length: 66
     Host: localhost:8080
     
     {
    -  "content" : "변경된 내용"
    +  "openProfileUrl" : "https://open.kakao.com/o/openprofileurl"
     }
    - - ----- - - - - - - - - - - - - - - -
    Table 26. HTTP request 설명
    PathTypeDescription

    content

    String

    변경할 댓글 내용

    - - ---- - - - - - - - - - - - - -
    Table 27. /comments/{comment-id}
    ParameterDescription

    comment-id

    수정할 댓글의 ID

    HTTP response
    -
    HTTP/1.1 200 OK
    -Content-Type: application/json
    -Content-Length: 259
    -
    -{
    -  "content" : "댓",
    -  "commentId" : 5,
    -  "parentId" : null,
    -  "eventId" : 1,
    -  "createdAt" : "2023:07:26:14:24:19",
    -  "updatedAt" : "2023:07:26:14:24:19",
    -  "memberId" : 1,
    -  "memberImageUrl" : "이미지",
    -  "memberName" : "이름1",
    -  "deleted" : false
    -}
    +
    HTTP/1.1 200 OK
    - - ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Table 28. HTTP response 설명
    PathTypeDescription

    content

    String

    저장된 댓글 내용

    commentId

    Number

    저장된 댓글 id

    parentId

    Null

    대댓글일 경우 부모 댓글 id

    eventId

    Number

    행사 id

    createdAt

    String

    댓글 생성 시간

    updatedAt

    String

    댓글 최근 수정 시간

    deleted

    Boolean

    댓글 삭제 여부

    memberId

    Number

    댓글 작성자 ID

    memberImageUrl

    String

    댓글 작성자 이미지 Url

    memberName

    String

    댓글 작성자 이름

    @@ -2056,11 +1591,11 @@

    - \ No newline at end of file + diff --git a/backend/emm-sale/src/test/java/com/emmsale/member/MemberFixture.java b/backend/emm-sale/src/test/java/com/emmsale/member/MemberFixture.java new file mode 100644 index 000000000..ca553b129 --- /dev/null +++ b/backend/emm-sale/src/test/java/com/emmsale/member/MemberFixture.java @@ -0,0 +1,14 @@ +package com.emmsale.member; + +import com.emmsale.member.domain.Member; + +public class MemberFixture { + + public static Member memberFixture() { + return new Member( + 1234L, + "https://image-url.com", + "member" + ); + } +} 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 16fc18a59..ae15d7d31 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 @@ -9,16 +9,19 @@ 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.request.MockMvcRequestBuilders.put; 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.MemberActivityService; +import com.emmsale.member.application.MemberUpdateService; import com.emmsale.member.application.dto.MemberActivityAddRequest; import com.emmsale.member.application.dto.MemberActivityDeleteRequest; import com.emmsale.member.application.dto.MemberActivityInitialRequest; import com.emmsale.member.application.dto.MemberActivityResponse; import com.emmsale.member.application.dto.MemberActivityResponses; +import com.emmsale.member.application.dto.OpenProfileUrlRequest; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,12 +31,12 @@ import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.test.web.servlet.ResultActions; @WebMvcTest(MemberApi.class) class MemberApiTest extends MockMvcTestHelper { private static final ResponseFieldsSnippet RESPONSE_FIELDS = responseFields( - fieldWithPath("[].activityType").type(JsonFieldType.STRING).description("activity 분류"), fieldWithPath("[].memberActivityResponses[].id").type(JsonFieldType.NUMBER) .description("activity id"), @@ -42,8 +45,11 @@ class MemberApiTest extends MockMvcTestHelper { ); private static final RequestFieldsSnippet REQUEST_FIELDS = requestFields( fieldWithPath("activityIds").description("활동 id들")); + @MockBean private MemberActivityService memberActivityService; + @MockBean + private MemberUpdateService memberUpdateService; @Test @DisplayName("사용자 정보를 잘 저장하면, 204 no Content를 반환해줄 수 있다.") @@ -54,6 +60,7 @@ void register() throws Exception { final MemberActivityInitialRequest request = new MemberActivityInitialRequest(name, activityIds); + final RequestFieldsSnippet REQUEST_FIELDS = requestFields( fieldWithPath("activityIds").description("활동 id들"), fieldWithPath("name").description("사용자 이름")); @@ -169,4 +176,48 @@ void test_findCareer() throws Exception { .andDo(print()) .andDo(document("find-activity", RESPONSE_FIELDS)); } + + @Test + @DisplayName("사용자의 openProfileUrl을 성공적으로 업데이트하면, 200 OK가 반환된다.") + void test_updateOpenProfileUrl() throws Exception { + // given + final String openProfileUrl = "https://open.kakao.com/o/openprofileurl"; + final OpenProfileUrlRequest request = new OpenProfileUrlRequest(openProfileUrl); + + final RequestFieldsSnippet REQUEST_FIELDS = requestFields( + fieldWithPath("openProfileUrl").description("오픈 채팅 url") + ); + + // when + final ResultActions result = mockMvc.perform(put("/members/open-profile-url") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isNoContent()) + .andDo(print()) + .andDo(document("update-open-profile-url", REQUEST_FIELDS)); + } + + @Test + @DisplayName("사용자의 openProfileUrl이 유효하지 않으면, 400 BAD_REQUEST를 반환한다.") + void test_updateOpenProfileUrlWithInvalidUrl() throws Exception { + // given + final String openProfileUrl = "https://invalid.kakao.com/profile"; + final OpenProfileUrlRequest request = new OpenProfileUrlRequest(openProfileUrl); + + final RequestFieldsSnippet REQUEST_FIELDS = requestFields( + fieldWithPath("openProfileUrl").description("오픈 채팅 url") + ); + + // when + final ResultActions result = mockMvc.perform(put("/members/open-profile-url") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + // then + result.andExpect(status().isBadRequest()) + .andDo(print()) + .andDo(document("update-open-profile-url", REQUEST_FIELDS)); + } } diff --git a/backend/emm-sale/src/test/java/com/emmsale/member/application/MemberUpdateServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/member/application/MemberUpdateServiceTest.java new file mode 100644 index 000000000..744c958fc --- /dev/null +++ b/backend/emm-sale/src/test/java/com/emmsale/member/application/MemberUpdateServiceTest.java @@ -0,0 +1,38 @@ +package com.emmsale.member.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.emmsale.helper.ServiceIntegrationTestHelper; +import com.emmsale.member.MemberFixture; +import com.emmsale.member.application.dto.OpenProfileUrlRequest; +import com.emmsale.member.domain.Member; +import com.emmsale.member.domain.MemberRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class MemberUpdateServiceTest extends ServiceIntegrationTestHelper { + + @Autowired + private MemberUpdateService memberUpdateService; + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("오픈 프로필 URL을 업데이트한다.") + void updateOpenProfileUrlTest() { + // given + final Member member = memberRepository.save(MemberFixture.memberFixture()); + + final String expectOpenProfileUrl = "https://open.kakao.com/new/profile/url"; + final OpenProfileUrlRequest request = new OpenProfileUrlRequest(expectOpenProfileUrl); + + // when + memberUpdateService.updateOpenProfileUrl(member, request); + + final Member actualMember = memberRepository.findById(member.getId()).get(); + + // then + assertThat(actualMember.getOpenProfileUrl()).isEqualTo(expectOpenProfileUrl); + } +}