diff --git a/src/main/java/com/gamegoo/gamegoo_v2/content/board/controller/BoardController.java b/src/main/java/com/gamegoo/gamegoo_v2/content/board/controller/BoardController.java index 7edab7fe..60bce88d 100644 --- a/src/main/java/com/gamegoo/gamegoo_v2/content/board/controller/BoardController.java +++ b/src/main/java/com/gamegoo/gamegoo_v2/content/board/controller/BoardController.java @@ -7,6 +7,7 @@ import com.gamegoo.gamegoo_v2.account.member.domain.Tier; import com.gamegoo.gamegoo_v2.content.board.dto.request.BoardInsertRequest; import com.gamegoo.gamegoo_v2.content.board.dto.request.BoardUpdateRequest; +import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardBumpResponse; import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardByIdResponse; import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardByIdResponseForMember; import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardInsertResponse; @@ -124,4 +125,16 @@ public ApiResponse getMyBoardList(@ValidPage @RequestParam(name return ApiResponse.ok(boardFacadeService.getMyBoardList(member, page)); } + /** + * 게시글 끌올(bump) API + * 사용자가 "끌올" 버튼을 누르면 해당 게시글의 bumpTime이 업데이트되어 상단으로 노출됩니다. + */ + @PostMapping("/{boardId}/bump") + @Operation(summary = "게시글 끌올 API", description = "게시글을 끌올하여 상단 노출시키는 API 입니다. 마지막 끌올 후 1시간 제한이 적용됩니다.") + @Parameter(name = "boardId", description = "끌올할 게시판 글 id 입니다.") + public ApiResponse bumpBoard(@PathVariable Long boardId, + @AuthMember Member member) { + return ApiResponse.ok(boardFacadeService.bumpBoard(boardId, member)); + } + } diff --git a/src/main/java/com/gamegoo/gamegoo_v2/content/board/domain/Board.java b/src/main/java/com/gamegoo/gamegoo_v2/content/board/domain/Board.java index 79ac8330..8dc0af4a 100644 --- a/src/main/java/com/gamegoo/gamegoo_v2/content/board/domain/Board.java +++ b/src/main/java/com/gamegoo/gamegoo_v2/content/board/domain/Board.java @@ -21,7 +21,9 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Formula; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -71,6 +73,11 @@ public class Board extends BaseDateTimeEntity { @OneToMany(mappedBy = "board", cascade = CascadeType.PERSIST, orphanRemoval = true) private List boardGameStyles = new ArrayList<>(); + private LocalDateTime bumpTime; + + @Formula("GREATEST(COALESCE(bump_time, created_at), created_at)") + private LocalDateTime activityTime; + public static Board create(Member member, GameMode gameMode, Position mainP, Position subP, Position wantP, @@ -143,5 +150,13 @@ public void setDeleted(boolean deleted) { this.deleted = deleted; } + public void bump(LocalDateTime bumpTime) { + this.bumpTime = bumpTime; + } + + public LocalDateTime getActivityTime() { + return activityTime; + } + } diff --git a/src/main/java/com/gamegoo/gamegoo_v2/content/board/dto/response/BoardBumpResponse.java b/src/main/java/com/gamegoo/gamegoo_v2/content/board/dto/response/BoardBumpResponse.java new file mode 100644 index 00000000..79b7694c --- /dev/null +++ b/src/main/java/com/gamegoo/gamegoo_v2/content/board/dto/response/BoardBumpResponse.java @@ -0,0 +1,20 @@ +package com.gamegoo.gamegoo_v2.content.board.dto.response; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class BoardBumpResponse { + + private Long boardId; + private LocalDateTime bumpTime; + + public static BoardBumpResponse of(Long boardId, LocalDateTime bumpTime) { + BoardBumpResponse response = new BoardBumpResponse(); + response.boardId = boardId; + response.bumpTime = bumpTime; + return response; + } + +} diff --git a/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardFacadeService.java b/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardFacadeService.java index e335a16b..f97af0b7 100644 --- a/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardFacadeService.java +++ b/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardFacadeService.java @@ -8,6 +8,7 @@ import com.gamegoo.gamegoo_v2.content.board.domain.Board; import com.gamegoo.gamegoo_v2.content.board.dto.request.BoardInsertRequest; import com.gamegoo.gamegoo_v2.content.board.dto.request.BoardUpdateRequest; +import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardBumpResponse; import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardByIdResponse; import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardByIdResponseForMember; import com.gamegoo.gamegoo_v2.content.board.dto.response.BoardInsertResponse; @@ -131,5 +132,16 @@ public MyBoardResponse getMyBoardList(Member member, int pageIdx) { return MyBoardResponse.of(boardPage); } + /** + * 게시글 끌올(bump) 기능 (파사드) + * 사용자가 "끌올" 버튼을 누르면 해당 게시글의 bumpTime을 업데이트합니다. + * 단, 마지막 끌올 후 1시간이 지나지 않았다면 예외를 발생시킵니다. + */ + + @Transactional + public BoardBumpResponse bumpBoard(Long boardId, Member member) { + Board board = boardService.bumpBoard(boardId, member.getId()); + return BoardBumpResponse.of(board.getId(), board.getBumpTime()); + } } diff --git a/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardService.java b/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardService.java index 49ef0e81..edde1771 100644 --- a/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardService.java +++ b/src/main/java/com/gamegoo/gamegoo_v2/content/board/service/BoardService.java @@ -19,6 +19,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Duration; +import java.time.LocalDateTime; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -27,6 +30,7 @@ public class BoardService { private final BoardRepository boardRepository; public static final int PAGE_SIZE = 20; public static final int MY_PAGE_SIZE = 10; + private static final Duration BUMP_INTERVAL = Duration.ofMinutes(1); /** * 게시글 엔티티 생성 및 저장 @@ -63,7 +67,7 @@ public Page findBoards(GameMode gameMode, Tier tier, Position mainP, Mike public Page getBoardsWithPagination(GameMode gameMode, Tier tier, Position mainP, Mike mike, int pageIdx) { - Pageable pageable = PageRequest.of(pageIdx - 1, PAGE_SIZE, Sort.by(Sort.Direction.DESC, "createdAt")); + Pageable pageable = PageRequest.of(pageIdx - 1, PAGE_SIZE, Sort.by(Sort.Direction.DESC, "activityTime")); return findBoards(gameMode, tier, mainP, mike, pageable); } @@ -136,4 +140,28 @@ public Board saveBoard(Board board) { return boardRepository.save(board); } + /** + * 끌올 기능: 사용자가 게시글을 끌올하면 bumpTime을 현재 시간으로 업데이트 + */ + @Transactional + public Board bumpBoard(Long boardId, Long memberId) { + Board board = boardRepository.findByIdAndDeleted(boardId, false) + .orElseThrow(() -> new BoardException(ErrorCode.BOARD_NOT_FOUND)); + + if (!board.getMember().getId().equals(memberId)) { + throw new BoardException(ErrorCode.BUMP_ACCESS_DENIED); + } + + LocalDateTime now = LocalDateTime.now(); + if (board.getBumpTime() != null) { + Duration diff = Duration.between(board.getBumpTime(), now); + if (diff.compareTo(BUMP_INTERVAL) < 0) { + throw new BoardException(ErrorCode.BUMP_TIME_LIMIT); + } + } + + board.bump(now); + return boardRepository.save(board); + } + } diff --git a/src/main/java/com/gamegoo/gamegoo_v2/core/exception/common/ErrorCode.java b/src/main/java/com/gamegoo/gamegoo_v2/core/exception/common/ErrorCode.java index 51a46827..039a625d 100644 --- a/src/main/java/com/gamegoo/gamegoo_v2/core/exception/common/ErrorCode.java +++ b/src/main/java/com/gamegoo/gamegoo_v2/core/exception/common/ErrorCode.java @@ -130,7 +130,8 @@ public enum ErrorCode { BOARD_PAGE_BAD_REQUEST(BAD_REQUEST, "BOARD_407", "페이지 값은 0 이상만 가능합니다."), BOARD_FORBIDDEN_WORD(BAD_REQUEST, "BOARD_408", "금지어가 포함되어 있습니다."), BOARD_FORBIDDEN_WORD_LOAD_FAILED(INTERNAL_SERVER_ERROR, "BOARD_409", "금지어 파일을 읽어오는데 실패했습니다."), - + BUMP_ACCESS_DENIED(FORBIDDEN, "BOARD_410", "게시글 끌어올리기 권한이 없습니다."), + BUMP_TIME_LIMIT(BAD_REQUEST, "BOARD_411", "게시글 끌어올리기는 24시간에 1회만 가능합니다."), /** * 매너평가 관련 에러 */