Skip to content
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: 즐겨찾기 기능을 구현한다. #50

Merged
merged 18 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
public enum AllcllErrorCode {

PIN_LIMIT_EXCEEDED("이미 %d개의 핀을 등록했습니다."),
STAR_LIMIT_EXCEEDED("이미 %d개의 즐겨찾기를 등록했습니다."),
DUPLICATE_PIN("%s은(는) 이미 핀 등록된 과목입니다."),
DUPLICATE_STAR("%s은(는) 이미 핀 등록된 과목입니다."),
PIN_SUBJECT_MISMATCH("핀에 등록된 과목이 아닙니다."),
TOKEN_NOT_FOUND("쿠키에 토큰이 존재하지 않습니다."),
SUBJECT_NOT_FOUND("존재하지 않는 과목 입니다.");
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 = "star")
@Entity
@Getter
@NoArgsConstructor
public class Star {

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

@Column(name = "token")
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;
}
}
21 changes: 21 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,21 @@
package kr.allcll.seatfinder.star;

import kr.allcll.seatfinder.ThreadLocalHolder;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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();
}
}
12 changes: 12 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,12 @@
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);
}
39 changes: 39 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,39 @@
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.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 = 10;

private final StarRepository starRepository;
private final SubjectRepository subjectRepository;

@Transactional
public void addStarOnSubject(Long subjectId, String token) {
List<Star> starsByToken = starRepository.findAllByToken(token);
Subject subject = subjectRepository.findById(subjectId)
.orElseThrow(() -> new AllcllException(AllcllErrorCode.SUBJECT_NOT_FOUND));
validateCanAddStar(starsByToken, subject, token);
starRepository.save(new Star(token, subject));
}

private void validateCanAddStar(List<Star> userStars, Subject subject, String token) {
if (userStars.size() >= 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());
}
}
}
95 changes: 95 additions & 0 deletions src/test/java/kr/allcll/seatfinder/star/StarServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package kr.allcll.seatfinder.star;

import static kr.allcll.seatfinder.support.fixture.SubjectFixture.createSubject;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.List;
import kr.allcll.seatfinder.exception.AllcllErrorCode;
import kr.allcll.seatfinder.exception.AllcllException;
import kr.allcll.seatfinder.subject.Subject;
import kr.allcll.seatfinder.subject.SubjectRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
class StarServiceTest {

private static final int MAX_STAR_NUMBER = 10;
private static final String TOKEN = "token";

@Autowired
private StarService starService;

@Autowired
private SubjectRepository subjectRepository;

@Autowired
private StarRepository starRepository;

@BeforeEach
void setUp() {
starRepository.deleteAllInBatch();
subjectRepository.deleteAllInBatch();
}

@Test
@DisplayName("즐겨찾기 5개 미만일 경우 정상 등록을 검증한다.")
void addStarOnProject() {
// given
Subject subjectA = createSubject("컴퓨터구조", "003278", "001", "김보예");
subjectRepository.save(subjectA);
starService.addStarOnSubject(subjectA.getId(), TOKEN);

// when
List<Star> result = starRepository.findAllByToken(TOKEN);

// then
assertThat(result).hasSize(1);
}

@Test
@DisplayName("즐겨찾기가 5개 이상일 경우 예외를 검증한다.")
void canNotAddStarOnSubject() {
// given
Subject subjectA = createSubject("컴퓨터구조", "003278", "001", "김보예");
Subject subjectB = createSubject("운영체제", "003279", "001", "김수민");
Subject subjectC = createSubject("자료구조", "003280", "001", "김봉케");
Subject subjectD = createSubject("알고리즘", "003281", "001", "오현지");
Subject subjectE = createSubject("컴퓨터구조", "003278", "002", "전유채");
Subject overCountSubject = createSubject("컴퓨터구조", "003278", "002", "전유채");
subjectRepository.saveAll(List.of(subjectA, subjectB, subjectC, subjectD, subjectE, overCountSubject));
starRepository.saveAll(
List.of(
new Star(TOKEN, subjectA),
new Star(TOKEN, subjectB),
new Star(TOKEN, subjectC),
new Star(TOKEN, subjectD),
new Star(TOKEN, subjectE))
);

// when, then
assertThatThrownBy(() -> starService.addStarOnSubject(overCountSubject.getId(), TOKEN))

Check failure on line 76 in src/test/java/kr/allcll/seatfinder/star/StarServiceTest.java

View workflow job for this annotation

GitHub Actions / JUnit Test Report

StarServiceTest.즐겨찾기가 5개 이상일 경우 예외를 검증한다.

java.lang.AssertionError: Expecting code to raise a throwable.
Raw output
java.lang.AssertionError: 
Expecting code to raise a throwable.
	at kr.allcll.seatfinder.star.StarServiceTest.canNotAddStarOnSubject(StarServiceTest.java:76)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
.isInstanceOf(AllcllException.class)
.hasMessageContaining(String.format(AllcllErrorCode.STAR_LIMIT_EXCEEDED.getMessage(), MAX_STAR_NUMBER));
}

@Test
@DisplayName("이미 즐겨찾기 등록된 과목일 경우 예외를 검증한다.")
void alreadyExistStarSubject() {
// given
Subject subject = createSubject("컴퓨터구조", "003278", "001", "김보예");
subjectRepository.save(subject);
starRepository.save(new Star(TOKEN, subject));
String expectExceptionMessage = new AllcllException(AllcllErrorCode.DUPLICATE_STAR, "컴퓨터구조").getMessage();

// when, then
assertThatThrownBy(() -> starService.addStarOnSubject(subject.getId(), TOKEN))
.isInstanceOf(AllcllException.class)
.hasMessageContaining(expectExceptionMessage);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expectedExceptionMessage를 문자열 하드코딩하는 것은 어떨까요? 위랑 다르네요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위와 다르네요 가 어떤 말일까요..?
어느 위를 말하는걸까요???
어떤 말을 하는지 잘 이해가 안가는데 다시 한번 설명해주실 수 있으실까용

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바로 위에 있는 테스트 78번 라인을 보면

.hasMessageContaining(String.format(AllcllErrorCode.STAR_LIMIT_EXCEEDED.getMessage(), MAX_STAR_NUMBER));

라고 되어 있어요. 하지만 이 테스트는 Exception 객체를 생성하고 거기에서 예외 메시지를 뽑아내고 있어요. 일관성이 안 맞아서 남긴 코멘트였어요.

}
}
Loading