diff --git a/src/main/java/umc/th/juinjang/api/note/liked/controller/LikedNoteController.java b/src/main/java/umc/th/juinjang/api/note/liked/controller/LikedNoteController.java new file mode 100644 index 00000000..3e6ee00c --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/controller/LikedNoteController.java @@ -0,0 +1,38 @@ +package umc.th.juinjang.api.note.liked.controller; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.dto.ApiResponse; +import umc.th.juinjang.api.note.liked.service.LikedNoteCommandService; +import umc.th.juinjang.api.note.liked.service.response.LikedNoteDeleteResponse; +import umc.th.juinjang.api.note.liked.service.response.LikedNotePostResponse; +import umc.th.juinjang.domain.member.model.Member; + +@RestController +@RequestMapping("/api/v2/shared-notes") +@RequiredArgsConstructor +public class LikedNoteController { + + private final LikedNoteCommandService likedNoteCommandService; + + @Operation(summary = "공유노트 좋아요 등록 API") + @PostMapping("/{sharedNoteId}/likes") + public ApiResponse createLikedNote(@AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + return ApiResponse.onSuccess(likedNoteCommandService.createLikedNote(member, sharedNoteId)); + } + + @Operation(summary = "공유노트 좋아요 취소 API") + @DeleteMapping("/{sharedNoteId}/likes") + public ApiResponse deleteLikedNote(@AuthenticationPrincipal Member member, + @PathVariable("sharedNoteId") Long sharedNoteId) { + return ApiResponse.onSuccess(likedNoteCommandService.deleteLikedNote(member, sharedNoteId)); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteCommandService.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteCommandService.java new file mode 100644 index 00000000..764703f0 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteCommandService.java @@ -0,0 +1,47 @@ +package umc.th.juinjang.api.note.liked.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.api.note.liked.service.response.LikedNoteDeleteResponse; +import umc.th.juinjang.api.note.liked.service.response.LikedNotePostResponse; +import umc.th.juinjang.api.note.shared.service.SharedNoteFinder; +import umc.th.juinjang.api.note.shared.service.SharedNoteUpdater; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.shared.model.SharedNote; + +@Service +@RequiredArgsConstructor +public class LikedNoteCommandService { + + private final LikedNoteUpdater likedNoteUpdater; + private final SharedNoteFinder sharedNoteFinder; + private final SharedNoteUpdater sharedNoteUpdater; + private final LikedNoteFinder likedNoteFinder; + private final LikedNoteDeleter likedNoteDeleter; + + @Transactional + public LikedNotePostResponse createLikedNote(Member member, Long sharedNoteId) { + SharedNote sharedNote = sharedNoteFinder.getById(sharedNoteId); + LikedNote likedNote = LikedNote.create(member, sharedNote); + + likedNoteUpdater.save(likedNote); + sharedNoteUpdater.incrementLikedCountById(sharedNoteId); + + return new LikedNotePostResponse(sharedNoteFinder.getLikedNoteById(sharedNoteId)); + } + + @Transactional + public LikedNoteDeleteResponse deleteLikedNote(Member member, Long sharedNoteId) { + SharedNote sharedNote = sharedNoteFinder.getById(sharedNoteId); + LikedNote likedNote = likedNoteFinder.getByMemberAndSharedNote(member, sharedNote); + + likedNoteDeleter.delete(likedNote); + sharedNoteUpdater.decrementLikedCountById(sharedNoteId); + + return new LikedNoteDeleteResponse(sharedNoteFinder.getLikedNoteById(sharedNoteId)); + } + +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteDeleter.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteDeleter.java new file mode 100644 index 00000000..5aaf7d50 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteDeleter.java @@ -0,0 +1,18 @@ +package umc.th.juinjang.api.note.liked.service; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.liked.model.repository.LikedNoteRepository; + +@Component +@RequiredArgsConstructor +public class LikedNoteDeleter { + + private final LikedNoteRepository likedNoteRepository; + + void delete(LikedNote likedNote) { + likedNoteRepository.delete(likedNote); + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteFinder.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteFinder.java index cb548abe..3fd8d730 100644 --- a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteFinder.java +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteFinder.java @@ -3,7 +3,10 @@ import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LikedNoteHandler; import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; import umc.th.juinjang.domain.note.liked.model.repository.LikedNoteRepository; import umc.th.juinjang.domain.note.shared.model.SharedNote; @@ -16,4 +19,9 @@ public class LikedNoteFinder { public boolean existsByMemberAndSharedNote(Member member, SharedNote sharedNote) { return likedNoteRepository.existsByMemberAndSharedNote(member, sharedNote); } + + public LikedNote getByMemberAndSharedNote(Member member, SharedNote sharedNote) { + return likedNoteRepository.findByMemberAndSharedNote(member, sharedNote) + .orElseThrow(() -> new LikedNoteHandler(ErrorStatus.LIKEDNOTE_NOT_FOUND)); + } } diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteUpdater.java b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteUpdater.java new file mode 100644 index 00000000..ea11e6ec --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/LikedNoteUpdater.java @@ -0,0 +1,27 @@ +package umc.th.juinjang.api.note.liked.service; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import umc.th.juinjang.common.code.status.ErrorStatus; +import umc.th.juinjang.common.exception.handler.LikedNoteHandler; +import umc.th.juinjang.common.exception.handler.SharedNoteHandler; +import umc.th.juinjang.domain.member.model.Member; +import umc.th.juinjang.domain.note.liked.model.LikedNote; +import umc.th.juinjang.domain.note.liked.model.repository.LikedNoteRepository; + +@Component +@RequiredArgsConstructor +public class LikedNoteUpdater { + + private final LikedNoteRepository likedNoteRepository; + + public void save(LikedNote likedNote) { + try { + likedNoteRepository.save(likedNote); + } catch (DataIntegrityViolationException e) { + throw new LikedNoteHandler(ErrorStatus.LIKEDNOTE_CONFLICT); + } + } +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNoteDeleteResponse.java b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNoteDeleteResponse.java new file mode 100644 index 00000000..2130c384 --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNoteDeleteResponse.java @@ -0,0 +1,4 @@ +package umc.th.juinjang.api.note.liked.service.response; + +public record LikedNoteDeleteResponse(Long count) { +} diff --git a/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNotePostResponse.java b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNotePostResponse.java new file mode 100644 index 00000000..4192f40f --- /dev/null +++ b/src/main/java/umc/th/juinjang/api/note/liked/service/response/LikedNotePostResponse.java @@ -0,0 +1,4 @@ +package umc.th.juinjang.api.note.liked.service.response; + +public record LikedNotePostResponse(long count) { +} diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteFinder.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteFinder.java index 097d3d94..3a69a3f5 100644 --- a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteFinder.java +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteFinder.java @@ -16,7 +16,7 @@ public class SharedNoteFinder { private final SharedNoteRepository sharedNoteRepository; - SharedNote getById(Long id) { + public SharedNote getById(Long id) { return sharedNoteRepository.findById(id) .orElseThrow(() -> new SharedNoteHandler(ErrorStatus.SHAREDNOTE_NOT_FOUND)); } @@ -29,4 +29,9 @@ SharedNote findByIdWithNoteAndAddress(Long id) { Optional findById(Long id) { return sharedNoteRepository.findById(id); } + + public Long getLikedNoteById(Long id) { + return sharedNoteRepository.getLikeCountById(id); + } + } diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java index 43bc8281..a07293ff 100644 --- a/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/SharedNoteUpdater.java @@ -14,4 +14,12 @@ public class SharedNoteUpdater { void updateViewCount(long sharedNoteId, long addAmount) { sharedNoteRepository.incrementViewCount(sharedNoteId, addAmount); } + + public void incrementLikedCountById(Long sharedNoteId) { + sharedNoteRepository.incrementLikedCountById(sharedNoteId); + } + + public void decrementLikedCountById(Long sharedNoteId) { + sharedNoteRepository.decrementLikedCountById(sharedNoteId); + } } diff --git a/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteGetResponse.java b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteGetResponse.java index 350aafbc..02b339d4 100644 --- a/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteGetResponse.java +++ b/src/main/java/umc/th/juinjang/api/note/shared/service/response/SharedNoteGetResponse.java @@ -30,7 +30,7 @@ public record SharedNoteGetResponse( String price, String monthlyRent, boolean isLiked, - int likedCount, + Long likedCount, String period, String updatedAt, Long viewCount, diff --git a/src/main/java/umc/th/juinjang/common/code/status/ErrorStatus.java b/src/main/java/umc/th/juinjang/common/code/status/ErrorStatus.java index 03f4e688..f5219c16 100644 --- a/src/main/java/umc/th/juinjang/common/code/status/ErrorStatus.java +++ b/src/main/java/umc/th/juinjang/common/code/status/ErrorStatus.java @@ -104,10 +104,14 @@ public enum ErrorStatus implements BaseErrorCode { // PencilAccount alert PENCIL_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST, "ACCOUNT4000", "멤버에 해당하는 계좌가 존재하지 않습니다."), - SHAREDNOTE_NOT_FOUND(HttpStatus.BAD_REQUEST, "SHAREDNOTE4000", "해당하는 공유노트가 존재하지 않습니다."), + SHAREDNOTE_NOT_FOUND(HttpStatus.NOT_FOUND, "SHAREDNOTE4000", "해당하는 공유노트가 존재하지 않습니다."), SHAREDNOTE_NOT_ENOUGH_PENCIL(HttpStatus.BAD_REQUEST, "SHAREDNOTE4001", "보유한 연필 수가 부족합니다."), SHAREDNOTE_CONFLICT(HttpStatus.CONFLICT, "SHAREDNOTE4002", "이미 구매한 노트입니다."), - SHAREDNOTE_DEADLOCK(HttpStatus.LOCKED, "SHAREDNOTE4003", "잠시 후 다시 시도해주세요. 현재 다른 요청이 처리 중입니다."); + SHAREDNOTE_DEADLOCK(HttpStatus.LOCKED, "SHAREDNOTE4003", "잠시 후 다시 시도해주세요. 현재 다른 요청이 처리 중입니다."), + + // LikedNote + LIKEDNOTE_CONFLICT(HttpStatus.CONFLICT, "LIKEDNOTE4000", "이미 좋아요한 노트입니다"), + LIKEDNOTE_NOT_FOUND(HttpStatus.NOT_FOUND, "LIKEDNOTE4001", "이미 취소했거나 좋아요한 적이 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/umc/th/juinjang/common/exception/handler/LikedNoteHandler.java b/src/main/java/umc/th/juinjang/common/exception/handler/LikedNoteHandler.java new file mode 100644 index 00000000..838c96ac --- /dev/null +++ b/src/main/java/umc/th/juinjang/common/exception/handler/LikedNoteHandler.java @@ -0,0 +1,10 @@ +package umc.th.juinjang.common.exception.handler; + +import umc.th.juinjang.common.code.BaseErrorCode; +import umc.th.juinjang.common.exception.GeneralException; + +public class LikedNoteHandler extends GeneralException { + public LikedNoteHandler(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/umc/th/juinjang/domain/note/liked/model/LikedNote.java b/src/main/java/umc/th/juinjang/domain/note/liked/model/LikedNote.java index cfc0ca93..b3fab114 100644 --- a/src/main/java/umc/th/juinjang/domain/note/liked/model/LikedNote.java +++ b/src/main/java/umc/th/juinjang/domain/note/liked/model/LikedNote.java @@ -8,7 +8,10 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import umc.th.juinjang.domain.member.model.Member; @@ -16,6 +19,14 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(uniqueConstraints = { + @UniqueConstraint( + columnNames = { + "member_id", + "shared_note_id" + } + ) +}) @Entity public class LikedNote { @@ -30,5 +41,14 @@ public class LikedNote { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "shared_note_id", nullable = false) private SharedNote sharedNote; - + + @Builder + private LikedNote(Member member, SharedNote sharedNote) { + this.member = member; + this.sharedNote = sharedNote; + } + + public static LikedNote create(Member member, SharedNote sharedNote) { + return LikedNote.builder().member(member).sharedNote(sharedNote).build(); + } } diff --git a/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteRepository.java b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteRepository.java index 1fee6078..f3a26ee8 100644 --- a/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteRepository.java +++ b/src/main/java/umc/th/juinjang/domain/note/liked/model/repository/LikedNoteRepository.java @@ -1,5 +1,7 @@ package umc.th.juinjang.domain.note.liked.model.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import umc.th.juinjang.domain.member.model.Member; @@ -9,4 +11,6 @@ public interface LikedNoteRepository extends JpaRepository { boolean existsByMemberAndSharedNote(Member member, SharedNote sharedNote); + + Optional findByMemberAndSharedNote(Member member, SharedNote sharedNote); } diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java b/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java index e0a932ab..a613f0e6 100644 --- a/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java +++ b/src/main/java/umc/th/juinjang/domain/note/shared/model/SharedNote.java @@ -53,7 +53,7 @@ public class SharedNote extends BaseEntity { @Comment("임장 가격") private Long price; - private int likeCount; + private Long likeCount; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "limjang_id", nullable = false, unique = true) @@ -62,5 +62,9 @@ public class SharedNote extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id", nullable = false) private Member member; + + public Long increaseLikedCount() { + return this.likeCount = (likeCount == null ? 1L : likeCount + 1); + } } diff --git a/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java index b821d39f..e7e28201 100644 --- a/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java +++ b/src/main/java/umc/th/juinjang/domain/note/shared/repository/SharedNoteRepository.java @@ -17,4 +17,15 @@ public interface SharedNoteRepository extends JpaRepository { @Modifying @Query("UPDATE SharedNote s SET s.viewCount = COALESCE(s.viewCount, 0) + :addAmount WHERE s.sharedNoteId = :sharedNoteId") void incrementViewCount(@Param("sharedNoteId") Long sharedNoteId, @Param("addAmount") Long addAmount); + + @Modifying + @Query("UPDATE SharedNote sn SET sn.likeCount = sn.likeCount + 1 WHERE sn.sharedNoteId = :sharedNoteId") + void incrementLikedCountById(@Param("sharedNoteId") Long sharedNoteId); + + @Query("SELECT sn.likeCount FROM SharedNote sn WHERE sn.sharedNoteId = :sharedNoteId") + Long getLikeCountById(@Param("sharedNoteId") Long sharedNoteId); + + @Modifying + @Query("UPDATE SharedNote sn SET sn.likeCount = sn.likeCount - 1 WHERE sn.sharedNoteId = :sharedNoteId") + void decrementLikedCountById(@Param("sharedNoteId") Long sharedNoteId); } \ No newline at end of file