Skip to content
Open
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.kilometer.backend.controller;

import com.kilometer.domain.archive.ArchiveService;
import com.kilometer.domain.archive.request.ArchiveCreateRequest;
import com.kilometer.domain.archive.service.ArchiveService;
import com.kilometer.domain.archive.dto.ArchiveDeleteResponse;
import com.kilometer.domain.archive.dto.ArchiveDetailResponse;
import com.kilometer.domain.archive.dto.ArchiveInfo;
import com.kilometer.domain.archive.dto.ArchiveResponse;
import com.kilometer.domain.archive.dto.ArchiveSortType;
import com.kilometer.domain.archive.dto.MyArchiveResponse;
import com.kilometer.domain.archive.like.dto.LikeResponse;
import com.kilometer.domain.archive.request.ArchiveRequest;
import com.kilometer.domain.archive.request.ArchiveUpdateRequest;
import com.kilometer.domain.dto.GeneralResponse;
import com.kilometer.domain.paging.RequestPagingStatus;
import com.kilometer.domain.util.ApiUrlUtils;
Expand Down Expand Up @@ -60,7 +61,7 @@ public ArchiveDetailResponse archive(
@PostMapping(ApiUrlUtils.ARCHIVE_ROOT)
@ApiOperation(value = "신규 아카이브 등록")
public ArchiveInfo saveArchive(
@ApiParam(value = "등록할 아카이브 데이터", required = true) @RequestBody ArchiveRequest request) {
@ApiParam(value = "등록할 아카이브 데이터", required = true) @RequestBody ArchiveCreateRequest request) {
long userId = getLoginUserId();
return archiveService.save(userId, request);
}
Expand All @@ -69,7 +70,7 @@ public ArchiveInfo saveArchive(
@ApiOperation(value = "아카이브 수정")
public ArchiveInfo updateArchive(
@ApiParam(value = "수정할 아카이브 아이디", required = true) @PathVariable Long archiveId,
@ApiParam(value = "수정할 아카이브 데이터", required = true) @RequestBody ArchiveRequest request) {
@ApiParam(value = "수정할 아카이브 데이터", required = true) @RequestBody ArchiveUpdateRequest request) {
long userId = getLoginUserId();
return archiveService.update(userId, archiveId, request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,35 @@
import com.kilometer.domain.archive.dto.ItemArchiveDto;
import com.kilometer.domain.archive.dto.MyArchiveDto;
import com.kilometer.domain.archive.dto.MyArchiveInfo;
import com.kilometer.domain.archive.like.dto.ArchiveLike;
import com.kilometer.domain.archive.like.dto.ArchiveLikeGenerator;
import com.kilometer.domain.archive.userVisitPlace.UserVisitPlaceEntity;
import com.kilometer.domain.badge.ItemBadge;
import com.kilometer.domain.badge.ItemBadgeGenerator;
import com.kilometer.domain.item.dto.ItemSummary;
import com.kilometer.domain.item.enumType.ExhibitionType;
import com.kilometer.domain.archive.like.dto.ArchiveLike;
import com.kilometer.domain.archive.like.dto.ArchiveLikeGenerator;
import com.kilometer.domain.linkInfo.LinkInfo;
import com.kilometer.domain.user.dto.UserResponse;
import com.kilometer.domain.user.User;
import com.kilometer.domain.util.ApiUrlUtils;
import com.kilometer.domain.util.FrontUrlUtils;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@Transactional
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

꼭 다붙여야 하나용?

@RequiredArgsConstructor
public class ArchiveAggregateConverter {

private final ArchiveLikeGenerator archiveLikeGenerator;
private final ItemBadgeGenerator itemBadgeGenerator;

public ArchiveInfo convertArchiveInfo(ItemArchiveDto itemArchiveDto,
List<ArchiveImageEntity> archiveImageEntities, List<UserVisitPlaceEntity> userVisitPlaceEntities) {
List<ArchiveImageEntity> archiveImageEntities,
List<UserVisitPlaceEntity> userVisitPlaceEntities) {

Map<PlaceType, String> placeTypes = convertFoodAndCafe(userVisitPlaceEntities);

Expand All @@ -59,47 +60,22 @@ public ArchiveInfo convertArchiveInfo(ItemArchiveDto itemArchiveDto,
.build();
}

public ArchiveInfo convertArchiveInfo(ArchiveEntity archiveEntity,
List<ArchiveImageEntity> archiveImageEntities,
List<UserVisitPlaceEntity> userVisitPlaceEntities) {
public ArchiveInfo convertArchiveInfo(final ArchiveEntity archiveEntity) {

Map<PlaceType, String> placeTypes = convertFoodAndCafe(userVisitPlaceEntities);
Map<PlaceType, String> placeTypes = convertFoodAndCafe(archiveEntity.getUserVisitPlaces());

ArchiveLike archiveLike = archiveLikeGenerator.generateArchiveLike(archiveEntity.getId());

List<String> photoUrls = archiveImageEntities.stream()
List<String> photoUrls = archiveEntity.getArchiveImages()
.stream()
.map(ArchiveImageEntity::getImageUrl)
.collect(Collectors.toList());
User user = archiveEntity.getUser();

return ArchiveInfo.builder()
.id(archiveEntity.getId())
.userProfileUrl(archiveEntity.getUser().getImageUrl())
.userName(archiveEntity.getUser().getName())
.updatedAt(archiveEntity.getUpdatedAt())
.starRating(archiveEntity.getStarRating())
.likeCount(archiveEntity.getLikeCount())
.heart(archiveLike)
.comment(archiveEntity.getComment())
.food(placeTypes.getOrDefault(PlaceType.FOOD, ""))
.cafe(placeTypes.getOrDefault(PlaceType.CAFE, ""))
.photoUrls(photoUrls)
.build();
}

public ArchiveInfo convertArchiveInfo(ArchiveEntity archiveEntity, UserResponse userResponse,
List<ArchiveImageEntity> archiveImageEntities, List<UserVisitPlaceEntity> userVisitPlaceEntities) {

Map<PlaceType, String> placeTypes = convertFoodAndCafe(userVisitPlaceEntities);

ArchiveLike archiveLike = archiveLikeGenerator.generateArchiveLike(archiveEntity.getId());

List<String> photoUrls = archiveImageEntities.stream()
.map(ArchiveImageEntity::getImageUrl)
.collect(Collectors.toList());
return ArchiveInfo.builder()
.id(archiveEntity.getId())
.userProfileUrl(userResponse.getImageUrl())
.userName(userResponse.getName())
.userProfileUrl(user.getImageUrl())
.userName(user.getName())
.updatedAt(archiveEntity.getUpdatedAt())
.starRating(archiveEntity.getStarRating())
.likeCount(archiveEntity.getLikeCount())
Expand All @@ -112,7 +88,7 @@ public ArchiveInfo convertArchiveInfo(ArchiveEntity archiveEntity, UserResponse
}

public MyArchiveInfo convertMyArchiveInfo(MyArchiveDto myArchiveDto,
boolean existImages, List<UserVisitPlaceEntity> userVisitPlaceEntities) {
boolean existImages, List<UserVisitPlaceEntity> userVisitPlaceEntities) {

ItemBadge itemBadge = itemBadgeGenerator.generateTypeItemBadge(ExhibitionType.EXHIBITION);

Expand All @@ -129,7 +105,8 @@ public MyArchiveInfo convertMyArchiveInfo(MyArchiveDto myArchiveDto,
}

public ArchiveDetailResponse convertArchiveDetail(ArchiveDetailDto archiveDetailDto,
List<UserVisitPlaceEntity> visitPlaces, List<ArchiveImageEntity> archiveImageEntities) {
List<UserVisitPlaceEntity> visitPlaces,
List<ArchiveImageEntity> archiveImageEntities) {
ItemBadge itemBadge = itemBadgeGenerator.generateTypeItemBadge(
archiveDetailDto.getItemExhibitionType());
Map<PlaceType, String> placeTypes = convertFoodAndCafe(visitPlaces);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.kilometer.domain.archive;

import com.kilometer.domain.archive.request.ArchiveRequest;
import com.kilometer.domain.archive.archiveImage.ArchiveImageEntity;
import com.kilometer.domain.archive.userVisitPlace.UserVisitPlaceEntity;
import com.kilometer.domain.item.ItemEntity;
import com.kilometer.domain.user.User;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
Expand All @@ -12,6 +16,7 @@
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -65,6 +70,35 @@ public class ArchiveEntity {
@JoinColumn(name = "item")
private ItemEntity item;

@OneToMany(mappedBy = "archiveEntity", cascade = {CascadeType.PERSIST, CascadeType.MERGE},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 cascade 설정을 이렇게 해주신 이유가 있나요?

Copy link
Contributor Author

@gudonghee2000 gudonghee2000 Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 다대일 양방향 매핑을 사용하면서 발생한 영속성전이 부분이예요.
Archive와 ArchiveImage 중 연관관계 주인이 ArchiveImage가 되면서 양방향 매핑을 위해 @onetomany에 mappedBy 옵션이 붙게 되었어요

하지만 mappedBy는 읽기 전용이기 때문에 쓰기가 불가능해요.
그래서 CaseCade.Persist 옵션을 주어 Archive를 DB에 영속시킬때 영속성 전이를 통해 ArchiveImages를 같이 영속화 시킴을 명시해주어야 해요

만약, 위와 같이 구성하지 않게되면
ArchiveRepository.save(), ArchiveImageRepository.save() 로직이 두번 발생하게되는데요.
ArchiveImage는 Archive에 생명주기가 종속적인 Entity이기 때문에 영속성 전이를 사용했어요!

fetch = FetchType.LAZY, orphanRemoval = true)
private final List<ArchiveImageEntity> archiveImages = new ArrayList<>();

@OneToMany(mappedBy = "archiveEntity", cascade = {CascadeType.PERSIST, CascadeType.MERGE},
fetch = FetchType.LAZY, orphanRemoval = true)
private final List<UserVisitPlaceEntity> userVisitPlaces = new ArrayList<>();

public void initArchiveImages(final List<ArchiveImageEntity> archiveImages) {
this.archiveImages.clear();
this.archiveImages.addAll(archiveImages);
archiveImages.forEach(archiveImage -> archiveImage.initArchiveEntity(this));
}

public void initUserVisitPlaces(final List<UserVisitPlaceEntity> userVisitPlaces) {
this.userVisitPlaces.clear();
this.userVisitPlaces.addAll(userVisitPlaces);
userVisitPlaces.forEach(userVisitPlace -> userVisitPlace.initArchiveEntity(this));
}

public void update(final String comment, final int starRating, final boolean isVisibleAtItem,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isVisibleAtItem이라는 작명은 어떤의미일까요?

final List<ArchiveImageEntity> archiveImages, final List<UserVisitPlaceEntity> userVisitPlaces) {
this.comment = comment;
this.starRating = starRating;
this.isVisibleAtItem = isVisibleAtItem;
initArchiveImages(archiveImages);
initUserVisitPlaces(userVisitPlaces);
}

public void setUser(User user) {
this.user = user;
}
Expand All @@ -73,12 +107,6 @@ public void setItem(ItemEntity item) {
this.item = item;
}

public void update(ArchiveRequest request) {
this.comment = request.getComment();
this.isVisibleAtItem = request.isVisibleAtItem();
this.starRating = request.getStarRating();
}

public ArchiveEntity plusLikeCount() {
this.likeCount++;
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package com.kilometer.domain.archive;

import com.kilometer.domain.archive.exception.ArchiveNotFoundException;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ArchiveRepository extends JpaRepository<ArchiveEntity, Long>, ArchiveRepositoryCustom {

Optional<ArchiveEntity> findByItemIdAndUserId(Long itemId, Long userId);

Optional<ArchiveEntity> findByIdAndUserId(final Long id, final Long userId);

boolean existsByItemIdAndUserId(Long itemId, Long userId);

default ArchiveEntity getByIdAndUserId(final Long id, final Long userId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 왜 default인건가요??
그리고 orElseThrow 로직을 Repository에 둔 이유가 뭔가요???

Copy link
Contributor Author

@gudonghee2000 gudonghee2000 Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 어제 논쟁과 유사한 로직인것 같아요!(사실 아직 미완성 PR이여서 수정할 예정입니다ㅎㅎ)

interface는 메서드 선언부만 가질수 있지만 JAVA8(JAVA5일수도 있어요)부터 default 메서드가 되입되면서 interface도 메서드 구현체를 만들수 있게되었는데요!

보통은 ApplicationLayer에서 예외 처리를 한다가 일반적이기에 JPA를 사용하는 경우에는 Reposistory에서 Optional을 반환하고 Service에서 예외 처리를 하는것이 일반적이라고 생각하는데요!

하지만 개인적인 취향으로 Service 객체까지 예외 검증을 올려서 처리할 필요가 있을까default 메서드를 씀으로써 Service객체의 추상도가 높아져 코드가 간결해진다. 라는 점때문에 위와 같이 Repository에게 단순 조회에 대한 예외 검증의 Domain 객체 책임을 분산해준다.라는 구성을 해보았어요.
이부분도 굉장히 찬반 논의가 많은 부분이라고 해요!

외람되게는 JAVA8의 interface default 메서드 등장배경은 기존 서비스 회사들이 사용하던 JAVA의 Interface SPEC이 바뀌면서 코드를 수정해야하는 불편함들을 해결해주기 위해 interface에 default 메서드로 구현체를 만들었다고 해요

그래서 JPARepository에서 디폴트 메서드를 쓰는것은 등장배경과는 불일치 하기에 반대하시는 분들도 계시다고 해요!

return this.findByIdAndUserId(id, userId)
.orElseThrow(() -> new ArchiveNotFoundException("해당 회원에게 일치하는 아카이브가 없습니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class ArchiveImageEntity {
@JoinColumn(name = "archive")
private ArchiveEntity archiveEntity;

public void setArchiveEntity(ArchiveEntity archiveEntity) {
public void initArchiveEntity(final ArchiveEntity archiveEntity) {
this.archiveEntity = archiveEntity;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package com.kilometer.domain.archive.archiveImage;

import com.google.common.base.Preconditions;
import com.kilometer.domain.archive.ArchiveEntity;
import java.util.List;
import lombok.RequiredArgsConstructor;


import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -16,16 +13,6 @@ public class ArchiveImageService {

private final ArchiveImageRepository archiveImageRepository;

@Transactional
public List<ArchiveImageEntity> saveAll(List<ArchiveImageEntity> archiveImageEntities, Long archiveId) {
Preconditions.checkNotNull(archiveImageEntities, "Archive images must not be null");
if(archiveImageEntities.isEmpty()) {
return List.of();
}
ArchiveEntity archiveEntity = ArchiveEntity.builder().id(archiveId).build();
archiveImageEntities.forEach(archiveImage -> archiveImage.setArchiveEntity(archiveEntity));
return archiveImageRepository.saveAll(archiveImageEntities);
}
@Transactional
public void deleteAllByArchiveId(Long archiveId) {
Preconditions.checkNotNull(archiveId, "Archive id must not be null : " + archiveId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package com.kilometer.domain.archive.domain;

import com.kilometer.domain.archive.ArchiveEntity;
import com.kilometer.domain.archive.archiveImage.ArchiveImageEntity;
import com.kilometer.domain.archive.domain.archiveFilter.ArchiveFilter;
import com.kilometer.domain.archive.domain.userVisitPlace.UserVisitPlace;
import com.kilometer.domain.archive.exception.ArchiveValidationException;
import com.kilometer.domain.archive.userVisitPlace.UserVisitPlaceEntity;
import com.kilometer.domain.item.ItemEntity;
import com.kilometer.domain.user.User;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Getter;

@Getter
Expand All @@ -13,15 +19,17 @@ public class Archive {
private static final int MAX_STAR_RATING = 5;
private static final int MIN_STAR_RATING = 1;

private final Long id;
private final String comment;
private final int starRating;
private final boolean isVisibleAtItem;

private final List<ArchiveImage> archiveImages;
private final List<UserVisitPlace> userVisitPlaces;

private Archive(final String comment, final int starRating, final boolean isVisibleAtItem,
private Archive(final Long id, final String comment, final int starRating, final boolean isVisibleAtItem,
final List<ArchiveImage> archiveImages, final List<UserVisitPlace> userVisitPlaces) {
this.id = id;
this.comment = comment;
this.starRating = starRating;
this.isVisibleAtItem = isVisibleAtItem;
Expand All @@ -34,7 +42,22 @@ public static Archive createArchive(final String comment, final int starRating,
final List<UserVisitPlace> userVisitPlaces) {
validateComment(comment);
validateStarRating(starRating);
return new Archive(comment, starRating, isVisibleAtItem, archiveImages, userVisitPlaces);
return new Archive(null, comment, starRating, isVisibleAtItem, archiveImages, userVisitPlaces);
}

public static Archive createArchiveForUpdate(final Long id, final String comment, final int starRating,
final boolean isVisibleAtItem, final List<ArchiveImage> archiveImages,
final List<UserVisitPlace> userVisitPlaces) {
validateId(id);
validateComment(comment);
validateStarRating(starRating);
return new Archive(id, comment, starRating, isVisibleAtItem, archiveImages, userVisitPlaces);
}

private static void validateId(final Long id) {
if (id == null) {
throw new ArchiveValidationException("입력된 id가 없습니다.");
}
}

private static void validateComment(final String comment) {
Expand All @@ -51,4 +74,30 @@ private static void validateStarRating(final int starRating) {
throw new ArchiveValidationException("별점은 0~5 사이의 양수이어야 합니다.");
}
}

public ArchiveEntity toEntity(final User user, final ItemEntity itemEntity) {
return ArchiveEntity.builder()
.comment(this.getComment())
.starRating(this.getStarRating())
.isVisibleAtItem(this.getIsVisibleAtItem())
.user(user)
.item(itemEntity)
.build();
}

public List<ArchiveImageEntity> toArchiveImageEntities() {
return this.archiveImages.stream()
.map(ArchiveImage::toEntity)
.collect(Collectors.toList());
}

public List<UserVisitPlaceEntity> createUserVisitPlaceEntities() {
return this.userVisitPlaces.stream()
.map(UserVisitPlace::toEntity)
.collect(Collectors.toList());
}

public boolean getIsVisibleAtItem() {
return isVisibleAtItem;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kilometer.domain.archive.domain;

import com.kilometer.domain.archive.archiveImage.ArchiveImageEntity;
import com.kilometer.domain.archive.exception.ArchiveValidationException;
import lombok.Getter;
import org.junit.platform.commons.util.StringUtils;
Expand All @@ -23,4 +24,10 @@ private static void validateImageUrl(final String imageUrl) {
throw new ArchiveValidationException("이미지 링크가 없습니다.");
}
}

public ArchiveImageEntity toEntity() {
return ArchiveImageEntity.builder()
.imageUrl(this.imageUrl)
.build();
}
}
Loading