-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
✨ feat(api): add post recommendation sentence API #517
Changes from 6 commits
6180213
4e0bb03
daa73f4
2822ac7
cbe74db
d9d7bb0
53df9cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.depromeet.domains.recommend.controller; | ||
|
||
import com.depromeet.common.dto.ResponseDto; | ||
import com.depromeet.domains.recommend.dto.response.PostRecommendSentenceResponseDto; | ||
import com.depromeet.domains.recommend.service.PostRecommendService; | ||
import com.depromeet.security.annotation.ReqUser; | ||
import com.depromeet.user.User; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
@RestController | ||
@RequestMapping | ||
@RequiredArgsConstructor | ||
@Tag(name = "💁♀️Post Recommend", description = "Post Recommend API") | ||
public class PostRecommendController { | ||
|
||
private final PostRecommendService postRecommendService; | ||
|
||
@Operation(summary = "홈 화면 드랍 유도 - 무작위 문장 추천") | ||
@GetMapping("/post-recommend/random-sentence") | ||
public ResponseEntity<PostRecommendSentenceResponseDto> getRandomPhrase( | ||
@ReqUser User user | ||
) { | ||
var response = postRecommendService.getOneRandomSentence(user); | ||
return ResponseDto.ok(response); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.depromeet.domains.recommend.dto.response; | ||
|
||
public record PostRecommendSentenceResponseDto( | ||
String sentence | ||
) { | ||
public static PostRecommendSentenceResponseDto empty() { | ||
return new PostRecommendSentenceResponseDto(null); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.depromeet.domains.recommend.provider; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.List; | ||
import java.util.Random; | ||
|
||
@Component | ||
public final class RandomProvider { | ||
|
||
private static final Random RANDOM = new Random(); | ||
|
||
public static <T> T getRandomElement(List<T> list) { | ||
return list.get(RANDOM.nextInt(list.size())); | ||
} | ||
|
||
private RandomProvider() { | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.depromeet.domains.recommend.repository; | ||
|
||
import org.springframework.stereotype.Repository; | ||
|
||
import java.time.LocalDateTime; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
|
||
@Repository | ||
public class MemoryUserReadRepository implements UserReadRepository { | ||
|
||
private static Map<Long, LocalDateTime> store = new HashMap<>(); | ||
|
||
@Override | ||
public void save(Long userId) { | ||
store.put(userId, LocalDateTime.now()); | ||
} | ||
|
||
@Override | ||
public Boolean isSent(Long userId) { | ||
return Optional.ofNullable(store.get(userId)) | ||
.map(readTime -> !readTime.isBefore(LocalDateTime.now().minusDays(1))) | ||
.orElse(false); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하루에 한 번 응답값을 전달합니다. |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.depromeet.domains.recommend.repository; | ||
|
||
import com.depromeet.recommend.post.PostRecommendSentence; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface PostRecommendSentenceRepository extends JpaRepository<PostRecommendSentence, Long> { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.depromeet.domains.recommend.repository; | ||
|
||
public interface UserReadRepository { | ||
void save(Long userId); | ||
Boolean isSent(Long userId); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package com.depromeet.domains.recommend.service; | ||
|
||
import com.depromeet.common.error.dto.CommonErrorCode; | ||
import com.depromeet.common.error.exception.internal.NotFoundException; | ||
import com.depromeet.domains.recommend.dto.response.PostRecommendSentenceResponseDto; | ||
import com.depromeet.domains.recommend.provider.RandomProvider; | ||
import com.depromeet.domains.recommend.repository.PostRecommendSentenceRepository; | ||
import com.depromeet.domains.recommend.repository.UserReadRepository; | ||
import com.depromeet.recommend.post.PostRecommendSentence; | ||
import com.depromeet.user.User; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class PostRecommendService { | ||
|
||
private final PostRecommendSentenceRepository postRecommendSentenceRepository; | ||
private final UserReadRepository userReadRepository; | ||
|
||
public PostRecommendSentenceResponseDto getOneRandomSentence(User user) { | ||
if (userReadRepository.isSent(user.getId())) { | ||
return PostRecommendSentenceResponseDto.empty(); | ||
} | ||
String randomSentence = getRandomSentence(); | ||
userReadRepository.save(user.getId()); | ||
return new PostRecommendSentenceResponseDto(randomSentence); | ||
} | ||
|
||
private String getRandomSentence() { | ||
var postRecommendSentences = postRecommendSentenceRepository.findAll(); | ||
if (postRecommendSentences.isEmpty()) { | ||
throw new NotFoundException(CommonErrorCode.SENTENCES_NOT_FOUND); | ||
} | ||
PostRecommendSentence randomPostRecommendSentence = RandomProvider.getRandomElement(postRecommendSentences); | ||
return randomPostRecommendSentence.getSentence(); | ||
} | ||
Comment on lines
+30
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요구사항
다양한 방법들
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DB에는 20개의 문장이 저장된다고 가정했습니다. 규모가 작을 때는 어떤 방법을 하든 비슷할 것이에요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 문장 추천의 경우 저희가 직접 넣어주는 방식이다보니, findAll 쪽에 캐시를 걸어두어서, 1일에 한번씩 갱신하도록 해두면 매 조회 쿼리가 나가는 것이 아닌 서비스 측에서 가지게 되는 것인 추후에는 캐시가 들어가는 것도 좋아보입니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. findAll()에 캐시를 두는 방법 아주 좋은 것 같습니다! |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package unit.domains.recommend.controller; | ||
|
||
import com.depromeet.common.error.GlobalExceptionHandler; | ||
import com.depromeet.domains.recommend.controller.PostRecommendController; | ||
import com.depromeet.domains.recommend.dto.response.PostRecommendSentenceResponseDto; | ||
import com.depromeet.domains.recommend.service.PostRecommendService; | ||
import com.depromeet.user.User; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; | ||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; | ||
import org.springframework.boot.test.mock.mockito.MockBean; | ||
import org.springframework.context.annotation.Import; | ||
import org.springframework.test.context.ContextConfiguration; | ||
import org.springframework.test.web.servlet.MockMvc; | ||
import unit.annotation.MockAnonymousUser; | ||
|
||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.Mockito.when; | ||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
||
@ContextConfiguration(classes = {PostRecommendController.class, ValidationAutoConfiguration.class}) | ||
@WebMvcTest(controllers = {PostRecommendController.class}) | ||
@Import({PostRecommendController.class, GlobalExceptionHandler.class}) | ||
@DisplayName("[API][Controller] PostRecommendController 테스트") | ||
public class PostRecommendControllerTest { | ||
|
||
@Autowired | ||
MockMvc mvc; | ||
|
||
@MockBean | ||
PostRecommendService postRecommendService; | ||
|
||
User user; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
user = User.builder() | ||
.idfv("new-idfv") | ||
.build(); | ||
} | ||
|
||
@DisplayName("[GET] 홈 화면 드랍 유도 - 무작위 문장 추천") | ||
@Nested | ||
@MockAnonymousUser | ||
class GetRandomSentenceTest { | ||
@Nested | ||
@DisplayName("성공") | ||
@MockAnonymousUser | ||
class Success { | ||
@DisplayName("무작위 추천 문장 1개 조회") | ||
@Test | ||
void getOneRandomSentenceSuccess1() throws Exception { | ||
|
||
var randomSentence = new PostRecommendSentenceResponseDto("random sentence"); | ||
when(postRecommendService.getOneRandomSentence(any(User.class))).thenReturn(randomSentence); | ||
|
||
var response = mvc.perform( | ||
get("/post-recommend/random-sentence") | ||
.header("x-sdp-idfv", "new-idfv") | ||
); | ||
response.andExpect(status().isOk()) | ||
.andExpect(jsonPath("$.sentence").value("random sentence")); | ||
} | ||
|
||
@DisplayName("무작위 추천 문장이 없는 경우") | ||
@Test | ||
void getOneRandomSentenceSuccess2() throws Exception { | ||
|
||
var emptySentence = PostRecommendSentenceResponseDto.empty(); | ||
when(postRecommendService.getOneRandomSentence(any(User.class))).thenReturn(emptySentence); | ||
|
||
var response = mvc.perform( | ||
get("/post-recommend/random-sentence") | ||
.header("x-sdp-idfv", "new-idfv") | ||
); | ||
response.andExpect(status().isOk()) | ||
.andExpect(jsonPath("$.sentence").isEmpty()); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package unit.domains.recommend.service; | ||
|
||
import com.depromeet.common.error.exception.internal.NotFoundException; | ||
import com.depromeet.domains.recommend.repository.PostRecommendSentenceRepository; | ||
import com.depromeet.domains.recommend.repository.UserReadRepository; | ||
import com.depromeet.domains.recommend.service.PostRecommendService; | ||
import com.depromeet.recommend.post.PostRecommendSentence; | ||
import com.depromeet.user.User; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.InjectMocks; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
|
||
import java.lang.reflect.Field; | ||
import java.util.List; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; | ||
import static org.mockito.BDDMockito.given; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
@DisplayName("[Service] PostRecommendService 테스트") | ||
public class PostRecommendServiceTest { | ||
|
||
@InjectMocks | ||
private PostRecommendService postRecommendService; | ||
|
||
@Mock | ||
private PostRecommendSentenceRepository postRecommendSentenceRepository; | ||
|
||
@Mock | ||
private UserReadRepository userReadRepository; | ||
|
||
User user; | ||
|
||
@BeforeEach | ||
void setUp() throws NoSuchFieldException, IllegalAccessException { | ||
user = User.builder().build(); | ||
Field userIdField = User.class.getDeclaredField("id"); | ||
userIdField.setAccessible(true); | ||
userIdField.set(user, 1L); | ||
} | ||
|
||
@DisplayName("무작위 문장 추천") | ||
@Nested | ||
class GetOneRandomSentenceTest { | ||
@Nested | ||
@DisplayName("성공") | ||
class Success { | ||
@DisplayName("이미 추천 문장을 받은 사용자인 경우") | ||
@Test | ||
void getOneRandomSentenceSuccess1() { | ||
given(userReadRepository.isSent(user.getId())).willReturn(true); | ||
var result = postRecommendService.getOneRandomSentence(user); | ||
|
||
assertThat(result.sentence()).isNull(); | ||
} | ||
|
||
@DisplayName("무작위 추천 문장 1개 조회") | ||
@Test | ||
void getOneRandomSentenceSuccess2() { | ||
List<PostRecommendSentence> sentences = List.of( | ||
new PostRecommendSentence("First sentence"), | ||
new PostRecommendSentence("Second sentence"), | ||
new PostRecommendSentence("Third sentence") | ||
); | ||
|
||
given(postRecommendSentenceRepository.findAll()).willReturn(sentences); | ||
given(userReadRepository.isSent(user.getId())).willReturn(false); | ||
var result = postRecommendService.getOneRandomSentence(user); | ||
|
||
assertThat(result).isNotNull(); | ||
assertThat(result.sentence()).isIn( | ||
"First sentence", | ||
"Second sentence", | ||
"Third sentence" | ||
); | ||
} | ||
} | ||
|
||
@Nested | ||
@DisplayName("실패") | ||
class Fail { | ||
@DisplayName("저장소에 추천 문장이 없는 경우") | ||
@Test | ||
void getOneRandomSentenceFail() { | ||
given(userReadRepository.isSent(user.getId())).willReturn(false); | ||
given(postRecommendSentenceRepository.findAll()).willReturn(List.of()); | ||
|
||
assertThatThrownBy(() -> postRecommendService.getOneRandomSentence(user)) | ||
.isInstanceOf(NotFoundException.class); | ||
} | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.depromeet.recommend.post; | ||
|
||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.Id; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
import static jakarta.persistence.GenerationType.IDENTITY; | ||
import static lombok.AccessLevel.PROTECTED; | ||
|
||
@Getter | ||
@NoArgsConstructor(access = PROTECTED) | ||
@Entity | ||
public class PostRecommendSentence { | ||
|
||
@Id | ||
@GeneratedValue(strategy = IDENTITY) | ||
@Column(name = "post_recommend_sentence_id") | ||
private Long id; | ||
|
||
@Column(nullable = false) | ||
private String sentence; | ||
|
||
@Builder | ||
public PostRecommendSentence(String sentence) { | ||
this.sentence = sentence; | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
CREATE TABLE IF NOT EXISTS post_recommend_sentence ( | ||
post_recommend_sentence_id BIGINT NOT NULL AUTO_INCREMENT, | ||
sentence VARCHAR(255) NOT NULL, | ||
PRIMARY KEY (post_recommend_sentence_id) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RandomProvider로 제공되는 거 좋은 것 같습니다