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,73 @@
package kr.co.bookpool.app.bookmark.controller;

import static org.springframework.http.HttpStatus.*;

import java.util.List;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import kr.co.bookpool.app.bookmark.controller.docs.BookmarkControllerDocs;
import kr.co.bookpool.app.bookmark.dto.request.BookmarkRequest;
import kr.co.bookpool.app.bookmark.service.BookmarkService;
import kr.co.bookpool.app.campaign.dto.response.CampaignResponse;
import kr.co.bookpool.common.response.PageResponse;
import kr.co.bookpool.common.response.ApiResult;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/me/bookmarks")
public class BookmarkController implements BookmarkControllerDocs {

private final BookmarkService bookmarkService;

@Override
@ResponseStatus(OK)
@GetMapping("/ids")
public ApiResult<List<Long>> getBookmarkedCampaignIds(@AuthenticationPrincipal Long memberId) {
return ApiResult.success(bookmarkService.getBookmarkedCampaignIds(memberId));
}

@Override
@ResponseStatus(OK)
@GetMapping
public ApiResult<PageResponse<CampaignResponse>> getBookmarkedCampaigns(
@AuthenticationPrincipal Long memberId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return ApiResult.success(bookmarkService.getBookmarkedCampaigns(memberId, page, size));
}

@Override
@ResponseStatus(CREATED)
@PostMapping
public ApiResult<Void> add(
@AuthenticationPrincipal Long memberId,
@Valid @RequestBody BookmarkRequest request
) {
bookmarkService.add(memberId, request.campaignId());
return ApiResult.<Void>success(null);
}

@Override
@ResponseStatus(OK)
@DeleteMapping("/{campaignId}")
public ApiResult<Void> remove(
@AuthenticationPrincipal Long memberId,
@PathVariable Long campaignId
) {
bookmarkService.remove(memberId, campaignId);
return ApiResult.<Void>success(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package kr.co.bookpool.app.bookmark.controller.docs;

import java.util.List;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.co.bookpool.app.bookmark.dto.request.BookmarkRequest;
import kr.co.bookpool.app.campaign.dto.response.CampaignResponse;
import kr.co.bookpool.common.response.PageResponse;
import kr.co.bookpool.common.response.ApiResult;

@Tag(name = "Bookmark", description = "즐겨찾기 API")
public interface BookmarkControllerDocs {

@Operation(
summary = "즐겨찾기 캠페인 ID 목록 조회",
description = "로그인한 회원이 즐겨찾기한 캠페인 ID 목록을 조회합니다. 목록 화면에서 즐겨찾기 여부 비교용입니다.",
security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponse(
responseCode = "200", description = "조회 성공",
content = @Content(examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS",
"message": "요청에 성공했습니다.",
"data": [3, 7, 12]
}""")))
ApiResult<List<Long>> getBookmarkedCampaignIds(Long memberId);

@Operation(
summary = "즐겨찾기 캠페인 목록 조회",
description = "로그인한 회원이 즐겨찾기한 캠페인을 최신순으로 페이징 조회합니다. (마이페이지용)",
security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponse(responseCode = "200", description = "조회 성공")
ApiResult<PageResponse<CampaignResponse>> getBookmarkedCampaigns(
Long memberId,
@Parameter(description = "페이지 번호 (0부터 시작)") int page,
@Parameter(description = "페이지 크기") int size
);

@Operation(
summary = "즐겨찾기 추가",
description = "캠페인을 즐겨찾기에 추가합니다.",
security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponses({
@ApiResponse(responseCode = "201", description = "추가 성공"),
@ApiResponse(
responseCode = "409", description = "이미 즐겨찾기한 캠페인",
content = @Content(examples = @ExampleObject(value = """
{
"success": false,
"code": "B001",
"message": "이미 즐겨찾기한 공고입니다."
}"""))),
@ApiResponse(
responseCode = "404", description = "캠페인을 찾을 수 없음",
content = @Content(examples = @ExampleObject(value = """
{
"success": false,
"code": "CP001",
"message": "캠페인을 찾을 수 없습니다."
}""")))
})
ApiResult<Void> add(Long memberId, BookmarkRequest request);

@Operation(
summary = "즐겨찾기 삭제",
description = "캠페인을 즐겨찾기에서 삭제합니다.",
security = @SecurityRequirement(name = "bearerAuth"))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "삭제 성공"),
@ApiResponse(
responseCode = "404", description = "즐겨찾기를 찾을 수 없음",
content = @Content(examples = @ExampleObject(value = """
{
"success": false,
"code": "B002",
"message": "즐겨찾기를 찾을 수 없습니다."
}""")))
})
ApiResult<Void> remove(Long memberId, Long campaignId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package kr.co.bookpool.app.bookmark.dto.request;

import jakarta.validation.constraints.NotNull;

public record BookmarkRequest(@NotNull Long campaignId) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,13 @@ public class Bookmark extends BaseTimeEntity {
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "campaign_id", nullable = false)
private Campaign campaign;

private Bookmark(Member member, Campaign campaign) {
this.member = member;
this.campaign = campaign;
}

public static Bookmark create(Member member, Campaign campaign) {
return new Bookmark(member, campaign);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package kr.co.bookpool.app.bookmark.repository;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import kr.co.bookpool.app.bookmark.entity.Bookmark;

public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {

@Query("select b.campaign.id from Bookmark b where b.member.id = :memberId")
List<Long> findCampaignIdsByMemberId(@Param("memberId") Long memberId);

@Query(
value = "select b from Bookmark b join fetch b.campaign where b.member.id = :memberId",
countQuery = "select count(b) from Bookmark b where b.member.id = :memberId"
)
Page<Bookmark> findAllByMemberIdWithCampaign(@Param("memberId") Long memberId, Pageable pageable);

long deleteByMemberIdAndCampaignId(Long memberId, Long campaignId);

boolean existsByMemberIdAndCampaignId(Long memberId, Long campaignId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package kr.co.bookpool.app.bookmark.service;

import static kr.co.bookpool.common.exception.ErrorCode.*;

import java.util.List;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import kr.co.bookpool.app.bookmark.entity.Bookmark;
import kr.co.bookpool.app.bookmark.repository.BookmarkRepository;
import kr.co.bookpool.app.campaign.dto.response.CampaignResponse;
import kr.co.bookpool.common.response.PageResponse;
import kr.co.bookpool.app.campaign.entity.Campaign;
import kr.co.bookpool.app.campaign.repository.CampaignRepository;
import kr.co.bookpool.app.member.entity.Member;
import kr.co.bookpool.app.member.repository.MemberRepository;
import kr.co.bookpool.common.exception.BusinessException;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookmarkService {

private final BookmarkRepository bookmarkRepository;
private final CampaignRepository campaignRepository;
private final MemberRepository memberRepository;

public List<Long> getBookmarkedCampaignIds(Long memberId) {
return bookmarkRepository.findCampaignIdsByMemberId(memberId);
}

public PageResponse<CampaignResponse> getBookmarkedCampaigns(Long memberId, int page, int size) {
// createdAt 동률 시 순서가 비결정적이 되지 않도록 id를 2차 정렬 키로 둔다.
Sort sort = Sort.by(Sort.Direction.DESC, "createdAt").and(Sort.by(Sort.Direction.DESC, "id"));
Pageable pageable = PageRequest.of(page, size, sort);
return PageResponse.from(
bookmarkRepository.findAllByMemberIdWithCampaign(memberId, pageable)
.map(bookmark -> CampaignResponse.from(bookmark.getCampaign()))
);
}

@Transactional
public void add(Long memberId, Long campaignId) {
if (bookmarkRepository.existsByMemberIdAndCampaignId(memberId, campaignId)) {
throw new BusinessException(BOOKMARK_ALREADY_EXISTS);
}
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND));
Campaign campaign = campaignRepository.findById(campaignId)
.orElseThrow(() -> new BusinessException(CAMPAIGN_NOT_FOUND));
bookmarkRepository.save(Bookmark.create(member, campaign));
}

@Transactional
public void remove(Long memberId, Long campaignId) {
long deleted = bookmarkRepository.deleteByMemberIdAndCampaignId(memberId, campaignId);
if (deleted == 0) {
throw new BusinessException(BOOKMARK_NOT_FOUND);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package kr.co.bookpool.app.campaign.controller;

import static org.springframework.http.HttpStatus.*;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import kr.co.bookpool.app.campaign.controller.docs.AdminCampaignControllerDocs;
import kr.co.bookpool.app.campaign.dto.request.CampaignCreateRequest;
import kr.co.bookpool.app.campaign.dto.request.CampaignUpdateRequest;
import kr.co.bookpool.app.campaign.dto.response.CampaignResponse;
import kr.co.bookpool.common.response.PageResponse;
import kr.co.bookpool.app.campaign.service.AdminCampaignService;
import kr.co.bookpool.common.response.ApiResult;
import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/campaigns")
public class AdminCampaignController implements AdminCampaignControllerDocs {

private final AdminCampaignService adminCampaignService;

@Override
@ResponseStatus(OK)
@GetMapping
public ApiResult<PageResponse<CampaignResponse>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return ApiResult.success(adminCampaignService.list(page, size));
}

@Override
@ResponseStatus(OK)
@GetMapping("/{id}")
public ApiResult<CampaignResponse> detail(@PathVariable Long id) {
return ApiResult.success(adminCampaignService.get(id));
}

@Override
@ResponseStatus(CREATED)
@PostMapping
public ApiResult<CampaignResponse> create(@Valid @RequestBody CampaignCreateRequest request) {
return ApiResult.success(adminCampaignService.create(request));
}

@Override
@ResponseStatus(OK)
@PutMapping("/{id}")
public ApiResult<CampaignResponse> update(
@PathVariable Long id,
@Valid @RequestBody CampaignUpdateRequest request
) {
return ApiResult.success(adminCampaignService.update(id, request));
}

@Override
@ResponseStatus(OK)
@DeleteMapping("/{id}")
public ApiResult<Void> delete(@PathVariable Long id) {
adminCampaignService.delete(id);
return ApiResult.<Void>success(null);
}
}
Loading
Loading