Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.security:spring-security-crypto'
implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.flywaydb:flyway-core'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
implementation 'com.github.ben-manes.caffeine:caffeine'
implementation 'org.apache.poi:poi:5.2.5'
implementation 'org.apache.poi:poi-ooxml:5.2.5'

Expand Down
34 changes: 34 additions & 0 deletions src/main/java/inu/timetable/config/CacheConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package inu.timetable.config;

import com.github.benmanes.caffeine.cache.Caffeine;
import inu.timetable.service.SubjectCacheNames;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.Duration;

@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager(
@Value("${subject.cache.maximum-size:1000}") long maximumSize,
@Value("${subject.cache.expire-after-write:10m}") String expireAfterWrite) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager(SubjectCacheNames.ALL.toArray(String[]::new));
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(parseDuration(expireAfterWrite))
.recordStats());
return cacheManager;
}

private Duration parseDuration(String value) {
return DurationStyle.detectAndParse(value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import inu.timetable.entity.Subject;
import inu.timetable.enums.ClassMethod;
import inu.timetable.enums.SubjectType;
import inu.timetable.event.SubjectDataChangedEvent;
import inu.timetable.repository.SubjectRepository;
import inu.timetable.service.AdminAccessGuard;
import inu.timetable.service.AdminOperationLockService;
Expand All @@ -15,6 +16,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -46,6 +48,7 @@ public class AdminSubjectController {
private final AdminOperationLockService adminOperationLockService;
private final SubjectAdminService subjectAdminService;
private final OfficialSubjectImportService officialSubjectImportService;
private final ApplicationEventPublisher eventPublisher;

@PostMapping
public ResponseEntity<SubjectManagementResponse> createSubject(
Expand Down Expand Up @@ -133,7 +136,11 @@ public List<Subject> addSubjectsManually(
subjects.add(subject);
}

return subjectRepository.saveAll(subjects);
List<Subject> savedSubjects = subjectRepository.saveAll(subjects);
if (!savedSubjects.isEmpty()) {
eventPublisher.publishEvent(new SubjectDataChangedEvent("manual-subject-import"));
}
return savedSubjects;
}

private List<Schedule> parseTime(String timeString) {
Expand Down
63 changes: 11 additions & 52 deletions src/main/java/inu/timetable/controller/SubjectController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

import inu.timetable.entity.Subject;
import inu.timetable.enums.SubjectType;
import inu.timetable.repository.SubjectRepository;
import inu.timetable.dto.SubjectDto;
import inu.timetable.dto.SubjectFilterCriteria;
import inu.timetable.repository.SubjectRepository;
import inu.timetable.service.SubjectQueryService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/subjects")
@RequiredArgsConstructor
public class SubjectController {

private final SubjectRepository subjectRepository;
private final SubjectQueryService subjectQueryService;

@GetMapping
public Page<Subject> getAllSubjects(
Expand All @@ -30,7 +32,7 @@ public Page<Subject> getAllSubjects(

@GetMapping("/count")
public long getCount() {
return subjectRepository.countByActiveTrue();
return subjectQueryService.countActiveSubjects();
}

@GetMapping("/type/{type}")
Expand All @@ -45,12 +47,12 @@ public List<Subject> getSubjectsByDepartment(@PathVariable String department) {

@GetMapping("/departments")
public List<String> getAllDepartments() {
return subjectRepository.findDistinctDepartments();
return subjectQueryService.findDistinctDepartments();
}

@GetMapping("/grades")
public List<Integer> getAllGrades() {
return subjectRepository.findDistinctGrades();
return subjectQueryService.findDistinctGrades();
}

@GetMapping("/grade/{grade}")
Expand All @@ -67,30 +69,14 @@ public List<Subject> getSubjectsByProfessor(@PathVariable String professor) {
public List<SubjectDto> searchSubjects(
@RequestParam String keyword,
@RequestParam(required = false) Integer grade) {
List<Subject> subjects;
if (grade != null) {
subjects = subjectRepository.findBySubjectNameContainingAndGradeAndActiveTrue(keyword, grade);
} else {
subjects = subjectRepository.findBySubjectNameContainingAndActiveTrue(keyword);
}
return subjects.stream()
.map(SubjectDto::from)
.collect(Collectors.toList());
return subjectQueryService.searchBySubjectName(keyword, grade);
}

@GetMapping("/search/professor")
public List<SubjectDto> searchByProfessor(
@RequestParam String keyword,
@RequestParam(required = false) Integer grade) {
List<Subject> subjects;
if (grade != null) {
subjects = subjectRepository.findByProfessorContainingAndGradeAndActiveTrue(keyword, grade);
} else {
subjects = subjectRepository.findByProfessorContainingAndActiveTrue(keyword);
}
return subjects.stream()
.map(SubjectDto::from)
.collect(Collectors.toList());
return subjectQueryService.searchByProfessor(keyword, grade);
}

@GetMapping("/filter")
Expand All @@ -107,36 +93,9 @@ public Page<SubjectDto> filterSubjects(
@RequestParam(required = false) Integer credits,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {

System.out.println(">>> [API] Received request for /filter with page=" + page + ", size=" + size);

Pageable pageable = PageRequest.of(page, size);

// 1단계: 필터로 과목 ID 조회 (페이지네이션 적용)
Page<Long> subjectIdPage = subjectRepository.findIdsWithFilters(
return subjectQueryService.filterSubjects(SubjectFilterCriteria.of(
subjectName, professor, department, dayOfWeek,
startTime, endTime, subjectType, grade, isNight, credits, pageable);

// 2단계: 조회된 ID로 과목 상세 정보와 시간표를 함께 조회
List<Long> subjectIds = subjectIdPage.getContent();
System.out.println(">>> [DB] Found " + subjectIds.size() + " subject IDs for this page.");

if (subjectIds.isEmpty()) {
System.out.println(">>> [API] Returning empty page.");
return new org.springframework.data.domain.PageImpl<>(new java.util.ArrayList<>(), pageable,
subjectIdPage.getTotalElements());
}

List<Subject> subjectsWithSchedules = subjectRepository.findWithSchedulesByIds(subjectIds);
System.out.println(">>> [API] Returning page with " + subjectsWithSchedules.size() + " subjects.");

// DTO로 변환
List<SubjectDto> subjectDtos = subjectsWithSchedules.stream()
.map(SubjectDto::from)
.collect(Collectors.toList());

// Page 객체는 유지하되, 내용물만 교체
return new org.springframework.data.domain.PageImpl<>(subjectDtos, pageable, subjectIdPage.getTotalElements());
startTime, endTime, subjectType, grade, isNight, credits, page, size));
}

}
53 changes: 53 additions & 0 deletions src/main/java/inu/timetable/dto/SubjectFilterCriteria.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package inu.timetable.dto;

import inu.timetable.enums.SubjectType;

public record SubjectFilterCriteria(
String subjectName,
String professor,
String department,
String dayOfWeek,
Double startTime,
Double endTime,
SubjectType subjectType,
Integer grade,
Boolean isNight,
Integer credits,
int page,
int size) {

public static SubjectFilterCriteria of(
String subjectName,
String professor,
String department,
String dayOfWeek,
Double startTime,
Double endTime,
SubjectType subjectType,
Integer grade,
Boolean isNight,
Integer credits,
int page,
int size) {
return new SubjectFilterCriteria(
trimToNull(subjectName),
trimToNull(professor),
trimToNull(department),
trimToNull(dayOfWeek),
startTime,
endTime,
subjectType,
grade,
isNight,
credits,
page,
size);
}

private static String trimToNull(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
}
15 changes: 15 additions & 0 deletions src/main/java/inu/timetable/dto/SubjectSearchCriteria.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package inu.timetable.dto;

public record SubjectSearchCriteria(String keyword, Integer grade) {

public static SubjectSearchCriteria of(String keyword, Integer grade) {
return new SubjectSearchCriteria(trim(keyword), grade);
}

private static String trim(String value) {
if (value == null) {
return null;
}
return value.trim();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package inu.timetable.event;

public record SubjectDataChangedEvent(String source) {
}
11 changes: 10 additions & 1 deletion src/main/java/inu/timetable/service/ExcelParseService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,19 @@
import inu.timetable.entity.Subject;
import inu.timetable.enums.ClassMethod;
import inu.timetable.enums.SubjectType;
import inu.timetable.event.SubjectDataChangedEvent;
import inu.timetable.repository.SubjectRepository;
import inu.timetable.repository.UserRepository;
import inu.timetable.repository.UserTimetableRepository;
import inu.timetable.repository.WishlistRepository;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.context.ApplicationEventPublisher;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
Expand All @@ -31,6 +38,7 @@ public class ExcelParseService {
private final WishlistRepository wishlistRepository;
private final UserTimetableRepository userTimetableRepository;
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;
private final WebClient webClient = WebClient.builder().build();
private final ObjectMapper objectMapper = new ObjectMapper();

Expand Down Expand Up @@ -124,6 +132,7 @@ public int parseAndSaveSubjectsIncremental(MultipartFile file) throws IOExceptio
if (!newSubjects.isEmpty()) {
List<Subject> savedSubjects = subjectRepository.saveAll(newSubjects);
System.out.println("성공적으로 저장된 과목 수: " + savedSubjects.size());
eventPublisher.publishEvent(new SubjectDataChangedEvent("excel-incremental-import"));
} else {
System.out.println("저장할 새로운 과목이 없습니다.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import inu.timetable.entity.Subject;
import inu.timetable.enums.ClassMethod;
import inu.timetable.enums.SubjectType;
import inu.timetable.event.SubjectDataChangedEvent;
import inu.timetable.repository.SubjectRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
Expand Down Expand Up @@ -44,6 +46,7 @@ public class OfficialSubjectImportService {
private static final String DAYS = "월화수목금토일";

private final SubjectRepository subjectRepository;
private final ApplicationEventPublisher eventPublisher;

@Transactional(readOnly = true)
public OfficialSubjectImportResponse preview(MultipartFile file, String semester) throws IOException {
Expand Down Expand Up @@ -80,6 +83,7 @@ public OfficialSubjectImportResponse apply(MultipartFile file, String semester,
}
}

eventPublisher.publishEvent(new SubjectDataChangedEvent("official-subject-import"));
return toResponse(true, normalizedSemester, records, diff, deactivateMissing);
}

Expand Down
8 changes: 7 additions & 1 deletion src/main/java/inu/timetable/service/PdfParseService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import inu.timetable.entity.Subject;
import inu.timetable.enums.ClassMethod;
import inu.timetable.enums.SubjectType;
import inu.timetable.event.SubjectDataChangedEvent;
import inu.timetable.repository.SubjectRepository;
import inu.timetable.repository.WishlistRepository;
import java.io.IOException;
Expand All @@ -25,6 +26,7 @@
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
Expand All @@ -37,6 +39,7 @@ public class PdfParseService {
private final SubjectRepository subjectRepository;
private final WishlistRepository wishlistRepository;
private final DepartmentMappingService departmentMappingService;
private final ApplicationEventPublisher eventPublisher;
private final WebClient webClient = WebClient.builder().build();
private final ObjectMapper objectMapper = new ObjectMapper();

Expand All @@ -46,10 +49,12 @@ public class PdfParseService {
@Autowired
public PdfParseService(SubjectRepository subjectRepository,
WishlistRepository wishlistRepository,
DepartmentMappingService departmentMappingService) {
DepartmentMappingService departmentMappingService,
ApplicationEventPublisher eventPublisher) {
this.subjectRepository = subjectRepository;
this.wishlistRepository = wishlistRepository;
this.departmentMappingService = departmentMappingService;
this.eventPublisher = eventPublisher;
}

/**
Expand Down Expand Up @@ -88,6 +93,7 @@ public int parseAndSaveSubjectsIncremental(MultipartFile file) throws IOExceptio
if (!newSubjects.isEmpty()) {
List<Subject> savedSubjects = subjectRepository.saveAll(newSubjects);
System.out.println("성공적으로 저장된 과목 수: " + savedSubjects.size());
eventPublisher.publishEvent(new SubjectDataChangedEvent("pdf-incremental-import"));
} else {
System.out.println("저장할 새로운 과목이 없습니다.");
}
Expand Down
Loading