Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -21,6 +35,7 @@ public class NoteController {

private final NoteCommandService noteCommandService;
private final NoteQueryService noteQueryService;
private final ViewCommandService viewCommandService;

@PostMapping
public ResponseEntity<NoteCreateResponse> create(
Expand Down Expand Up @@ -62,8 +77,15 @@ public ResponseEntity<NoteDeleteResponse> delete(
@GetMapping("/{noteId}")
public ResponseEntity<NoteDetailResponse> 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()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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.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.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("/{noteId}")
public ResponseEntity<ViewCreateResponse> create(
@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
.ok(new ViewCreateResponse(true));
}
}
Original file line number Diff line number Diff line change
@@ -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 = "views")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class View 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 View(
Note note,
User user,
String deviceId
) {
this.note = note;
this.user = user;
this.deviceId = deviceId;
}

public static View create(ViewCreate viewCreate) {
return new View(
viewCreate.note(),
viewCreate.user(),
viewCreate.deviceId()
);
}

public static View createWithId(Long id, ViewCreate viewCreate) {
return new View(
id,
viewCreate.note(),
viewCreate.user(),
viewCreate.deviceId()
);
}
}
Original file line number Diff line number Diff line change
@@ -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 ViewCreate(
Note note,
User user,
String deviceId
) {
public static ViewCreate of(Note note, User user, String deviceId) {
return new ViewCreate(note, user, deviceId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.projectlyrics.server.domain.view.dto.response;

public record ViewCreateResponse (
boolean success
){
}
Original file line number Diff line number Diff line change
@@ -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);}
}
Original file line number Diff line number Diff line change
@@ -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<View, Long> {
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.projectlyrics.server.domain.view.service;

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.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(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(Long noteId, String deviceId) {
Note note = noteQueryRepository.findById(noteId);
return viewCommandRepository.save(View.create(ViewCreate.of(note, null, deviceId)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("소셜 인증 토큰"),
Expand Down Expand Up @@ -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("소셜 인증 토큰"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand All @@ -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")
)
Expand Down Expand Up @@ -129,7 +130,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))
Expand All @@ -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:일주일간 보지 않기)"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
Expand All @@ -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")
Expand Down
Loading
Loading