Skip to content

Commit

Permalink
feat: 즐겨찾기 기능을 구현한다. (#50)
Browse files Browse the repository at this point in the history
* feat: 즐겨찾기 엔티티 추가

* refactor: 즐겨찾기 엔티티 생성자 추가

* feat: 즐겨찾기 기능 구현

* test: 즐겨찾기 추가 기능 테스트 작성

* refactor: 즐겨찾기 최대 개수 수정

* feat: 즐겨찾기 조회 및 삭제 구현

* test: 즐겨찾기 조회 및 삭제 테스트 작성

* refactor: Star 엔티티의 token 필드에 nullable 옵션 추가

* refactor: 리뷰 기반 즐겨찾기 숫자 검증 로직 수정

* refactor: 리뷰 기반 핀 등록 숫자 검증 로직 수정

* refactor: 테스트 리뷰 반영

* refactor: 테이블 명 수정

* test: Star Api 테스트 작성

* refactor: 리뷰 반영

* refactor: 리뷰 반영 및 테스트 수정

* refactor: SubjectService 테스트에 StarRepository 삭제 추가

* refactor: Star 엔티티 테이블 명 수정

* refactor: 정책 수정에 따른 테스트 수정
  • Loading branch information
boyekim authored Feb 8, 2025
1 parent e2d79e3 commit 71e7da3
Show file tree
Hide file tree
Showing 13 changed files with 433 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
public enum AllcllErrorCode {

PIN_LIMIT_EXCEEDED("이미 %d개의 핀을 등록했습니다."),
STAR_LIMIT_EXCEEDED("이미 %d개의 즐겨찾기를 등록했습니다."),
DUPLICATE_PIN("%s은(는) 이미 핀 등록된 과목입니다."),
DUPLICATE_STAR("%s은(는) 이미 핀 등록된 과목입니다."),
PIN_SUBJECT_MISMATCH("핀에 등록된 과목이 아닙니다."),
STAR_SUBJECT_MISMATCH("즐겨찾기에 등록된 과목이 아닙니다."),
TOKEN_NOT_FOUND("쿠키에 토큰이 존재하지 않습니다."),
SUBJECT_NOT_FOUND("존재하지 않는 과목 입니다.");

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/kr/allcll/seatfinder/pin/Pin.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "PIN")
@Table(name = "pins")
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/kr/allcll/seatfinder/pin/PinRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface PinRepository extends JpaRepository<Pin, Long> {
Optional<Pin> findBySubjectAndToken(Subject subject, String token);

boolean existsBySubjectAndToken(Subject subject, String token);

Long countAllByToken(String token);
}
8 changes: 4 additions & 4 deletions src/main/java/kr/allcll/seatfinder/pin/PinService.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ public class PinService {

@Transactional
public void addPinOnSubject(Long subjectId, String token) {
List<Pin> userPins = pinRepository.findAllByToken(token);
Subject subject = subjectRepository.findById(subjectId)
.orElseThrow(() -> new AllcllException(AllcllErrorCode.SUBJECT_NOT_FOUND));
validateCanAddPin(userPins, subject, token);
validateCanAddPin(subject, token);
pinRepository.save(new Pin(token, subject));
}

private void validateCanAddPin(List<Pin> userPins, Subject subject, String token) {
if (userPins.size() >= MAX_PIN_NUMBER) {
private void validateCanAddPin(Subject subject, String token) {
Long pinCount = pinRepository.countAllByToken(token);
if (pinCount >= MAX_PIN_NUMBER) {
throw new AllcllException(AllcllErrorCode.PIN_LIMIT_EXCEEDED, MAX_PIN_NUMBER);
}
if (pinRepository.existsBySubjectAndToken(subject, token)) {
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/kr/allcll/seatfinder/star/Star.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kr.allcll.seatfinder.star;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import kr.allcll.seatfinder.subject.Subject;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "stars")
@Entity
@Getter
@NoArgsConstructor
public class Star {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "token", nullable = false)
private String token;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "subject_id", nullable = false)
private Subject subject;

public Star(String token, Subject subject) {
this.token = token;
this.subject = subject;
}
}
37 changes: 37 additions & 0 deletions src/main/java/kr/allcll/seatfinder/star/StarApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kr.allcll.seatfinder.star;

import kr.allcll.seatfinder.ThreadLocalHolder;
import kr.allcll.seatfinder.star.dto.StarredSubjectIdsResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class StarApi {

private final StarService starService;

@PostMapping("/api/stars")
ResponseEntity<Void> addStarOnSubject(@RequestParam Long subjectId) {
starService.addStarOnSubject(subjectId, ThreadLocalHolder.SHARED_TOKEN.get());
return ResponseEntity.ok().build();
}

@DeleteMapping("/api/stars/{subjectId}")
public ResponseEntity<Void> deleteStarOnSubject(@PathVariable Long subjectId) {
starService.deleteStarOnSubject(subjectId, ThreadLocalHolder.SHARED_TOKEN.get());
return ResponseEntity.ok().build();
}

@GetMapping("/api/stars")
public ResponseEntity<StarredSubjectIdsResponse> retrieveStars() {
StarredSubjectIdsResponse response = starService.retrieveStars(ThreadLocalHolder.SHARED_TOKEN.get());
return ResponseEntity.ok(response);
}
}
16 changes: 16 additions & 0 deletions src/main/java/kr/allcll/seatfinder/star/StarRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package kr.allcll.seatfinder.star;

import java.util.List;
import kr.allcll.seatfinder.subject.Subject;
import org.springframework.data.jpa.repository.JpaRepository;

public interface StarRepository extends JpaRepository<Star, Long> {

boolean existsBySubjectAndToken(Subject subject, String token);

List<Star> findAllByToken(String token);

Long countAllByToken(String token);

void deleteStarBySubjectIdAndToken(Long subjectId, String token);
}
54 changes: 54 additions & 0 deletions src/main/java/kr/allcll/seatfinder/star/StarService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package kr.allcll.seatfinder.star;

import java.util.List;
import kr.allcll.seatfinder.exception.AllcllErrorCode;
import kr.allcll.seatfinder.exception.AllcllException;
import kr.allcll.seatfinder.star.dto.StarredSubjectIdResponse;
import kr.allcll.seatfinder.star.dto.StarredSubjectIdsResponse;
import kr.allcll.seatfinder.subject.Subject;
import kr.allcll.seatfinder.subject.SubjectRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

private static final int MAX_STAR_NUMBER = 50;

private final StarRepository starRepository;
private final SubjectRepository subjectRepository;

@Transactional
public void addStarOnSubject(Long subjectId, String token) {

Subject subject = subjectRepository.findById(subjectId)
.orElseThrow(() -> new AllcllException(AllcllErrorCode.SUBJECT_NOT_FOUND));
validateCanAddStar(subject, token);
starRepository.save(new Star(token, subject));
}

private void validateCanAddStar(Subject subject, String token) {
Long starCount = starRepository.countAllByToken(token);
if (starCount >= MAX_STAR_NUMBER) {
throw new AllcllException(AllcllErrorCode.STAR_LIMIT_EXCEEDED, MAX_STAR_NUMBER);
}
if (starRepository.existsBySubjectAndToken(subject, token)) {
throw new AllcllException(AllcllErrorCode.DUPLICATE_STAR, subject.getCuriNm());
}
}

@Transactional
public void deleteStarOnSubject(Long subjectId, String token) {
starRepository.deleteStarBySubjectIdAndToken(subjectId, token);
}

public StarredSubjectIdsResponse retrieveStars(String token) {
List<Star> stars = starRepository.findAllByToken(token);
return new StarredSubjectIdsResponse(stars.stream()
.map(star -> new StarredSubjectIdResponse(star.getSubject().getId()))
.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.allcll.seatfinder.star.dto;

public record StarredSubjectIdResponse(
Long subjectId
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kr.allcll.seatfinder.star.dto;

import java.util.List;

public record StarredSubjectIdsResponse(
List<StarredSubjectIdResponse> subjects
) {

}
81 changes: 81 additions & 0 deletions src/test/java/kr/allcll/seatfinder/star/StarApiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package kr.allcll.seatfinder.star;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import jakarta.servlet.http.Cookie;
import java.util.List;
import kr.allcll.seatfinder.star.dto.StarredSubjectIdResponse;
import kr.allcll.seatfinder.star.dto.StarredSubjectIdsResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

@WebMvcTest(StarApi.class)
class StarApiTest {

@Autowired
private MockMvc mockMvc;

@MockitoBean
private StarService starService;

@Test
@DisplayName("관담 기능의 즐겨찾기 등록을 할 때에 요청과 응답을 확인한다.")
void addStarOnSubjectWhenStarNotExist() throws Exception {
// when, then
mockMvc.perform(post("/api/stars")
.param("subjectId", "1"))
.andExpect(status().isOk());
}

@Test
@DisplayName("관담 기능의 즐겨찾기를 삭제할 때 요청과 응답을 확인한다.")
void deleteStarOnSubject() throws Exception {
// when, then
mockMvc.perform(delete("/api/stars/{subjectId}", 1L))
.andExpect(status().isOk());
}

@Test
@DisplayName("관담기능을 조회할 때 요청과 응답을 확인한다.")
void retrieveStars() throws Exception {
// given
String expected = """
{
"subjects":[
{
"subjectId":1
},
{
"subjectId":2
}
]
}
""";

List<StarredSubjectIdResponse> subjects = List.of(
new StarredSubjectIdResponse(1L),
new StarredSubjectIdResponse(2L)
);
when(starService.retrieveStars("tokenValue"))
.thenReturn(new StarredSubjectIdsResponse(subjects));

// when
MvcResult result = mockMvc.perform(get("/api/stars")
.cookie(new Cookie("token", "tokenValue")))
.andExpect(status().isOk())
.andReturn();

// then
assertThat(result.getResponse().getContentAsString()).isEqualToIgnoringWhitespace(expected);
}
}
Loading

0 comments on commit 71e7da3

Please sign in to comment.