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: 과목 조회 API 구현 #5

Merged
merged 10 commits into from
Jan 25, 2025
2 changes: 2 additions & 0 deletions src/main/java/kr/allcll/seatfinder/subject/Subject.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PUBLIC)
public class Subject {
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/kr/allcll/seatfinder/subject/SubjectApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import kr.allcll.seatfinder.excel.SubjectSheetParser;
import kr.allcll.seatfinder.excel.SubjectsParsingResponse;
import kr.allcll.seatfinder.subject.dto.SubjectsRequest;
import kr.allcll.seatfinder.subject.dto.SubjectsResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -18,6 +20,24 @@ public class SubjectApi {
private final SubjectService subjectService;
private final SubjectSheetParser subjectSheetParser;

@GetMapping("/api/subjects")
public ResponseEntity<SubjectsResponse> getSubjects(
@RequestParam(required = false) Long subjectId,
@RequestParam(required = false) String subjectName,
@RequestParam(required = false) String subjectCode,
@RequestParam(required = false) String classCode,
@RequestParam(required = false) String professorName
) {
SubjectsResponse subjectsResponse = subjectService.findSubjectsByCondition(
subjectId,
subjectName,
subjectCode,
classCode,
professorName
);
return ResponseEntity.ok(subjectsResponse);
}

@PostMapping("/api/subject/upload")
public ResponseEntity<String> uploadSubjects(@RequestParam MultipartFile file) throws IOException {
SubjectsParsingResponse parsedSubjects = subjectSheetParser.parse(file);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package kr.allcll.seatfinder.subject;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface SubjectRepository extends JpaRepository<Subject, Long> {
public interface SubjectRepository extends JpaRepository<Subject, Long>, JpaSpecificationExecutor<Subject> {

}
33 changes: 33 additions & 0 deletions src/main/java/kr/allcll/seatfinder/subject/SubjectService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,50 @@

import java.util.List;
import kr.allcll.seatfinder.subject.dto.SubjectsRequest;
import kr.allcll.seatfinder.subject.dto.SubjectsResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

private final SubjectRepository subjectRepository;

@Transactional
public void save(SubjectsRequest subjectRequests) {
List<Subject> subjects = subjectRequests.toEntity();
subjectRepository.saveAll(subjects);
}

public SubjectsResponse findSubjectsByCondition(
Long subjectId,
String subjectName,
String subjectCode,
String classCode,
String professorName
) {
Specification<Subject> condition = getCondition(subjectId, subjectName, subjectCode, classCode, professorName);
List<Subject> subjects = subjectRepository.findAll(condition);
Copy link
Member

Choose a reason for hiding this comment

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

요게 만약 subjectName만 있다면 findBySubjectName을 사용했을 때와 같은 쿼리가 나가는지 궁금해지네용
findAll()에 인자 넣어서 하는 방식 첨봐서 신기해여

return SubjectsResponse.from(subjects);
}

private Specification<Subject> getCondition(
Long subjectId,
String subjectName,
String subjectCode,
String classCode,
String professorName
) {
return Specification.where(
SubjectSpecifications.hasSubjectId(subjectId))
.and(SubjectSpecifications.hasSubjectName(subjectName))
.and(SubjectSpecifications.hasSubjectCode(subjectCode))
.and(SubjectSpecifications.hasClassCode(classCode))
.and(SubjectSpecifications.hasProfessorName(professorName)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package kr.allcll.seatfinder.subject;

import org.springframework.data.jpa.domain.Specification;

public class SubjectSpecifications {

public static Specification<Subject> hasSubjectId(Long subjectId) {
return (root, query, builder) ->
subjectId == null ? null : builder.equal(root.get("id"), subjectId);
}

public static Specification<Subject> hasSubjectName(String subjectName) {
return (root, query, builder) ->
subjectName == null ? null : builder.equal(root.get("subjectName"), subjectName);
}

public static Specification<Subject> hasSubjectCode(String subjectCode) {
return (root, query, builder) ->
subjectCode == null ? null : builder.equal(root.get("subjectCode"), subjectCode);
}

public static Specification<Subject> hasClassCode(String classCode) {
return (root, query, builder) ->
classCode == null ? null : builder.equal(root.get("classCode"), classCode);
}

public static Specification<Subject> hasProfessorName(String professorName) {
return (root, query, builder) ->
professorName == null ? null : builder.equal(root.get("professorName"), professorName);
}
Comment on lines +5 to +30
Copy link
Member

Choose a reason for hiding this comment

The 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,60 @@
package kr.allcll.seatfinder.subject.dto;

import kr.allcll.seatfinder.subject.Subject;

public record SubjectResponse(
Long subjectId,
String offeringUniversity,
String offeringDepartment,
String subjectCode,
String classCode,
String subjectName,
String subjectType,
String electiveArea,
String academicYearSemester,
String credit,
String theoryHours,
String practiceHours,
String classType,
String exchangeEligible,
String schedule,
String lectureRoom,
String professorName,
String supervisingDepartment,
String notesAndRestrictions,
String classCategory,
String onlineLecture,
String lectureLanguage,
String foreignerOnly,
String domesticOnly
) {

public static SubjectResponse from(Subject subject) {
return new SubjectResponse(
subject.getId(),
subject.getOfferingUniversity(),
subject.getOfferingDepartment(),
subject.getSubjectCode(),
subject.getClassCode(),
subject.getSubjectName(),
subject.getSubjectType(),
subject.getElectiveArea(),
subject.getAcademicYearSemester(),
subject.getCredit(),
subject.getTheoryHours(),
subject.getPracticeHours(),
subject.getClassType(),
subject.getExchangeEligible(),
subject.getSchedule(),
subject.getLectureRoom(),
subject.getProfessorName(),
subject.getSupervisingDepartment(),
subject.getNotesAndRestrictions(),
subject.getClassCategory(),
subject.getOnlineLecture(),
subject.getLectureLanguage(),
subject.getForeignerOnly(),
subject.getDomesticOnly()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.allcll.seatfinder.subject.dto;

import java.util.List;
import kr.allcll.seatfinder.subject.Subject;

public record SubjectsResponse(
List<SubjectResponse> subjectResponses
) {

public static SubjectsResponse from(List<Subject> subjects) {
return new SubjectsResponse(subjects.stream()
.map(SubjectResponse::from)
.toList());
}
}
12 changes: 12 additions & 0 deletions src/test/java/kr/allcll/seatfinder/subject/SubjectApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
Expand Down Expand Up @@ -32,6 +33,17 @@ class SubjectApiTest {
@MockitoBean
private SubjectService subjectService;

@DisplayName("과목을 조회한다.")
@Test
void getSubjectsApiTest() throws Exception {
mockMvc.perform(get("/api/subjects")
.param("subjectId", "1")
.param("subjectName", "컴퓨터구조"))
.andExpect(status().isOk());

then(subjectService).should().findSubjectsByCondition(1L, "컴퓨터구조", null, null, null);
}

@DisplayName("과목 엑셀 파일을 업로드한다.")
@Test
void uploadSubjectsApiTest() throws Exception {
Expand Down
90 changes: 90 additions & 0 deletions src/test/java/kr/allcll/seatfinder/subject/SubjectServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package kr.allcll.seatfinder.subject;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;

import java.util.List;
import kr.allcll.seatfinder.subject.dto.SubjectResponse;
import kr.allcll.seatfinder.subject.dto.SubjectsResponse;
import kr.allcll.seatfinder.support.fixture.SubjectFixture;
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 SubjectServiceTest {

@Autowired
private SubjectService subjectService;

@Autowired
private SubjectRepository subjectRepository;

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

private void initializeSubjects() {
subjectRepository.saveAll(
List.of(
SubjectFixture.createSubject(null, "컴퓨터구조", "003278", "001", "유재석"),
SubjectFixture.createSubject(null, "운영체제", "003279", "001", "노홍철"),
SubjectFixture.createSubject(null, "자료구조", "003280", "001", "하하"),
SubjectFixture.createSubject(null, "알고리즘", "003281", "001", "길"),
SubjectFixture.createSubject(null, "컴퓨터구조", "003278", "002", "정형돈"),
SubjectFixture.createSubject(null, "운영체제", "003279", "002", "나영석"),
SubjectFixture.createSubject(null, "자료구조", "003280", "003", "박명수"),
SubjectFixture.createSubject(null, "알고리즘", "003281", "004", "전진")
)
);
}

@DisplayName("과목명으로 과목을 조회한다.")
@Test
void findSubjectByQueryTest() {
SubjectsResponse subjectsResponse = subjectService.findSubjectsByCondition(null, "컴퓨터구조", null, null, null);

assertThat(subjectsResponse.subjectResponses()).hasSize(2)
.extracting(
SubjectResponse::subjectName,
SubjectResponse::subjectCode,
SubjectResponse::classCode,
SubjectResponse::professorName
)
.containsExactly(
tuple("컴퓨터구조", "003278", "001", "유재석"),
tuple("컴퓨터구조", "003278", "002", "정형돈")
);
}

@DisplayName("학수번호, 분반, 교수명으로 과목을 조회한다.")
@Test
void findSubjectByQueryTest2() {
SubjectsResponse subjectsResponse = subjectService.findSubjectsByCondition(null, null, "003279", "001", "노홍철");

assertThat(subjectsResponse.subjectResponses()).hasSize(1)
.extracting(
SubjectResponse::subjectName,
SubjectResponse::subjectCode,
SubjectResponse::classCode,
SubjectResponse::professorName
)
.containsExactly(
tuple("운영체제", "003279", "001", "노홍철")
);
}

@DisplayName("존재하지 않는 조건으로 과목을 조회한다.")
@Test
void findSubjectByQueryTest3() {
SubjectsResponse subjectsResponse = subjectService.findSubjectsByCondition(100L, null, "003279", "001",
"유재석");

assertThat(subjectsResponse.subjectResponses()).hasSize(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kr.allcll.seatfinder.support.fixture;

import kr.allcll.seatfinder.subject.Subject;

public class SubjectFixture {

public static Subject createSubject(
Long subjectId,
String subjectName,
String subjectCode,
String classCode,
String professorName
) {
return new Subject(subjectId, "", "", subjectCode, classCode, subjectName, "", "", "", "", "", "", "", "", "",
"", professorName, "", "", "", "", "", "", "");
}
}
20 changes: 20 additions & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
spring:
datasource:
driver-class-name: org.h2.Driver
url: 'jdbc:h2:mem:test'
username: sa

h2:
console:
enabled: true
path: /h2-console

jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
format_sql: true
show_sql: true
Loading