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
@@ -0,0 +1,45 @@
package com.back.domain.board.post.controller;

import com.back.domain.board.post.dto.PostBookmarkResponse;
import com.back.domain.board.post.service.PostBookmarkService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/posts/{postId}/bookmark")
@RequiredArgsConstructor
public class PostBookmarkController implements PostBookmarkControllerDocs {
private final PostBookmarkService postBookmarkService;

// 게시글 북마크
@PostMapping
public ResponseEntity<RsData<PostBookmarkResponse>> bookmarkPost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
) {
PostBookmarkResponse response = postBookmarkService.bookmarkPost(postId, user.getUserId());
return ResponseEntity
.ok(RsData.success(
"게시글 북마크가 등록되었습니다.",
response
));
}

// 게시글 북마크 취소
@DeleteMapping
public ResponseEntity<RsData<PostBookmarkResponse>> cancelBookmarkPost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
) {
PostBookmarkResponse response = postBookmarkService.cancelBookmarkPost(postId, user.getUserId());
return ResponseEntity
.ok(RsData.success(
"게시글 북마크가 취소되었습니다.",
response
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package com.back.domain.board.post.controller;

import com.back.domain.board.post.dto.PostBookmarkResponse;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PathVariable;

@Tag(name = "Post Bookmark API", description = "게시글 북마크 등록/취소 API")
public interface PostBookmarkControllerDocs {

@Operation(
summary = "게시글 북마크 등록",
description = "로그인한 사용자가 특정 게시글을 북마크(즐겨찾기)로 등록합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "게시글 북마크 등록 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "게시글 북마크가 등록되었습니다.",
"data": {
"postId": 101,
"bookmarkCount": 5
}
}
"""))
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청(파라미터 누락 등)",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_400",
"message": "잘못된 요청입니다.",
"data": null
}
"""))
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (Access Token 없음/만료/잘못됨)",
content = @Content(mediaType = "application/json",
examples = {
@ExampleObject(name = "토큰 없음", value = """
{
"success": false,
"code": "AUTH_001",
"message": "인증이 필요합니다.",
"data": null
}
"""),
@ExampleObject(name = "잘못된 토큰", value = """
{
"success": false,
"code": "AUTH_002",
"message": "유효하지 않은 액세스 토큰입니다.",
"data": null
}
"""),
@ExampleObject(name = "만료된 토큰", value = """
{
"success": false,
"code": "AUTH_004",
"message": "만료된 액세스 토큰입니다.",
"data": null
}
""")
})
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 사용자 또는 게시글",
content = @Content(mediaType = "application/json",
examples = {
@ExampleObject(name = "존재하지 않는 사용자", value = """
{
"success": false,
"code": "USER_001",
"message": "존재하지 않는 사용자입니다.",
"data": null
}
"""),
@ExampleObject(name = "존재하지 않는 게시글", value = """
{
"success": false,
"code": "POST_001",
"message": "존재하지 않는 게시글입니다.",
"data": null
}
""")
})
),
@ApiResponse(
responseCode = "409",
description = "이미 북마크된 게시글",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "POST_007",
"message": "이미 북마크한 게시글입니다.",
"data": null
}
"""))
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_500",
"message": "서버 오류가 발생했습니다.",
"data": null
}
"""))
)
})
ResponseEntity<RsData<PostBookmarkResponse>> bookmarkPost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
);

@Operation(
summary = "게시글 북마크 취소",
description = "로그인한 사용자가 특정 게시글의 북마크(즐겨찾기)를 취소합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "게시글 북마크 취소 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "게시글 북마크가 취소되었습니다.",
"data": {
"postId": 101,
"bookmarkCount": 4
}
}
"""))
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청(파라미터 누락 등)",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_400",
"message": "잘못된 요청입니다.",
"data": null
}
"""))
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (Access Token 없음/만료/잘못됨)",
content = @Content(mediaType = "application/json",
examples = {
@ExampleObject(name = "토큰 없음", value = """
{
"success": false,
"code": "AUTH_001",
"message": "인증이 필요합니다.",
"data": null
}
"""),
@ExampleObject(name = "잘못된 토큰", value = """
{
"success": false,
"code": "AUTH_002",
"message": "유효하지 않은 액세스 토큰입니다.",
"data": null
}
"""),
@ExampleObject(name = "만료된 토큰", value = """
{
"success": false,
"code": "AUTH_004",
"message": "만료된 액세스 토큰입니다.",
"data": null
}
""")
})
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 사용자 / 게시글 / 북마크 내역 없음",
content = @Content(mediaType = "application/json",
examples = {
@ExampleObject(name = "존재하지 않는 사용자", value = """
{
"success": false,
"code": "USER_001",
"message": "존재하지 않는 사용자입니다.",
"data": null
}
"""),
@ExampleObject(name = "존재하지 않는 게시글", value = """
{
"success": false,
"code": "POST_001",
"message": "존재하지 않는 게시글입니다.",
"data": null
}
"""),
@ExampleObject(name = "북마크 내역 없음", value = """
{
"success": false,
"code": "POST_008",
"message": "해당 게시글에 대한 북마크 기록이 없습니다.",
"data": null
}
""")
})
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_500",
"message": "서버 오류가 발생했습니다.",
"data": null
}
"""))
)
})
ResponseEntity<RsData<PostBookmarkResponse>> cancelBookmarkPost(
@PathVariable Long postId,
@AuthenticationPrincipal CustomUserDetails user
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.board.post.dto;

import com.back.domain.board.post.entity.Post;

/**
* 게시글 북마크 응답 DTO
*
* @param postId 게시글 id
* @param bookmarkCount 북마크 수
*/
public record PostBookmarkResponse(
Long postId,
Long bookmarkCount
) {
public static PostBookmarkResponse from(Post post) {
return new PostBookmarkResponse(
post.getId(),
post.getBookmarkCount()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

import com.back.domain.user.entity.User;
import com.back.global.entity.BaseEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Table(
uniqueConstraints = {
@UniqueConstraint(columnNames = {"post_id", "user_id"})
}
)
public class PostBookmark extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.domain.board.post.repository;

import com.back.domain.board.post.entity.PostBookmark;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface PostBookmarkRepository extends JpaRepository<PostBookmark, Long> {
boolean existsByUserIdAndPostId(Long userId, Long postId);
Optional<PostBookmark> findByUserIdAndPostId(Long userId, Long postId);
}
Loading