diff --git a/src/main/java/com/juu/juulabel/alcohol/controller/TastingNoteController.java b/src/main/java/com/juu/juulabel/alcohol/controller/TastingNoteController.java index fb106bb..d02e888 100644 --- a/src/main/java/com/juu/juulabel/alcohol/controller/TastingNoteController.java +++ b/src/main/java/com/juu/juulabel/alcohol/controller/TastingNoteController.java @@ -5,7 +5,7 @@ import com.juu.juulabel.common.response.CommonResponse; import com.juu.juulabel.common.dto.request.*; import com.juu.juulabel.common.dto.response.*; -import com.juu.juulabel.alcohol.service.TastingNoteService; +import com.juu.juulabel.tastingnote.service.TastingNoteService; import com.juu.juulabel.member.domain.Member; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/com/juu/juulabel/alcohol/service/TastingNoteService.java b/src/main/java/com/juu/juulabel/alcohol/service/TastingNoteService.java deleted file mode 100644 index c9b3bd4..0000000 --- a/src/main/java/com/juu/juulabel/alcohol/service/TastingNoteService.java +++ /dev/null @@ -1,528 +0,0 @@ -package com.juu.juulabel.alcohol.service; - -import com.juu.juulabel.alcohol.domain.*; -import com.juu.juulabel.alcohol.repository.*; -import com.juu.juulabel.alcohol.response.*; -import com.juu.juulabel.common.constants.FileConstants; -import com.juu.juulabel.common.dto.ImageInfo; -import com.juu.juulabel.common.dto.comment.CommentSummary; -import com.juu.juulabel.common.dto.comment.ReplySummary; -import com.juu.juulabel.common.dto.request.*; -import com.juu.juulabel.common.dto.response.*; -import com.juu.juulabel.common.exception.InvalidParamException; -import com.juu.juulabel.common.exception.code.ErrorCode; -import com.juu.juulabel.member.domain.Member; -import com.juu.juulabel.member.request.MemberInfo; -import com.juu.juulabel.notification.service.NotificationService; -import com.juu.juulabel.s3.S3Service; -import com.juu.juulabel.s3.UploadImageInfo; -import com.juu.juulabel.tastingnote.domain.*; -import com.juu.juulabel.tastingnote.domain.embedded.AlcoholicDrinksSnapshot; -import com.juu.juulabel.tastingnote.request.AlcoholicDrinksInfo; -import com.juu.juulabel.tastingnote.request.AlcoholicDrinksTastingNoteSummary; -import com.juu.juulabel.tastingnote.request.TastingNoteDetailInfo; -import com.juu.juulabel.tastingnote.request.TastingNoteSummary; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import java.util.*; -import java.util.stream.Collectors; - -@Slf4j -@Service -@RequiredArgsConstructor -public class TastingNoteService { - - private final TastingNoteReader tastingNoteReader; - private final AlcoholTypeReader alcoholTypeReader; - private final AlcoholicDrinksReader alcoholicDrinksReader; - private final AlcoholTypeColorReader alcoholTypeColorReader; - private final AlcoholTypeScentReader alcoholTypeScentReader; - private final AlcoholTypeFlavorReader alcoholTypeFlavorReader; - private final AlcoholTypeSensoryReader alcoholTypeSensoryReader; - private final S3Service s3Service; - private final TastingNoteWriter tastingNoteWriter; - private final TastingNoteImageWriter tastingNoteImageWriter; - private final TastingNoteImageReader tastingNoteImageReader; - private final TastingNoteLikeReader tastingNoteLikeReader; - private final TastingNoteLikeWriter tastingNoteLikeWriter; - private final TastingNoteCommentReader tastingNoteCommentReader; - private final TastingNoteCommentWriter tastingNoteCommentWriter; - private final TastingNoteCommentLikeReader tastingNoteCommentLikeReader; - private final TastingNoteCommentLikeWriter tastingNoteCommentLikeWriter; - private final NotificationService notificationService; - - @Transactional(readOnly = true) - public AlcoholDrinksListResponse searchAlcoholDrinksList(final SearchAlcoholDrinksListRequest request) { - final Slice alcoholicDrinks = tastingNoteReader.getAllAlcoholicDrinks(request.search(), - request.lastAlcoholicDrinksName(), request.pageSize()); - - long totalCount = tastingNoteReader.countBySearch(request.search()); - -// return SliceResponseFactory.create( -// AlcoholDrinksListResponse.class, -// alcoholicDrinks.isLast(), -// totalCount, -// alcoholicDrinks.getContent() -// ); - return new AlcoholDrinksListResponse( - alcoholicDrinks.isLast(), - totalCount, - alcoholicDrinks.getContent() - ); - } - - @Transactional(readOnly = true) - public TastingNoteColorListResponse loadTastingNoteColorsList(final Long alcoholTypeId) { - final List colors = alcoholTypeColorReader.getAllColorInfoByAlcoholTypeId(alcoholTypeId); - return new TastingNoteColorListResponse(colors); - } - - @Transactional(readOnly = true) - public TastingNoteSensoryListResponse loadTastingNoteSensoryList(final Long alcoholTypeId) { - final List sensoryLevels = alcoholTypeSensoryReader.getAllSensoryLevelInfoByAlcoholTypeId( - alcoholTypeId); - return new TastingNoteSensoryListResponse(sensoryLevels); - } - - @Transactional(readOnly = true) - public TastingNoteScentListResponse loadTastingNoteScentList(final Long alcoholTypeId) { - final List categories = alcoholTypeScentReader.getAllCategoryWithScentByAlcoholTypeId( - alcoholTypeId); - return new TastingNoteScentListResponse(categories); - } - - @Transactional(readOnly = true) - public TastingNoteFlavorListResponse loadTastingNoteFlavorList(final Long alcoholTypeId) { - final List flavorLevels = alcoholTypeFlavorReader.getAllFlavorLevelInfoByAlcoholTypeId( - alcoholTypeId); - return new TastingNoteFlavorListResponse(flavorLevels); - } - - @Transactional - public TastingNoteWriteResponse write(final Member loginMember, final TastingNoteWriteRequest request, - List files) { - // 1. 입력된 주종 확인 - final Long alcoholTypeId = request.alcoholTypeId(); - final AlcoholType alcoholType = alcoholTypeReader.getById(alcoholTypeId); - - // 2. 전통주 정보 확인 (OD, UD) - final AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getByIdOrElseNull(request.alcoholicDrinksId()); - final AlcoholicDrinksSnapshot alcoholicDrinksInfo = AlcoholicDrinksSnapshot.fromDto( - request.alcoholicDrinksDetails()); - - // 3. 감각 정보 확인 (시각 정보, 촉각 정보, 미각 정보, 후각 정보) - final Color color = getValidColorOrElseThrow(alcoholTypeId, request.colorId()); - final List scents = getValidScentsOrElseThrow(alcoholTypeId, request.scentIds()); - final List flavorLevels = getValidFlavorLevelsOrElseThrow(alcoholTypeId, request.flavorLevelIds()); - final List sensoryLevels = getValidSensoryLevelsOrElseThrow(alcoholTypeId, - request.sensoryLevelIds()); - - // 4. 시음 노트 정보 생성 (작성) - final TastingNote tastingNote = createBy(loginMember, alcoholType, alcoholicDrinks, color, alcoholicDrinksInfo, - request); - final TastingNote result = tastingNoteWriter.create( - tastingNote, - TastingNoteScent.of(tastingNote, scents), - TastingNoteFlavorLevel.of(tastingNote, flavorLevels), - TastingNoteSensoryLevel.of(tastingNote, sensoryLevels)); - - List imageUrlList = new ArrayList<>(); - storeImageList(files, imageUrlList, tastingNote); - - if (!Objects.isNull(alcoholicDrinks)) { - alcoholicDrinks.addRating(request.rating()); - } - - return TastingNoteWriteResponse.fromEntity(result); - } - - private Color getValidColorOrElseThrow(final Long alcoholTypeId, final Long colorId) { - final List colors = alcoholTypeColorReader.getAllColorByAlcoholTypeId(alcoholTypeId); - return colors.stream() - .filter(c -> Objects.equals(c.getId(), colorId)) - .findFirst() - .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_COLOR)); - } - - private List getValidScentsOrElseThrow(final Long alcoholTypeId, final List scentIds) { - final List scents = alcoholTypeScentReader.getAllScentByAlcoholTypeId(alcoholTypeId); - final Map scentMap = scents.stream() - .collect(Collectors.toMap(Scent::getId, scent -> scent)); - return scentIds.stream() - .map(scentId -> - Optional.ofNullable(scentMap.get(scentId)) - .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_SCENT)) - ) - .toList(); - } - - private List getValidSensoryLevelsOrElseThrow(final Long alcoholTypeId, - final List sensoryLevelIds) { - final List sensoryLevels = alcoholTypeSensoryReader.getAllSensoryLevelByAlcoholTypeId( - alcoholTypeId); - final Map sensoryLevelMap = sensoryLevels.stream() - .collect(Collectors.toMap(SensoryLevel::getId, sensoryLevel -> sensoryLevel)); - return sensoryLevelIds.stream() - .map(sensoryLevelId -> - Optional.ofNullable(sensoryLevelMap.get(sensoryLevelId)) - .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_SENSORY)) - ) - .toList(); - } - - private List getValidFlavorLevelsOrElseThrow(final Long alcoholTypeId, - final List flavorLevelIds) { - final List flavorLevels = alcoholTypeFlavorReader.getAllFlavorLevelByAlcoholTypeId(alcoholTypeId); - final Map flavorMap = flavorLevels.stream() - .collect(Collectors.toMap(FlavorLevel::getId, flavorLevel -> flavorLevel)); - return flavorLevelIds.stream() - .map(flavorLevelId -> - Optional.ofNullable(flavorMap.get(flavorLevelId)) - .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_FLAVOR)) - ) - .toList(); - } - - private TastingNote createBy(final Member member, - final AlcoholType alcoholType, - final AlcoholicDrinks alcoholicDrinks, - final Color color, - final AlcoholicDrinksSnapshot alcoholicDrinksInfo, - final TastingNoteWriteRequest request) { - return TastingNote.of( - member, - alcoholType, - alcoholicDrinks, - color, - alcoholicDrinksInfo, - request.rating(), - request.content(), - request.isPrivate() - ); - } - - private void storeImageList( - final List files, - final List newImageUrlList, - final TastingNote tastingNote - ) { - if (!Objects.isNull(files) && !files.isEmpty()) { - // TODO : 파일 크기 및 확장자 validate - validateFileListSize(files); - - for (MultipartFile file : files) { - UploadImageInfo uploadImageInfo = s3Service.uploadTastingNoteImage(file); - newImageUrlList.add(uploadImageInfo.ImageUrl()); - tastingNoteImageWriter.store(tastingNote, newImageUrlList.size(), uploadImageInfo.ImageUrl()); - } - } - } - - private void validateFileListSize(final List nonEmptyFiles) { - if (nonEmptyFiles.size() > FileConstants.FILE_MAX_SIZE_COUNT) { - throw new InvalidParamException(ErrorCode.EXCEEDED_FILE_COUNT); - } - } - - @Transactional(readOnly = true) - public TastingNoteListResponse loadTastingNoteList(Member member, TastingNoteListRequest request) { - Slice tastingNoteList = - tastingNoteReader.getAllTastingNotes(member, request.lastTastingNoteId(), request.pageSize()); - - return new TastingNoteListResponse(tastingNoteList); - } - - @Transactional(readOnly = true) - public TastingNoteListResponseForAlcoholicDrinks loadTastingNoteListByAlcoholicDrinksId(Member member, TastingNoteListRequest request, Long alcoholicDrinksId) { - Slice tastingNoteList = - tastingNoteReader.getAllTastingNotesByAlcoholicDrinksId(member, request.lastTastingNoteId(), request.pageSize(), alcoholicDrinksId); - - return new TastingNoteListResponseForAlcoholicDrinks(tastingNoteList); - } - - @Transactional(readOnly = true) - public TastingNoteResponse loadTastingNote(Member member, Long tastingNoteId) { - TastingNoteDetailInfo tastingNoteDetailInfo = tastingNoteReader.getTastingNoteDetailById(tastingNoteId, member); - List urlList = tastingNoteImageReader.getImageUrlList(tastingNoteId); - Long alcoholicDrinksId = tastingNoteReader.getAlcoholicDrinksByTastingNoteId(tastingNoteId); - boolean isOfficialData = !Objects.isNull(alcoholicDrinksId); - - return new TastingNoteResponse( - tastingNoteDetailInfo, - tastingNoteReader.getSensoryLevelIds(tastingNoteId, member), - tastingNoteReader.getScentIds(tastingNoteId, member), - tastingNoteReader.getFlavorLevelIds(tastingNoteId, member), - new ImageInfo(urlList, urlList.size()), - new AlcoholicDrinksInfo(isOfficialData, alcoholicDrinksId) - ); - } - - @Transactional - public TastingNoteWriteResponse updateTastingNote( - Member member, - Long tastingNoteId, - TastingNoteWriteRequest request, - List files - ) { - TastingNote tastingNote = getTastingNote(tastingNoteId); - validateTastingNoteWriter(member, tastingNote); - double rating = tastingNote.getRating(); - - // 입력된 주종 확인 - final Long alcoholTypeId = request.alcoholTypeId(); - final AlcoholType alcoholType = alcoholTypeReader.getById(alcoholTypeId); - - // 전통주 정보 확인 (OD, UD) - final AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getByIdOrElseNull(request.alcoholicDrinksId()); - final AlcoholicDrinksSnapshot alcoholicDrinksInfo = AlcoholicDrinksSnapshot.fromDto( - request.alcoholicDrinksDetails()); - - // 감각 정보 확인 (시각 정보, 촉각 정보, 미각 정보, 후각 정보) - final Color color = getValidColorOrElseThrow(alcoholTypeId, request.colorId()); - final List scents = getValidScentsOrElseThrow(alcoholTypeId, request.scentIds()); - final List flavorLevels = getValidFlavorLevelsOrElseThrow(alcoholTypeId, request.flavorLevelIds()); - final List sensoryLevels = getValidSensoryLevelsOrElseThrow(alcoholTypeId, - request.sensoryLevelIds()); - - tastingNote.update(alcoholType, alcoholicDrinks, color, alcoholicDrinksInfo, request.rating(), - request.content(), request.isPrivate()); - tastingNoteWriter.update( - tastingNote.getId(), - TastingNoteScent.of(tastingNote, scents), - TastingNoteFlavorLevel.of(tastingNote, flavorLevels), - TastingNoteSensoryLevel.of(tastingNote, sensoryLevels)); - - List tastingNoteImageList = tastingNoteImageReader.getImageList(tastingNote.getId()); - tastingNoteImageList.forEach(TastingNoteImage::delete); - - List imageUrlList = new ArrayList<>(); - storeImageList(files, imageUrlList, tastingNote); - - if (rating != request.rating() && !Objects.isNull(alcoholicDrinks)) { - alcoholicDrinks.updateRating(rating, request.rating()); - } - - return new TastingNoteWriteResponse(tastingNote.getId()); - } - - private TastingNote getTastingNote(Long tastingNoteId) { - return tastingNoteReader.getById(tastingNoteId); - } - - private static void validateTastingNoteWriter(Member member, TastingNote tastingNote) { - if (!member.getId().equals(tastingNote.getMember().getId())) { - throw new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_WRITER); - } - } - - @Transactional - public DeleteTastingNoteResponse deleteTastingNote(Member member, Long tastingNoteId) { - TastingNote tastingNote = getTastingNote(tastingNoteId); - validateTastingNoteWriter(member, tastingNote); - - if (!Objects.isNull(tastingNote.getAlcoholicDrinks())) { - AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getById(tastingNote.getAlcoholicDrinks().getId()); - alcoholicDrinks.removeRating(tastingNote.getRating()); - } - - tastingNote.delete(); - return new DeleteTastingNoteResponse(tastingNote.getId()); - } - - @Transactional - public boolean toggleTastingNoteLike(Member member, Long tastingNoteId) { - TastingNote tastingNote = getTastingNote(tastingNoteId); - Optional tastingNoteLike = tastingNoteLikeReader.findByMemberAndTastingNote(member, - tastingNote); - String notificationRelatedUrl = getRelatedUrl(tastingNoteId); - boolean isNotSameUser = !tastingNote.getMember().isSameUser(member); - - // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 - return tastingNoteLike - .map(like -> { - tastingNoteLikeWriter.delete(like); - - if (isNotSameUser) { - notificationService.deletePostLikeNotification(tastingNote.getMember(), member, - notificationRelatedUrl); - } - return false; - }) - .orElseGet(() -> { - tastingNoteLikeWriter.store(member, tastingNote); - - if (isNotSameUser) { - notificationService.sendPostLikeNotification(tastingNote.getMember(), member, - notificationRelatedUrl); - } - return true; - }); - } - - @Transactional - public WriteTastingNoteCommentResponse writeComment( - Member member, - WriteTastingNoteCommentRequest request, - Long tastingNoteId - ) { - TastingNote tastingNote = getTastingNote(tastingNoteId); - TastingNoteComment tastingNoteComment = createCommentOrReply(request, member, tastingNote); - TastingNoteComment comment = tastingNoteCommentWriter.store(tastingNoteComment); - - String notificationRelatedUrl = getRelatedUrl(tastingNoteId); - String notificationMessage; - if (Objects.isNull(request.parentCommentId()) && (!tastingNote.getMember().isSameUser(member))) { - notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; - notificationService.sendCommentNotification(tastingNote.getMember(), notificationRelatedUrl, - notificationMessage, comment.getId(), member.getProfileImage()); - } else if (!comment.getMember().isSameUser(member)) { - notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; - notificationService.sendCommentNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage, comment.getId(), member.getProfileImage()); - } - - return new WriteTastingNoteCommentResponse( - comment.getContent(), - tastingNote.getId(), - new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage()) - ); - } - - private TastingNoteComment createCommentOrReply(WriteTastingNoteCommentRequest request, Member member, - TastingNote tastingNote) { - if (Objects.isNull(request.parentCommentId())) { - return TastingNoteComment.createComment(member, tastingNote, request.content()); - } else { - TastingNoteComment parentComment = tastingNoteCommentReader.getById(request.parentCommentId()); - return TastingNoteComment.createReply(member, tastingNote, request.content(), parentComment); - } - } - - @Transactional(readOnly = true) - public TastingNoteCommentListResponse loadCommentList(Member member, CommentListRequest request, - Long tastingNoteId) { - TastingNote tastingNote = getTastingNote(tastingNoteId); - - Slice commentList = - tastingNoteCommentReader.getAllByTastingNoteId(member, tastingNote.getId(), request.lastCommentId(), - request.pageSize()); - - return new TastingNoteCommentListResponse(commentList); - } - - @Transactional(readOnly = true) - public TastingNoteReplyListResponse loadReplyList( - Member member, - ReplyListRequest request, - Long tastingNoteId, - Long tastingNoteCommentId - ) { - TastingNote tastingNote = getTastingNote(tastingNoteId); - - Slice replyList = tastingNoteCommentReader.getAllRepliesByParentId( - member, - tastingNote.getId(), - tastingNoteCommentId, - request.lastReplyId(), - request.pageSize() - ); - - return new TastingNoteReplyListResponse(replyList); - } - - @Transactional - public UpdateCommentResponse updateComment(Member member, UpdateCommentRequest request, Long tastingNoteId, - Long commentId) { - getTastingNote(tastingNoteId); - TastingNoteComment comment = getComment(commentId); - - validateCommentWriter(member, comment); - - comment.updateContent(request.content()); - - return new UpdateCommentResponse( - comment.getContent(), - new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage()) - ); - } - - private TastingNoteComment getComment(Long commentId) { - return tastingNoteCommentReader.getById(commentId); - } - - private static void validateCommentWriter(Member member, TastingNoteComment comment) { - if (!member.getId().equals(comment.getMember().getId())) { - throw new InvalidParamException(ErrorCode.COMMENT_NOT_WRITER); - } - } - - @Transactional - public DeleteCommentResponse deleteComment(Member member, Long tastingNoteId, Long commentId) { - TastingNote tastingNote = getTastingNote(tastingNoteId); - TastingNoteComment comment = getComment(commentId); - validateCommentWriter(member, comment); - comment.delete(); - - String notificationRelatedUrl = getRelatedUrl(tastingNoteId); - String notificationMessage; - if (Objects.isNull(comment.getParent())) { - notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; - notificationService.deleteCommentNotification(tastingNote.getMember(), notificationRelatedUrl, - notificationMessage, commentId); - } else { - notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; - notificationService.deleteCommentNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage, commentId); - } - - return new DeleteCommentResponse(comment.getId()); - } - - @Transactional - public boolean toggleCommentLike(Member member, Long tastingNoteId, Long commentId) { - getTastingNote(tastingNoteId); - TastingNoteComment comment = getComment(commentId); - - Optional tastingNoteCommentLike = - tastingNoteCommentLikeReader.findByMemberAndTastingNoteComment(member, comment); - - String notificationRelatedUrl = getRelatedUrl(tastingNoteId); - String notificationMessage; - if (Objects.isNull(comment.getParent())) { - notificationMessage = member.getNickname() + "님이 내 댓글에 좋아요를 눌렀어요."; - } else { - notificationMessage = member.getNickname() + "님이 내 답글에 좋아요를 눌렀어요."; - } - boolean isNotSameUser = !comment.getMember().isSameUser(member); - - // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 - return tastingNoteCommentLike - .map(like -> { - tastingNoteCommentLikeWriter.delete(like); - - if (isNotSameUser) { - notificationService.deleteCommentLikeNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage); - } - return false; - }) - .orElseGet(() -> { - tastingNoteCommentLikeWriter.store(member, comment); - - if (isNotSameUser) { - notificationService.sendCommentLikeNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage, member.getProfileImage()); - } - return true; - }); - } - - private static String getRelatedUrl(Long tastingNoteId) { - return "/v1/api/shared-space/tasting-notes/" + tastingNoteId; - } -} diff --git a/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java b/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java index 201ecc1..11793ac 100644 --- a/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java +++ b/src/main/java/com/juu/juulabel/common/exception/code/ErrorCode.java @@ -74,9 +74,10 @@ public enum ErrorCode { /** * DailyLife */ - DAILY_LIFE_NOT_FOUND(HttpStatus.BAD_REQUEST, "일상생활 게시글을 찾을 수 없습니다."), + DAILY_LIFE_NOT_FOUND(HttpStatus.NOT_FOUND, "일상생활 게시글을 찾을 수 없습니다."), DAILY_LIFE_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 일상생활 게시글입니다."), DAILY_LIFE_NOT_WRITER(HttpStatus.BAD_REQUEST, "게시글 작성자가 아닙니다."), + DAILY_LIFE_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "일상생활 댓글을 찾을 수 없습니다."), /** * FILE @@ -97,7 +98,14 @@ public enum ErrorCode { TASTING_NOTE_NOT_FOUND(HttpStatus.BAD_REQUEST, "시음노트 게시글을 찾을 수 없습니다."), TASTING_NOTE_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 시음노트 게시글입니다."), - TASTING_NOTE_NOT_WRITER(HttpStatus.BAD_REQUEST, "게시글 작성자가 아닙니다.") + TASTING_NOTE_NOT_WRITER(HttpStatus.BAD_REQUEST, "게시글 작성자가 아닙니다."), + + TASTING_NOTE_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "시음노트 댓글을 찾을 수 없습니다."), + + /** + * Report + */ + REPORT_PROCESSOR_NOT_FOUND(HttpStatus.BAD_REQUEST, "처리할 수 있는 신고 유형이 아닙니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/juu/juulabel/dailylife/service/DailyLifeCommentService.java b/src/main/java/com/juu/juulabel/dailylife/service/DailyLifeCommentService.java new file mode 100644 index 0000000..9a3086b --- /dev/null +++ b/src/main/java/com/juu/juulabel/dailylife/service/DailyLifeCommentService.java @@ -0,0 +1,22 @@ +package com.juu.juulabel.dailylife.service; + +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.dailylife.domain.DailyLifeComment; +import com.juu.juulabel.dailylife.repository.jpa.DailyLifeCommentJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class DailyLifeCommentService { + private final DailyLifeCommentJpaRepository dailyLifeCommentJpaRepository; + + public DailyLifeComment findById(long id) { + return dailyLifeCommentJpaRepository.findById(id) + .orElseThrow(() -> new BaseException(ErrorCode.DAILY_LIFE_COMMENT_NOT_FOUND)); + } +} + diff --git a/src/main/java/com/juu/juulabel/dailylife/service/DailyLifeService.java b/src/main/java/com/juu/juulabel/dailylife/service/DailyLifeService.java index d89e540..4b8098c 100644 --- a/src/main/java/com/juu/juulabel/dailylife/service/DailyLifeService.java +++ b/src/main/java/com/juu/juulabel/dailylife/service/DailyLifeService.java @@ -7,14 +7,12 @@ import com.juu.juulabel.common.dto.comment.ReplySummary; import com.juu.juulabel.common.dto.request.*; import com.juu.juulabel.common.dto.response.*; +import com.juu.juulabel.common.exception.BaseException; import com.juu.juulabel.common.exception.InvalidParamException; import com.juu.juulabel.common.exception.code.ErrorCode; -import com.juu.juulabel.dailylife.domain.DailyLife; -import com.juu.juulabel.dailylife.domain.DailyLifeComment; -import com.juu.juulabel.dailylife.domain.DailyLifeImage; -import com.juu.juulabel.dailylife.domain.DailyLifeCommentLike; -import com.juu.juulabel.dailylife.domain.DailyLifeLike; +import com.juu.juulabel.dailylife.domain.*; import com.juu.juulabel.dailylife.repository.*; +import com.juu.juulabel.dailylife.repository.jpa.DailyLifeJpaRepository; import com.juu.juulabel.dailylife.response.DailyLifeDetailInfo; import com.juu.juulabel.dailylife.response.DailyLifeListRequest; import com.juu.juulabel.dailylife.response.DailyLifeSummary; @@ -40,326 +38,332 @@ @RequiredArgsConstructor public class DailyLifeService { - private final DailyLifeWriter dailyLifeWriter; - private final DailyLifeReader dailyLifeReader; - private final DailyLifeImageWriter dailyLifeImageWriter; - private final DailyLifeImageReader dailyLifeImageReader; - private final DailyLifeCommentWriter dailyLifeCommentWriter; - private final DailyLifeCommentReader dailyLifeCommentReader; - private final DailyLifeLikeWriter dailyLifeLikeWriter; - private final DailyLifeLikeReader dailyLifeLikeReader; - private final DailyLifeCommentLikeWriter dailyLifeCommentLikeWriter; - private final DailyLifeCommentLikeReader dailyLifeCommentLikeReader; - private final NotificationService notificationService; - private final S3Service s3Service; - - @Transactional - public WriteDailyLifeResponse writeDailyLife( - final Member member, - final WriteDailyLifeRequest request, - final List files - ) { - final DailyLife dailyLife = dailyLifeWriter.store(member, request); - - List imageUrlList = new ArrayList<>(); - storeImageList(files, imageUrlList, dailyLife); - - return new WriteDailyLifeResponse( - dailyLife.getTitle(), - dailyLife.getContent(), - dailyLife.getId(), - new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage()), - imageUrlList.isEmpty() ? null : imageUrlList, - imageUrlList.isEmpty() ? 0 : imageUrlList.size() - ); - } - - @Transactional(readOnly = true) - public DailyLifeResponse loadDailyLife(final Member member, final Long dailyLifeId) { - final DailyLifeDetailInfo dailyLifeDetailInfo = dailyLifeReader.getDailyLifeDetailById(dailyLifeId, member); - final List urlList = dailyLifeImageReader.getImageUrlList(dailyLifeId); - - return new DailyLifeResponse( - dailyLifeDetailInfo, - new ImageInfo(urlList, urlList.size()) - ); - } - - @Transactional(readOnly = true) - public DailyLifeListResponse loadDailyLifeList(Member member, DailyLifeListRequest request) { - final Slice dailyLifeList = - dailyLifeReader.getAllDailyLives(member, request.lastDailyLifeId(), request.pageSize()); - - return new DailyLifeListResponse(dailyLifeList); - } - - @Transactional - public UpdateDailyLifeResponse updateDailyLife( - final Member member, - final Long dailyLifeId, - final UpdateDailyLifeRequest request, - final List files - ) { - DailyLife dailyLife = getDailyLife(dailyLifeId); - validateDailyLifeWriter(member, dailyLife); - - updateIfNotBlank(request.title(), dailyLife::updateTitle); - updateIfNotBlank(request.content(), dailyLife::updateContent); - dailyLife.updateIsPrivate(request.isPrivate()); - - final List dailyLifeImageList = dailyLifeImageReader.getImageList(dailyLife.getId()); - dailyLifeImageList.forEach(DailyLifeImage::delete); - - List newImageUrlList = new ArrayList<>(); - storeImageList(files, newImageUrlList, dailyLife); - - return new UpdateDailyLifeResponse(dailyLife.getId()); - } - - @Transactional - public DeleteDailyLifeResponse deleteDailyLife(final Member member, final Long dailyLifeId) { - DailyLife dailyLife = getDailyLife(dailyLifeId); - validateDailyLifeWriter(member, dailyLife); - - dailyLife.delete(); - return new DeleteDailyLifeResponse(dailyLife.getId()); - } - - @Transactional - public boolean toggleDailyLifeLike(final Member member, final Long dailyLifeId) { - final DailyLife dailyLife = getDailyLife(dailyLifeId); - Optional dailyLifeLike = dailyLifeLikeReader.findByMemberAndDailyLife(member, dailyLife); - String notificationRelatedUrl = getRelatedUrl(dailyLifeId); - boolean isNotSameUser = !dailyLife.getMember().isSameUser(member); - - // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 - return dailyLifeLike - .map(like -> { - dailyLifeLikeWriter.delete(like); - - if (isNotSameUser) { - notificationService.deletePostLikeNotification(dailyLife.getMember(), member, - notificationRelatedUrl); - } - return false; - }) - .orElseGet(() -> { - dailyLifeLikeWriter.store(member, dailyLife); - - if (isNotSameUser) { - notificationService.sendPostLikeNotification(dailyLife.getMember(), member, notificationRelatedUrl); - } - return true; - }); - } - - @Transactional - public WriteDailyLifeCommentResponse writeComment( - final Member member, - final WriteDailyLifeCommentRequest request, - final Long dailyLifeId - ) { - final DailyLife dailyLife = getDailyLife(dailyLifeId); - final DailyLifeComment dailyLifeComment = createCommentOrReply(request, member, dailyLife); - final DailyLifeComment comment = dailyLifeCommentWriter.store(dailyLifeComment); - - String notificationRelatedUrl = getRelatedUrl(dailyLifeId); - String notificationMessage; - if (Objects.isNull(request.parentCommentId()) && (!dailyLife.getMember().isSameUser(member))) { - notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; - notificationService.sendCommentNotification(dailyLife.getMember(), notificationRelatedUrl, - notificationMessage, comment.getId(), member.getProfileImage()); - } else if (!comment.getMember().isSameUser(member)) { - notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; - notificationService.sendCommentNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage, comment.getId(), member.getProfileImage()); - } - - return new WriteDailyLifeCommentResponse( - comment.getContent(), - dailyLife.getId(), - new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage())); - } - - @Transactional(readOnly = true) - public DailyLifeCommentListResponse loadCommentList( - final Member member, - final CommentListRequest request, - final Long dailyLifeId - ) { - final DailyLife dailyLife = getDailyLife(dailyLifeId); - - final Slice commentList = - dailyLifeCommentReader.getAllByDailyLifeId(member, dailyLife.getId(), request.lastCommentId(), - request.pageSize()); - - return new DailyLifeCommentListResponse(commentList); - } - - @Transactional(readOnly = true) - public DailyLifeReplyListResponse loadReplyList( - final Member member, - final ReplyListRequest request, - final Long dailyLifeId, - final Long dailyLifeCommentId - ) { - final DailyLife dailyLife = getDailyLife(dailyLifeId); - - final Slice replyList = - dailyLifeCommentReader.getAllRepliesByParentId(member, dailyLife.getId(), dailyLifeCommentId, - request.lastReplyId(), request.pageSize()); - - return new DailyLifeReplyListResponse(replyList); - } - - @Transactional - public UpdateCommentResponse updateComment( - final Member member, - final UpdateCommentRequest request, - final Long dailyLifeId, - final Long commentId - ) { - getDailyLife(dailyLifeId); - DailyLifeComment comment = getComment(commentId); - - validateCommentWriter(member, comment); - - comment.updateContent(request.content()); - - return new UpdateCommentResponse( - comment.getContent(), - new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage())); - } - - @Transactional - public DeleteCommentResponse deleteComment( - final Member member, - final Long dailyLifeId, - final Long commentId - ) { - DailyLife dailyLife = getDailyLife(dailyLifeId); - DailyLifeComment comment = getComment(commentId); - validateCommentWriter(member, comment); - comment.delete(); - - String notificationRelatedUrl = getRelatedUrl(dailyLifeId); - String notificationMessage; - if (Objects.isNull(comment.getParent())) { - notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; - notificationService.deleteCommentNotification(dailyLife.getMember(), notificationRelatedUrl, - notificationMessage, commentId); - } else { - notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; - notificationService.deleteCommentNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage, commentId); - } - - return new DeleteCommentResponse(comment.getId()); - } - - @Transactional - public boolean toggleCommentLike(final Member member, final Long dailyLifeId, final Long commentId) { - getDailyLife(dailyLifeId); - final DailyLifeComment comment = getComment(commentId); - - Optional dailyLifeCommentLike = - dailyLifeCommentLikeReader.findByMemberAndDailyLifeComment(member, comment); - - String notificationRelatedUrl = getRelatedUrl(dailyLifeId); - String notificationMessage; - if (Objects.isNull(comment.getParent())) { - notificationMessage = member.getNickname() + "님이 내 댓글에 좋아요를 눌렀어요."; - } else { - notificationMessage = member.getNickname() + "님이 내 답글에 좋아요를 눌렀어요."; - } - boolean isNotSameUser = !comment.getMember().isSameUser(member); - - // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 - return dailyLifeCommentLike - .map(like -> { - dailyLifeCommentLikeWriter.delete(like); - - if (isNotSameUser) { - notificationService.deleteCommentLikeNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage); - } - return false; - }) - .orElseGet(() -> { - dailyLifeCommentLikeWriter.store(member, comment); - - if (isNotSameUser) { - notificationService.sendCommentLikeNotification(comment.getMember(), notificationRelatedUrl, - notificationMessage, member.getProfileImage()); - } - return true; - }); - } - - private static String getRelatedUrl(Long dailyLifeId) { - return "/v1/api/daily-lives/" + dailyLifeId; - } - - private static void validateCommentWriter(final Member member, final DailyLifeComment comment) { - if (!member.getId().equals(comment.getMember().getId())) { - throw new InvalidParamException(ErrorCode.COMMENT_NOT_WRITER); - } - } - - private static void validateDailyLifeWriter(final Member member, final DailyLife dailyLife) { - if (!member.getId().equals(dailyLife.getMember().getId())) { - throw new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_WRITER); - } - } - - private DailyLifeComment createCommentOrReply( - final WriteDailyLifeCommentRequest request, - final Member member, - final DailyLife dailyLife - ) { - if (Objects.isNull(request.parentCommentId())) { - return DailyLifeComment.createComment(member, dailyLife, request.content()); - } else { - DailyLifeComment parentComment = dailyLifeCommentReader.getById(request.parentCommentId()); - return DailyLifeComment.createReply(member, dailyLife, request.content(), parentComment); - } - } - - private DailyLifeComment getComment(final Long commentId) { - return dailyLifeCommentReader.getById(commentId); - } - - private DailyLife getDailyLife(final Long dailyLifeId) { - return dailyLifeReader.getById(dailyLifeId); - } - - private void updateIfNotBlank(final String value, final Consumer updater) { - if (StringUtils.hasText(value)) { - updater.accept(value); - } - } - - private void storeImageList( - final List files, - final List newImageUrlList, - final DailyLife dailyLife - ) { - if (!Objects.isNull(files) && !files.isEmpty()) { - // TODO : 파일 크기 및 확장자 validate - validateFileListSize(files); - - for (MultipartFile file : files) { - UploadImageInfo uploadImageInfo = s3Service.uploadDailyLifeImage(file); - newImageUrlList.add(uploadImageInfo.ImageUrl()); - dailyLifeImageWriter.store(dailyLife, newImageUrlList.size(), uploadImageInfo.ImageUrl()); - } - } - } - - private static void validateFileListSize(final List nonEmptyFiles) { - if (nonEmptyFiles.size() > FileConstants.FILE_MAX_SIZE_COUNT) { - throw new InvalidParamException(ErrorCode.EXCEEDED_FILE_COUNT); - } - } - + private final DailyLifeWriter dailyLifeWriter; + private final DailyLifeReader dailyLifeReader; + private final DailyLifeImageWriter dailyLifeImageWriter; + private final DailyLifeImageReader dailyLifeImageReader; + private final DailyLifeCommentWriter dailyLifeCommentWriter; + private final DailyLifeCommentReader dailyLifeCommentReader; + private final DailyLifeLikeWriter dailyLifeLikeWriter; + private final DailyLifeLikeReader dailyLifeLikeReader; + private final DailyLifeCommentLikeWriter dailyLifeCommentLikeWriter; + private final DailyLifeCommentLikeReader dailyLifeCommentLikeReader; + private final NotificationService notificationService; + private final S3Service s3Service; + private final DailyLifeJpaRepository dailyLifeJpaRepository; + + @Transactional + public WriteDailyLifeResponse writeDailyLife( + final Member member, + final WriteDailyLifeRequest request, + final List files + ) { + final DailyLife dailyLife = dailyLifeWriter.store(member, request); + + List imageUrlList = new ArrayList<>(); + storeImageList(files, imageUrlList, dailyLife); + + return new WriteDailyLifeResponse( + dailyLife.getTitle(), + dailyLife.getContent(), + dailyLife.getId(), + new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage()), + imageUrlList.isEmpty() ? null : imageUrlList, + imageUrlList.isEmpty() ? 0 : imageUrlList.size() + ); + } + + @Transactional(readOnly = true) + public DailyLifeResponse loadDailyLife(final Member member, final Long dailyLifeId) { + final DailyLifeDetailInfo dailyLifeDetailInfo = dailyLifeReader.getDailyLifeDetailById(dailyLifeId, member); + final List urlList = dailyLifeImageReader.getImageUrlList(dailyLifeId); + + return new DailyLifeResponse( + dailyLifeDetailInfo, + new ImageInfo(urlList, urlList.size()) + ); + } + + @Transactional(readOnly = true) + public DailyLifeListResponse loadDailyLifeList(Member member, DailyLifeListRequest request) { + final Slice dailyLifeList = + dailyLifeReader.getAllDailyLives(member, request.lastDailyLifeId(), request.pageSize()); + + return new DailyLifeListResponse(dailyLifeList); + } + + @Transactional + public UpdateDailyLifeResponse updateDailyLife( + final Member member, + final Long dailyLifeId, + final UpdateDailyLifeRequest request, + final List files + ) { + DailyLife dailyLife = getDailyLife(dailyLifeId); + validateDailyLifeWriter(member, dailyLife); + + updateIfNotBlank(request.title(), dailyLife::updateTitle); + updateIfNotBlank(request.content(), dailyLife::updateContent); + dailyLife.updateIsPrivate(request.isPrivate()); + + final List dailyLifeImageList = dailyLifeImageReader.getImageList(dailyLife.getId()); + dailyLifeImageList.forEach(DailyLifeImage::delete); + + List newImageUrlList = new ArrayList<>(); + storeImageList(files, newImageUrlList, dailyLife); + + return new UpdateDailyLifeResponse(dailyLife.getId()); + } + + @Transactional + public DeleteDailyLifeResponse deleteDailyLife(final Member member, final Long dailyLifeId) { + DailyLife dailyLife = getDailyLife(dailyLifeId); + validateDailyLifeWriter(member, dailyLife); + + dailyLife.delete(); + return new DeleteDailyLifeResponse(dailyLife.getId()); + } + + @Transactional + public boolean toggleDailyLifeLike(final Member member, final Long dailyLifeId) { + final DailyLife dailyLife = getDailyLife(dailyLifeId); + Optional dailyLifeLike = dailyLifeLikeReader.findByMemberAndDailyLife(member, dailyLife); + String notificationRelatedUrl = getRelatedUrl(dailyLifeId); + boolean isNotSameUser = !dailyLife.getMember().isSameUser(member); + + // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 + return dailyLifeLike + .map(like -> { + dailyLifeLikeWriter.delete(like); + + if (isNotSameUser) { + notificationService.deletePostLikeNotification(dailyLife.getMember(), member, + notificationRelatedUrl); + } + return false; + }) + .orElseGet(() -> { + dailyLifeLikeWriter.store(member, dailyLife); + + if (isNotSameUser) { + notificationService.sendPostLikeNotification(dailyLife.getMember(), member, notificationRelatedUrl); + } + return true; + }); + } + + @Transactional + public WriteDailyLifeCommentResponse writeComment( + final Member member, + final WriteDailyLifeCommentRequest request, + final Long dailyLifeId + ) { + final DailyLife dailyLife = getDailyLife(dailyLifeId); + final DailyLifeComment dailyLifeComment = createCommentOrReply(request, member, dailyLife); + final DailyLifeComment comment = dailyLifeCommentWriter.store(dailyLifeComment); + + String notificationRelatedUrl = getRelatedUrl(dailyLifeId); + String notificationMessage; + if (Objects.isNull(request.parentCommentId()) && (!dailyLife.getMember().isSameUser(member))) { + notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; + notificationService.sendCommentNotification(dailyLife.getMember(), notificationRelatedUrl, + notificationMessage, comment.getId(), member.getProfileImage()); + } else if (!comment.getMember().isSameUser(member)) { + notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; + notificationService.sendCommentNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage, comment.getId(), member.getProfileImage()); + } + + return new WriteDailyLifeCommentResponse( + comment.getContent(), + dailyLife.getId(), + new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage())); + } + + @Transactional(readOnly = true) + public DailyLifeCommentListResponse loadCommentList( + final Member member, + final CommentListRequest request, + final Long dailyLifeId + ) { + final DailyLife dailyLife = getDailyLife(dailyLifeId); + + final Slice commentList = + dailyLifeCommentReader.getAllByDailyLifeId(member, dailyLife.getId(), request.lastCommentId(), + request.pageSize()); + + return new DailyLifeCommentListResponse(commentList); + } + + @Transactional(readOnly = true) + public DailyLifeReplyListResponse loadReplyList( + final Member member, + final ReplyListRequest request, + final Long dailyLifeId, + final Long dailyLifeCommentId + ) { + final DailyLife dailyLife = getDailyLife(dailyLifeId); + + final Slice replyList = + dailyLifeCommentReader.getAllRepliesByParentId(member, dailyLife.getId(), dailyLifeCommentId, + request.lastReplyId(), request.pageSize()); + + return new DailyLifeReplyListResponse(replyList); + } + + @Transactional + public UpdateCommentResponse updateComment( + final Member member, + final UpdateCommentRequest request, + final Long dailyLifeId, + final Long commentId + ) { + getDailyLife(dailyLifeId); + DailyLifeComment comment = getComment(commentId); + + validateCommentWriter(member, comment); + + comment.updateContent(request.content()); + + return new UpdateCommentResponse( + comment.getContent(), + new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage())); + } + + @Transactional + public DeleteCommentResponse deleteComment( + final Member member, + final Long dailyLifeId, + final Long commentId + ) { + DailyLife dailyLife = getDailyLife(dailyLifeId); + DailyLifeComment comment = getComment(commentId); + validateCommentWriter(member, comment); + comment.delete(); + + String notificationRelatedUrl = getRelatedUrl(dailyLifeId); + String notificationMessage; + if (Objects.isNull(comment.getParent())) { + notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; + notificationService.deleteCommentNotification(dailyLife.getMember(), notificationRelatedUrl, + notificationMessage, commentId); + } else { + notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; + notificationService.deleteCommentNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage, commentId); + } + + return new DeleteCommentResponse(comment.getId()); + } + + @Transactional + public boolean toggleCommentLike(final Member member, final Long dailyLifeId, final Long commentId) { + getDailyLife(dailyLifeId); + final DailyLifeComment comment = getComment(commentId); + + Optional dailyLifeCommentLike = + dailyLifeCommentLikeReader.findByMemberAndDailyLifeComment(member, comment); + + String notificationRelatedUrl = getRelatedUrl(dailyLifeId); + String notificationMessage; + if (Objects.isNull(comment.getParent())) { + notificationMessage = member.getNickname() + "님이 내 댓글에 좋아요를 눌렀어요."; + } else { + notificationMessage = member.getNickname() + "님이 내 답글에 좋아요를 눌렀어요."; + } + boolean isNotSameUser = !comment.getMember().isSameUser(member); + + // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 + return dailyLifeCommentLike + .map(like -> { + dailyLifeCommentLikeWriter.delete(like); + + if (isNotSameUser) { + notificationService.deleteCommentLikeNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage); + } + return false; + }) + .orElseGet(() -> { + dailyLifeCommentLikeWriter.store(member, comment); + + if (isNotSameUser) { + notificationService.sendCommentLikeNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage, member.getProfileImage()); + } + return true; + }); + } + + private static String getRelatedUrl(Long dailyLifeId) { + return "/v1/api/daily-lives/" + dailyLifeId; + } + + private static void validateCommentWriter(final Member member, final DailyLifeComment comment) { + if (!member.getId().equals(comment.getMember().getId())) { + throw new InvalidParamException(ErrorCode.COMMENT_NOT_WRITER); + } + } + + private static void validateDailyLifeWriter(final Member member, final DailyLife dailyLife) { + if (!member.getId().equals(dailyLife.getMember().getId())) { + throw new InvalidParamException(ErrorCode.DAILY_LIFE_NOT_WRITER); + } + } + + private DailyLifeComment createCommentOrReply( + final WriteDailyLifeCommentRequest request, + final Member member, + final DailyLife dailyLife + ) { + if (Objects.isNull(request.parentCommentId())) { + return DailyLifeComment.createComment(member, dailyLife, request.content()); + } else { + DailyLifeComment parentComment = dailyLifeCommentReader.getById(request.parentCommentId()); + return DailyLifeComment.createReply(member, dailyLife, request.content(), parentComment); + } + } + + private DailyLifeComment getComment(final Long commentId) { + return dailyLifeCommentReader.getById(commentId); + } + + private DailyLife getDailyLife(final Long dailyLifeId) { + return dailyLifeReader.getById(dailyLifeId); + } + + private void updateIfNotBlank(final String value, final Consumer updater) { + if (StringUtils.hasText(value)) { + updater.accept(value); + } + } + + private void storeImageList( + final List files, + final List newImageUrlList, + final DailyLife dailyLife + ) { + if (!Objects.isNull(files) && !files.isEmpty()) { + // TODO : 파일 크기 및 확장자 validate + validateFileListSize(files); + + for (MultipartFile file : files) { + UploadImageInfo uploadImageInfo = s3Service.uploadDailyLifeImage(file); + newImageUrlList.add(uploadImageInfo.ImageUrl()); + dailyLifeImageWriter.store(dailyLife, newImageUrlList.size(), uploadImageInfo.ImageUrl()); + } + } + } + + private static void validateFileListSize(final List nonEmptyFiles) { + if (nonEmptyFiles.size() > FileConstants.FILE_MAX_SIZE_COUNT) { + throw new InvalidParamException(ErrorCode.EXCEEDED_FILE_COUNT); + } + } + + @Transactional(readOnly = true) + public DailyLife findById(long dailyLifeId) { + return dailyLifeJpaRepository.findById(dailyLifeId) + .orElseThrow(() -> new BaseException(ErrorCode.DAILY_LIFE_NOT_FOUND)); + } } diff --git a/src/main/java/com/juu/juulabel/report/Report.java b/src/main/java/com/juu/juulabel/report/Report.java index 9b89854..08d1b22 100644 --- a/src/main/java/com/juu/juulabel/report/Report.java +++ b/src/main/java/com/juu/juulabel/report/Report.java @@ -24,9 +24,8 @@ public class Report extends BaseTimeEntity { @JoinColumn(name = "reporter_id", nullable = false) private Member reporter; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "reported_user_id", nullable = false) - private Member reportedUser; + @Column(name = "reported_content_id", nullable = false) + private Long reportedContentId; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "reviewer") diff --git a/src/main/java/com/juu/juulabel/report/ReportCreateRequest.java b/src/main/java/com/juu/juulabel/report/ReportCreateRequest.java index c525742..d1898c9 100644 --- a/src/main/java/com/juu/juulabel/report/ReportCreateRequest.java +++ b/src/main/java/com/juu/juulabel/report/ReportCreateRequest.java @@ -4,11 +4,10 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; - public record ReportCreateRequest( - @NotNull(message = "신고 대상을 넣어주세요") + @NotNull(message = "신고할 컨텐츠를 넣어주세요") @Min(value = 1, message = "ID는 1 이상이어야 합니다.") - Long reportedUserId, + Long reportedContentId, @NotBlank(message = "신고 사유를 넣어주세요.") String reason, diff --git a/src/main/java/com/juu/juulabel/report/ReportService.java b/src/main/java/com/juu/juulabel/report/ReportService.java index 6633475..6987b13 100644 --- a/src/main/java/com/juu/juulabel/report/ReportService.java +++ b/src/main/java/com/juu/juulabel/report/ReportService.java @@ -2,6 +2,8 @@ import com.juu.juulabel.member.domain.Member; import com.juu.juulabel.member.service.MemberService; +import com.juu.juulabel.report.processor.ReportProcessor; +import com.juu.juulabel.report.processor.ReportProcessorFactory; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -11,16 +13,19 @@ public class ReportService { private final ReportRepository reportRepository; - private final MemberService MemberService; + private final MemberService memberService; + private final ReportProcessorFactory reportProcessorFactory; @Transactional - public void createReport(Long reporterId, ReportCreateRequest request) { - Member reporter = MemberService.findById(reporterId); - Member reportedUser = MemberService.findById(request.reportedUserId()); + public void createReport(long reporterId, ReportCreateRequest request) { + Member reporter = memberService.findById(reporterId); + + ReportProcessor processor = reportProcessorFactory.getProcessor(request.type()); + processor.process(request); Report report = Report.builder() .reporter(reporter) - .reportedUser(reportedUser) + .reportedContentId(request.reportedContentId()) .reason(request.reason()) .type(request.type()) .status(ReportStatus.PENDING) diff --git a/src/main/java/com/juu/juulabel/report/ReportType.java b/src/main/java/com/juu/juulabel/report/ReportType.java index 0183887..0310db3 100644 --- a/src/main/java/com/juu/juulabel/report/ReportType.java +++ b/src/main/java/com/juu/juulabel/report/ReportType.java @@ -4,10 +4,11 @@ @AllArgsConstructor public enum ReportType { - USER("유저"), + MEMBER("멤버"), TASTING_NOTE("시음노트"), + TASTING_NOTE_COMMENT("시음노트 댓글"), DAILY_LIFE("일상생활"), - COMMENT("댓글"); + DAILY_LIFE_COMMENT("일상생활 댓글"); private final String description; } \ No newline at end of file diff --git a/src/main/java/com/juu/juulabel/report/processor/DailyLifeCommentReportProcessor.java b/src/main/java/com/juu/juulabel/report/processor/DailyLifeCommentReportProcessor.java new file mode 100644 index 0000000..4b07ac2 --- /dev/null +++ b/src/main/java/com/juu/juulabel/report/processor/DailyLifeCommentReportProcessor.java @@ -0,0 +1,24 @@ +package com.juu.juulabel.report.processor; + +import com.juu.juulabel.dailylife.service.DailyLifeCommentService; +import com.juu.juulabel.report.ReportCreateRequest; +import com.juu.juulabel.report.ReportType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DailyLifeCommentReportProcessor implements ReportProcessor { + + private final DailyLifeCommentService dailyLifeCommentService; + + @Override + public ReportType getReportType() { + return ReportType.DAILY_LIFE_COMMENT; + } + + @Override + public void process(ReportCreateRequest request) { + dailyLifeCommentService.findById(request.reportedContentId()); + } +} diff --git a/src/main/java/com/juu/juulabel/report/processor/DailyLifeReportProcessor.java b/src/main/java/com/juu/juulabel/report/processor/DailyLifeReportProcessor.java new file mode 100644 index 0000000..0df950e --- /dev/null +++ b/src/main/java/com/juu/juulabel/report/processor/DailyLifeReportProcessor.java @@ -0,0 +1,24 @@ +package com.juu.juulabel.report.processor; + +import com.juu.juulabel.dailylife.service.DailyLifeService; +import com.juu.juulabel.report.ReportCreateRequest; +import com.juu.juulabel.report.ReportType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DailyLifeReportProcessor implements ReportProcessor { + + private final DailyLifeService dailyLifeService; + + @Override + public ReportType getReportType() { + return ReportType.DAILY_LIFE; + } + + @Override + public void process(ReportCreateRequest request) { + dailyLifeService.findById(request.reportedContentId()); + } +} diff --git a/src/main/java/com/juu/juulabel/report/processor/MemberReportProcessor.java b/src/main/java/com/juu/juulabel/report/processor/MemberReportProcessor.java new file mode 100644 index 0000000..92ae7f2 --- /dev/null +++ b/src/main/java/com/juu/juulabel/report/processor/MemberReportProcessor.java @@ -0,0 +1,24 @@ +package com.juu.juulabel.report.processor; + +import com.juu.juulabel.member.service.MemberService; +import com.juu.juulabel.report.ReportCreateRequest; +import com.juu.juulabel.report.ReportType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MemberReportProcessor implements ReportProcessor { + + private final MemberService memberService; + + @Override + public ReportType getReportType() { + return ReportType.MEMBER; + } + + @Override + public void process(ReportCreateRequest request) { + memberService.findById(request.reportedContentId()); + } +} diff --git a/src/main/java/com/juu/juulabel/report/processor/ReportProcessor.java b/src/main/java/com/juu/juulabel/report/processor/ReportProcessor.java new file mode 100644 index 0000000..53681ae --- /dev/null +++ b/src/main/java/com/juu/juulabel/report/processor/ReportProcessor.java @@ -0,0 +1,10 @@ +package com.juu.juulabel.report.processor; + +import com.juu.juulabel.report.ReportCreateRequest; +import com.juu.juulabel.report.ReportType; + +public interface ReportProcessor { + ReportType getReportType(); + + void process(ReportCreateRequest request); +} diff --git a/src/main/java/com/juu/juulabel/report/processor/ReportProcessorFactory.java b/src/main/java/com/juu/juulabel/report/processor/ReportProcessorFactory.java new file mode 100644 index 0000000..fdf97fe --- /dev/null +++ b/src/main/java/com/juu/juulabel/report/processor/ReportProcessorFactory.java @@ -0,0 +1,29 @@ +package com.juu.juulabel.report.processor; + +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.report.ReportType; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +public class ReportProcessorFactory { + + private final Map processorMap; + + public ReportProcessorFactory(List reportProcessors) { + this.processorMap = reportProcessors.stream() + .collect(Collectors.toMap(ReportProcessor::getReportType, Function.identity())); + } + public ReportProcessor getProcessor(ReportType type) { + ReportProcessor processor = processorMap.get(type); + if (processor == null) { + throw new BaseException(ErrorCode.REPORT_PROCESSOR_NOT_FOUND); + } + return processor; + } +} diff --git a/src/main/java/com/juu/juulabel/report/processor/TastingNoteCommentReportProcessor.java b/src/main/java/com/juu/juulabel/report/processor/TastingNoteCommentReportProcessor.java new file mode 100644 index 0000000..3086171 --- /dev/null +++ b/src/main/java/com/juu/juulabel/report/processor/TastingNoteCommentReportProcessor.java @@ -0,0 +1,24 @@ +package com.juu.juulabel.report.processor; + +import com.juu.juulabel.report.ReportCreateRequest; +import com.juu.juulabel.report.ReportType; +import com.juu.juulabel.tastingnote.service.TastingNoteCommentService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TastingNoteCommentReportProcessor implements ReportProcessor { + + private final TastingNoteCommentService tastingNoteCommentService; + + @Override + public ReportType getReportType() { + return ReportType.TASTING_NOTE_COMMENT; + } + + @Override + public void process(ReportCreateRequest request) { + tastingNoteCommentService.findById(request.reportedContentId()); + } +} diff --git a/src/main/java/com/juu/juulabel/report/processor/TastingNoteReportProcessor.java b/src/main/java/com/juu/juulabel/report/processor/TastingNoteReportProcessor.java new file mode 100644 index 0000000..d3c4bcc --- /dev/null +++ b/src/main/java/com/juu/juulabel/report/processor/TastingNoteReportProcessor.java @@ -0,0 +1,24 @@ +package com.juu.juulabel.report.processor; + +import com.juu.juulabel.report.ReportCreateRequest; +import com.juu.juulabel.report.ReportType; +import com.juu.juulabel.tastingnote.service.TastingNoteService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TastingNoteReportProcessor implements ReportProcessor { + + private final TastingNoteService tastingNoteService; + + @Override + public ReportType getReportType() { + return ReportType.TASTING_NOTE; + } + + @Override + public void process(ReportCreateRequest request) { + tastingNoteService.findById(request.reportedContentId()); + } +} diff --git a/src/main/java/com/juu/juulabel/tastingnote/service/TastingNoteCommentService.java b/src/main/java/com/juu/juulabel/tastingnote/service/TastingNoteCommentService.java new file mode 100644 index 0000000..5468922 --- /dev/null +++ b/src/main/java/com/juu/juulabel/tastingnote/service/TastingNoteCommentService.java @@ -0,0 +1,19 @@ +package com.juu.juulabel.tastingnote.service; + +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.tastingnote.domain.TastingNoteComment; +import com.juu.juulabel.tastingnote.repository.jpa.TastingNoteCommentJpaRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TastingNoteCommentService { + private final TastingNoteCommentJpaRepository tastingNoteCommentJpaRepository; + + public TastingNoteComment findById(long id) { + return tastingNoteCommentJpaRepository.findById(id) + .orElseThrow(() -> new BaseException(ErrorCode.TASTING_NOTE_COMMENT_NOT_FOUND)); + } +} diff --git a/src/main/java/com/juu/juulabel/tastingnote/service/TastingNoteService.java b/src/main/java/com/juu/juulabel/tastingnote/service/TastingNoteService.java new file mode 100644 index 0000000..3e20617 --- /dev/null +++ b/src/main/java/com/juu/juulabel/tastingnote/service/TastingNoteService.java @@ -0,0 +1,539 @@ +package com.juu.juulabel.tastingnote.service; + +import com.juu.juulabel.alcohol.domain.*; +import com.juu.juulabel.alcohol.repository.*; +import com.juu.juulabel.alcohol.response.*; +import com.juu.juulabel.common.constants.FileConstants; +import com.juu.juulabel.common.dto.ImageInfo; +import com.juu.juulabel.common.dto.comment.CommentSummary; +import com.juu.juulabel.common.dto.comment.ReplySummary; +import com.juu.juulabel.common.dto.request.*; +import com.juu.juulabel.common.dto.response.*; +import com.juu.juulabel.common.exception.BaseException; +import com.juu.juulabel.common.exception.InvalidParamException; +import com.juu.juulabel.common.exception.code.ErrorCode; +import com.juu.juulabel.member.domain.Member; +import com.juu.juulabel.member.request.MemberInfo; +import com.juu.juulabel.notification.service.NotificationService; +import com.juu.juulabel.s3.S3Service; +import com.juu.juulabel.s3.UploadImageInfo; +import com.juu.juulabel.tastingnote.domain.*; +import com.juu.juulabel.tastingnote.domain.embedded.AlcoholicDrinksSnapshot; +import com.juu.juulabel.tastingnote.repository.jpa.TastingNoteCommentJpaRepository; +import com.juu.juulabel.tastingnote.repository.jpa.TastingNoteJpaRepository; +import com.juu.juulabel.tastingnote.request.AlcoholicDrinksInfo; +import com.juu.juulabel.tastingnote.request.AlcoholicDrinksTastingNoteSummary; +import com.juu.juulabel.tastingnote.request.TastingNoteDetailInfo; +import com.juu.juulabel.tastingnote.request.TastingNoteSummary; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TastingNoteService { + + private final TastingNoteReader tastingNoteReader; + private final AlcoholTypeReader alcoholTypeReader; + private final AlcoholicDrinksReader alcoholicDrinksReader; + private final AlcoholTypeColorReader alcoholTypeColorReader; + private final AlcoholTypeScentReader alcoholTypeScentReader; + private final AlcoholTypeFlavorReader alcoholTypeFlavorReader; + private final AlcoholTypeSensoryReader alcoholTypeSensoryReader; + private final S3Service s3Service; + private final TastingNoteWriter tastingNoteWriter; + private final TastingNoteImageWriter tastingNoteImageWriter; + private final TastingNoteImageReader tastingNoteImageReader; + private final TastingNoteLikeReader tastingNoteLikeReader; + private final TastingNoteLikeWriter tastingNoteLikeWriter; + private final TastingNoteCommentReader tastingNoteCommentReader; + private final TastingNoteCommentWriter tastingNoteCommentWriter; + private final TastingNoteCommentLikeReader tastingNoteCommentLikeReader; + private final TastingNoteCommentLikeWriter tastingNoteCommentLikeWriter; + private final NotificationService notificationService; + private final TastingNoteCommentJpaRepository tastingNoteCommentJpaRepository; + private final TastingNoteJpaRepository tastingNoteJpaRepository; + + @Transactional(readOnly = true) + public AlcoholDrinksListResponse searchAlcoholDrinksList(final SearchAlcoholDrinksListRequest request) { + final Slice alcoholicDrinks = tastingNoteReader.getAllAlcoholicDrinks(request.search(), + request.lastAlcoholicDrinksName(), request.pageSize()); + + long totalCount = tastingNoteReader.countBySearch(request.search()); + +// return SliceResponseFactory.create( +// AlcoholDrinksListResponse.class, +// alcoholicDrinks.isLast(), +// totalCount, +// alcoholicDrinks.getContent() +// ); + return new AlcoholDrinksListResponse( + alcoholicDrinks.isLast(), + totalCount, + alcoholicDrinks.getContent() + ); + } + + @Transactional(readOnly = true) + public TastingNoteColorListResponse loadTastingNoteColorsList(final Long alcoholTypeId) { + final List colors = alcoholTypeColorReader.getAllColorInfoByAlcoholTypeId(alcoholTypeId); + return new TastingNoteColorListResponse(colors); + } + + @Transactional(readOnly = true) + public TastingNoteSensoryListResponse loadTastingNoteSensoryList(final Long alcoholTypeId) { + final List sensoryLevels = alcoholTypeSensoryReader.getAllSensoryLevelInfoByAlcoholTypeId( + alcoholTypeId); + return new TastingNoteSensoryListResponse(sensoryLevels); + } + + @Transactional(readOnly = true) + public TastingNoteScentListResponse loadTastingNoteScentList(final Long alcoholTypeId) { + final List categories = alcoholTypeScentReader.getAllCategoryWithScentByAlcoholTypeId( + alcoholTypeId); + return new TastingNoteScentListResponse(categories); + } + + @Transactional(readOnly = true) + public TastingNoteFlavorListResponse loadTastingNoteFlavorList(final Long alcoholTypeId) { + final List flavorLevels = alcoholTypeFlavorReader.getAllFlavorLevelInfoByAlcoholTypeId( + alcoholTypeId); + return new TastingNoteFlavorListResponse(flavorLevels); + } + + @Transactional + public TastingNoteWriteResponse write(final Member loginMember, final TastingNoteWriteRequest request, + List files) { + // 1. 입력된 주종 확인 + final Long alcoholTypeId = request.alcoholTypeId(); + final AlcoholType alcoholType = alcoholTypeReader.getById(alcoholTypeId); + + // 2. 전통주 정보 확인 (OD, UD) + final AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getByIdOrElseNull(request.alcoholicDrinksId()); + final AlcoholicDrinksSnapshot alcoholicDrinksInfo = AlcoholicDrinksSnapshot.fromDto( + request.alcoholicDrinksDetails()); + + // 3. 감각 정보 확인 (시각 정보, 촉각 정보, 미각 정보, 후각 정보) + final Color color = getValidColorOrElseThrow(alcoholTypeId, request.colorId()); + final List scents = getValidScentsOrElseThrow(alcoholTypeId, request.scentIds()); + final List flavorLevels = getValidFlavorLevelsOrElseThrow(alcoholTypeId, request.flavorLevelIds()); + final List sensoryLevels = getValidSensoryLevelsOrElseThrow(alcoholTypeId, + request.sensoryLevelIds()); + + // 4. 시음 노트 정보 생성 (작성) + final TastingNote tastingNote = createBy(loginMember, alcoholType, alcoholicDrinks, color, alcoholicDrinksInfo, + request); + final TastingNote result = tastingNoteWriter.create( + tastingNote, + TastingNoteScent.of(tastingNote, scents), + TastingNoteFlavorLevel.of(tastingNote, flavorLevels), + TastingNoteSensoryLevel.of(tastingNote, sensoryLevels)); + + List imageUrlList = new ArrayList<>(); + storeImageList(files, imageUrlList, tastingNote); + + if (!Objects.isNull(alcoholicDrinks)) { + alcoholicDrinks.addRating(request.rating()); + } + + return TastingNoteWriteResponse.fromEntity(result); + } + + private Color getValidColorOrElseThrow(final Long alcoholTypeId, final Long colorId) { + final List colors = alcoholTypeColorReader.getAllColorByAlcoholTypeId(alcoholTypeId); + return colors.stream() + .filter(c -> Objects.equals(c.getId(), colorId)) + .findFirst() + .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_COLOR)); + } + + private List getValidScentsOrElseThrow(final Long alcoholTypeId, final List scentIds) { + final List scents = alcoholTypeScentReader.getAllScentByAlcoholTypeId(alcoholTypeId); + final Map scentMap = scents.stream() + .collect(Collectors.toMap(Scent::getId, scent -> scent)); + return scentIds.stream() + .map(scentId -> + Optional.ofNullable(scentMap.get(scentId)) + .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_SCENT)) + ) + .toList(); + } + + private List getValidSensoryLevelsOrElseThrow(final Long alcoholTypeId, + final List sensoryLevelIds) { + final List sensoryLevels = alcoholTypeSensoryReader.getAllSensoryLevelByAlcoholTypeId( + alcoholTypeId); + final Map sensoryLevelMap = sensoryLevels.stream() + .collect(Collectors.toMap(SensoryLevel::getId, sensoryLevel -> sensoryLevel)); + return sensoryLevelIds.stream() + .map(sensoryLevelId -> + Optional.ofNullable(sensoryLevelMap.get(sensoryLevelId)) + .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_SENSORY)) + ) + .toList(); + } + + private List getValidFlavorLevelsOrElseThrow(final Long alcoholTypeId, + final List flavorLevelIds) { + final List flavorLevels = alcoholTypeFlavorReader.getAllFlavorLevelByAlcoholTypeId(alcoholTypeId); + final Map flavorMap = flavorLevels.stream() + .collect(Collectors.toMap(FlavorLevel::getId, flavorLevel -> flavorLevel)); + return flavorLevelIds.stream() + .map(flavorLevelId -> + Optional.ofNullable(flavorMap.get(flavorLevelId)) + .orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_ALCOHOL_TYPE_FLAVOR)) + ) + .toList(); + } + + private TastingNote createBy(final Member member, + final AlcoholType alcoholType, + final AlcoholicDrinks alcoholicDrinks, + final Color color, + final AlcoholicDrinksSnapshot alcoholicDrinksInfo, + final TastingNoteWriteRequest request) { + return TastingNote.of( + member, + alcoholType, + alcoholicDrinks, + color, + alcoholicDrinksInfo, + request.rating(), + request.content(), + request.isPrivate() + ); + } + + private void storeImageList( + final List files, + final List newImageUrlList, + final TastingNote tastingNote + ) { + if (!Objects.isNull(files) && !files.isEmpty()) { + // TODO : 파일 크기 및 확장자 validate + validateFileListSize(files); + + for (MultipartFile file : files) { + UploadImageInfo uploadImageInfo = s3Service.uploadTastingNoteImage(file); + newImageUrlList.add(uploadImageInfo.ImageUrl()); + tastingNoteImageWriter.store(tastingNote, newImageUrlList.size(), uploadImageInfo.ImageUrl()); + } + } + } + + private void validateFileListSize(final List nonEmptyFiles) { + if (nonEmptyFiles.size() > FileConstants.FILE_MAX_SIZE_COUNT) { + throw new InvalidParamException(ErrorCode.EXCEEDED_FILE_COUNT); + } + } + + @Transactional(readOnly = true) + public TastingNoteListResponse loadTastingNoteList(Member member, TastingNoteListRequest request) { + Slice tastingNoteList = + tastingNoteReader.getAllTastingNotes(member, request.lastTastingNoteId(), request.pageSize()); + + return new TastingNoteListResponse(tastingNoteList); + } + + @Transactional(readOnly = true) + public TastingNoteListResponseForAlcoholicDrinks loadTastingNoteListByAlcoholicDrinksId(Member member, TastingNoteListRequest request, Long alcoholicDrinksId) { + Slice tastingNoteList = + tastingNoteReader.getAllTastingNotesByAlcoholicDrinksId(member, request.lastTastingNoteId(), request.pageSize(), alcoholicDrinksId); + + return new TastingNoteListResponseForAlcoholicDrinks(tastingNoteList); + } + + @Transactional(readOnly = true) + public TastingNoteResponse loadTastingNote(Member member, Long tastingNoteId) { + TastingNoteDetailInfo tastingNoteDetailInfo = tastingNoteReader.getTastingNoteDetailById(tastingNoteId, member); + List urlList = tastingNoteImageReader.getImageUrlList(tastingNoteId); + Long alcoholicDrinksId = tastingNoteReader.getAlcoholicDrinksByTastingNoteId(tastingNoteId); + boolean isOfficialData = !Objects.isNull(alcoholicDrinksId); + + return new TastingNoteResponse( + tastingNoteDetailInfo, + tastingNoteReader.getSensoryLevelIds(tastingNoteId, member), + tastingNoteReader.getScentIds(tastingNoteId, member), + tastingNoteReader.getFlavorLevelIds(tastingNoteId, member), + new ImageInfo(urlList, urlList.size()), + new AlcoholicDrinksInfo(isOfficialData, alcoholicDrinksId) + ); + } + + @Transactional + public TastingNoteWriteResponse updateTastingNote( + Member member, + Long tastingNoteId, + TastingNoteWriteRequest request, + List files + ) { + TastingNote tastingNote = getTastingNote(tastingNoteId); + validateTastingNoteWriter(member, tastingNote); + double rating = tastingNote.getRating(); + + // 입력된 주종 확인 + final Long alcoholTypeId = request.alcoholTypeId(); + final AlcoholType alcoholType = alcoholTypeReader.getById(alcoholTypeId); + + // 전통주 정보 확인 (OD, UD) + final AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getByIdOrElseNull(request.alcoholicDrinksId()); + final AlcoholicDrinksSnapshot alcoholicDrinksInfo = AlcoholicDrinksSnapshot.fromDto( + request.alcoholicDrinksDetails()); + + // 감각 정보 확인 (시각 정보, 촉각 정보, 미각 정보, 후각 정보) + final Color color = getValidColorOrElseThrow(alcoholTypeId, request.colorId()); + final List scents = getValidScentsOrElseThrow(alcoholTypeId, request.scentIds()); + final List flavorLevels = getValidFlavorLevelsOrElseThrow(alcoholTypeId, request.flavorLevelIds()); + final List sensoryLevels = getValidSensoryLevelsOrElseThrow(alcoholTypeId, + request.sensoryLevelIds()); + + tastingNote.update(alcoholType, alcoholicDrinks, color, alcoholicDrinksInfo, request.rating(), + request.content(), request.isPrivate()); + tastingNoteWriter.update( + tastingNote.getId(), + TastingNoteScent.of(tastingNote, scents), + TastingNoteFlavorLevel.of(tastingNote, flavorLevels), + TastingNoteSensoryLevel.of(tastingNote, sensoryLevels)); + + List tastingNoteImageList = tastingNoteImageReader.getImageList(tastingNote.getId()); + tastingNoteImageList.forEach(TastingNoteImage::delete); + + List imageUrlList = new ArrayList<>(); + storeImageList(files, imageUrlList, tastingNote); + + if (rating != request.rating() && !Objects.isNull(alcoholicDrinks)) { + alcoholicDrinks.updateRating(rating, request.rating()); + } + + return new TastingNoteWriteResponse(tastingNote.getId()); + } + + private TastingNote getTastingNote(Long tastingNoteId) { + return tastingNoteReader.getById(tastingNoteId); + } + + private static void validateTastingNoteWriter(Member member, TastingNote tastingNote) { + if (!member.getId().equals(tastingNote.getMember().getId())) { + throw new InvalidParamException(ErrorCode.TASTING_NOTE_NOT_WRITER); + } + } + + @Transactional + public DeleteTastingNoteResponse deleteTastingNote(Member member, Long tastingNoteId) { + TastingNote tastingNote = getTastingNote(tastingNoteId); + validateTastingNoteWriter(member, tastingNote); + + if (!Objects.isNull(tastingNote.getAlcoholicDrinks())) { + AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getById(tastingNote.getAlcoholicDrinks().getId()); + alcoholicDrinks.removeRating(tastingNote.getRating()); + } + + tastingNote.delete(); + return new DeleteTastingNoteResponse(tastingNote.getId()); + } + + @Transactional + public boolean toggleTastingNoteLike(Member member, Long tastingNoteId) { + TastingNote tastingNote = getTastingNote(tastingNoteId); + Optional tastingNoteLike = tastingNoteLikeReader.findByMemberAndTastingNote(member, + tastingNote); + String notificationRelatedUrl = getRelatedUrl(tastingNoteId); + boolean isNotSameUser = !tastingNote.getMember().isSameUser(member); + + // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 + return tastingNoteLike + .map(like -> { + tastingNoteLikeWriter.delete(like); + + if (isNotSameUser) { + notificationService.deletePostLikeNotification(tastingNote.getMember(), member, + notificationRelatedUrl); + } + return false; + }) + .orElseGet(() -> { + tastingNoteLikeWriter.store(member, tastingNote); + + if (isNotSameUser) { + notificationService.sendPostLikeNotification(tastingNote.getMember(), member, + notificationRelatedUrl); + } + return true; + }); + } + + @Transactional + public WriteTastingNoteCommentResponse writeComment( + Member member, + WriteTastingNoteCommentRequest request, + Long tastingNoteId + ) { + TastingNote tastingNote = getTastingNote(tastingNoteId); + TastingNoteComment tastingNoteComment = createCommentOrReply(request, member, tastingNote); + TastingNoteComment comment = tastingNoteCommentWriter.store(tastingNoteComment); + + String notificationRelatedUrl = getRelatedUrl(tastingNoteId); + String notificationMessage; + if (Objects.isNull(request.parentCommentId()) && (!tastingNote.getMember().isSameUser(member))) { + notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; + notificationService.sendCommentNotification(tastingNote.getMember(), notificationRelatedUrl, + notificationMessage, comment.getId(), member.getProfileImage()); + } else if (!comment.getMember().isSameUser(member)) { + notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; + notificationService.sendCommentNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage, comment.getId(), member.getProfileImage()); + } + + return new WriteTastingNoteCommentResponse( + comment.getContent(), + tastingNote.getId(), + new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage()) + ); + } + + private TastingNoteComment createCommentOrReply(WriteTastingNoteCommentRequest request, Member member, + TastingNote tastingNote) { + if (Objects.isNull(request.parentCommentId())) { + return TastingNoteComment.createComment(member, tastingNote, request.content()); + } else { + TastingNoteComment parentComment = tastingNoteCommentReader.getById(request.parentCommentId()); + return TastingNoteComment.createReply(member, tastingNote, request.content(), parentComment); + } + } + + @Transactional(readOnly = true) + public TastingNoteCommentListResponse loadCommentList(Member member, CommentListRequest request, + Long tastingNoteId) { + TastingNote tastingNote = getTastingNote(tastingNoteId); + + Slice commentList = + tastingNoteCommentReader.getAllByTastingNoteId(member, tastingNote.getId(), request.lastCommentId(), + request.pageSize()); + + return new TastingNoteCommentListResponse(commentList); + } + + @Transactional(readOnly = true) + public TastingNoteReplyListResponse loadReplyList( + Member member, + ReplyListRequest request, + Long tastingNoteId, + Long tastingNoteCommentId + ) { + TastingNote tastingNote = getTastingNote(tastingNoteId); + + Slice replyList = tastingNoteCommentReader.getAllRepliesByParentId( + member, + tastingNote.getId(), + tastingNoteCommentId, + request.lastReplyId(), + request.pageSize() + ); + + return new TastingNoteReplyListResponse(replyList); + } + + @Transactional + public UpdateCommentResponse updateComment(Member member, UpdateCommentRequest request, Long tastingNoteId, + Long commentId) { + getTastingNote(tastingNoteId); + TastingNoteComment comment = getComment(commentId); + + validateCommentWriter(member, comment); + + comment.updateContent(request.content()); + + return new UpdateCommentResponse( + comment.getContent(), + new MemberInfo(member.getId(), member.getNickname(), member.getProfileImage()) + ); + } + + private TastingNoteComment getComment(Long commentId) { + return tastingNoteCommentReader.getById(commentId); + } + + private static void validateCommentWriter(Member member, TastingNoteComment comment) { + if (!member.getId().equals(comment.getMember().getId())) { + throw new InvalidParamException(ErrorCode.COMMENT_NOT_WRITER); + } + } + + @Transactional + public DeleteCommentResponse deleteComment(Member member, Long tastingNoteId, Long commentId) { + TastingNote tastingNote = getTastingNote(tastingNoteId); + TastingNoteComment comment = getComment(commentId); + validateCommentWriter(member, comment); + comment.delete(); + + String notificationRelatedUrl = getRelatedUrl(tastingNoteId); + String notificationMessage; + if (Objects.isNull(comment.getParent())) { + notificationMessage = member.getNickname() + "님이 내 게시물에 댓글을 남겼어요."; + notificationService.deleteCommentNotification(tastingNote.getMember(), notificationRelatedUrl, + notificationMessage, commentId); + } else { + notificationMessage = member.getNickname() + "님이 내 댓글에 답글을 남겼어요."; + notificationService.deleteCommentNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage, commentId); + } + + return new DeleteCommentResponse(comment.getId()); + } + + @Transactional + public boolean toggleCommentLike(Member member, Long tastingNoteId, Long commentId) { + getTastingNote(tastingNoteId); + TastingNoteComment comment = getComment(commentId); + + Optional tastingNoteCommentLike = + tastingNoteCommentLikeReader.findByMemberAndTastingNoteComment(member, comment); + + String notificationRelatedUrl = getRelatedUrl(tastingNoteId); + String notificationMessage; + if (Objects.isNull(comment.getParent())) { + notificationMessage = member.getNickname() + "님이 내 댓글에 좋아요를 눌렀어요."; + } else { + notificationMessage = member.getNickname() + "님이 내 답글에 좋아요를 눌렀어요."; + } + boolean isNotSameUser = !comment.getMember().isSameUser(member); + + // 좋아요가 등록되어 있다면 삭제, 등록되어 있지 않다면 등록 + return tastingNoteCommentLike + .map(like -> { + tastingNoteCommentLikeWriter.delete(like); + + if (isNotSameUser) { + notificationService.deleteCommentLikeNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage); + } + return false; + }) + .orElseGet(() -> { + tastingNoteCommentLikeWriter.store(member, comment); + + if (isNotSameUser) { + notificationService.sendCommentLikeNotification(comment.getMember(), notificationRelatedUrl, + notificationMessage, member.getProfileImage()); + } + return true; + }); + } + + private static String getRelatedUrl(Long tastingNoteId) { + return "/v1/api/shared-space/tasting-notes/" + tastingNoteId; + } + + @Transactional(readOnly = true) + public TastingNote findById(long id) { + return tastingNoteJpaRepository.findById(id) + .orElseThrow(() -> new BaseException(ErrorCode.TASTING_NOTE_NOT_FOUND)); + } +}