diff --git a/build.gradle b/build.gradle index c66c10a..090b44d 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.3' id 'io.spring.dependency-management' version '1.1.7' + id 'org.jetbrains.kotlin.jvm' } group = 'com.example' @@ -36,6 +37,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j:8.0.33' implementation 'io.jsonwebtoken:jjwt-api:0.12.3' @@ -65,7 +67,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation 'com.chuseok22:sejong-portal-login:1.0.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation 'org.apache.poi:poi-ooxml:5.2.3' } tasks.named('test') { diff --git a/settings.gradle b/settings.gradle index 6204996..85fbffb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,6 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version '2.1.21' + } +} rootProject.name = 'SmartAir' diff --git a/src/main/java/com/example/enjoy/controller/HomeController.java b/src/main/java/com/example/enjoy/controller/HomeController.java index c4f2119..d54a626 100644 --- a/src/main/java/com/example/enjoy/controller/HomeController.java +++ b/src/main/java/com/example/enjoy/controller/HomeController.java @@ -5,9 +5,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -19,10 +18,8 @@ public class HomeController { private final TrackService trackService; @Operation(summary = "트랙 진행률 조회", description = "현재 학생의 전체 트랙 진행률을 조회합니다.") - @GetMapping("/home") - public List showMyProgress() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String currentStudentId = authentication.getName(); - return trackService.calculateTrackProgress(currentStudentId); + @GetMapping("/home/{studentId}") + public List showMyProgress(@PathVariable String studentId) { + return trackService.calculateTrackProgress(studentId); } } \ No newline at end of file diff --git a/src/main/java/com/example/enjoy/controller/StudentDataController.java b/src/main/java/com/example/enjoy/controller/StudentDataController.java index 664b4f4..37eb5ef 100644 --- a/src/main/java/com/example/enjoy/controller/StudentDataController.java +++ b/src/main/java/com/example/enjoy/controller/StudentDataController.java @@ -1,7 +1,43 @@ package com.example.enjoy.controller; -import org.springframework.web.bind.annotation.RestController; +import com.example.enjoy.dto.TrackProgressDto; +import com.example.enjoy.service.StudentDataService; +import com.example.enjoy.service.TrackService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; @RestController +@RequestMapping("/student-data") +@RequiredArgsConstructor public class StudentDataController { + + private final StudentDataService studentDataService; + private TrackService trackService; + + @Operation( + summary = "엑셀 파일 업로드", + description = "기이수 성적 엑셀 파일(.xlsx)을 업로드하고, 과목 정보를 서버에 저장합니다." + ) + @ApiResponse(responseCode = "200", description = "업로드 성공") + @PostMapping(value = "/upload/{studentId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResponseEntity> uploadCourseExcel( + @RequestParam("file") MultipartFile file, + @PathVariable String studentId + ) { + // 1. 엑셀 파싱 및 DB 저장 + studentDataService.parseAndSaveCourses(file, studentId); + + // 2. 트랙 진행률 계산 (업로드 직후 기준) + List progress = trackService.calculateTrackProgress(studentId); + + // 3. 진행률 반환 + return ResponseEntity.ok(progress); + } } diff --git a/src/main/java/com/example/enjoy/dto/ParsedCourseDto.java b/src/main/java/com/example/enjoy/dto/ParsedCourseDto.java new file mode 100644 index 0000000..c55c2e3 --- /dev/null +++ b/src/main/java/com/example/enjoy/dto/ParsedCourseDto.java @@ -0,0 +1,11 @@ +package com.example.enjoy.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class ParsedCourseDto { + private String courseName; + private StudentCourseStatus status; +} diff --git a/src/main/java/com/example/enjoy/dto/TrackProgressDto.java b/src/main/java/com/example/enjoy/dto/TrackProgressDto.java index 626ea96..7b46b8b 100644 --- a/src/main/java/com/example/enjoy/dto/TrackProgressDto.java +++ b/src/main/java/com/example/enjoy/dto/TrackProgressDto.java @@ -1,5 +1,6 @@ package com.example.enjoy.dto; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import java.util.List; @@ -9,6 +10,7 @@ */ @Getter @Setter +@AllArgsConstructor public class TrackProgressDto { private String trackName; // 트랙 이름 (예: "AI 콘텐츠") diff --git a/src/main/java/com/example/enjoy/service/StudentDataService.java b/src/main/java/com/example/enjoy/service/StudentDataService.java new file mode 100644 index 0000000..c148d43 --- /dev/null +++ b/src/main/java/com/example/enjoy/service/StudentDataService.java @@ -0,0 +1,66 @@ +package com.example.enjoy.service; + +import com.example.enjoy.dto.ParsedCourseDto; +import com.example.enjoy.dto.StudentCourseStatus; +import com.example.enjoy.entity.StudentCourse; +import com.example.enjoy.repository.StudentCourseRepository; +import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.*; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class StudentDataService { + + private final StudentCourseRepository studentCourseRepository; + + public void parseAndSaveCourses(MultipartFile file, String studentId) { + List parsedList = new ArrayList<>(); + + try (Workbook workbook = WorkbookFactory.create(file.getInputStream())) { + Sheet sheet = workbook.getSheetAt(0); + for (Row row : sheet) { + if (row.getRowNum() == 0) continue; + + Cell courseCell = row.getCell(4); // 교과목명 + Cell gradeCell = row.getCell(10); // 등급 + + if (courseCell == null || gradeCell == null) continue; + + String courseName = courseCell.getStringCellValue().trim(); + String grade = gradeCell.getStringCellValue().trim(); + + StudentCourseStatus status = mapGradeToStatus(grade); + + parsedList.add(new ParsedCourseDto(courseName, status)); + } + } catch (Exception e) { + throw new RuntimeException("엑셀 파일 파싱 실패: " + e.getMessage(), e); + } + + List courses = parsedList.stream() + .map(dto -> StudentCourse.builder() + .studentId(studentId) + .courseName(dto.getCourseName()) + .status(dto.getStatus()) + .manual(false) + .createdAt(LocalDateTime.now()) + .build()) + .toList(); + + studentCourseRepository.saveAll(courses); + } + + private StudentCourseStatus mapGradeToStatus(String grade) { + return switch (grade.toUpperCase()) { + case "A+", "A0", "B+", "B0", "C+", "C0", "P" -> StudentCourseStatus.COMPLETED; + case "F", "NP" -> StudentCourseStatus.FAILED; + default -> throw new IllegalArgumentException("잘못된 등급값: " + grade); + }; + } +} diff --git a/src/main/java/com/example/enjoy/service/TrackService.java b/src/main/java/com/example/enjoy/service/TrackService.java index ef11a9a..39ed361 100644 --- a/src/main/java/com/example/enjoy/service/TrackService.java +++ b/src/main/java/com/example/enjoy/service/TrackService.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -26,73 +25,38 @@ public class TrackService { private final TrackRepository trackRepository; private final StudentCourseRepository studentCourseRepository; // 기존 기능 - /** - * 모든 트랙 정보를 학과별로 그룹화하여 반환하는 메서드 - */ - public Map> getAllTracksGroupedByDepartment() { - // 1. DB에서 모든 트랙과 관련 과목들을 한번에 조회 - List allTracks = trackRepository.findAllWithCourses(); - - // 2. 조회된 트랙 리스트를 '학과' 이름으로 그룹화하여 Map으로 변환 후 반환 - return allTracks.stream() - .collect(Collectors.groupingBy(Track::getDepartment)); - } - - /** - * 학생이 이수한 과목 이름을 Set으로 반환하는 private 메서드 - */ public List calculateTrackProgress(String studentId) { - // 1. 학생의 이수 과목 목록 조회 - Set completedCourseNames = studentCourseRepository.findByStudentId(studentId) - .stream() - .map(StudentCourse::getCourseName) - .collect(Collectors.toSet()); - - // 2. 모든 트랙 정보 조회 - List allTracks = trackRepository.findAllWithCourses(); - - List progressList = new ArrayList<>(); - - // 3. 각 트랙별로 진행 현황 계산 - for (Track track : allTracks) { + Set completedCourseNames = getCompletedCourseNames(studentId); // 이수 과목명 목록 - // 현재 트랙에서 완료한 과목과 남은 과목을 담을 리스트 초기화 - List completedInThisTrack = new ArrayList<>(); - List remainingInThisTrack = new ArrayList<>(); + List allTracks = trackRepository.findAll(); - // 현재 트랙에 속한 모든 교과목을 하나씩 확인 - for (TrackCourse trackCourse : track.getCourses()) { + return allTracks.stream().map(track -> { + List courses = track.getCourses(); // 이 트랙의 모든 과목 + List completed = new ArrayList<>(); + List remaining = new ArrayList<>(); - // 학생이 해당 과목을 이수했는지 확인 (현재 이름 또는 과거 이름으로 체크) - if (completedCourseNames.contains(trackCourse.getCourseName()) || - (trackCourse.getCourseAlias() != null && completedCourseNames.contains(trackCourse.getCourseAlias()))) { - // 이수한 경우: 완료 리스트에 추가 - completedInThisTrack.add(new CourseDto(trackCourse.getCourseName(), trackCourse.getCourseAlias())); + for (TrackCourse course : courses) { + CourseDto dto = new CourseDto(course.getCourseName(), course.getCourseAlias()); + if (isCourseCompleted(course, completedCourseNames)) { + completed.add(dto); } else { - // 이수하지 않은 경우: 남은 과목 리스트에 추가 - remainingInThisTrack.add(new CourseDto(trackCourse.getCourseName(), trackCourse.getCourseAlias())); + remaining.add(dto); } } - // 최종 결과를 담을 DTO 객체 생성 및 데이터 세팅 - TrackProgressDto progressDto = new TrackProgressDto(); - progressDto.setTrackName(track.getName()); - progressDto.setDepartment(track.getDepartment()); - - int completedCount = completedInThisTrack.size(); - progressDto.setCompletedCount(completedCount); - progressDto.setRequiredCount(6); // 트랙 이수 요구 과목 수는 6개 - progressDto.setCompleted(completedCount >= 6); // 6개 이상이면 true - - progressDto.setCompletedCourses(completedInThisTrack); - progressDto.setRemainingCourses(remainingInThisTrack); + return new TrackProgressDto( + track.getName(), + track.getDepartment(), + completed.size(), + courses.size(), + completed.size() == courses.size(), + completed, + remaining + ); + }).toList(); + } - // 완성된 DTO를 최종 결과 리스트에 추가 - progressList.add(progressDto); - } - return progressList; - } /** * 학생이 이수한 과목 이름을 Set으로 반환하는 메서드