Skip to content

Commit 8266787

Browse files
authored
feat: 축제 부스 대비 tag search
# 변경점 👍 - 토큰 없이 태그 기반 조회를 위한 임시 api 두 개 추가했습니다. - 태그로 유저 조회 - 유저 킬링파트 플레이리스트 조회 # 비고 ✏ - 미사용 시점에 booth 패키지 통째로 삭제하고 whiteList에서 booth 제거하면 됩니다.
1 parent c74e10d commit 8266787

7 files changed

Lines changed: 283 additions & 1 deletion

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package apptive.team5.booth.controller;
2+
3+
import apptive.team5.booth.dto.PublicKillingPartResponse;
4+
import apptive.team5.booth.dto.PublicUserResponse;
5+
import apptive.team5.booth.service.BoothService;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.data.domain.Page;
8+
import org.springframework.data.domain.PageRequest;
9+
import org.springframework.http.HttpStatus;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.PathVariable;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RequestParam;
15+
import org.springframework.web.bind.annotation.RestController;
16+
17+
@RestController
18+
@RequestMapping("/api/public")
19+
@RequiredArgsConstructor
20+
public class BoothController {
21+
22+
private final BoothService boothService;
23+
24+
@GetMapping("/users/search")
25+
public ResponseEntity<PublicUserResponse> findUserByTag(@RequestParam String tag) {
26+
PublicUserResponse response = boothService.findUserByTag(tag);
27+
28+
return ResponseEntity.status(HttpStatus.OK).body(response);
29+
}
30+
31+
@GetMapping("/users/{userId}/playlist")
32+
public ResponseEntity<Page<PublicKillingPartResponse>> getUserPlaylist(
33+
@PathVariable
34+
Long userId,
35+
@RequestParam(defaultValue = "0")
36+
int page,
37+
@RequestParam(defaultValue = "5")
38+
int size
39+
) {
40+
Page<PublicKillingPartResponse> response =
41+
boothService.getUserPlaylist(userId, PageRequest.of(page, size));
42+
43+
return ResponseEntity.status(HttpStatus.OK).body(response);
44+
}
45+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package apptive.team5.booth.dto;
2+
3+
import apptive.team5.diary.domain.DiaryEntity;
4+
5+
public record PublicKillingPartResponse(
6+
Long diaryId,
7+
String artist,
8+
String musicTitle,
9+
String albumImageUrl,
10+
String videoUrl,
11+
String totalDuration,
12+
String start,
13+
String end
14+
) {
15+
public static PublicKillingPartResponse from(DiaryEntity diary) {
16+
return new PublicKillingPartResponse(
17+
diary.getId(),
18+
diary.getArtist(),
19+
diary.getMusicTitle(),
20+
diary.getAlbumImageUrl(),
21+
diary.getVideoUrl(),
22+
diary.getTotalDuration(),
23+
diary.getStart(),
24+
diary.getEnd()
25+
);
26+
}
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package apptive.team5.booth.dto;
2+
3+
import apptive.team5.global.util.S3Util;
4+
import apptive.team5.user.domain.UserEntity;
5+
6+
public record PublicUserResponse(
7+
Long userId,
8+
String username,
9+
String tag,
10+
String profileImageUrl
11+
) {
12+
public static PublicUserResponse from(UserEntity user) {
13+
return new PublicUserResponse(
14+
user.getId(),
15+
user.getUsername(),
16+
user.getTag(),
17+
S3Util.s3Url + user.getProfileImage()
18+
);
19+
}
20+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package apptive.team5.booth.service;
2+
3+
import apptive.team5.booth.dto.PublicKillingPartResponse;
4+
import apptive.team5.booth.dto.PublicUserResponse;
5+
import apptive.team5.diary.domain.DiaryEntity;
6+
import apptive.team5.diary.domain.DiaryScope;
7+
import apptive.team5.diary.service.DiaryLowService;
8+
import apptive.team5.user.domain.UserEntity;
9+
import apptive.team5.user.service.UserLowService;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.data.domain.Page;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
import java.util.List;
17+
18+
@Transactional
19+
@Service
20+
@RequiredArgsConstructor
21+
public class BoothService {
22+
23+
private final UserLowService userLowService;
24+
private final DiaryLowService diaryLowService;
25+
26+
private static final List<DiaryScope> VISIBLE_SCOPES =
27+
List.of(DiaryScope.PUBLIC, DiaryScope.KILLING_PART);
28+
29+
@Transactional(readOnly = true)
30+
public PublicUserResponse findUserByTag(String tag) {
31+
UserEntity user = userLowService.findByTag(tag);
32+
return PublicUserResponse.from(user);
33+
}
34+
35+
@Transactional(readOnly = true)
36+
public Page<PublicKillingPartResponse> getUserPlaylist(Long userId, Pageable pageable) {
37+
Page<DiaryEntity> diaryPage =
38+
diaryLowService.findDiaryByUserAndScopeIn(userId, VISIBLE_SCOPES, pageable);
39+
40+
return diaryPage.map(PublicKillingPartResponse::from);
41+
}
42+
}

src/main/java/apptive/team5/config/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public CorsConfigurationSource corsConfigurationSource() {
7373
private final String[] whiteList =
7474
{
7575
"/api/jwt/exchange",
76-
"/api/oauth2/**"
76+
"/api/oauth2/**",
77+
"/api/public/**"
7778
};
7879
}

src/main/java/apptive/team5/user/service/UserLowService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ public boolean existsByTag(String tag) {
5656
return userRepository.findByTag(tag).isPresent();
5757
}
5858

59+
@Transactional(readOnly = true)
60+
public UserEntity findByTag(String tag) {
61+
return userRepository.findByTag(tag)
62+
.orElseThrow(() -> new NotFoundEntityException(ExceptionCode.NOT_FOUND_USER.getDescription()));
63+
}
64+
5965
@Transactional(readOnly = true)
6066
public Page<UserEntity> findByTagOrUsernameExcludingBlocked(Set<Long> blockedUserIds, String searchCond, Pageable pageable) {
6167
return qUserRepository.findByTagOrUsernameExcludingBlocked(blockedUserIds, searchCond,pageable);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package apptive.team5.booth.service;
2+
3+
import apptive.team5.booth.dto.PublicKillingPartResponse;
4+
import apptive.team5.booth.dto.PublicUserResponse;
5+
import apptive.team5.diary.domain.DiaryEntity;
6+
import apptive.team5.diary.domain.DiaryScope;
7+
import apptive.team5.diary.service.DiaryLowService;
8+
import apptive.team5.global.exception.NotFoundEntityException;
9+
import apptive.team5.user.domain.UserEntity;
10+
import apptive.team5.user.service.UserLowService;
11+
import apptive.team5.util.TestUtil;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.api.extension.ExtendWith;
15+
import org.mockito.InjectMocks;
16+
import org.mockito.Mock;
17+
import org.mockito.junit.jupiter.MockitoExtension;
18+
import org.springframework.data.domain.Page;
19+
import org.springframework.data.domain.PageImpl;
20+
import org.springframework.data.domain.PageRequest;
21+
import org.springframework.test.util.ReflectionTestUtils;
22+
23+
import java.util.List;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
27+
import static org.mockito.BDDMockito.given;
28+
import static org.mockito.Mockito.verify;
29+
import static org.mockito.Mockito.verifyNoMoreInteractions;
30+
31+
@ExtendWith(MockitoExtension.class)
32+
public class BoothServiceTest {
33+
34+
@InjectMocks
35+
private BoothService boothService;
36+
37+
@Mock
38+
private UserLowService userLowService;
39+
40+
@Mock
41+
private DiaryLowService diaryLowService;
42+
43+
@Test
44+
@DisplayName("태그로 유저 조회 - 성공")
45+
void findUserByTag_success() {
46+
// given
47+
UserEntity user = TestUtil.makeUserEntityWithId();
48+
String tag = user.getTag();
49+
50+
given(userLowService.findByTag(tag)).willReturn(user);
51+
52+
// when
53+
PublicUserResponse response = boothService.findUserByTag(tag);
54+
55+
// then
56+
assertThat(response.userId()).isEqualTo(user.getId());
57+
assertThat(response.username()).isEqualTo(user.getUsername());
58+
assertThat(response.tag()).isEqualTo(tag);
59+
assertThat(response.profileImageUrl()).contains(user.getProfileImage());
60+
61+
verify(userLowService).findByTag(tag);
62+
verifyNoMoreInteractions(userLowService, diaryLowService);
63+
}
64+
65+
@Test
66+
@DisplayName("태그로 유저 조회 - 태그 없으면 NotFoundEntityException")
67+
void findUserByTag_notFound() {
68+
// given
69+
String tag = "MISSING_TAG";
70+
given(userLowService.findByTag(tag))
71+
.willThrow(new NotFoundEntityException("존재하지 않는 회원입니다."));
72+
73+
// when / then
74+
assertThatThrownBy(() -> boothService.findUserByTag(tag))
75+
.isInstanceOf(NotFoundEntityException.class);
76+
77+
verify(userLowService).findByTag(tag);
78+
verifyNoMoreInteractions(userLowService, diaryLowService);
79+
}
80+
81+
@Test
82+
@DisplayName("플레이리스트 조회 - PUBLIC/KILLING_PART 스코프 필터로 호출")
83+
void getUserPlaylist_filtersScopes() {
84+
// given
85+
UserEntity owner = TestUtil.makeUserEntityWithId();
86+
Long userId = owner.getId();
87+
88+
DiaryEntity publicDiary = TestUtil.makeDiaryEntityWithScope(owner, DiaryScope.PUBLIC);
89+
ReflectionTestUtils.setField(publicDiary, "id", 10L);
90+
DiaryEntity killingPartDiary = TestUtil.makeDiaryEntityWithScope(owner, DiaryScope.KILLING_PART);
91+
ReflectionTestUtils.setField(killingPartDiary, "id", 11L);
92+
93+
Page<DiaryEntity> diaryPage = new PageImpl<>(List.of(publicDiary, killingPartDiary));
94+
PageRequest pageRequest = PageRequest.of(0, 5);
95+
List<DiaryScope> visibleScopes = List.of(DiaryScope.PUBLIC, DiaryScope.KILLING_PART);
96+
97+
given(diaryLowService.findDiaryByUserAndScopeIn(userId, visibleScopes, pageRequest))
98+
.willReturn(diaryPage);
99+
100+
// when
101+
Page<PublicKillingPartResponse> result = boothService.getUserPlaylist(userId, pageRequest);
102+
103+
// then
104+
assertThat(result.getContent()).hasSize(2);
105+
106+
PublicKillingPartResponse publicDto = result.getContent().get(0);
107+
assertThat(publicDto.diaryId()).isEqualTo(10L);
108+
assertThat(publicDto.musicTitle()).isEqualTo("Test Music");
109+
assertThat(publicDto.artist()).isEqualTo("Test Artist");
110+
assertThat(publicDto.albumImageUrl()).isEqualTo("image.url");
111+
assertThat(publicDto.videoUrl()).isEqualTo("video.url");
112+
assertThat(publicDto.totalDuration()).isEqualTo("PT2M58S");
113+
assertThat(publicDto.start()).isEqualTo("PT1M1S");
114+
assertThat(publicDto.end()).isEqualTo("PT1M31S");
115+
116+
PublicKillingPartResponse killingPartDto = result.getContent().get(1);
117+
assertThat(killingPartDto.diaryId()).isEqualTo(11L);
118+
119+
verify(diaryLowService).findDiaryByUserAndScopeIn(userId, visibleScopes, pageRequest);
120+
verifyNoMoreInteractions(userLowService, diaryLowService);
121+
}
122+
123+
@Test
124+
@DisplayName("플레이리스트 조회 - 다이어리 없으면 빈 페이지")
125+
void getUserPlaylist_emptyResult() {
126+
// given
127+
Long userId = 999L;
128+
PageRequest pageRequest = PageRequest.of(0, 5);
129+
List<DiaryScope> visibleScopes = List.of(DiaryScope.PUBLIC, DiaryScope.KILLING_PART);
130+
131+
given(diaryLowService.findDiaryByUserAndScopeIn(userId, visibleScopes, pageRequest))
132+
.willReturn(new PageImpl<>(List.of()));
133+
134+
// when
135+
Page<PublicKillingPartResponse> result = boothService.getUserPlaylist(userId, pageRequest);
136+
137+
// then
138+
assertThat(result.getContent()).isEmpty();
139+
verify(diaryLowService).findDiaryByUserAndScopeIn(userId, visibleScopes, pageRequest);
140+
}
141+
}

0 commit comments

Comments
 (0)