-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ feat(api): add post recommendation sentence API (#517)
* ✨ feat(api): add post recommendation sentence API * ✅ test(api): add post recommendation sentence * ✨ feat(api): get once a day * ✅ test(api): get once a day * ♻️ refactor(api): remove unnecessary codes * ♻️ refactor(api): rename UserTimestampRepository -> UserReadRepository * ♻️ refactor(api): rename UserReadRepository -> userRecommendSendHistoryRepository
- Loading branch information
Showing
13 changed files
with
368 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
...api/src/main/java/com/depromeet/domains/recommend/controller/PostRecommendController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
9 changes: 9 additions & 0 deletions
9
...n/java/com/depromeet/domains/recommend/dto/response/PostRecommendSentenceResponseDto.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
...streetdrop-api/src/main/java/com/depromeet/domains/recommend/provider/RandomProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | ||
} | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
.../com/depromeet/domains/recommend/repository/MemoryUserRecommendSendHistoryRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 MemoryUserRecommendSendHistoryRepository implements UserRecommendSendHistoryRepository { | ||
|
||
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); | ||
} | ||
|
||
} |
7 changes: 7 additions & 0 deletions
7
...main/java/com/depromeet/domains/recommend/repository/PostRecommendSentenceRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> { | ||
} |
6 changes: 6 additions & 0 deletions
6
...n/java/com/depromeet/domains/recommend/repository/UserRecommendSendHistoryRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.depromeet.domains.recommend.repository; | ||
|
||
public interface UserRecommendSendHistoryRepository { | ||
void save(Long userId); | ||
Boolean isSent(Long userId); | ||
} |
39 changes: 39 additions & 0 deletions
39
...tdrop-api/src/main/java/com/depromeet/domains/recommend/service/PostRecommendService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.UserRecommendSendHistoryRepository; | ||
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 UserRecommendSendHistoryRepository userRecommendSendHistoryRepository; | ||
|
||
public PostRecommendSentenceResponseDto getOneRandomSentence(User user) { | ||
if (userRecommendSendHistoryRepository.isSent(user.getId())) { | ||
return PostRecommendSentenceResponseDto.empty(); | ||
} | ||
String randomSentence = getRandomSentence(); | ||
userRecommendSendHistoryRepository.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(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
87 changes: 87 additions & 0 deletions
87
...drop-api/src/test/java/unit/domains/recommend/controller/PostRecommendControllerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
...streetdrop-api/src/test/java/unit/domains/recommend/service/PostRecommendServiceTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.UserRecommendSendHistoryRepository; | ||
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 UserRecommendSendHistoryRepository userRecommendSendHistoryRepository; | ||
|
||
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(userRecommendSendHistoryRepository.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(userRecommendSendHistoryRepository.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(userRecommendSendHistoryRepository.isSent(user.getId())).willReturn(false); | ||
given(postRecommendSentenceRepository.findAll()).willReturn(List.of()); | ||
|
||
assertThatThrownBy(() -> postRecommendService.getOneRandomSentence(user)) | ||
.isInstanceOf(NotFoundException.class); | ||
} | ||
} | ||
} | ||
|
||
} |
32 changes: 32 additions & 0 deletions
32
...d/streetdrop-domain/src/main/java/com/depromeet/recommend/post/PostRecommendSentence.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
5 changes: 5 additions & 0 deletions
5
...p-domain/src/main/resources/db/migration/V20241017__add_post_recommend_sentence_table.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
) |