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 @@ -23,6 +23,9 @@ public enum ErrorCode {
NOT_VALID_COUPLE_CODE(HttpStatus.BAD_REQUEST, 40011, "유효하지 않은 커플 코드입니다."),
NO_SUCH_COUPLE_QUESTION(HttpStatus.BAD_REQUEST, 40012, "커플 질문이 존재하지 않습니다."),
NO_SUCH_TEMP_LOVE_TYPE(HttpStatus.BAD_REQUEST, 40013, "애착 유형 결과가 존재하지 않습니다."),
NO_SUCH_BOOKMARK(HttpStatus.BAD_REQUEST, 40014, "북마크가 존재하지 않습니다."),
BOOKMARK_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 40015, "이미 북마크된 메시지입니다."),
NO_SUCH_MESSAGE(HttpStatus.BAD_REQUEST, 40016, "메시지가 존재하지 않습니다."),

// 401 Unauthorized
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 40100, "인증되지 않은 사용자입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ public ResponseEntity<ErrorResponse> handleTempLoveTypeNotFoundException(TempLov
return ErrorResponse.of(ErrorCode.NO_SUCH_TEMP_LOVE_TYPE);
}

@ExceptionHandler({BookmarkNotFoundException.class})
public ResponseEntity<ErrorResponse> handleBookmarkNotFoundException(BookmarkNotFoundException e) {
log.warn("[GlobalExceptionHandler: handleBookmarkNotFoundException 호출] {}", e.getMessage());
return ErrorResponse.of(ErrorCode.NO_SUCH_BOOKMARK);
}

@ExceptionHandler({BookmarkAlreadyExistsException.class})
public ResponseEntity<ErrorResponse> handleBookmarkAlreadyExistsException(BookmarkAlreadyExistsException e) {
log.info("[GlobalExceptionHandler: handleBookmarkAlreadyExistsException 호출] {}", e.getMessage());
return ErrorResponse.of(ErrorCode.BOOKMARK_ALREADY_EXISTS);
}

@ExceptionHandler({MessageNotFoundException.class})
public ResponseEntity<ErrorResponse> handleMessageNotFoundException(MessageNotFoundException e) {
log.warn("[GlobalExceptionHandler: handleMessageNotFoundException 호출] {}", e.getMessage());
return ErrorResponse.of(ErrorCode.NO_SUCH_MESSAGE);
}



/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package makeus.cmc.malmo.adaptor.in.web.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import makeus.cmc.malmo.adaptor.in.web.docs.ApiCommonResponses;
import makeus.cmc.malmo.adaptor.in.web.dto.BaseListResponse;
import makeus.cmc.malmo.adaptor.in.web.dto.BaseResponse;
import makeus.cmc.malmo.application.port.in.chat.CreateBookmarkUseCase;
import makeus.cmc.malmo.application.port.in.chat.DeleteBookmarksUseCase;
import makeus.cmc.malmo.application.port.in.chat.GetBookmarkListUseCase;
import makeus.cmc.malmo.application.port.in.chat.GetMessagesByBookmarkUseCase;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "북마크 API", description = "채팅 메시지 북마크 관리를 위한 API")
@RestController
@RequestMapping("/chatrooms/{chatRoomId}/bookmarks")
@RequiredArgsConstructor
public class BookmarkController {

private final CreateBookmarkUseCase createBookmarkUseCase;
private final DeleteBookmarksUseCase deleteBookmarksUseCase;
private final GetBookmarkListUseCase getBookmarkListUseCase;
private final GetMessagesByBookmarkUseCase getMessagesByBookmarkUseCase;

@Operation(
summary = "북마크 생성",
description = "채팅 메시지를 북마크합니다. JWT 토큰이 필요합니다.",
security = @SecurityRequirement(name = "Bearer Authentication")
)
@ApiResponse(
responseCode = "200",
description = "북마크 생성 성공"
)
@ApiCommonResponses.RequireAuth
@PostMapping
public BaseResponse<CreateBookmarkUseCase.CreateBookmarkResponse> createBookmark(
@AuthenticationPrincipal User user,
@PathVariable Long chatRoomId,
@RequestBody CreateBookmarkRequestDto requestDto) {

CreateBookmarkUseCase.CreateBookmarkCommand command =
CreateBookmarkUseCase.CreateBookmarkCommand.builder()
.userId(Long.valueOf(user.getUsername()))
.chatRoomId(chatRoomId)
.messageId(requestDto.getMessageId())
.build();

return BaseResponse.success(createBookmarkUseCase.createBookmark(command));
}

@Operation(
summary = "북마크 삭제 (다건)",
description = "북마크를 삭제합니다 (소프트 삭제). JWT 토큰이 필요합니다.",
security = @SecurityRequirement(name = "Bearer Authentication")
)
@ApiResponse(
responseCode = "200",
description = "북마크 삭제 성공"
)
@ApiCommonResponses.RequireAuth
@DeleteMapping
public BaseResponse<Void> deleteBookmarks(
@AuthenticationPrincipal User user,
@PathVariable Long chatRoomId,
@RequestBody DeleteBookmarksRequestDto requestDto) {

DeleteBookmarksUseCase.DeleteBookmarksCommand command =
DeleteBookmarksUseCase.DeleteBookmarksCommand.builder()
.userId(Long.valueOf(user.getUsername()))
.chatRoomId(chatRoomId)
.bookmarkIdList(requestDto.getBookmarkIdList())
.build();

deleteBookmarksUseCase.deleteBookmarks(command);
return BaseResponse.success(null);
}

@Operation(
summary = "북마크 리스트 조회",
description = "채팅방의 북마크 목록을 조회합니다. JWT 토큰이 필요합니다.",
security = @SecurityRequirement(name = "Bearer Authentication")
)
@ApiResponse(
responseCode = "200",
description = "북마크 리스트 조회 성공"
)
@ApiCommonResponses.RequireAuth
@GetMapping
public BaseResponse<BaseListResponse<GetBookmarkListUseCase.BookmarkDto>> getBookmarkList(
@AuthenticationPrincipal User user,
@PathVariable Long chatRoomId,
@PageableDefault(page = 0, size = 10) Pageable pageable) {

GetBookmarkListUseCase.GetBookmarkListCommand command =
GetBookmarkListUseCase.GetBookmarkListCommand.builder()
.userId(Long.valueOf(user.getUsername()))
.chatRoomId(chatRoomId)
.pageable(pageable)
.build();

GetBookmarkListUseCase.GetBookmarkListResponse response =
getBookmarkListUseCase.getBookmarkList(command);

return BaseListResponse.success(response.getBookmarkList(), response.getTotalCount());
}

@Operation(
summary = "북마크 기반 메시지 조회",
description = "북마크된 메시지 위치로 이동하기 위한 메시지 목록을 조회합니다. JWT 토큰이 필요합니다.",
security = @SecurityRequirement(name = "Bearer Authentication")
)
@ApiResponse(
responseCode = "200",
description = "북마크 기반 메시지 조회 성공"
)
@ApiCommonResponses.RequireAuth
@GetMapping("/{bookmarkId}/messages")
public BaseResponse<GetMessagesByBookmarkUseCase.GetMessagesByBookmarkResponse> getMessagesByBookmark(
@AuthenticationPrincipal User user,
@PathVariable Long chatRoomId,
@PathVariable Long bookmarkId,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "ASC") String sort) {

GetMessagesByBookmarkUseCase.GetMessagesByBookmarkCommand command =
GetMessagesByBookmarkUseCase.GetMessagesByBookmarkCommand.builder()
.userId(Long.valueOf(user.getUsername()))
.bookmarkId(bookmarkId)
.size(size)
.sort(sort)
.build();

return BaseResponse.success(getMessagesByBookmarkUseCase.getMessagesByBookmark(command));
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CreateBookmarkRequestDto {
private Long messageId;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class DeleteBookmarksRequestDto {
private List<Long> bookmarkIdList;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package makeus.cmc.malmo.adaptor.out.persistence.adapter;

import lombok.RequiredArgsConstructor;
import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.BookmarkEntity;
import makeus.cmc.malmo.adaptor.out.persistence.mapper.BookmarkMapper;
import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.BookmarkRepository;
import makeus.cmc.malmo.application.port.out.chat.DeleteBookmarkPort;
import makeus.cmc.malmo.application.port.out.chat.LoadBookmarkPort;
import makeus.cmc.malmo.application.port.out.chat.SaveBookmarkPort;
import makeus.cmc.malmo.domain.model.chat.Bookmark;
import makeus.cmc.malmo.domain.value.id.BookmarkId;
import makeus.cmc.malmo.domain.value.id.ChatRoomId;
import makeus.cmc.malmo.domain.value.id.MemberId;
import makeus.cmc.malmo.domain.value.state.BookmarkState;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Optional;

@Component
@RequiredArgsConstructor
public class BookmarkPersistenceAdapter implements LoadBookmarkPort, SaveBookmarkPort, DeleteBookmarkPort {

private final BookmarkRepository bookmarkRepository;
private final BookmarkMapper bookmarkMapper;

@Override
public Optional<Bookmark> loadBookmarkById(BookmarkId bookmarkId) {
return bookmarkRepository.findByIdAndBookmarkState(bookmarkId.getValue(), BookmarkState.ALIVE)
.map(bookmarkMapper::toDomain);
}

@Override
public Optional<Bookmark> loadBookmarkByMemberAndMessage(MemberId memberId, Long chatMessageId) {
return bookmarkRepository.findByMemberEntityIdValueAndChatMessageEntityIdValueAndBookmarkState(
memberId.getValue(), chatMessageId, BookmarkState.ALIVE)
.map(bookmarkMapper::toDomain);
}

@Override
public Page<BookmarkDto> loadBookmarksByMemberAndChatRoom(
MemberId memberId, ChatRoomId chatRoomId, Pageable pageable) {
return bookmarkRepository.loadBookmarksByMemberAndChatRoom(
memberId.getValue(), chatRoomId.getValue(), pageable);
}

@Override
public boolean existsByMemberAndMessage(MemberId memberId, Long chatMessageId) {
return bookmarkRepository.existsByMemberEntityIdValueAndChatMessageEntityIdValueAndBookmarkState(
memberId.getValue(), chatMessageId, BookmarkState.ALIVE);
}

@Override
public boolean isMemberOwnerOfBookmarks(MemberId memberId, List<BookmarkId> bookmarkIds) {
return bookmarkRepository.isMemberOwnerOfBookmarks(
memberId.getValue(),
bookmarkIds.stream().map(BookmarkId::getValue).toList());
}

@Override
public long countMessagesBeforeId(ChatRoomId chatRoomId, Long messageId, String sort) {
return bookmarkRepository.countMessagesBeforeId(chatRoomId.getValue(), messageId, sort);
}

@Override
public Bookmark saveBookmark(Bookmark bookmark) {
BookmarkEntity entity = bookmarkMapper.toEntity(bookmark);
BookmarkEntity saved = bookmarkRepository.save(entity);
return bookmarkMapper.toDomain(saved);
}

@Override
public void softDeleteBookmarks(List<BookmarkId> bookmarkIds) {
bookmarkRepository.softDeleteBookmarks(
bookmarkIds.stream().map(BookmarkId::getValue).toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ public class ChatRoomPersistenceAdapter
private final ChatMessageMapper chatMessageMapper;

@Override
public Page<ChatRoomMessageRepositoryDto> loadMessagesDto(ChatRoomId chatRoomId, Pageable pageable) {
return chatMessageRepository.loadCurrentMessagesDto(chatRoomId.getValue(), pageable);
public Optional<ChatMessage> loadMessageById(Long messageId) {
return chatMessageRepository.findById(messageId)
.map(chatMessageMapper::toDomain);
}

@Override
public Page<ChatRoomMessageRepositoryDto> loadMessagesDtoAsc(ChatRoomId chatRoomId, Pageable pageable) {
return chatMessageRepository.loadCurrentMessagesDtoAsc(chatRoomId.getValue(), pageable);
public Page<ChatRoomMessageRepositoryDto> loadMessagesDto(ChatRoomId chatRoomId, MemberId memberId, Pageable pageable) {
return chatMessageRepository.loadCurrentMessagesDto(chatRoomId.getValue(), memberId.getValue(), pageable);
}

@Override
public Page<ChatRoomMessageRepositoryDto> loadMessagesDtoAsc(ChatRoomId chatRoomId, MemberId memberId, Pageable pageable) {
return chatMessageRepository.loadCurrentMessagesDtoAsc(chatRoomId.getValue(), memberId.getValue(), pageable);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package makeus.cmc.malmo.adaptor.out.persistence.entity.chat;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import makeus.cmc.malmo.adaptor.out.persistence.entity.BaseTimeEntity;
import makeus.cmc.malmo.adaptor.out.persistence.entity.value.ChatMessageEntityId;
import makeus.cmc.malmo.adaptor.out.persistence.entity.value.ChatRoomEntityId;
import makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId;
import makeus.cmc.malmo.domain.value.state.BookmarkState;

@Getter
@SuperBuilder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "bookmark_entity",
indexes = {
@Index(name = "idx_bookmark_member_chatroom", columnList = "member_id, chat_room_id"),
@Index(name = "idx_bookmark_member_message", columnList = "member_id, chat_message_id")
})
public class BookmarkEntity extends BaseTimeEntity {

@Column(name = "bookmarkId")
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Embedded
private ChatRoomEntityId chatRoomEntityId;

@Embedded
private ChatMessageEntityId chatMessageEntityId;

@Embedded
private MemberEntityId memberEntityId;

@Enumerated(EnumType.STRING)
private BookmarkState bookmarkState;

public void softDelete() {
this.bookmarkState = BookmarkState.DELETED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package makeus.cmc.malmo.adaptor.out.persistence.mapper;

import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.BookmarkEntity;
import makeus.cmc.malmo.adaptor.out.persistence.entity.value.ChatMessageEntityId;
import makeus.cmc.malmo.adaptor.out.persistence.entity.value.ChatRoomEntityId;
import makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId;
import makeus.cmc.malmo.domain.model.chat.Bookmark;
import makeus.cmc.malmo.domain.value.id.ChatRoomId;
import makeus.cmc.malmo.domain.value.id.MemberId;
import org.springframework.stereotype.Component;

@Component
public class BookmarkMapper {

public Bookmark toDomain(BookmarkEntity entity) {
return Bookmark.from(
entity.getId(),
entity.getChatRoomEntityId() != null
? ChatRoomId.of(entity.getChatRoomEntityId().getValue()) : null,
entity.getChatMessageEntityId() != null
? entity.getChatMessageEntityId().getValue() : null,
entity.getMemberEntityId() != null
? MemberId.of(entity.getMemberEntityId().getValue()) : null,
entity.getBookmarkState(),
entity.getCreatedAt(),
entity.getModifiedAt(),
entity.getDeletedAt()
);
}

public BookmarkEntity toEntity(Bookmark domain) {
return BookmarkEntity.builder()
.id(domain.getId())
.chatRoomEntityId(domain.getChatRoomId() != null
? ChatRoomEntityId.of(domain.getChatRoomId().getValue()) : null)
.chatMessageEntityId(domain.getChatMessageId() != null
? ChatMessageEntityId.of(domain.getChatMessageId()) : null)
.memberEntityId(domain.getMemberId() != null
? MemberEntityId.of(domain.getMemberId().getValue()) : null)
.bookmarkState(domain.getBookmarkState())
.createdAt(domain.getCreatedAt())
.modifiedAt(domain.getModifiedAt())
.deletedAt(domain.getDeletedAt())
.build();
}
}
Loading