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
24 changes: 24 additions & 0 deletions src/docs/asciidoc/user-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,27 @@ include::{snippetsDir}/userExit/1/http-response.adoc[]

==== Response Body Fields
include::{snippetsDir}/userExit/1/response-fields.adoc[]

=== **10. 북마크 생성 api**

게시글 북마크를 생성

==== Request
include::{snippetsDir}/createBookmark/1/http-request.adoc[]

==== Request Body Fields
include::{snippetsDir}/createBookmark/1/request-fields.adoc[]

==== 성공 Response
성공 1. 북마크가 새롭게 생성된 경우 : CREATED
include::{snippetsDir}/createBookmark/1/http-response.adoc[]

성공 2. 이미 존재하는 북마크에 대해 생성 요청을 보낸 경우 : OK
include::{snippetsDir}/createBookmark/2/http-response.adoc[]

==== Response Body Fields
include::{snippetsDir}/createBookmark/1/response-fields.adoc[]

==== 실패 Response
실패1.
include::{snippetsDir}/createBookmark/3/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.ftm.server.adapter.in.web.user.controller;

import com.ftm.server.adapter.in.web.user.dto.request.CreateBookmarkRequest;
import com.ftm.server.application.command.user.CreateBookmarkCommand;
import com.ftm.server.application.port.in.user.CreateBookmarkUseCase;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.infrastructure.security.UserPrincipal;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class CreateBookmarkController {

private final CreateBookmarkUseCase createBookmarkUseCase;

@PostMapping("/api/users/bookmarks")
public ResponseEntity<ApiResponse> createBookMark(
@AuthenticationPrincipal UserPrincipal user,
@RequestBody CreateBookmarkRequest request) {
Boolean isCreated =
createBookmarkUseCase.execute(
CreateBookmarkCommand.of(user.getId(), request.getPostId()));
HttpStatus httpStatus = isCreated ? HttpStatus.CREATED : HttpStatus.OK;
SuccessResponseCode successResponseCode =
isCreated ? SuccessResponseCode.CREATED : SuccessResponseCode.OK;
return ResponseEntity.status(httpStatus).body(ApiResponse.success(successResponseCode));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ftm.server.adapter.in.web.user.dto.request;

import lombok.Data;

@Data
public class CreateBookmarkRequest {
private final Long postId;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.ftm.server.adapter.out.persistence.adapter.user;

import com.ftm.server.adapter.out.persistence.mapper.EmailVerificationLogsMapper;
import com.ftm.server.adapter.out.persistence.mapper.PostMapper;
import com.ftm.server.adapter.out.persistence.mapper.UserImageMapper;
import com.ftm.server.adapter.out.persistence.mapper.UserMapper;
import com.ftm.server.adapter.out.persistence.mapper.*;
import com.ftm.server.adapter.out.persistence.model.*;
import com.ftm.server.adapter.out.persistence.repository.*;
import com.ftm.server.application.command.user.*;
Expand All @@ -18,6 +15,7 @@
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;

@Adapter
@RequiredArgsConstructor
Expand All @@ -38,7 +36,10 @@ public class UserDomainPersistenceAdapter
DeleteUserImagePort,
DeleteGroomingTestResultPort,
DeleteUserPort,
DeleteBookmarkPort {
DeleteBookmarkPort,
SaveBookmarkPort,
CheckBookmarkPort,
CheckPostPort {

// repository
private final EmailVerificationLogsRepository emailVerificationLogsRepository;
Expand All @@ -54,6 +55,7 @@ public class UserDomainPersistenceAdapter
private final UserMapper userMapper;
private final UserImageMapper userImageMapper;
private final PostMapper postMapper;
private final BookmarkMapper bookmarkMapper;

@Override
public Optional<EmailVerificationLogs> loadEmailVerificationLogByEmail(FindByEmailQuery query) {
Expand Down Expand Up @@ -110,6 +112,11 @@ public Boolean checksUserBySocialValue(FindBySocialValueQuery query) {
query.getSocialId(), query.getSocialProvider());
}

@Override
public Boolean checksUserById(FindByUserIdQuery query) {
return userRepository.existsById(query.getUserId());
}

@Override
public User saveSocialUser(User user) {
UserJpaEntity userJpaEntity = userMapper.toJpaEntity(user, null);
Expand Down Expand Up @@ -223,4 +230,30 @@ public void deleteAllUserByIdList(DeleteAllUserByIdListCommand command) {
public void deleteBookmarkByUserList(DeleteBookmarkByUserIdCommand command) {
bookmarkRepository.deleteAllByUserIdList(command.getUserIdList());
}

@Override
public Boolean saveBookmark(Bookmark bookmark) {
// 이미 생성된 북마크인 경우 -> false
// 새롭게 생성된 북마크인 경우 -> true
int isCreated;
try {
isCreated = bookmarkRepository.saveOrUpdate(bookmark.getUserId(), bookmark.getPostId());
} catch (
DataIntegrityViolationException
e) { // on conflict로 문제 예방 했지만, 동시성 문제로 unique key 위반 에러 나는 경우, 에러 반환하지 않고
// 그냥 처리(북마크 생성하지 않고 OK 반환)
return false;
}
return isCreated == 1;
}

@Override
public Boolean checkIfBookmarkExists(FindBookmarkByUserIdAndPostIdQuery query) {
return bookmarkRepository.existsByUserIdAndPostId(query.getUserId(), query.getPostId());
}

@Override
public Boolean checksPostById(FindByPostIdQuery query) {
return postRepository.existsById(query.getPostId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,16 @@ public interface BookmarkRepository extends JpaRepository<BookmarkJpaEntity, Lon
@Modifying
@Query("DELETE FROM BookmarkJpaEntity b WHERE b.user.id in (:userIds)")
void deleteAllByUserIdList(@Param("userIds") List<Long> userIds);

@Modifying
@Query(
value =
"INSERT INTO bookmark (user_id, post_id) "
+ "VALUES (:userId, :postId) "
+ "ON CONFLICT (user_id, post_id) "
+ "DO NOTHING;",
nativeQuery = true)
int saveOrUpdate(@Param("userId") Long userId, @Param("postId") Long postId);

Boolean existsByUserIdAndPostId(Long userId, Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface PostRepository extends JpaRepository<PostJpaEntity, Long>, Post
@Modifying
@Query("DELETE FROM PostJpaEntity p WHERE p.id IN (:postIds)")
void deleteAllByIdInBatch(@Param("postIds") List<Long> postIds);

boolean existsById(Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ftm.server.application.command.user;

import lombok.Data;

@Data
public class CreateBookmarkCommand {
private final Long userId;
private final Long postId;

public static CreateBookmarkCommand of(Long userId, Long postId) {
return new CreateBookmarkCommand(userId, postId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ftm.server.application.port.in.user;

import com.ftm.server.application.command.user.CreateBookmarkCommand;
import com.ftm.server.common.annotation.UseCase;

@UseCase
public interface CreateBookmarkUseCase {
Boolean execute(CreateBookmarkCommand command);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ftm.server.application.port.out.persistence.user;

import com.ftm.server.application.query.FindBookmarkByUserIdAndPostIdQuery;
import com.ftm.server.common.annotation.Port;

@Port
public interface CheckBookmarkPort {

Boolean checkIfBookmarkExists(FindBookmarkByUserIdAndPostIdQuery query);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ftm.server.application.port.out.persistence.user;

import com.ftm.server.application.query.FindByPostIdQuery;

public interface CheckPostPort {
Boolean checksPostById(FindByPostIdQuery query);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import com.ftm.server.application.query.FindByEmailQuery;
import com.ftm.server.application.query.FindBySocialValueQuery;
import com.ftm.server.application.query.FindByUserIdQuery;
import com.ftm.server.common.annotation.Port;

@Port
public interface CheckUserPort {
Boolean checksUserByEmail(FindByEmailQuery query);

Boolean checksUserBySocialValue(FindBySocialValueQuery query);

Boolean checksUserById(FindByUserIdQuery query);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ftm.server.application.port.out.persistence.user;

import com.ftm.server.common.annotation.Port;
import com.ftm.server.domain.entity.Bookmark;

@Port
public interface SaveBookmarkPort {
Boolean saveBookmark(Bookmark bookmark);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.ftm.server.application.query;

import lombok.Data;

@Data
public class FindBookmarkByUserIdAndPostIdQuery {
private final Long userId;
private final Long postId;

public static FindBookmarkByUserIdAndPostIdQuery of(Long userId, Long postId) {
return new FindBookmarkByUserIdAndPostIdQuery(userId, postId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.ftm.server.application.service.user;

import com.ftm.server.application.command.user.CreateBookmarkCommand;
import com.ftm.server.application.port.in.user.CreateBookmarkUseCase;
import com.ftm.server.application.port.out.persistence.user.*;
import com.ftm.server.application.query.FindBookmarkByUserIdAndPostIdQuery;
import com.ftm.server.application.query.FindByPostIdQuery;
import com.ftm.server.application.query.FindByUserIdQuery;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import com.ftm.server.domain.entity.Bookmark;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CreateBookmarkService implements CreateBookmarkUseCase {

private final CheckUserPort checkUserPort;
private final CheckPostPort checkPostPort;
private final SaveBookmarkPort saveBookmarkPort;
private final CheckBookmarkPort checkBookmarkPort;

@Override
@Transactional
public Boolean execute(CreateBookmarkCommand command) {
// user id와 post id 유효성 검사-> 없는 경우 exception 반환
Boolean userExists =
checkUserPort.checksUserById(FindByUserIdQuery.of(command.getUserId()));
Boolean postExists =
checkPostPort.checksPostById(FindByPostIdQuery.of(command.getPostId()));
if (!userExists) {
throw CustomException.USER_NOT_FOUND;
}
if (!postExists) {
throw new CustomException(ErrorResponseCode.POST_NOT_FOUND);
}

// 북마크 기존에 존재하는지 확인(중복 생성 방지)
if (checkBookmarkPort.checkIfBookmarkExists(
FindBookmarkByUserIdAndPostIdQuery.of(command.getUserId(), command.getPostId()))) {
return false;
}
// 북마크 생성 후 저장
Bookmark bookmark = Bookmark.createBookmark(command.getUserId(), command.getPostId());

// 북마크 존재하는지 검사 후 - 북마크 저장 과정 사이에 북마크가 생성되어 중복 생성되는 것을 막고자
// sql 단에서 한번 더 검사 진행
// -> 이미 동일한 북마크가 존재할 경우 do nothing & false 반환
// -> 동일한 북마크가 없어서 새로 생성&저장한 경우 true 반환
return saveBookmarkPort.saveBookmark(bookmark);
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/ftm/server/domain/entity/Bookmark.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ public static Bookmark of(
.updatedAt(updatedAt)
.build();
}

public static Bookmark createBookmark(Long userId, Long postId) {
return Bookmark.builder().userId(userId).postId(postId).build();
}
}
28 changes: 28 additions & 0 deletions src/test/java/com/ftm/server/BaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,32 @@ protected MockHttpSession login(String email) {

return session;
}

public record SessionAndUser(MockHttpSession mockHttpSession, User user) {}

// Session과 함께 User도 반환
protected SessionAndUser createUserAndLoginAndReturnUser() {
return createUserAndLoginAndReturnUser("test@gmail.com", "123456qwe!");
}

protected SessionAndUser createUserAndLoginAndReturnUser(String email, String password) {

// 사용자 생성
User user = createTestUser(email, password);

// session 생성
SecurityContext context = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
UserPrincipal.of(user),
null,
List.of(new SimpleGrantedAuthority(UserRole.USER.name())));
context.setAuthentication(auth);

MockHttpSession session = new MockHttpSession();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);

return new SessionAndUser(session, user);
}
}
Loading
Loading