Skip to content

Commit

Permalink
feat: 북마크 추가, 취소 기능 구현 (#184)
Browse files Browse the repository at this point in the history
* chore: 북마크 스키마 추가

* feat: 북마크 추가, 삭제 기능

* test: 북마크 추가, 삭제 테스트 작성

- 북마크 추가 케이스
- 북마크 취소 케이스
- 모임 단일 응답에 북마크 여부 추

* feat: bookmarked -> isBookmarked, V1__ 언더바 누락 수정
  • Loading branch information
ddingmin authored Feb 10, 2024
1 parent be58b5b commit a2a901c
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public MeetingResponse createMeeting(
@GetMapping("/{meetingId}")
@ResponseStatus(HttpStatus.OK)
public MeetingResponse getMeetingById(@PathVariable("meetingId") Long meetingId) {
return meetingService.getMeetingById(meetingId);
Long userId = securityService.getCurrentUserId();
return meetingService.getMeetingById(meetingId, userId);
}

@GetMapping
Expand Down Expand Up @@ -103,6 +104,20 @@ public void reportMeeting(@PathVariable("meetingId") Long meetingId) {
meetingService.reportMeeting(meetingId, userId);
}

@PostMapping("/{meetingId}/bookmarks")
@ResponseStatus(HttpStatus.CREATED)
public void addBookmark(@PathVariable("meetingId") Long meetingId) {
Long userId = securityService.getCurrentUserId();
meetingService.addBookmark(meetingId, userId);
}

@DeleteMapping("/{meetingId}/bookmarks")
@ResponseStatus(HttpStatus.OK)
public void deleteBookmark(@PathVariable("meetingId") Long meetingId) {
Long userId = securityService.getCurrentUserId();
meetingService.cancelBookmark(meetingId, userId);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(IllegalArgumentException illegalArgumentException) {
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/net/teumteum/meeting/domain/Meeting.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ public class Meeting extends TimeBaseEntity {
@ElementCollection(fetch = FetchType.EAGER)
private Set<String> imageUrls = new LinkedHashSet<>();

@Builder.Default
@ElementCollection(fetch = FetchType.EAGER)
private Set<Long> bookmarkedUserIds = new HashSet<>();

public void update(Meeting updateMeeting) {
this.title = updateMeeting.title;
this.topic = updateMeeting.topic;
Expand All @@ -90,6 +94,18 @@ public boolean alreadyParticipant(Long userId) {
return participantUserIds.contains(userId);
}

public void addBookmark(Long userId) {
bookmarkedUserIds.add(userId);
}

public void cancelBookmark(Long userId) {
bookmarkedUserIds.remove(userId);
}

public boolean isBookmarked(Long userId) {
return bookmarkedUserIds.contains(userId);
}

public boolean isOpen() {
return promiseDateTime.isAfter(LocalDateTime.now());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public record UpdateMeetingRequest(
public static final Long IGNORE_HOST_ID = null;
public static final Set<Long> IGNORE_PARTICIPANT_USER_IDS = null;
public static final Set<String> IGNORE_IMAGE_URLS = null;
public static final Set<Long> IGNORE_BOOKMARKED_USER_IDS = null;

public Meeting toMeeting() {
return new Meeting(
Expand All @@ -44,7 +45,8 @@ public Meeting toMeeting() {
NewMeetingArea.of(meetingArea),
numberOfRecruits,
promiseDateTime,
IGNORE_IMAGE_URLS
IGNORE_IMAGE_URLS,
IGNORE_BOOKMARKED_USER_IDS
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ public record MeetingResponse(
LocalDateTime promiseDateTime,
int numberOfRecruits,
MeetingArea meetingArea,
Set<Long> participantIds
Set<Long> participantIds,
Boolean isBookmarked
) {

public static MeetingResponse of(
Meeting meeting
Meeting meeting,
Boolean isBookmarked
) {
return new MeetingResponse(
meeting.getId(),
Expand All @@ -33,7 +35,8 @@ public static MeetingResponse of(
meeting.getPromiseDateTime(),
meeting.getNumberOfRecruits(),
MeetingArea.of(meeting),
meeting.getParticipantUserIds()
meeting.getParticipantUserIds(),
isBookmarked
);
}

Expand Down
32 changes: 27 additions & 5 deletions src/main/java/net/teumteum/meeting/service/MeetingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ public MeetingResponse createMeeting(List<MultipartFile> images, CreateMeetingRe

uploadMeetingImages(images, meeting);

return MeetingResponse.of(meeting);
return MeetingResponse.of(meeting, meeting.isBookmarked(userId));
}

@Transactional(readOnly = true)
public MeetingResponse getMeetingById(Long meetingId) {
public MeetingResponse getMeetingById(Long meetingId, Long userId) {
var existMeeting = getMeeting(meetingId);

return MeetingResponse.of(existMeeting);
return MeetingResponse.of(existMeeting, existMeeting.isBookmarked(userId));
}

@Transactional
Expand All @@ -72,7 +72,7 @@ public MeetingResponse updateMeeting(Long meetingId, List<MultipartFile> images,

existMeeting.update(updateMeetingRequest.toMeeting());
uploadMeetingImages(images, existMeeting);
return MeetingResponse.of(existMeeting);
return MeetingResponse.of(existMeeting, existMeeting.isBookmarked(userId));
}

@Transactional
Expand Down Expand Up @@ -126,7 +126,7 @@ public MeetingResponse addParticipant(Long meetingId, Long userId) {
}

existMeeting.addParticipant(userId);
return MeetingResponse.of(existMeeting);
return MeetingResponse.of(existMeeting, existMeeting.isBookmarked(userId));
}

@Transactional
Expand All @@ -148,6 +148,28 @@ public void cancelParticipant(Long meetingId, Long userId) {
existMeeting.cancelParticipant(userId);
}

@Transactional
public void addBookmark(Long meetingId, Long userId) {
var existMeeting = getMeeting(meetingId);

if (existMeeting.isBookmarked(userId)) {
throw new IllegalArgumentException("이미 북마크한 모임입니다.");
}

existMeeting.addBookmark(userId);
}

@Transactional
public void cancelBookmark(Long meetingId, Long userId) {
var existMeeting = getMeeting(meetingId);

if (!existMeeting.isBookmarked(userId)) {
throw new IllegalArgumentException("북마크하지 않은 모임입니다.");
}

existMeeting.cancelBookmark(userId);
}

private void uploadMeetingImages(List<MultipartFile> images, Meeting meeting) {
Assert.isTrue(!images.isEmpty() && images.size() <= 5, "이미지는 1개 이상 5개 이하로 업로드해야 합니다.");
meeting.getImageUrls().clear();
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/db/migration/V12__create_bookmark.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
create table if not exists meeting_bookmarked_user_ids
(
meeting_id bigint not null,
bookmarked_user_ids bigint null,
foreign key (meeting_id) references meeting (id)
);
14 changes: 14 additions & 0 deletions src/test/java/net/teumteum/integration/Api.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ ResponseSpec cancelMeeting(String token, Long meetingId) {
.exchange();
}

ResponseSpec addBookmark(String token, Long meetingId) {
return webTestClient.post()
.uri("/meetings/" + meetingId + "/bookmarks")
.header(HttpHeaders.AUTHORIZATION, token)
.exchange();
}

ResponseSpec cancelBookmark(String token, Long meetingId) {
return webTestClient.delete()
.uri("/meetings/" + meetingId + "/bookmarks")
.header(HttpHeaders.AUTHORIZATION, token)
.exchange();
}

ResponseSpec getCommonInterests(String token, List<Long> userIds) {
var param = new StringBuilder();
for (Long userId : userIds) {
Expand Down
96 changes: 94 additions & 2 deletions src/test/java/net/teumteum/integration/MeetingIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ class Find_meeting_api {
@DisplayName("존재하는 모임의 id가 주어지면, 모임 정보를 응답한다.")
void Return_meeting_info_if_exist_meeting_id_received() {
// given
var user = repository.saveAndGetUser();
securityContextSetting.set(user.getId());
var meeting = repository.saveAndGetOpenMeeting();
var expected = MeetingResponse.of(meeting);
var expected = MeetingResponse.of(meeting, false);
// when
var result = api.getMeetingById(VALID_TOKEN, meeting.getId());
// then
Expand All @@ -52,13 +54,34 @@ void Return_meeting_info_if_exist_meeting_id_received() {
@DisplayName("존재하지 않는 모임의 id가 주어지면, 400 Bad Request를 응답한다.")
void Return_400_bad_request_if_not_exists_meeting_id_received() {
// given
var user = repository.saveAndGetUser();
securityContextSetting.set(user.getId());
var notExistMeetingId = 1L;
// when
var result = api.getMeetingById(VALID_TOKEN, notExistMeetingId);
// then
result.expectStatus().isBadRequest()
.expectBody(ErrorResponse.class);
}

@Test
@DisplayName("유저가 북마크한 모임이라면, isBookmarked를 true로 응답한다.")
void Return_is_bookmarked_true_if_user_bookmarked_meeting() {
// given
var user = repository.saveAndGetUser();
securityContextSetting.set(user.getId());
var meeting = repository.saveAndGetOpenMeeting();
api.addBookmark(VALID_TOKEN, meeting.getId());
// when
var result = api.getMeetingById(VALID_TOKEN, meeting.getId());
// then
Assertions.assertThat(
result.expectStatus().isOk()
.expectBody(MeetingResponse.class)
.returnResult().getResponseBody())
.extracting(MeetingResponse::isBookmarked)
.isEqualTo(true);
}
}

@Nested
Expand All @@ -72,7 +95,6 @@ void Delete_meeting_if_exist_meeting_id_received() {
var host = repository.saveAndGetUser();
securityContextSetting.set(host.getId());


var meeting = repository.saveAndGetOpenMeetingWithHostId(host.getId());
// when
var result = api.deleteMeeting(VALID_TOKEN, meeting.getId());
Expand Down Expand Up @@ -380,4 +402,74 @@ void Return_400_bad_request_if_closed_meeting_id_received() {
.isEqualTo("종료된 모임에서 참여를 취소할 수 없습니다.");
}
}

@Nested
@DisplayName("북마크 추가 API는")
class Add_bookmark_api {

@Test
@DisplayName("존재하는 모임의 id가 주어지면, 모임을 북마크한다.")
void Add_bookmark_if_exist_meeting_id_received() {
// given
var me = repository.saveAndGetUser();
var meeting = repository.saveAndGetOpenMeeting();

securityContextSetting.set(me.getId());
// when
var result = api.addBookmark(VALID_TOKEN, meeting.getId());
// then
result.expectStatus().isCreated();
}

@Test
@DisplayName("이미 북마크한 모임의 id가 주어지면, 400 Bad Request를 응답한다.")
void Return_400_bad_request_if_already_bookmarked_meeting_id_received() {
// given
var me = repository.saveAndGetUser();
var meeting = repository.saveAndGetOpenMeeting();

securityContextSetting.set(me.getId());
api.addBookmark(VALID_TOKEN, meeting.getId());
// when
var result = api.addBookmark(VALID_TOKEN, meeting.getId());
// then
result.expectStatus().isBadRequest()
.expectBody(ErrorResponse.class);
}
}

@Nested
@DisplayName("북마크 취소 API는")
class Cancel_bookmark_api {

@Test
@DisplayName("존재하는 모임의 id가 주어지면, 모임의 북마크를 취소한다.")
void Cancel_bookmark_if_exist_meeting_id_received() {
// given
var me = repository.saveAndGetUser();
var meeting = repository.saveAndGetOpenMeeting();

securityContextSetting.set(me.getId());
api.addBookmark(VALID_TOKEN, meeting.getId());
// when
var result = api.cancelBookmark(VALID_TOKEN, meeting.getId());
// then
result.expectStatus().isOk();
}

@Test
@DisplayName("북마크하지 않은 모임의 id가 주어지면, 400 Bad Request를 응답한다.")
void Return_400_bad_request_if_not_bookmarked_meeting_id_received() {
// given
var me = repository.saveAndGetUser();
var meeting = repository.saveAndGetOpenMeeting();

securityContextSetting.set(me.getId());
// when
var result = api.cancelBookmark(VALID_TOKEN, meeting.getId());
// then
result.expectStatus().isBadRequest()
.expectBody(ErrorResponse.class);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ public static Meeting newMeetingByBuilder(MeetingBuilder meetingBuilder) {
meetingBuilder.meetingArea,
meetingBuilder.numberOfRecruits,
meetingBuilder.promiseDateTime,
meetingBuilder.imageUrls
meetingBuilder.imageUrls,
meetingBuilder.bookmarkedUserIds
);
}

Expand Down Expand Up @@ -177,6 +178,9 @@ public static class MeetingBuilder {

@Builder.Default
private Set<String> imageUrls = new HashSet<>(List.of("/1/image.jpg", "/2/image.jpg"));

@Builder.Default
private Set<Long> bookmarkedUserIds = new HashSet<>(List.of());
}

}
7 changes: 7 additions & 0 deletions src/test/resources/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,10 @@ create table if not exists alert(
updated_at timestamp(6) not null,
primary key (id)
);

create table if not exists meeting_bookmarked_user_ids
(
meeting_id bigint not null,
bookmarked_user_ids bigint null,
foreign key (meeting_id) references meeting (id)
);

0 comments on commit a2a901c

Please sign in to comment.