Skip to content

Commit

Permalink
서포터 프로필 변경 API (#315)
Browse files Browse the repository at this point in the history
* feat: 로그인 한 사용자 조회 API 구현

* test: 인수 테스트 성공 케이스 작성

* refactor: restdocsConfig 변경

* test: 로그인된 사용자 조회 API mock mvc 테스트 진행

* refactor: 코드 리뷰 반영

* feat: request dto 생성

* test: 인수 테스트 작성

* test: 인수 테스트 수정

* feat: technical tag 이름으로 technical tag 찾는 기능 추가

* feat: supporter 와 technicalTag 로 supporterTechnicalTag 찾는 기능 구현

* refactor: supporter 와 technicalTag 로 supporterTechnicalTag 찾는 기능 제거

* feat: API 구현

* test: restdocs 테스트 구현

* refactor: submodule 변경

* refactor: submodule 수정

* refactor: Location 반환하도록 변경

* refactor: static 으로 된 메서드들 non-static 으로 변경

* feat: 서포터로 SupporterTechnicalTag 제거 기능 구현

* refactor: RunnerTechnicalTag nullable 하게 변경

* refactor: Batch delete 방식으로 데이터 삭제하도록 변경

* refactor: uri 매핑 형식 변경

* test: 공백 제거

* refactor: batch delete 시에 flush & clear

* test: 오타 수정
  • Loading branch information
shb03323 authored Aug 11, 2023
1 parent 7671338 commit 3ad7f10
Show file tree
Hide file tree
Showing 26 changed files with 741 additions and 74 deletions.
2 changes: 1 addition & 1 deletion backend/baton/secret
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ endif::[]

== *로그인된 사용자 프로필 조회*

=== *로그인된 사용자 프로필 조회 조회 API*
=== *로그인된 사용자 프로필 조회 API*

==== *Http Request*
include::{snippets}/../../build/generated-snippets/member-login-profile-read-api-test/read-login-member-by-access-token/http-request.adoc[]
Expand Down
30 changes: 30 additions & 0 deletions backend/baton/src/docs/asciidoc/SupporterProfileUpdateApi.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]
:doctype: investment
:icons: font
:source-highlighter: highlight.js
:toc: left
:toclevels: 2
:sectlinks:
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response

== *서포터 프로필 수정*

=== *서포터 프로필 수정 API*

==== *Http Request*
include::{snippets}/../../build/generated-snippets/supporter-profile-update-api-test/update-supporter-profile/http-request.adoc[]

==== *Http Request Headers*
include::{snippets}/../../build/generated-snippets/supporter-profile-update-api-test/update-supporter-profile/request-headers.adoc[]

==== *Http Request Body*
include::{snippets}/../../build/generated-snippets/supporter-profile-update-api-test/update-supporter-profile/request-body.adoc[]

==== *Http Response*
include::{snippets}/../../build/generated-snippets/supporter-profile-update-api-test/update-supporter-profile/http-response.adoc[]

==== *Http Response Headers*
include::{snippets}/../../build/generated-snippets/supporter-profile-update-api-test/update-supporter-profile/response-headers.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@ public enum ClientErrorCode {
SUPPORTER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "FB002", "서포터 식별자를 입력해주세요."),
RUNNER_ID_IS_NULL(HttpStatus.BAD_REQUEST, "FB003", "러너 식별자를 입력해주세요."),

COMPANY_IS_NULL(HttpStatus.BAD_REQUEST, "OM001", "사용자의 회사 정보를 입력해주세요."),
NAME_IS_NULL(HttpStatus.BAD_REQUEST, "MB001", "사용자의 이름을 입력해주세요."),
COMPANY_IS_NULL(HttpStatus.BAD_REQUEST, "MB002", "사용자의 회사 정보를 입력해주세요."),
SUPPORTER_TECHNICAL_TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "MB003", "서포터 기술 태그 목록을 빈 값이라도 입력해주세요."),
RUNNER_TECHNICAL_TAGS_ARE_NULL(HttpStatus.BAD_REQUEST, "MB004", "러너 기술 태그 목록을 빈 값이라도 입력해주세요."),

OAUTH_REQUEST_URL_PROVIDER_IS_WRONG(HttpStatus.UNAUTHORIZED, "OA001", "redirect 할 url 이 조회되지 않는 잘못된 소셜 타입입니다."),
OAUTH_INFORMATION_CLIENT_IS_WRONG(HttpStatus.UNAUTHORIZED, "OA002", " 소셜 계정 정보를 조회할 수 없는 잘못된 소셜 타입입니다."),
OAUTH_AUTHORIZATION_VALUE_IS_NULL(HttpStatus.UNAUTHORIZED, "OA003", "Authorization 값을 입력해주세요."),
OAUTH_AUTHORIZATION_BEARER_TYPE_NOT_FOUND(HttpStatus.UNAUTHORIZED, "OA004", "Authorization 값을 Bearer 타입으로 입력해주세요."),

JWT_SIGNATURE_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW001", "시그니처가 다른 잘못된 JWT 입니다."),
JWT_FORM_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW002", "잘못 생성된 JWT 로 디코딩 할 수 없습니다."),
JWT_CLAIM_IS_WRONG(HttpStatus.UNAUTHORIZED, "JW003", "JWT 에 기대한 정보를 모두 포함하고 있지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
package touch.baton.domain.common.response;

import lombok.Getter;
import touch.baton.domain.common.exception.ClientRequestException;

@Getter
public class ErrorResponse {

private final String errorCode;
private final String message;

private ErrorResponse(final String errorCode, final String message) {
this.errorCode = errorCode;
this.message = message;
}
public record ErrorResponse(String errorCode, String message) {

public static ErrorResponse from(final ClientRequestException e) {
return new ErrorResponse(e.getErrorCode().getErrorCode(), e.getMessage());
Expand Down
51 changes: 40 additions & 11 deletions backend/baton/src/main/java/touch/baton/domain/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,57 @@ private void validateNotNull(final MemberName memberName,
final Company company,
final ImageUrl imageUrl
) {
if (Objects.isNull(memberName)) {
throw new MemberDomainException("Member 의 name 은 null 일 수 없습니다.");
}
validateMemberNameNotNull(memberName);
validateSocialIdNotNull(socialId);
validateOauthIdNotNull(oauthId);
validateGithubUrlNotNull(githubUrl);
validateCompanyNotNull(company);
validateImageUrlNotNull(imageUrl);
}

if (Objects.isNull(socialId)) {
throw new MemberDomainException("Member 의 socialId 은 null 일 수 없습니다.");
private void validateImageUrlNotNull(final ImageUrl imageUrl) {
if (Objects.isNull(imageUrl)) {
throw new MemberDomainException("Member 의 imageUrl 은 null 일 수 없습니다.");
}
}

if (Objects.isNull(oauthId)) {
throw new MemberDomainException("Member 의 oauthId 는 null 일 수 없습니다.");
private void validateCompanyNotNull(final Company company) {
if (Objects.isNull(company)) {
throw new MemberDomainException("Member 의 company 는 null 일 수 없습니다.");
}
}

private void validateGithubUrlNotNull(final GithubUrl githubUrl) {
if (Objects.isNull(githubUrl)) {
throw new MemberDomainException("Member 의 githubUrl 은 null 일 수 없습니다.");
}
}

if (Objects.isNull(company)) {
throw new MemberDomainException("Member 의 company 는 null 일 수 없습니다.");
private void validateOauthIdNotNull(final OauthId oauthId) {
if (Objects.isNull(oauthId)) {
throw new MemberDomainException("Member 의 oauthId 는 null 일 수 없습니다.");
}
}

if (Objects.isNull(imageUrl)) {
throw new MemberDomainException("Member 의 imageUrl 은 null 일 수 없습니다.");
private void validateSocialIdNotNull(final SocialId socialId) {
if (Objects.isNull(socialId)) {
throw new MemberDomainException("Member 의 socialId 은 null 일 수 없습니다.");
}
}

private void validateMemberNameNotNull(final MemberName memberName) {
if (Objects.isNull(memberName)) {
throw new MemberDomainException("Member 의 name 은 null 일 수 없습니다.");
}
}

public void updateMemberName(final MemberName memberName) {
validateMemberNameNotNull(memberName);
this.memberName = memberName;
}

public void updateCompany(final Company company) {
validateCompanyNotNull(company);
this.company = company;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,17 @@ private Runner(final Long id,
final Member member,
final RunnerTechnicalTags runnerTechnicalTags
) {
validateNotNull(member, runnerTechnicalTags);
validateMemberNotNull(member);
this.id = id;
this.introduction = introduction;
this.member = member;
this.runnerTechnicalTags = runnerTechnicalTags;
}

private void validateNotNull(final Member member, final RunnerTechnicalTags runnerTechnicalTags) {
private void validateMemberNotNull(final Member member) {
if (Objects.isNull(member)) {
throw new RunnerDomainException("Runner 의 member 는 null 일 수 없습니다.");
}

if (Objects.isNull(runnerTechnicalTags)) {
throw new RunnerDomainException("Runner 의 runnerTechnicalTags 는 null 일 수 없습니다.");
}
}

public void addAllRunnerTechnicalTags(final List<RunnerTechnicalTag> runnerTechnicalTags) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import touch.baton.domain.common.BaseEntity;
import touch.baton.domain.common.vo.Introduction;
import touch.baton.domain.member.Member;
import touch.baton.domain.member.vo.Company;
import touch.baton.domain.member.vo.MemberName;
import touch.baton.domain.supporter.exception.SupporterDomainException;
import touch.baton.domain.supporter.vo.ReviewCount;
import touch.baton.domain.technicaltag.SupporterTechnicalTag;
Expand Down Expand Up @@ -74,23 +76,45 @@ private void validateNotNull(final ReviewCount reviewCount,
final Member member,
final SupporterTechnicalTags supporterTechnicalTags
) {
if (Objects.isNull(reviewCount)) {
throw new SupporterDomainException("Supporter 의 reviewCount 는 null 일 수 없습니다.");
validateReviewCountNotNull(reviewCount);
validateMemberNotNull(member);
validateSupporterTechnicalTagsNotNull(supporterTechnicalTags);
}

private void validateSupporterTechnicalTagsNotNull(final SupporterTechnicalTags supporterTechnicalTags) {
if (Objects.isNull(supporterTechnicalTags)) {
throw new SupporterDomainException("Supporter 의 supporterTechnicalTags 는 null 일 수 없습니다.");
}
}

private void validateMemberNotNull(final Member member) {
if (Objects.isNull(member)) {
throw new SupporterDomainException("Supporter 의 member 는 null 일 수 없습니다.");
}
}

if (Objects.isNull(supporterTechnicalTags)) {
throw new SupporterDomainException("Supporter 의 supporterTechnicalTags 는 null 일 수 없습니다.");
private void validateReviewCountNotNull(final ReviewCount reviewCount) {
if (Objects.isNull(reviewCount)) {
throw new SupporterDomainException("Supporter 의 reviewCount 는 null 일 수 없습니다.");
}
}

public void addAllSupporterTechnicalTags(final List<SupporterTechnicalTag> supporterTechnicalTags) {
this.supporterTechnicalTags.addAll(supporterTechnicalTags);
}

public void updateMemberName(final MemberName memberName) {
this.member.updateMemberName(memberName);
}

public void updateCompany(final Company company) {
this.member.updateCompany(company);
}

public void updateIntroduction(final Introduction introduction) {
this.introduction = introduction;
}

@Override
public boolean equals(final Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package touch.baton.domain.supporter.controller;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;
import touch.baton.domain.oauth.controller.resolver.AuthSupporterPrincipal;
import touch.baton.domain.supporter.Supporter;
import touch.baton.domain.supporter.controller.response.SupporterResponse;
import touch.baton.domain.supporter.service.SupporterService;
import touch.baton.domain.supporter.service.dto.SupporterUpdateRequest;

import java.net.URI;

@RequiredArgsConstructor
@RequestMapping("/api/v1/profile/supporter")
Expand All @@ -24,4 +32,12 @@ public ResponseEntity<SupporterResponse.Profile> readProfileBySupporterId(@PathV

return ResponseEntity.ok(response);
}

@PatchMapping("/me")
public ResponseEntity<Void> updateProfile(@AuthSupporterPrincipal final Supporter supporter,
@RequestBody @Valid final SupporterUpdateRequest request) {
supporterService.updateSupporter(supporter, request);
final URI redirectUri = UriComponentsBuilder.fromPath("/api/v1/profile/supporter/me").build().toUri();
return ResponseEntity.noContent().location(redirectUri).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import touch.baton.domain.common.vo.Introduction;
import touch.baton.domain.common.vo.TagName;
import touch.baton.domain.member.vo.Company;
import touch.baton.domain.member.vo.MemberName;
import touch.baton.domain.supporter.Supporter;
import touch.baton.domain.supporter.exception.SupporterBusinessException;
import touch.baton.domain.supporter.repository.SupporterRepository;
import touch.baton.domain.supporter.service.dto.SupporterUpdateRequest;
import touch.baton.domain.technicaltag.SupporterTechnicalTag;
import touch.baton.domain.technicaltag.TechnicalTag;
import touch.baton.domain.technicaltag.repository.SupporterTechnicalTagRepository;
import touch.baton.domain.technicaltag.repository.TechnicalTagRepository;

import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Service
public class SupporterService {

private final SupporterRepository supporterRepository;
private final TechnicalTagRepository technicalTagRepository;
private final SupporterTechnicalTagRepository supporterTechnicalTagRepository;

public List<Supporter> readAllSupporters() {
return supporterRepository.findAll();
Expand All @@ -24,4 +36,32 @@ public Supporter readBySupporterId(final Long supporterId) {
return supporterRepository.joinMemberBySupporterId(supporterId)
.orElseThrow(() -> new SupporterBusinessException("존재하지 않는 서포터 식별자값으로 조회할 수 없습니다."));
}

@Transactional
public void updateSupporter(final Supporter supporter, final SupporterUpdateRequest supporterUpdateRequest) {
supporter.updateMemberName(new MemberName(supporterUpdateRequest.name()));
supporter.updateCompany(new Company(supporterUpdateRequest.company()));
supporter.updateIntroduction(new Introduction(supporterUpdateRequest.introduction()));
supporterTechnicalTagRepository.deleteBySupporter(supporter);
supporterUpdateRequest.technicalTags()
.forEach(tagName -> createSupporterTechnicalTag(supporter, new TagName(tagName)));
}

private SupporterTechnicalTag createSupporterTechnicalTag(final Supporter supporter, final TagName tagName) {
final TechnicalTag technicalTag = findTechnicalTagIfExistElseCreate(tagName);
return supporterTechnicalTagRepository.save(SupporterTechnicalTag.builder()
.supporter(supporter)
.technicalTag(technicalTag)
.build()
);
}

private TechnicalTag findTechnicalTagIfExistElseCreate(final TagName tagName) {
final Optional<TechnicalTag> maybeTechnicalTag = technicalTagRepository.findByTagName(tagName);
return maybeTechnicalTag.orElseGet(() ->
technicalTagRepository.save(TechnicalTag.builder()
.tagName(tagName)
.build()
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package touch.baton.domain.supporter.service.dto;

import touch.baton.domain.common.exception.validator.ValidNotNull;

import java.util.List;

import static touch.baton.domain.common.exception.ClientErrorCode.*;

public record SupporterUpdateRequest(@ValidNotNull(clientErrorCode = NAME_IS_NULL)
String name,
@ValidNotNull(clientErrorCode = COMPANY_IS_NULL)
String company,
String introduction,
@ValidNotNull(clientErrorCode = SUPPORTER_TECHNICAL_TAGS_ARE_NULL)
List<String> technicalTags) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package touch.baton.domain.technicaltag.repository;

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;
import touch.baton.domain.supporter.Supporter;
import touch.baton.domain.technicaltag.SupporterTechnicalTag;

public interface SupporterTechnicalTagRepository extends JpaRepository<SupporterTechnicalTag, Long> {

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("DELETE FROM SupporterTechnicalTag st WHERE st.supporter = :supporter")
int deleteBySupporter(@Param("supporter") final Supporter supporter);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package touch.baton.domain.technicaltag.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import touch.baton.domain.common.vo.TagName;
import touch.baton.domain.technicaltag.TechnicalTag;

import java.util.Optional;

public interface TechnicalTagRepository extends JpaRepository<TechnicalTag, Long> {

Optional<TechnicalTag> findByTagName(final TagName tagName);
}
2 changes: 1 addition & 1 deletion backend/baton/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ oauth:
scope: ${OAUTH_GITHUB_SCOPE}

cors:
allowed-origin: http://localhost:8080
allowed-origin: http://localhost:3000

jwt:
token:
Expand Down
Loading

0 comments on commit 3ad7f10

Please sign in to comment.