Skip to content

Commit

Permalink
[BE] 핀(Pin) 패키지 코드 리팩터링(PinImage 기능 구현 포함) (#240)
Browse files Browse the repository at this point in the history
* [Docs] GitHub Issue 및 PR Template 설정 (#37)

* chore: .gitignore 추가

* chore: GitHub PR 및 Issue Template

* Revert "chore: GitHub PR 및 Issue Template"

This reverts commit 65915f7.

* Revert "chore: .gitignore 추가"

This reverts commit 1e1865a.

* chore: .gitignore 추가

* chore: GitHub Issue 및 PR Template 추가

* [Docs] GitHub Issue Template 파일명 오류 수정 (#39)

* chore: .gitignore 추가

* chore: GitHub PR 및 Issue Template

* Revert "chore: GitHub PR 및 Issue Template"

This reverts commit 65915f7.

* Revert "chore: .gitignore 추가"

This reverts commit 1e1865a.

* chore: .gitignore 추가

* chore: GitHub Issue 및 PR Template 추가

* chore: GitHub Issue 및 PR Template 추가

* feat: member 구현 중

Co-authored-by: jaeyeon kim <jakind@naver.com>

* feat: 패키지 분리, AuthMember 구현

Co-authored-by: jaeyeon kim <jakind@naver.com>

* feat: MemberArgumentResolver 구현

* feat: AuthTopic 구현

* refactor: API 명세 수정을 위한 임의 커밋

Co-authored-by: jaeyeon kim <jakind@naver.com>

* feat: Publicity, Permission 정적 팩토리 및 Converter 추가

* refactor: Topic 에 Publicity, Permission 추가로 인한 테스트 수정

* refactor: 모든 테스트가 통과하도록 수정

* refactor: 모든 테스트가 통과하도록 수정

* feat: Topic 에 관한 CRUD 에 권한 적용 완료

* fix: Converter 반환값 이상으로 인한 오류 해결

* feat: Pin 기능에 권한 설정 추가

* style: 사용하지 않는 import 문 제거 및 접근 제어자 조정

* refactor: .. 지송;;^^

- 패키지 재분배
- AUTH 관련 기능 구현
- TopicController 및 LocationController 분리

Co-authored-by: jaeyeon kim <jakind@naver.com>

* refactor: TopicStatus 로직 오류 수정 LocationController 오타 수정

* refactor: 불필요 상수 제거

* refactor: PinResponse 자료형 변경

* feat: Image 클래스 구현

* feat: LocationRepository에 하버사인을 이용한 find 메서드 구현

* refactor: LocationController의 메서드명 변경

* refactor: getTopicsWithPermission의 반환값 변경

* style: 불필요 공백 제거

* refactor: 로직 오류 수정 및 테스트 추가

* test: Topic 패키지 테스트 추가

* refactor: Auth 관련 리팩터링 및 TopicIntegrationTest

* refactor: auth 어노테이션 제거

* test: AddressTest 추가

* test: Pin 관련 테스트 추가

* refactor: Coordinate BigDecimal -> Double 로 수정

* refactor: 모든 테스트가 성공하도록 수정

* refactor: LocationRepositoryTest 수정

* feat: 멤버 단일 조회 기능 추가

* feat: 멤버 목록 조회 기능 구현

* feat : Member 를 Create 하는 기능 추가

* feat : Member 가 만든 Topic 들을 조회하는 기능 추가

* feat : Member 가 본인이 만든 Pin 을 조회하는 기능 추가

* feat : 토픽을 생성한 자가 특정 멤버에게 권한을 주는 기능 추가

* feat : 토픽을 생성한자가 특정 멤버의 권한을 삭제하는 기능 추가

* feat : 해당 토픽에 권한을 가진 모든 유저를 조회

* feat : 권한을 가진 유저를 ID 로 조회하는 기능 추가

* refactor: AuthTopic 객체 제거

* chore: AuthToic 변경 사항 반영을 위한 merge

* chore: AuthToic 변경 사항 반영을 위한 merge 추가

* refactor: Pin 예외 메시지 수정

- #127 과 내용 통일

* chore: AuthToic 변경 사항 반영을 위한 merge 추가

* feat : Topic, Pin, Member 에 연관관계 편의 메서드 추가

* refactor: RestDocs를 위한 Interceptor 조건문 추가

* refactor: 핀 조회 관련 API 문서화에 인증 헤더 추가

* test : Permission 추가와 관련된 Domain Test 작성

* test : 다른 유저에게 권한을 주는 기능 테스트 추가

* test : 권한 부여 인수테스트 추가

* docs : 권한 부여 API 명세 작성

* test : 권한 삭제 Service Test 작성

* test : 권한 삭제 인수테스트 작성

* docs : 권한 삭제 API 명세 작성

* test : memberResponse 정적 팩토리 메서드 테스트 작성

* test : 권한이 있는 멤버 전체를 조회하는 기능 Service Test 작성

* test : 해당 토픽에 권한을 가진 자들을 모두 조회하는 기능 인수테스트 작성

* docs : 해당 토픽에 권한을 가진 유저의 목록을 조회하는 API 명세 작성

* test : MemberDetailResponse 생성 테스트 추가

* test : 권한을 가진 유저를 조회하는 기능 Service Test 추가

* test : 권한을 가진 유저를 조회하는 기능 인수테스트 추가

* docs : 권한을 가진 유저를 조회하는 기능 API 명세 추가

* test : 유저를 저장하는 기능 Service Test 작성

* test : 유저를 저장하는 기능 인수테스트 작성

* docs : 유저를 저장하는 기능 API 명세 작성

* test : 유저를 목록 조회하는 기능 Service Test 작성

* test : 유저를 목록 조회하는 기능 인수테스트 작성

* docs : 유저를 목록 조회하는 기능 API 명세 작성

* refactor : AuthInterceptor mocking 을 RestDocs 에서 실행

* test : 유저 단일 조회 Service Test 작성

* test : 유저 단일 조회 인수테스트 작성

* docs : 유저 단일 조회 API 명세 작성

* test : 유저가 만든 토픽, 핀 조회 Service Test 작성

* test : 유저가 만든 토픽, 핀 조회 인수테스트 작성

* docs : 유저가 만든 토픽, 핀 조회 API 명세 작성

* refactor: Pin 생성 시 PinInfo를 묶어서 받도록 수정, 중복 테스트 제거

- PinInfo의 필드인 name, description을 직접 받는 대신 PinInfo를 받도록 함.
- PinInfo의 필드가 추가될 경우에 Pin의 생성자도 수정할 필요가 없도록 함.
- PinTest에서 PinInfoTest와 중복되는 테스트 제거
- PinInfo 업데이트 시 새 객체 할당하도록 하여 검증이 필요한 로직 최소화 (update 관련 테스트 삭제)

* refactor: Pin 생성 시 권한 예외처리 추가

* test: 테스트 코드 컨벤션 통일, 도메인 계층 간 테스트 내용 분리, fixture 사용

* refactor: PinImage 생성 시 Image 객체를 전달받도록 수정, getter 추가

- Pin 리팩터링과 마찬가지로, Image의 필드 추가 시에도 생성 메서드 시그니처 변경할 필요 없도록 함.
- imageUrl에 어떤 검증 로직이 있는지 명시적으로 보여줄 수 있음.

* refactor: 회원 조회 로직 권한 확인 후 수행하도록 변경, 상수 처리

* test: 테스트 클래스 내 픽스쳐 사용 방식 통일, 메서드 순서 변경

* test: Pin copy 테스트 추가

* test: Pin 통합테스트 request 객체 멤버변수로 관리

* feat: PinImage 추가 API 작성, 기존 Pin 생성 및 수정 명세 변경

* test: 중복되는 내용의 PinCommandServiceTest 테스트 삭제

- 하위 계층에서 테스트하는 내용에 대해 테스트하는 메서드 삭제

* feat: 핀 삭제 시 핀 이미지 삭제 구현

* test: 핀 삭제 테스트에서 핀 이미지도 삭제되는지 확인하도록 수정

* refactor: 핀 이미지 추가 시 location 응답헤더 반환하도록 수정

* feat: 핀 이미지 삭제 기능 구현

* feat: 핀 상세 조회 시 이미지 id 반환하도록 수정

* docs: RestDocs 문서화 설정에 새 API 추가

* feat: 핀 복사 시 핀 이미지도 복사하도록 수정

* chore: 불필요한 주석 삭제, 코드 리뷰를 위한 주석 추가

* fix: updatedAt 관련 테스트 실패 해결

* refactor: 중복 거리 상수 단위 표기

* fix: 핀 이미지 삭제 메서드명 수정

* fix: 핀 이미지 관련 핸들러메서드에 LoginRequired 추가

* refactor: 사용하지 않는 Location 헤더값 전달 삭제

* feat: 핀 이미지 삭제 hard delete에서 soft delete으로 변경

* refactor: null 검증 메서드 통일

* test: Pin, PinImage RepositoryTest setUp 메서드 처리

* refactor: Pin 서비스 클래스 메서드 분리

- 조회 및 예외처리 로직 메서드 분리
- 검증 로직 메서드 분리 (TopicCommandService 와 분리 방식 통일)

* test: Pin, PinImage RepositoryTest 픽스쳐 적용

* refactor: Pin 조회 서비스 메서드 네이밍 구체화

* refactor: PinQueryServiceTest 픽스쳐 적용

* refactor: Pin 변수명, 개행 수정

* fix: 864b851 충돌 해결 시 주석 처리 실수 해제, 테스트 실패 해결

* refactor: PinCommandServiceTest 픽스쳐 적용, 테스트 단순화

* test: Pin 서비스 계층 권한 관련 테스트 작성

* refactor: 핀 이미지 제거 메서드명 구체화

* test: 불필요한 객체 생성 삭제

* refactor: 정적 팩터리 메서드 내부에서 객체 생성하도록 변경

- 정적 팩터리 메서드를 의미있게 사용하기 위함
- 다른 도메인에서의 코드와의 통일성을 위해 변경함

---------

Co-authored-by: 준팍(junpak) <112045553+junpakPark@users.noreply.github.com>
Co-authored-by: jaeyeon kim <jakind@naver.com>
Co-authored-by: junpakPark <junpak.park@gmail.com>
  • Loading branch information
4 people authored Aug 9, 2023
1 parent 47cb6b3 commit e0fed7e
Show file tree
Hide file tree
Showing 30 changed files with 946 additions and 781 deletions.
7 changes: 7 additions & 0 deletions backend/src/docs/asciidoc/pin.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ operation::pin-controller-test/update[snippets='http-request,http-response']
=== 핀 삭제

operation::pin-controller-test/delete[snippets='http-request,http-response']

=== 핀 이미지 추가

operation::pin-controller-test/add-image[snippets='http-request,http-response']

=== 핀 이미지 삭제
operation::pin-controller-test/remove-image[snippets='http-request,http-response']
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ protected AuthMember(
public Long getMemberId() {
return memberId;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,12 @@ public double getLongitude() {
return coordinate.getLongitude();
}

public String getRoadBaseAddress() {
return address.getRoadBaseAddress();
}

public String getLegalDongCode() {
return address.getLegalDongCode();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,86 @@
import com.mapbefine.mapbefine.member.domain.MemberRepository;
import com.mapbefine.mapbefine.pin.domain.Pin;
import com.mapbefine.mapbefine.pin.domain.PinImage;
import com.mapbefine.mapbefine.pin.domain.PinImageRepository;
import com.mapbefine.mapbefine.pin.domain.PinRepository;
import com.mapbefine.mapbefine.pin.dto.request.PinCreateRequest;
import com.mapbefine.mapbefine.pin.dto.request.PinImageCreateRequest;
import com.mapbefine.mapbefine.pin.dto.request.PinUpdateRequest;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.domain.TopicRepository;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@Service
public class PinCommandService {

private static final double DUPLICATE_LOCATION_DISTANCE_METERS = 10.0;

private final PinRepository pinRepository;
private final LocationRepository locationRepository;
private final TopicRepository topicRepository;
private final MemberRepository memberRepository;
private final PinImageRepository pinImageRepository;

public PinCommandService(
PinRepository pinRepository,
LocationRepository locationRepository,
TopicRepository topicRepository,
MemberRepository memberRepository
MemberRepository memberRepository,
PinImageRepository pinImageRepository
) {
this.pinRepository = pinRepository;
this.locationRepository = locationRepository;
this.topicRepository = topicRepository;
this.memberRepository = memberRepository;
this.pinImageRepository = pinImageRepository;
}

public Long save(AuthMember authMember, PinCreateRequest request) {
Coordinate coordinate = Coordinate.of(request.latitude(), request.longitude());
Topic topic = topicRepository.findById(request.topicId())
.orElseThrow(NoSuchElementException::new);
Member member = memberRepository.findById(authMember.getMemberId())
.orElseThrow(NoSuchElementException::new);
authMember.canPinCreateOrUpdate(topic);

Location pinLocation = locationRepository.findAllByCoordinateAndDistanceInMeters(
coordinate, 10.0).stream()
.filter(location -> location.isSameAddress(request.address()))
.findFirst()
.orElseGet(() -> saveLocation(request, coordinate));

public long save(AuthMember authMember, PinCreateRequest request) {
Topic topic = findTopic(request.topicId());
validatePinCreateOrUpdate(authMember, topic);

Member member = findMember(authMember.getMemberId());
Pin pin = Pin.createPinAssociatedWithLocationAndTopicAndMember(
request.name(),
request.description(),
pinLocation,
findDuplicateOrCreatePinLocation(request),
topic,
member
);
pinRepository.save(pin);

return pin.getId();
}

private Topic findTopic(Long topicId) {
if (Objects.isNull(topicId)) {
throw new NoSuchElementException("토픽을 찾을 수 없습니다.");
}
return topicRepository.findById(topicId)
.orElseThrow(() -> new NoSuchElementException("토픽을 찾을 수 없습니다."));
}

for (String pinImage : request.images()) {
PinImage.createPinImageAssociatedWithPin(pinImage, pin);
private Member findMember(Long memberId) {
if (Objects.isNull(memberId)) {
throw new NoSuchElementException("회원 정보를 찾을 수 없습니다.");
}
return memberRepository.findById(memberId)
.orElseThrow(() -> new NoSuchElementException("회원 정보를 찾을 수 없습니다."));
}

private Location findDuplicateOrCreatePinLocation(PinCreateRequest request) {
Coordinate coordinate = Coordinate.of(request.latitude(), request.longitude());

return pinRepository.save(pin).getId();
return locationRepository.findAllByCoordinateAndDistanceInMeters(coordinate, DUPLICATE_LOCATION_DISTANCE_METERS)
.stream()
.filter(location -> location.isSameAddress(request.address()))
.findFirst()
.orElseGet(() -> saveLocation(request, coordinate));
}

private Location saveLocation(PinCreateRequest pinCreateRequest, Coordinate coordinate) {
Expand All @@ -74,31 +97,61 @@ private Location saveLocation(PinCreateRequest pinCreateRequest, Coordinate coor
pinCreateRequest.address(),
pinCreateRequest.legalDongCode()
);

Location location = new Location(address, coordinate);

return locationRepository.save(location);
}

public void update(
AuthMember member,
AuthMember authMember,
Long pinId,
PinUpdateRequest request
) {
Pin pin = pinRepository.findById(pinId)
.orElseThrow(NoSuchElementException::new);
member.canPinCreateOrUpdate(pin.getTopic());
Pin pin = findPin(pinId);
validatePinCreateOrUpdate(authMember, pin.getTopic());

pin.updatePinInfo(request.name(), request.description());
}

pinRepository.save(pin);
private Pin findPin(Long pinId) {
return pinRepository.findById(pinId)
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 핀입니다."));
}

public void removeById(AuthMember member, Long pinId) {
Pin pin = pinRepository.findById(pinId)
.orElseThrow(NoSuchElementException::new);
member.canDelete(pin.getTopic());
public void removeById(AuthMember authMember, Long pinId) {
Pin pin = findPin(pinId);
validatePinCreateOrUpdate(authMember, pin.getTopic());

pinRepository.deleteById(pinId);
pinImageRepository.deleteAllByPinId(pinId);
}

public void addImage(AuthMember authMember, PinImageCreateRequest request) {
Pin pin = findPin(request.pinId());
validatePinCreateOrUpdate(authMember, pin.getTopic());

PinImage pinImage = PinImage.createPinImageAssociatedWithPin(request.imageUrl(), pin);
pinImageRepository.save(pinImage);
}

public void removeImageById(AuthMember authMember, Long pinImageId) {
PinImage pinImage = findPinImage(pinImageId);
Pin pin = pinImage.getPin();
validatePinCreateOrUpdate(authMember, pin.getTopic());

pinImageRepository.deleteById(pinImageId);
}

private PinImage findPinImage(Long pinImageId) {
return pinImageRepository.findById(pinImageId)
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 핀 이미지입니다."));
}

private void validatePinCreateOrUpdate(AuthMember authMember, Topic topic) {
if (authMember.canPinCreateOrUpdate(topic)) {
return;
}

throw new IllegalArgumentException("핀 생성 및 수정 권한이 없습니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.mapbefine.mapbefine.pin.domain.PinRepository;
import com.mapbefine.mapbefine.pin.dto.response.PinDetailResponse;
import com.mapbefine.mapbefine.pin.dto.response.PinResponse;
import com.mapbefine.mapbefine.topic.domain.Topic;
import java.util.List;
import java.util.NoSuchElementException;
import org.springframework.stereotype.Service;
Expand All @@ -13,26 +14,37 @@
@Transactional(readOnly = true)
@Service
public class PinQueryService {

private final PinRepository pinRepository;

public PinQueryService(PinRepository pinRepository) {
this.pinRepository = pinRepository;
}

public List<PinResponse> findAll(AuthMember member) {
// TODO: 2023/08/08 isDeleted 제외하고 조회하기
public List<PinResponse> findAllReadable(AuthMember member) {
return pinRepository.findAll()
.stream()
.filter(pin -> member.canRead(pin.getTopic()))
.map(PinResponse::from)
.toList();
}

public PinDetailResponse findById(AuthMember member, Long pinId) {
// TODO: 2023/08/08 isDeleted 제외하고 조회하기
public PinDetailResponse findDetailById(AuthMember member, Long pinId) {
Pin pin = pinRepository.findById(pinId)
.filter(optionalPin -> member.canRead(optionalPin.getTopic()))
.orElseThrow(NoSuchElementException::new);
.orElseThrow(() -> new NoSuchElementException("존재하지 않는 핀입니다."));
validateReadAuth(member, pin.getTopic());

return PinDetailResponse.from(pin);
}

private void validateReadAuth(AuthMember member, Topic topic) {
if (member.canRead(topic)) {
return;
}

throw new IllegalArgumentException("조회 권한이 없습니다.");
}

}
33 changes: 20 additions & 13 deletions backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@ public class Pin extends BaseTimeEntity {
@Embedded
private PinInfo pinInfo;

@ManyToOne
@JoinColumn(name = "member_id")
private Member creator;

@OneToMany(mappedBy = "pin", cascade = CascadeType.PERSIST)
private List<PinImage> pinImages = new ArrayList<>();

@ManyToOne
@JoinColumn(name = "location_id", nullable = false)
private Location location;
Expand All @@ -50,10 +43,17 @@ public class Pin extends BaseTimeEntity {
@JoinColumn(name = "topic_id", nullable = false)
private Topic topic;

@ManyToOne
@JoinColumn(name = "member_id")
private Member creator;

@Column(nullable = false)
@ColumnDefault(value = "false")
private boolean isDeleted = false;

@OneToMany(mappedBy = "pin", cascade = CascadeType.PERSIST)
private List<PinImage> pinImages = new ArrayList<>();

private Pin(
PinInfo pinInfo,
Location location,
Expand All @@ -73,33 +73,40 @@ public static Pin createPinAssociatedWithLocationAndTopicAndMember(
Topic topic,
Member creator
) {
PinInfo pinInfo = PinInfo.of(name, description);

Pin pin = new Pin(
pinInfo,
PinInfo.of(name, description),
location,
topic,
creator
);

location.addPin(pin);
topic.addPin(pin);
creator.addPin(pin);

return pin;
}

public void updatePinInfo(String name, String description) {
pinInfo.update(name, description);
pinInfo = PinInfo.of(name, description);
}

public Pin copy(Topic topic, Member creator) {
return Pin.createPinAssociatedWithLocationAndTopicAndMember(
Pin copy = Pin.createPinAssociatedWithLocationAndTopicAndMember(
pinInfo.getName(),
pinInfo.getDescription(),
location,
topic,
creator
);
copyPinImages(copy);

return copy;
}

private void copyPinImages(Pin pin) {
for (PinImage pinImage : pinImages) {
PinImage.createPinImageAssociatedWithPin(pinImage.getImageUrl(), pin);
}
}

public void addPinImage(PinImage pinImage) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.mapbefine.mapbefine.pin.domain;

import com.mapbefine.mapbefine.common.entity.BaseTimeEntity;
import com.mapbefine.mapbefine.common.entity.Image;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand All @@ -11,11 +13,12 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class PinImage {
public class PinImage extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -28,7 +31,10 @@ public class PinImage {
@JoinColumn(name = "pin_id")
private Pin pin;


@Column(nullable = false)
@ColumnDefault(value = "false")
private boolean isDeleted = false;

private PinImage(Image image, Pin pin) {
this.image = image;
this.pin = pin;
Expand All @@ -41,4 +47,8 @@ public static PinImage createPinImageAssociatedWithPin(String imageUrl, Pin pin)
return pinImage;
}

public String getImageUrl() {
return image.getImageUrl();
}

}
Loading

0 comments on commit e0fed7e

Please sign in to comment.