Skip to content

Commit

Permalink
feat: 셀프 소개 기능을 구현한다 (#46)
Browse files Browse the repository at this point in the history
* feat: 셀프 소개글 기능에서 발생할 수 있는 예외클래스 지정

- InvalidContentException
- SelfIntroNotFoundException
- WriterNotEqualsException

* feat: 셀프 소개글 기능에서 사용할 request dto 생성

- CityRequest
- SelfIntroCreateRequest
- SelfIntroFilterRequest
- SelfIntroUpdateRequest

* feat: 셀프 소개글 기능에서 사용할 response dto 생성

- SelfIntroResponse
- SelfIntrosResponse

* feat: 셀프 소개글을 저장하고 관리하는 엔티티 구현

- SelfIntro

* feat: 셀프 소개글을 저장, 조회, 삭제하기 위한 repository 구현

- SelfIntroRepository
- SelfIntroJpaRepository
- SelfIntroQueryRepository
- SelfIntroRepositoryImpl

* feat: 셀프 소개글의 읽기 전용 Service 구현

- SelfIntroQueryService

* feat: 셀프 소개글의 쓰기 전용 Service 구현

- SelfIntroService

* feat: 셀프 소개글 Controller 구현

- SelfIntroController

* feat: 셀프 소개글 기능 이용시 인증을 위한 인터셉터에 url에 추가

* test: 테스트 패키지 구조 변경으로 인한 수정

* test: SelfIntro를 검증하기 위한 테스트 클래스 작성

* test: SelfIntroService를 검증하기 위한 테스트 클래스 작성

* test: SelfIntroQueryService를 검증하기 위한 테스트 클래스 작성

* test: SelfIntroQueryRepository를 검증하기 위한 테스트 클래스 작성

* test: SelfIntroJpaRepository를 검증하기 위한 테스트 클래스 작성

* test: SelfIntro 컨트롤러 단위테스트 작성

* test: SelfIntro 인수테스트 작성

* test: SelfIntro 인수테스트 설정을 하기 위해 helper 작성

* test: SelfIntro 서비스 계층 테스트시 사용할 FakeRepository 작성

* test: 테스트를 편하게 하기 위해 fixture 작

* test: 경로 변경으로 인한 테스트 코드 수정

* refactor: 개행 추가

* refactor: 메서드 구조 변경

* test: test 수정

* refactor: 상수 값 변경

* refactor: 줄바꿈 제거

* refactor: 사용하는 jpa 내장 메서드 가시화

* docs: selfintro api 명세서 작성
  • Loading branch information
eom-tae-in authored Jul 17, 2024
1 parent d780e20 commit 0a8d8cb
Show file tree
Hide file tree
Showing 75 changed files with 1,909 additions and 140 deletions.
71 changes: 70 additions & 1 deletion src/docs/asciidoc/member.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
:sectlinks:
:sectnums:

== Member
= Member


== Auth


=== 로그인 (POST api/members/auth/login)

Expand All @@ -18,6 +22,9 @@ include::{snippets}/member-auth-controller-web-mvc-test/유저_로그인/http-re
include::{snippets}/member-auth-controller-web-mvc-test/유저_로그인/request-fields.adoc[]


== Member


=== 닉네임 중복 확인 (GET /api/members/nickname/existence)

==== 요청
Expand All @@ -41,6 +48,7 @@ include::{snippets}/member-controller-web-mvc-test/회원_정보_초기화/http-


=== 회원 정보 조회(GET /api/members)

==== 요청
include::{snippets}/member-controller-web-mvc-test/회원_정보_조회/request-headers.adoc[]
include::{snippets}/member-controller-web-mvc-test/회원_정보_조회/http-request.adoc[]
Expand Down Expand Up @@ -69,3 +77,64 @@ include::{snippets}/member-controller-web-mvc-test/회원_삭제/http-request.ad

==== 응답
include::{snippets}/member-controller-web-mvc-test/회원_삭제/http-response.adoc[]


== SelfIntro


=== 셀프 소개글 저장 (POST /api/members/self-intros/me)

==== 요청
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_저장/http-request.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_저장/request-headers.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_저장/request-fields.adoc[]

==== 응답
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_저장/http-response.adoc[]


=== 셀프 소개글 페이징 조회 (GET /api/members/self-intros)

==== 요청
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회/http-request.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회/request-headers.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회/request-parts.adoc[]

==== 응답
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회/http-response.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회/response-fields.adoc[]


=== 셀프 소개글 페이징 조회 (필터 적용) (Get /api/members/self-intros/filter)

==== 요청
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회(필터_적용)/http-request.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회(필터_적용)/request-headers.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회(필터_적용)/request-parts.adoc[]

==== 응답
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회(필터_적용)/http-response.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_페이징_조회(필터_적용)/response-fields.adoc[]


=== 셀프 소개글 변경 (Patch /api/members/self-intros/{id})

==== 요청
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_변경/http-request.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_변경/request-headers.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_변경/path-parameters.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_변경/request-fields.adoc[]

==== 응답
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_변경/http-response.adoc[]


=== 셀프 소개글 삭제 (Delete /api/members/self-intros/{id})

==== 요청
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_삭제/http-request.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_삭제/request-headers.adoc[]
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_삭제/path-parameters.adoc[]

==== 응답
include::{snippets}/self-intro-controller-web-mvc-test/셀프_소개글_삭제/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.atwoz.member.application.selfintro;

import com.atwoz.member.application.selfintro.dto.SelfIntroFilterRequest;
import com.atwoz.member.application.selfintro.dto.SelfIntrosResponse;
import com.atwoz.member.domain.selfintro.SelfIntroRepository;
import com.atwoz.member.infrastructure.selfintro.dto.SelfIntroResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

private final SelfIntroRepository selfIntroRepository;

public SelfIntrosResponse findAllSelfIntrosWithPaging(final Pageable pageable,
final Long memberId) {
Page<SelfIntroResponse> selfIntroResponses = selfIntroRepository.findAllSelfIntrosWithPaging(pageable, memberId);

return SelfIntrosResponse.of(selfIntroResponses, pageable);
}

public SelfIntrosResponse findAllSelfIntrosWithPagingAndFiltering(final Pageable pageable,
final SelfIntroFilterRequest selfIntroFilterRequest,
final Long memberId) {
Page<SelfIntroResponse> selfIntroResponses = selfIntroRepository.findAllSelfIntrosWithPagingAndFiltering(
pageable,
selfIntroFilterRequest.minAge(),
selfIntroFilterRequest.maxAge(),
selfIntroFilterRequest.isOnlyOppositeGender(),
selfIntroFilterRequest.getCities(),
memberId
);

return SelfIntrosResponse.of(selfIntroResponses, pageable);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.atwoz.member.application.selfintro;

import com.atwoz.member.application.selfintro.dto.SelfIntroCreateRequest;
import com.atwoz.member.application.selfintro.dto.SelfIntroUpdateRequest;
import com.atwoz.member.domain.selfintro.SelfIntro;
import com.atwoz.member.domain.selfintro.SelfIntroRepository;
import com.atwoz.member.exception.exceptions.selfintro.SelfIntroNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
@Transactional
public class SelfIntroService {

private final SelfIntroRepository selfIntroRepository;

public void saveSelfIntro(final SelfIntroCreateRequest selfIntroCreateRequest,
final Long memberId) {
SelfIntro selfIntro = SelfIntro.createWith(memberId, selfIntroCreateRequest.content());
selfIntroRepository.save(selfIntro);
}

public void updateSelfIntro(final SelfIntroUpdateRequest selfIntroUpdateRequest,
final Long selfIntroId,
final Long memberId) {
SelfIntro foundSelfIntro = findSelfIntroById(selfIntroId);
foundSelfIntro.validateSameWriter(memberId);
foundSelfIntro.update(selfIntroUpdateRequest.content());
}

public void deleteSelfIntro(final Long selfIntroId, final Long memberId) {
SelfIntro foundSelfIntro = findSelfIntroById(selfIntroId);
foundSelfIntro.validateSameWriter(memberId);
selfIntroRepository.deleteById(foundSelfIntro.getId());
}

private SelfIntro findSelfIntroById(final Long selfIntroId) {
return selfIntroRepository.findById(selfIntroId)
.orElseThrow(SelfIntroNotFoundException::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.atwoz.member.application.selfintro.dto;

import jakarta.validation.constraints.NotBlank;

public record CityRequest(

@NotBlank(message = "선호하는 지역 정보를 입력해주세요.")
String city
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.atwoz.member.application.selfintro.dto;

import jakarta.validation.constraints.NotBlank;

public record SelfIntroCreateRequest(

@NotBlank(message = "소개글을 입력해주세요")
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.atwoz.member.application.selfintro.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;

public record SelfIntroFilterRequest(

@NotNull(message = "최소 나이를 입력해주세요")
Integer minAge,

@NotNull(message = "최대 나이를 입력해주세요")
Integer maxAge,

@NotNull(message = "성별을 선택해주세요")
Boolean isOnlyOppositeGender,

@Valid
@NotEmpty(message = "선호 지역을 하나 이상 입력해주세요.")
List<CityRequest> cityRequests
) {

public List<String> getCities() {
return cityRequests.stream()
.map(CityRequest::city)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.atwoz.member.application.selfintro.dto;

import jakarta.validation.constraints.NotBlank;

public record SelfIntroUpdateRequest(

@NotBlank(message = "소개글을 입력해주세요")
String content
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.atwoz.member.application.selfintro.dto;

import com.atwoz.member.infrastructure.selfintro.dto.SelfIntroResponse;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public record SelfIntrosResponse(
List<SelfIntroResponse> selfIntros,
int nowPage,
int nextPage,
int totalPages
) {

private static final int NEXT_PAGE_INDEX = 1;
private static final int NO_MORE_PAGE = -1;

public static SelfIntrosResponse of(final Page<SelfIntroResponse> selfIntros,
final Pageable pageable) {
return new SelfIntrosResponse(
selfIntros.getContent(),
pageable.getPageNumber(),
getNextPage(pageable.getPageNumber(), selfIntros),
selfIntros.getTotalPages()
);
}

private static int getNextPage(final int pageNumber, final Page<SelfIntroResponse> selfIntros) {
if (selfIntros.hasNext()) {
return pageNumber + NEXT_PAGE_INDEX;
}

return NO_MORE_PAGE;
}
}
4 changes: 3 additions & 1 deletion src/main/java/com/atwoz/member/config/MemberAuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ private HandlerInterceptor loginValidCheckerInterceptor() {
.excludePathPattern("/**", OPTIONS)
.excludePathPattern("/api/missions/**", GET, POST, PATCH, DELETE)
.excludePathPattern("/api/surveys/**", GET, POST)
.excludePathPattern("/api/members/auth/**", POST)
.addPathPatterns("/api/members/**", GET, POST, PATCH, DELETE)
.addPathPatterns("/api/reports/**", POST)
.addPathPatterns("/api/surveys/**", GET, POST)
.addPathPatterns("/api/members/me/missions/**", GET, POST, PATCH)
.addPathPatterns("/api/members/me/surveys/**", GET, POST);
.addPathPatterns("/api/members/me/surveys/**", GET, POST)
.addPathPatterns("/api/members/self-intros/**", GET, POST, PATCH, DELETE);
}

@Override
Expand Down
66 changes: 66 additions & 0 deletions src/main/java/com/atwoz/member/domain/selfintro/SelfIntro.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.atwoz.member.domain.selfintro;

import com.atwoz.global.domain.BaseEntity;
import com.atwoz.member.exception.exceptions.selfintro.InvalidContentException;
import com.atwoz.member.exception.exceptions.selfintro.WriterNotEqualsException;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@EqualsAndHashCode(of = "id", callSuper = false)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Entity
public class SelfIntro extends BaseEntity {

private static final int MIN_LENGTH = 1;
private static final int MAX_LENGTH = 30;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private Long memberId;

@Column(length = MAX_LENGTH, nullable = false)
private String content;

public static SelfIntro createWith(final Long memberId,
final String content) {
validateContent(content);
return SelfIntro.builder()
.memberId(memberId)
.content(content)
.build();
}

private static void validateContent(final String content) {
int contentLength = content.length();
if (contentLength < MIN_LENGTH || MAX_LENGTH < contentLength) {
throw new InvalidContentException();
}
}

public void validateSameWriter(final Long memberId) {
if (!Objects.equals(this.memberId, memberId)) {
throw new WriterNotEqualsException();
}
}

public void update(final String content) {
validateContent(content);
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.atwoz.member.domain.selfintro;

import com.atwoz.member.infrastructure.selfintro.dto.SelfIntroResponse;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface SelfIntroRepository {

SelfIntro save(SelfIntro selfIntro);

Optional<SelfIntro> findById(Long id);

void deleteById(Long id);

Page<SelfIntroResponse> findAllSelfIntrosWithPaging(Pageable pageable, Long memberId);

Page<SelfIntroResponse> findAllSelfIntrosWithPagingAndFiltering(Pageable pageable, int minAge, int maxAge,
boolean isOnlyOppositeGender, List<String> cities,
Long memberId);
}
Loading

0 comments on commit 0a8d8cb

Please sign in to comment.