From f5ef8d14b8e5215c04efdd37d4a102ad79a64d47 Mon Sep 17 00:00:00 2001 From: elive7 Date: Thu, 27 Mar 2025 17:13:06 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[SCRUM-200]=20feat:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=EC=88=98/=EC=A4=91=EB=B3=B5=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 중복 조회수와 중복 제외 조회수 도메인을 생성했습니다. 이때 중복 조회수는 후에 실제 서비스 분석에 사용될 수 있도록 유저 필드를 추가했습니다. --- .../server/domain/view/domain/TotalView.java | 67 +++++++++++++++++++ .../domain/view/domain/TotalViewCreate.java | 14 ++++ .../server/domain/view/domain/UniqueView.java | 57 ++++++++++++++++ .../domain/view/domain/UniqueViewCreate.java | 12 ++++ 4 files changed, 150 insertions(+) create mode 100644 src/main/java/com/projectlyrics/server/domain/view/domain/TotalView.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/domain/TotalViewCreate.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/TotalView.java b/src/main/java/com/projectlyrics/server/domain/view/domain/TotalView.java new file mode 100644 index 00000000..67f0cde8 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/domain/TotalView.java @@ -0,0 +1,67 @@ +package com.projectlyrics.server.domain.view.domain; + + +import com.projectlyrics.server.domain.common.entity.BaseEntity; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.user.entity.User; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "total_views") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TotalView extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "note_id") + @ManyToOne(fetch = FetchType.LAZY) + private Note note; + + @JoinColumn(name = "user_id") + @ManyToOne(fetch = FetchType.LAZY) + private User user; + + private String deviceId; + + private TotalView( + Note note, + User user, + String deviceId + ) { + this.note = note; + this.user = user; + this.deviceId = deviceId; + } + + public static TotalView create(TotalViewCreate totalViewCreate) { + return new TotalView( + totalViewCreate.note(), + totalViewCreate.user(), + totalViewCreate.deviceId() + ); + } + + public static TotalView createWithId(Long id, TotalViewCreate totalViewCreate) { + return new TotalView( + id, + totalViewCreate.note(), + totalViewCreate.user(), + totalViewCreate.deviceId() + ); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/TotalViewCreate.java b/src/main/java/com/projectlyrics/server/domain/view/domain/TotalViewCreate.java new file mode 100644 index 00000000..d022b08f --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/domain/TotalViewCreate.java @@ -0,0 +1,14 @@ +package com.projectlyrics.server.domain.view.domain; + +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.user.entity.User; + +public record TotalViewCreate( + Note note, + User user, + String deviceId +) { + public static TotalViewCreate of(Note note, User user, String deviceId) { + return new TotalViewCreate(note, user, deviceId); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java b/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java new file mode 100644 index 00000000..e2680d3c --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java @@ -0,0 +1,57 @@ +package com.projectlyrics.server.domain.view.domain; + +import com.projectlyrics.server.domain.common.entity.BaseEntity; +import com.projectlyrics.server.domain.note.entity.Note; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "unique_views") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class UniqueView extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "note_id") + @ManyToOne(fetch = FetchType.LAZY) + private Note note; + + private String deviceId; + + private UniqueView( + Note note, + String deviceId + ) { + this.note = note; + this.deviceId = deviceId; + } + + public static UniqueView create(UniqueViewCreate uniqueViewCreate) { + return new UniqueView( + uniqueViewCreate.note(), + uniqueViewCreate.deviceId() + ); + } + + public static UniqueView createWithId(Long id, UniqueViewCreate uniqueViewCreate) { + return new UniqueView( + id, + uniqueViewCreate.note(), + uniqueViewCreate.deviceId() + ); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java b/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java new file mode 100644 index 00000000..02891ab6 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java @@ -0,0 +1,12 @@ +package com.projectlyrics.server.domain.view.domain; + +import com.projectlyrics.server.domain.note.entity.Note; + +public record UniqueViewCreate( + Note note, + String deviceId +) { + public static UniqueViewCreate of(Note note, String deviceId) { + return new UniqueViewCreate(note, deviceId); + } +} From 48e49f3733eb0ac44dc22f07d1656fa3a7d15a73 Mon Sep 17 00:00:00 2001 From: elive7 Date: Thu, 27 Mar 2025 18:55:55 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[SCRUM-200]=20fix:=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EC=A0=9C=EC=99=B8=20=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 생각해보니, 중복 제외 조회수와 중복 조회수 table을 따로 만드는 것은 데이터 중복성이나 무결성 측면에서 좋지 않을 것 같습니다. 따라서 중복 제외 조회수의 경우는 mysql에서 가상테이블 뷰를 통해 구성할 예정입니다. --- .../server/domain/view/domain/UniqueView.java | 57 ------------------- .../domain/view/domain/UniqueViewCreate.java | 12 ---- 2 files changed, 69 deletions(-) delete mode 100644 src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java delete mode 100644 src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java b/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java deleted file mode 100644 index e2680d3c..00000000 --- a/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueView.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.projectlyrics.server.domain.view.domain; - -import com.projectlyrics.server.domain.common.entity.BaseEntity; -import com.projectlyrics.server.domain.note.entity.Note; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "unique_views") -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class UniqueView extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @JoinColumn(name = "note_id") - @ManyToOne(fetch = FetchType.LAZY) - private Note note; - - private String deviceId; - - private UniqueView( - Note note, - String deviceId - ) { - this.note = note; - this.deviceId = deviceId; - } - - public static UniqueView create(UniqueViewCreate uniqueViewCreate) { - return new UniqueView( - uniqueViewCreate.note(), - uniqueViewCreate.deviceId() - ); - } - - public static UniqueView createWithId(Long id, UniqueViewCreate uniqueViewCreate) { - return new UniqueView( - id, - uniqueViewCreate.note(), - uniqueViewCreate.deviceId() - ); - } -} diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java b/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java deleted file mode 100644 index 02891ab6..00000000 --- a/src/main/java/com/projectlyrics/server/domain/view/domain/UniqueViewCreate.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.projectlyrics.server.domain.view.domain; - -import com.projectlyrics.server.domain.note.entity.Note; - -public record UniqueViewCreate( - Note note, - String deviceId -) { - public static UniqueViewCreate of(Note note, String deviceId) { - return new UniqueViewCreate(note, deviceId); - } -} From 03fdf60cf08b595a411512e8bb0e90179785d9cf Mon Sep 17 00:00:00 2001 From: elive7 Date: Thu, 27 Mar 2025 18:57:32 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[SCRUM-200]=20refactor=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A1=B0=ED=9A=8C=EC=88=98=20totalView->View?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view/domain/{TotalView.java => View.java} | 26 +++++++++---------- .../{TotalViewCreate.java => ViewCreate.java} | 6 ++--- 2 files changed, 16 insertions(+), 16 deletions(-) rename src/main/java/com/projectlyrics/server/domain/view/domain/{TotalView.java => View.java} (70%) rename src/main/java/com/projectlyrics/server/domain/view/domain/{TotalViewCreate.java => ViewCreate.java} (59%) diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/TotalView.java b/src/main/java/com/projectlyrics/server/domain/view/domain/View.java similarity index 70% rename from src/main/java/com/projectlyrics/server/domain/view/domain/TotalView.java rename to src/main/java/com/projectlyrics/server/domain/view/domain/View.java index 67f0cde8..81046c1a 100644 --- a/src/main/java/com/projectlyrics/server/domain/view/domain/TotalView.java +++ b/src/main/java/com/projectlyrics/server/domain/view/domain/View.java @@ -19,10 +19,10 @@ @Getter @Entity -@Table(name = "total_views") +@Table(name = "views") @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class TotalView extends BaseEntity { +public class View extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -38,7 +38,7 @@ public class TotalView extends BaseEntity { private String deviceId; - private TotalView( + private View( Note note, User user, String deviceId @@ -48,20 +48,20 @@ private TotalView( this.deviceId = deviceId; } - public static TotalView create(TotalViewCreate totalViewCreate) { - return new TotalView( - totalViewCreate.note(), - totalViewCreate.user(), - totalViewCreate.deviceId() + public static View create(ViewCreate viewCreate) { + return new View( + viewCreate.note(), + viewCreate.user(), + viewCreate.deviceId() ); } - public static TotalView createWithId(Long id, TotalViewCreate totalViewCreate) { - return new TotalView( + public static View createWithId(Long id, ViewCreate viewCreate) { + return new View( id, - totalViewCreate.note(), - totalViewCreate.user(), - totalViewCreate.deviceId() + viewCreate.note(), + viewCreate.user(), + viewCreate.deviceId() ); } } diff --git a/src/main/java/com/projectlyrics/server/domain/view/domain/TotalViewCreate.java b/src/main/java/com/projectlyrics/server/domain/view/domain/ViewCreate.java similarity index 59% rename from src/main/java/com/projectlyrics/server/domain/view/domain/TotalViewCreate.java rename to src/main/java/com/projectlyrics/server/domain/view/domain/ViewCreate.java index d022b08f..64c0e9bf 100644 --- a/src/main/java/com/projectlyrics/server/domain/view/domain/TotalViewCreate.java +++ b/src/main/java/com/projectlyrics/server/domain/view/domain/ViewCreate.java @@ -3,12 +3,12 @@ import com.projectlyrics.server.domain.note.entity.Note; import com.projectlyrics.server.domain.user.entity.User; -public record TotalViewCreate( +public record ViewCreate( Note note, User user, String deviceId ) { - public static TotalViewCreate of(Note note, User user, String deviceId) { - return new TotalViewCreate(note, user, deviceId); + public static ViewCreate of(Note note, User user, String deviceId) { + return new ViewCreate(note, user, deviceId); } } From 2e3adbb0273f31f36ab1c8fc61de1f03393e26fb Mon Sep 17 00:00:00 2001 From: elive7 Date: Thu, 27 Mar 2025 19:12:26 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[SCRUM-200]=20feat:=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98=20=EC=83=9D=EC=84=B1=20api=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/message/ErrorCode.java | 3 ++ .../domain/view/api/ViewController.java | 38 +++++++++++++++++++ .../view/dto/request/ViewCreateRequest.java | 6 +++ .../view/dto/response/ViewCreateResponse.java | 6 +++ .../view/exception/ViewNotFoundException.java | 8 ++++ .../repository/ViewCommandRepository.java | 9 +++++ .../view/repository/ViewQueryRepository.java | 7 ++++ .../impl/QueryDslViewQueryRepository.java | 32 ++++++++++++++++ .../view/service/ViewCommandService.java | 31 +++++++++++++++ 9 files changed, 140 insertions(+) create mode 100644 src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/dto/response/ViewCreateResponse.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/exception/ViewNotFoundException.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/repository/ViewCommandRepository.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/repository/ViewQueryRepository.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/repository/impl/QueryDslViewQueryRepository.java create mode 100644 src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java diff --git a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java index 8a7d2434..2efd92e7 100644 --- a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java +++ b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java @@ -120,6 +120,9 @@ public enum ErrorCode { //Banner BANNER_NOT_FOUND(HttpStatus.NOT_FOUND, "18000", "해당 배너를 조회할 수 없습니다."), + + //View + VIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "19000", "해당 중복 제외 조회수를 조회할 수 없습니다."), ; private final HttpStatus responseStatus; diff --git a/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java b/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java new file mode 100644 index 00000000..635bb9ef --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java @@ -0,0 +1,38 @@ +package com.projectlyrics.server.domain.view.api; + +import com.projectlyrics.server.domain.auth.authentication.AuthContext; +import com.projectlyrics.server.domain.auth.authentication.Authenticated; +import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; +import com.projectlyrics.server.domain.view.dto.response.ViewCreateResponse; +import com.projectlyrics.server.domain.view.service.ViewCommandService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/views") +@RequiredArgsConstructor +public class ViewController { + + private final ViewCommandService viewCommandService; + + @PostMapping + public ResponseEntity create( + @Authenticated AuthContext authContext, + @RequestHeader("Device-Id") String deviceId, + @RequestBody ViewCreateRequest request + ) { + if (authContext.isAnonymous()) { + viewCommandService.create(request, null, deviceId); + } else { + viewCommandService.create(request, authContext.getId(), deviceId); + } + + return ResponseEntity + .ok(new ViewCreateResponse(true)); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java b/src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java new file mode 100644 index 00000000..14865e00 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java @@ -0,0 +1,6 @@ +package com.projectlyrics.server.domain.view.dto.request; + +public record ViewCreateRequest( + Long noteId +){ +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/dto/response/ViewCreateResponse.java b/src/main/java/com/projectlyrics/server/domain/view/dto/response/ViewCreateResponse.java new file mode 100644 index 00000000..80ff714b --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/dto/response/ViewCreateResponse.java @@ -0,0 +1,6 @@ +package com.projectlyrics.server.domain.view.dto.response; + +public record ViewCreateResponse ( + boolean success +){ +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/exception/ViewNotFoundException.java b/src/main/java/com/projectlyrics/server/domain/view/exception/ViewNotFoundException.java new file mode 100644 index 00000000..8b333f98 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/exception/ViewNotFoundException.java @@ -0,0 +1,8 @@ +package com.projectlyrics.server.domain.view.exception; + +import com.projectlyrics.server.domain.common.message.ErrorCode; +import com.projectlyrics.server.global.exception.FeelinException; + +public class ViewNotFoundException extends FeelinException { + public ViewNotFoundException() {super(ErrorCode.VIEW_NOT_FOUND);} +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/repository/ViewCommandRepository.java b/src/main/java/com/projectlyrics/server/domain/view/repository/ViewCommandRepository.java new file mode 100644 index 00000000..38c5c814 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/repository/ViewCommandRepository.java @@ -0,0 +1,9 @@ +package com.projectlyrics.server.domain.view.repository; + +import com.projectlyrics.server.domain.view.domain.View; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ViewCommandRepository extends JpaRepository { +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/repository/ViewQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/view/repository/ViewQueryRepository.java new file mode 100644 index 00000000..27adb085 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/repository/ViewQueryRepository.java @@ -0,0 +1,7 @@ +package com.projectlyrics.server.domain.view.repository; + +import com.projectlyrics.server.domain.view.domain.View; + +public interface ViewQueryRepository { + View findById(Long id); +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/repository/impl/QueryDslViewQueryRepository.java b/src/main/java/com/projectlyrics/server/domain/view/repository/impl/QueryDslViewQueryRepository.java new file mode 100644 index 00000000..502f714f --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/repository/impl/QueryDslViewQueryRepository.java @@ -0,0 +1,32 @@ +package com.projectlyrics.server.domain.view.repository.impl; + +import static com.projectlyrics.server.domain.view.domain.QView.view; + +import com.projectlyrics.server.domain.view.domain.View; +import com.projectlyrics.server.domain.view.exception.ViewNotFoundException; +import com.projectlyrics.server.domain.view.repository.ViewQueryRepository; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class QueryDslViewQueryRepository implements ViewQueryRepository { + private final JPAQueryFactory jpaQueryFactory; + + @Override + public View findById(Long id) { + return Optional.ofNullable( + jpaQueryFactory + .selectFrom(view) + .leftJoin(view.note).fetchJoin() + .leftJoin(view.user).fetchJoin() + .where( + view.id.eq(id), + view.deletedAt.isNull() + ) + .fetchOne() + ).orElseThrow(ViewNotFoundException::new); + } +} diff --git a/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java b/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java new file mode 100644 index 00000000..7cec53c3 --- /dev/null +++ b/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java @@ -0,0 +1,31 @@ +package com.projectlyrics.server.domain.view.service; + +import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.exception.UserNotFoundException; +import com.projectlyrics.server.domain.user.repository.UserQueryRepository; +import com.projectlyrics.server.domain.view.domain.View; +import com.projectlyrics.server.domain.view.domain.ViewCreate; +import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; +import com.projectlyrics.server.domain.view.repository.ViewCommandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +@RequiredArgsConstructor +public class ViewCommandService { + + private final ViewCommandRepository viewCommandRepository; + private final NoteQueryRepository noteQueryRepository; + private final UserQueryRepository userQueryRepository; + + public View create(ViewCreateRequest request, Long userId, String deviceId) { + Note note = noteQueryRepository.findById(request.noteId()); + User user = userId == null? null: userQueryRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + return viewCommandRepository.save(View.create(ViewCreate.of(note, user, deviceId))); + } +} From 7ce6f8783dfff4d2e398a197b40ebdbb377b3eb7 Mon Sep 17 00:00:00 2001 From: elive7 Date: Thu, 27 Mar 2025 19:14:01 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[SCRUM-200]=20refactor:=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../projectlyrics/server/domain/common/message/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java index 2efd92e7..4e364bdd 100644 --- a/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java +++ b/src/main/java/com/projectlyrics/server/domain/common/message/ErrorCode.java @@ -122,7 +122,7 @@ public enum ErrorCode { BANNER_NOT_FOUND(HttpStatus.NOT_FOUND, "18000", "해당 배너를 조회할 수 없습니다."), //View - VIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "19000", "해당 중복 제외 조회수를 조회할 수 없습니다."), + VIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "19000", "해당 조회수를 조회할 수 없습니다."), ; private final HttpStatus responseStatus; From 25f8e6c9bbeb41aafa42c27d8b970a792e81a6c1 Mon Sep 17 00:00:00 2001 From: elive7 Date: Thu, 27 Mar 2025 19:41:05 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[SCRUM-200]=20test:=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98=20=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/view/api/ViewControllerTest.java | 58 ++++++++++ .../view/service/ViewCommandServiceTest.java | 108 ++++++++++++++++++ .../server/support/ControllerTest.java | 4 + .../server/support/fixture/ViewFixture.java | 19 +++ 4 files changed, 189 insertions(+) create mode 100644 src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java create mode 100644 src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java create mode 100644 src/test/java/com/projectlyrics/server/support/fixture/ViewFixture.java diff --git a/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java b/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java new file mode 100644 index 00000000..58962a42 --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java @@ -0,0 +1,58 @@ +package com.projectlyrics.server.domain.view.api; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.epages.restdocs.apispec.Schema; +import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; +import com.projectlyrics.server.support.RestDocsTest; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; + +public class ViewControllerTest extends RestDocsTest { + private static final String deviceIdHeader = "Device-Id"; + private static final String deviceIdValue = "device_id"; + + @Test + void 조회수를_저장하면_200응답을_해야_한다() throws Exception { + // given + ViewCreateRequest request = new ViewCreateRequest( + 1L + ); + + // when, then + mockMvc.perform(post("/api/v1/views") + .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .header(deviceIdHeader, deviceIdValue) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andDo(getCreateViewDocument()); + } + + private RestDocumentationResultHandler getCreateViewDocument() { + return restDocs.document( + resource(ResourceSnippetParameters.builder() + .tag("View API") + .summary("조회 생성 API") + .requestHeaders(getAuthorizationHeader()) + .requestFields( + fieldWithPath("noteId").type(JsonFieldType.NUMBER) + .description("조회한 노트 ID") + ) + .requestSchema(Schema.schema("Create View Request")) + .responseFields( + fieldWithPath("success").type(JsonFieldType.BOOLEAN) + .description("성공 여부") + ) + .responseSchema(Schema.schema("Create View Response")) + .build()) + ); + } +} diff --git a/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java new file mode 100644 index 00000000..c304353a --- /dev/null +++ b/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java @@ -0,0 +1,108 @@ +package com.projectlyrics.server.domain.view.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.projectlyrics.server.domain.artist.entity.Artist; +import com.projectlyrics.server.domain.artist.repository.ArtistCommandRepository; +import com.projectlyrics.server.domain.note.dto.request.NoteCreateRequest; +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.note.entity.NoteBackground; +import com.projectlyrics.server.domain.note.entity.NoteCreate; +import com.projectlyrics.server.domain.note.entity.NoteStatus; +import com.projectlyrics.server.domain.note.repository.NoteCommandRepository; +import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; +import com.projectlyrics.server.domain.song.entity.Song; +import com.projectlyrics.server.domain.song.repository.SongCommandRepository; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.user.repository.UserCommandRepository; +import com.projectlyrics.server.domain.view.domain.View; +import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; +import com.projectlyrics.server.domain.view.repository.ViewQueryRepository; +import com.projectlyrics.server.support.IntegrationTest; +import com.projectlyrics.server.support.fixture.ArtistFixture; +import com.projectlyrics.server.support.fixture.SongFixture; +import com.projectlyrics.server.support.fixture.UserFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class ViewCommandServiceTest extends IntegrationTest { + + @Autowired + UserCommandRepository userCommandRepository; + + @Autowired + ArtistCommandRepository artistCommandRepository; + + @Autowired + SongCommandRepository songCommandRepository; + + @Autowired + NoteQueryRepository noteQueryRepository; + + @Autowired + NoteCommandRepository noteCommandRepository; + + @Autowired + ViewQueryRepository viewQueryRepository; + + @Autowired + ViewCommandService sut; + + private User user; + private Artist artist; + private Song song; + private Note note; + + @BeforeEach + void setUp() { + user = userCommandRepository.save(UserFixture.create()); + artist = artistCommandRepository.save(ArtistFixture.create()); + song = songCommandRepository.save(SongFixture.create(artist)); + NoteCreateRequest noteCreateRequest = new NoteCreateRequest( + "content", + "lyrics", + NoteBackground.DEFAULT, + NoteStatus.PUBLISHED, + song.getId() + ); + note = noteCommandRepository.save(Note.create(NoteCreate.from(noteCreateRequest, user, song))); + } + + @Test + void 조회수를_발행해야_한다() { + // given + String deviceId = "DEVICE_ID"; + ViewCreateRequest request = new ViewCreateRequest(note.getId()); + + // when + View view = sut.create(request, user.getId(), deviceId); + + // then + View result = viewQueryRepository.findById(view.getId()); + assertAll( + () -> assertThat(result.getNote().getId()).isEqualTo(view.getNote().getId()), + () -> assertThat(result.getDeviceId()).isEqualTo(view.getDeviceId()), + () -> assertThat(result.getUser().getId()).isEqualTo(view.getUser().getId()) + ); + } + + @Test + void 유저id_없이도_조회수를_발행해야_한다() { + // given + String deviceId = "DEVICE_ID"; + ViewCreateRequest request = new ViewCreateRequest(note.getId()); + + // when + View view = sut.create(request, null, deviceId); + + // then + View result = viewQueryRepository.findById(view.getId()); + assertAll( + () -> assertThat(result.getNote().getId()).isEqualTo(view.getNote().getId()), + () -> assertThat(result.getDeviceId()).isEqualTo(view.getDeviceId()), + () -> assertThat(result.getUser()).isNull() + ); + } +} diff --git a/src/test/java/com/projectlyrics/server/support/ControllerTest.java b/src/test/java/com/projectlyrics/server/support/ControllerTest.java index 2e33cbdc..7b546c12 100644 --- a/src/test/java/com/projectlyrics/server/support/ControllerTest.java +++ b/src/test/java/com/projectlyrics/server/support/ControllerTest.java @@ -31,6 +31,7 @@ import com.projectlyrics.server.domain.user.entity.Role; import com.projectlyrics.server.domain.user.service.UserCommandService; import com.projectlyrics.server.domain.user.service.UserQueryService; +import com.projectlyrics.server.domain.view.service.ViewCommandService; import com.projectlyrics.server.global.configuration.ClockConfig; import com.projectlyrics.server.global.slack.SlackClient; import com.projectlyrics.server.global.slack.service.SlackService; @@ -138,6 +139,9 @@ public abstract class ControllerTest { @MockBean protected BannerQueryService bannerQueryService; + @MockBean + protected ViewCommandService viewCommandService; + public String accessToken; public String refreshToken; diff --git a/src/test/java/com/projectlyrics/server/support/fixture/ViewFixture.java b/src/test/java/com/projectlyrics/server/support/fixture/ViewFixture.java new file mode 100644 index 00000000..bc5d2d33 --- /dev/null +++ b/src/test/java/com/projectlyrics/server/support/fixture/ViewFixture.java @@ -0,0 +1,19 @@ +package com.projectlyrics.server.support.fixture; + +import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.user.entity.User; +import com.projectlyrics.server.domain.view.domain.View; +import com.projectlyrics.server.domain.view.domain.ViewCreate; + +public class ViewFixture extends BaseFixture{ + public static View create(Note note, User user, String deviceId) { + return View.createWithId( + getUniqueId(), + ViewCreate.of( + note, + user, + deviceId + ) + ); + } +} From ba17760f162e5a28155632478fecaf03bc463815 Mon Sep 17 00:00:00 2001 From: elive7 Date: Sat, 29 Mar 2025 01:45:15 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[SCRUM-200]=20refactor:=20viewCommandServ?= =?UTF-8?q?ice=EC=97=90=EC=84=9C=20create=20=ED=95=A8=EC=88=98=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=EB=A1=9C=EB=94=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - viewCommandService의 create 함수를 userId를 인자로 받는 버전과 받지 않은 버전 2가지로 나누어 작성하였습니다. --- .../server/domain/view/api/ViewController.java | 2 +- .../server/domain/view/service/ViewCommandService.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java b/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java index 635bb9ef..4a12934b 100644 --- a/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java +++ b/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java @@ -27,7 +27,7 @@ public ResponseEntity create( @RequestBody ViewCreateRequest request ) { if (authContext.isAnonymous()) { - viewCommandService.create(request, null, deviceId); + viewCommandService.create(request, deviceId); } else { viewCommandService.create(request, authContext.getId(), deviceId); } diff --git a/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java b/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java index 7cec53c3..bb38b7bf 100644 --- a/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java @@ -24,8 +24,12 @@ public class ViewCommandService { public View create(ViewCreateRequest request, Long userId, String deviceId) { Note note = noteQueryRepository.findById(request.noteId()); - User user = userId == null? null: userQueryRepository.findById(userId) - .orElseThrow(UserNotFoundException::new); + User user = userQueryRepository.findById(userId).orElseThrow(UserNotFoundException::new); return viewCommandRepository.save(View.create(ViewCreate.of(note, user, deviceId))); } + + public View create(ViewCreateRequest request, String deviceId) { + Note note = noteQueryRepository.findById(request.noteId()); + return viewCommandRepository.save(View.create(ViewCreate.of(note, null, deviceId))); + } } From bd6a47526862a4cf321a1bb0a2dd4303c80c7226 Mon Sep 17 00:00:00 2001 From: elive7 Date: Sat, 29 Mar 2025 01:48:52 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[SCRUM-200]=20refactor:=20deviceIdHeader-?= =?UTF-8?q?>deviceIdKey=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deviceIdValue와 더 잘 매칭될 수 있도록 deviceIdHeader를 deviceIdKey 로 수정하였습니다. --- .../server/domain/event/api/EventControllerTest.java | 6 +++--- .../server/domain/view/api/ViewControllerTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java b/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java index 8cfab6d8..1e6dda3d 100644 --- a/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java +++ b/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java @@ -30,7 +30,7 @@ class EventControllerTest extends RestDocsTest { - private static final String deviceIdHeader = "Device-Id"; + private static final String deviceIdKey = "Device-Id"; private static final String deviceIdValue = "device_id"; @Test @@ -83,7 +83,7 @@ private RestDocumentationResultHandler getCreateEventDocument() { // when, then mockMvc.perform(post("/api/v1/events/refuse") .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .header(deviceIdHeader, deviceIdValue) + .header(deviceIdKey, deviceIdValue) .contentType(MediaType.APPLICATION_JSON) .queryParam("eventId", "1")) .andExpect(status().isOk()) @@ -129,7 +129,7 @@ private RestDocumentationResultHandler getRefuseEventDocument() { // when, then mockMvc.perform(get("/api/v1/events") .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .header(deviceIdHeader, deviceIdValue) + .header(deviceIdKey, deviceIdValue) .param("cursor", "1") .param("size", "10") .contentType(MediaType.APPLICATION_JSON)) diff --git a/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java b/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java index 58962a42..8b969c2d 100644 --- a/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java +++ b/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java @@ -16,7 +16,7 @@ import org.springframework.restdocs.payload.JsonFieldType; public class ViewControllerTest extends RestDocsTest { - private static final String deviceIdHeader = "Device-Id"; + private static final String deviceIdKey = "Device-Id"; private static final String deviceIdValue = "device_id"; @Test @@ -29,7 +29,7 @@ public class ViewControllerTest extends RestDocsTest { // when, then mockMvc.perform(post("/api/v1/views") .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .header(deviceIdHeader, deviceIdValue) + .header(deviceIdKey, deviceIdValue) .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(request))) .andExpect(status().isOk()) From 6c367ea426dbf956dde64476291275a49d19c244 Mon Sep 17 00:00:00 2001 From: elive7 Date: Sat, 29 Mar 2025 01:50:36 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[SCRUM-200]=20test:=20viewCommandService?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20=ED=9B=84=20=EA=B7=B8?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 오버로딩한 두 함수 모두를 테스트할 수 있도록 코드를 수정했습니다. --- .../server/domain/view/service/ViewCommandServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java index c304353a..a61ac137 100644 --- a/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java @@ -95,7 +95,7 @@ void setUp() { ViewCreateRequest request = new ViewCreateRequest(note.getId()); // when - View view = sut.create(request, null, deviceId); + View view = sut.create(request, deviceId); // then View result = viewQueryRepository.findById(view.getId()); From d0bc285166841c3b25c8c8de41e0c2bf805ac49c Mon Sep 17 00:00:00 2001 From: elive7 Date: Mon, 7 Apr 2025 21:21:49 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[SCRUM-200]=20feat:=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20api=20=ED=98=B8=EC=B6=9C=EC=8B=9C=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EC=88=98=20=EC=83=9D=EC=84=B1=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 노트 단건 조회 api시 자동으로 해당 노트에 대한 조회수가 자동으로 생성되도록 작성하였습니다. --- .../domain/note/api/NoteController.java | 26 +++++++++++++++++-- .../domain/view/api/ViewController.java | 11 ++++---- .../view/dto/request/ViewCreateRequest.java | 6 ----- .../view/service/ViewCommandService.java | 11 ++++---- 4 files changed, 34 insertions(+), 20 deletions(-) delete mode 100644 src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java diff --git a/src/main/java/com/projectlyrics/server/domain/note/api/NoteController.java b/src/main/java/com/projectlyrics/server/domain/note/api/NoteController.java index 9590d344..5e1682e1 100644 --- a/src/main/java/com/projectlyrics/server/domain/note/api/NoteController.java +++ b/src/main/java/com/projectlyrics/server/domain/note/api/NoteController.java @@ -5,14 +5,28 @@ import com.projectlyrics.server.domain.common.dto.util.CursorBasePaginatedResponse; import com.projectlyrics.server.domain.note.dto.request.NoteCreateRequest; import com.projectlyrics.server.domain.note.dto.request.NoteUpdateRequest; -import com.projectlyrics.server.domain.note.dto.response.*; +import com.projectlyrics.server.domain.note.dto.response.NoteCreateResponse; +import com.projectlyrics.server.domain.note.dto.response.NoteDeleteResponse; +import com.projectlyrics.server.domain.note.dto.response.NoteDetailResponse; +import com.projectlyrics.server.domain.note.dto.response.NoteGetResponse; +import com.projectlyrics.server.domain.note.dto.response.NoteUpdateResponse; import com.projectlyrics.server.domain.note.service.NoteCommandService; import com.projectlyrics.server.domain.note.service.NoteQueryService; +import com.projectlyrics.server.domain.view.service.ViewCommandService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/v1/notes") @@ -21,6 +35,7 @@ public class NoteController { private final NoteCommandService noteCommandService; private final NoteQueryService noteQueryService; + private final ViewCommandService viewCommandService; @PostMapping public ResponseEntity create( @@ -62,8 +77,15 @@ public ResponseEntity delete( @GetMapping("/{noteId}") public ResponseEntity getNote( @Authenticated AuthContext authContext, + @RequestHeader("Device-Id") String deviceId, @PathVariable(name = "noteId") Long noteId ) { + if (authContext.isAnonymous()) { + viewCommandService.create(noteId, deviceId); + } else { + viewCommandService.create(noteId, authContext.getId(), deviceId); + } + return ResponseEntity .status(HttpStatus.OK) .body(noteQueryService.getNoteById(noteId, authContext.getId())); diff --git a/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java b/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java index 4a12934b..244ba906 100644 --- a/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java +++ b/src/main/java/com/projectlyrics/server/domain/view/api/ViewController.java @@ -2,13 +2,12 @@ import com.projectlyrics.server.domain.auth.authentication.AuthContext; import com.projectlyrics.server.domain.auth.authentication.Authenticated; -import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; import com.projectlyrics.server.domain.view.dto.response.ViewCreateResponse; import com.projectlyrics.server.domain.view.service.ViewCommandService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -20,16 +19,16 @@ public class ViewController { private final ViewCommandService viewCommandService; - @PostMapping + @PostMapping("/{noteId}") public ResponseEntity create( @Authenticated AuthContext authContext, @RequestHeader("Device-Id") String deviceId, - @RequestBody ViewCreateRequest request + @PathVariable(name = "noteId") Long noteId ) { if (authContext.isAnonymous()) { - viewCommandService.create(request, deviceId); + viewCommandService.create(noteId, deviceId); } else { - viewCommandService.create(request, authContext.getId(), deviceId); + viewCommandService.create(noteId, authContext.getId(), deviceId); } return ResponseEntity diff --git a/src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java b/src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java deleted file mode 100644 index 14865e00..00000000 --- a/src/main/java/com/projectlyrics/server/domain/view/dto/request/ViewCreateRequest.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.projectlyrics.server.domain.view.dto.request; - -public record ViewCreateRequest( - Long noteId -){ -} diff --git a/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java b/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java index bb38b7bf..2fac5715 100644 --- a/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java +++ b/src/main/java/com/projectlyrics/server/domain/view/service/ViewCommandService.java @@ -1,13 +1,12 @@ package com.projectlyrics.server.domain.view.service; -import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; import com.projectlyrics.server.domain.note.entity.Note; +import com.projectlyrics.server.domain.note.repository.NoteQueryRepository; import com.projectlyrics.server.domain.user.entity.User; import com.projectlyrics.server.domain.user.exception.UserNotFoundException; import com.projectlyrics.server.domain.user.repository.UserQueryRepository; import com.projectlyrics.server.domain.view.domain.View; import com.projectlyrics.server.domain.view.domain.ViewCreate; -import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; import com.projectlyrics.server.domain.view.repository.ViewCommandRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,14 +21,14 @@ public class ViewCommandService { private final NoteQueryRepository noteQueryRepository; private final UserQueryRepository userQueryRepository; - public View create(ViewCreateRequest request, Long userId, String deviceId) { - Note note = noteQueryRepository.findById(request.noteId()); + public View create(Long noteId, Long userId, String deviceId) { + Note note = noteQueryRepository.findById(noteId); User user = userQueryRepository.findById(userId).orElseThrow(UserNotFoundException::new); return viewCommandRepository.save(View.create(ViewCreate.of(note, user, deviceId))); } - public View create(ViewCreateRequest request, String deviceId) { - Note note = noteQueryRepository.findById(request.noteId()); + public View create(Long noteId, String deviceId) { + Note note = noteQueryRepository.findById(noteId); return viewCommandRepository.save(View.create(ViewCreate.of(note, null, deviceId))); } } From 9ae074213996d4bb032d7fb35d1b31a5633dd700 Mon Sep 17 00:00:00 2001 From: elive7 Date: Mon, 7 Apr 2025 21:24:32 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[SCRUM-200]=20test:=20=EB=B0=94=EB=80=90?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NoteControllerTest에 조회수 추가 기능을 넣으면서 변경된 부분에 맞게 테스트를 수정했습니다. - RestDocsTest에 getDeviceIdHeader를 만들고 DeviceId 헤더가 필요한 api에서 해당 함수를 사용하도록 수정했습니다. --- .../domain/auth/api/AuthControllerTest.java | 2 ++ .../domain/event/api/EventControllerTest.java | 2 ++ .../domain/note/api/NoteControllerTest.java | 6 +++++- .../domain/view/api/ViewControllerTest.java | 17 +++++++---------- .../view/service/ViewCommandServiceTest.java | 7 ++----- .../server/support/RestDocsTest.java | 6 ++++++ 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/test/java/com/projectlyrics/server/domain/auth/api/AuthControllerTest.java b/src/test/java/com/projectlyrics/server/domain/auth/api/AuthControllerTest.java index 26b6442d..cf7e874b 100644 --- a/src/test/java/com/projectlyrics/server/domain/auth/api/AuthControllerTest.java +++ b/src/test/java/com/projectlyrics/server/domain/auth/api/AuthControllerTest.java @@ -114,6 +114,7 @@ private RestDocumentationResultHandler getSignInDocument(boolean successCase) { resource(ResourceSnippetParameters.builder() .tag("Auth API") .summary("로그인 API") + .requestHeaders(getDeviceIdHeader()) .requestFields( fieldWithPath("socialAccessToken").type(JsonFieldType.STRING) .description("소셜 인증 토큰"), @@ -243,6 +244,7 @@ private RestDocumentationResultHandler getSignUpDocument(boolean successCase) { resource(ResourceSnippetParameters.builder() .tag("Auth API") .summary("회원가입 API") + .requestHeaders(getDeviceIdHeader()) .requestFields( fieldWithPath("socialAccessToken").type(JsonFieldType.STRING) .description("소셜 인증 토큰"), diff --git a/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java b/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java index 1e6dda3d..51a46375 100644 --- a/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java +++ b/src/test/java/com/projectlyrics/server/domain/event/api/EventControllerTest.java @@ -96,6 +96,7 @@ private RestDocumentationResultHandler getRefuseEventDocument() { .tag("Event API") .summary("이벤트 거부 API") .requestHeaders(getAuthorizationHeader().optional()) + .requestHeaders(getDeviceIdHeader().optional()) .queryParameters(parameterWithName("eventId").type(SimpleType.NUMBER) .description("거부할 이벤트 ID") ) @@ -144,6 +145,7 @@ private RestDocumentationResultHandler getAllExceptRefusedDocument() { .tag("Event API") .summary("진행 중인 모든 이벤트 리스트 조회 API (사용자가 거부한 이벤트 제외)") .requestHeaders(getAuthorizationHeader().optional()) + .requestHeaders(getDeviceIdHeader().optional()) .responseFields( fieldWithPath("refusalPeriod").type(JsonFieldType.NUMBER) .description("거절 기간(1:하루동안 보지 않기/7:일주일간 보지 않기)"), diff --git a/src/test/java/com/projectlyrics/server/domain/note/api/NoteControllerTest.java b/src/test/java/com/projectlyrics/server/domain/note/api/NoteControllerTest.java index 672bf345..70824513 100644 --- a/src/test/java/com/projectlyrics/server/domain/note/api/NoteControllerTest.java +++ b/src/test/java/com/projectlyrics/server/domain/note/api/NoteControllerTest.java @@ -44,6 +44,8 @@ import org.springframework.restdocs.payload.JsonFieldType; class NoteControllerTest extends RestDocsTest { + private static final String deviceIdKey = "Device-Id"; + private static final String deviceIdValue = "device_id"; @Test void 노트를_저장하면_200응답을_해야_한다() throws Exception { @@ -188,6 +190,7 @@ private RestDocumentationResultHandler getNoteDeleteDocument() { // when, then mockMvc.perform(get("/api/v1/notes/{noteId}", 1) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) + .header(deviceIdKey, deviceIdValue) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(getNoteDetailDocument()); @@ -197,8 +200,9 @@ private RestDocumentationResultHandler getNoteDetailDocument() { return restDocs.document( resource(ResourceSnippetParameters.builder() .tag("Note API") - .summary("노트 단건 조회 API") + .summary("노트 단건 조회 API (API 호출시 deviceId를 통해 해당 노트에 대한 조회수 기록)") .requestHeaders(getAuthorizationHeader().optional()) + .requestHeaders(getDeviceIdHeader()) .pathParameters( parameterWithName("noteId").type(SimpleType.NUMBER) .description("노트 ID") diff --git a/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java b/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java index 8b969c2d..29cdc4ad 100644 --- a/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java +++ b/src/test/java/com/projectlyrics/server/domain/view/api/ViewControllerTest.java @@ -1,5 +1,6 @@ package com.projectlyrics.server.domain.view.api; +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -7,7 +8,7 @@ import com.epages.restdocs.apispec.ResourceSnippetParameters; import com.epages.restdocs.apispec.Schema; -import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; +import com.epages.restdocs.apispec.SimpleType; import com.projectlyrics.server.support.RestDocsTest; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -22,16 +23,12 @@ public class ViewControllerTest extends RestDocsTest { @Test void 조회수를_저장하면_200응답을_해야_한다() throws Exception { // given - ViewCreateRequest request = new ViewCreateRequest( - 1L - ); // when, then - mockMvc.perform(post("/api/v1/views") + mockMvc.perform(post("/api/v1/views/{noteId}", 1) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .header(deviceIdKey, deviceIdValue) - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(request))) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(getCreateViewDocument()); } @@ -42,9 +39,9 @@ private RestDocumentationResultHandler getCreateViewDocument() { .tag("View API") .summary("조회 생성 API") .requestHeaders(getAuthorizationHeader()) - .requestFields( - fieldWithPath("noteId").type(JsonFieldType.NUMBER) - .description("조회한 노트 ID") + .pathParameters( + parameterWithName("noteId").type(SimpleType.NUMBER) + .description("노트 ID") ) .requestSchema(Schema.schema("Create View Request")) .responseFields( diff --git a/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java b/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java index a61ac137..0bb4b133 100644 --- a/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java +++ b/src/test/java/com/projectlyrics/server/domain/view/service/ViewCommandServiceTest.java @@ -17,7 +17,6 @@ import com.projectlyrics.server.domain.user.entity.User; import com.projectlyrics.server.domain.user.repository.UserCommandRepository; import com.projectlyrics.server.domain.view.domain.View; -import com.projectlyrics.server.domain.view.dto.request.ViewCreateRequest; import com.projectlyrics.server.domain.view.repository.ViewQueryRepository; import com.projectlyrics.server.support.IntegrationTest; import com.projectlyrics.server.support.fixture.ArtistFixture; @@ -74,10 +73,9 @@ void setUp() { void 조회수를_발행해야_한다() { // given String deviceId = "DEVICE_ID"; - ViewCreateRequest request = new ViewCreateRequest(note.getId()); // when - View view = sut.create(request, user.getId(), deviceId); + View view = sut.create(note.getId(), user.getId(), deviceId); // then View result = viewQueryRepository.findById(view.getId()); @@ -92,10 +90,9 @@ void setUp() { void 유저id_없이도_조회수를_발행해야_한다() { // given String deviceId = "DEVICE_ID"; - ViewCreateRequest request = new ViewCreateRequest(note.getId()); // when - View view = sut.create(request, deviceId); + View view = sut.create(note.getId(), deviceId); // then View result = viewQueryRepository.findById(view.getId()); diff --git a/src/test/java/com/projectlyrics/server/support/RestDocsTest.java b/src/test/java/com/projectlyrics/server/support/RestDocsTest.java index ba41978d..a2c4996f 100644 --- a/src/test/java/com/projectlyrics/server/support/RestDocsTest.java +++ b/src/test/java/com/projectlyrics/server/support/RestDocsTest.java @@ -44,6 +44,12 @@ protected static HeaderDescriptorWithType getAuthorizationHeader() { .description("Bearer ${accessToken}"); } + protected static HeaderDescriptorWithType getDeviceIdHeader() { + return headerWithName("Device-Id").type(SimpleType.STRING) + .description("기기 식별자"); + } + + protected static FieldDescriptor[] getErrorResponseFields() { return new FieldDescriptor[]{ fieldWithPath("errorCode").type(JsonFieldType.STRING)