From 5085e888b227d0fc70869a4876eb7d49dcc63a5f Mon Sep 17 00:00:00 2001 From: gimgisu Date: Wed, 10 Sep 2025 01:33:38 +0900 Subject: [PATCH] feat : Load Lecture Metadata (keyword, tags, pre_course) --- .../controller/LectureUploadController.java | 23 +++++++ .../codin/domain/lecture/dto/MetaMode.java | 8 +++ .../codin/domain/lecture/entity/Keyword.java | 21 +++++++ .../domain/lecture/entity/LectureKeyword.java | 23 +++++++ .../lecture/service/LectureUploadService.java | 63 +++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 src/main/java/inu/codin/codin/domain/lecture/dto/MetaMode.java create mode 100644 src/main/java/inu/codin/codin/domain/lecture/entity/Keyword.java create mode 100644 src/main/java/inu/codin/codin/domain/lecture/entity/LectureKeyword.java diff --git a/src/main/java/inu/codin/codin/domain/lecture/controller/LectureUploadController.java b/src/main/java/inu/codin/codin/domain/lecture/controller/LectureUploadController.java index c616c85..3a42568 100644 --- a/src/main/java/inu/codin/codin/domain/lecture/controller/LectureUploadController.java +++ b/src/main/java/inu/codin/codin/domain/lecture/controller/LectureUploadController.java @@ -1,8 +1,10 @@ package inu.codin.codin.domain.lecture.controller; +import inu.codin.codin.domain.lecture.dto.MetaMode; import inu.codin.codin.domain.lecture.service.LectureUploadService; import inu.codin.codin.global.common.response.SingleResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -49,5 +51,26 @@ public ResponseEntity> uploadNewSemesterRooms(@RequestParam("e } + @Operation( + summary = "강의 메타(키워드/태그/선수과목) 엑셀 업로드", + description = "'단과대약어_연도_학기_meta'로 설정하여 업로드 ex) info_25_2_meta.xlxs. 기본값은 모두 true (즉 전체 처리)." + ) + @PostMapping(value = "/meta", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_MANAGER')") + public ResponseEntity> uploadLectureMeta( + @Parameter(description = "업로드할 엑셀 파일 (.xlsx)") + @RequestParam("excelFile") MultipartFile file, + + @Parameter(description = "처리 모드 (ALL | KEYWORDS | TAGS | PRE_COURSES). 기본값: ALL") + @RequestParam(name = "mode", defaultValue = "ALL") MetaMode mode + ) { + lectureUploadService.uploadLectureMeta(file, mode); + return ResponseEntity.status(HttpStatus.CREATED) + .body(new SingleResponse<>(201, + file.getOriginalFilename()+"의 태그,키워드,선수 과목등 메타데이터 포함 엑셀파일 업데이트", + null)); + + } + } diff --git a/src/main/java/inu/codin/codin/domain/lecture/dto/MetaMode.java b/src/main/java/inu/codin/codin/domain/lecture/dto/MetaMode.java new file mode 100644 index 0000000..b4a7d06 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/lecture/dto/MetaMode.java @@ -0,0 +1,8 @@ +package inu.codin.codin.domain.lecture.dto; + +public enum MetaMode { + ALL, // 키워드+태그+선수과목 + KEYWORDS, // 키워드만 + TAGS, // 태그만 + PRE_COURSES // 선수과목만 +} \ No newline at end of file diff --git a/src/main/java/inu/codin/codin/domain/lecture/entity/Keyword.java b/src/main/java/inu/codin/codin/domain/lecture/entity/Keyword.java new file mode 100644 index 0000000..a948257 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/lecture/entity/Keyword.java @@ -0,0 +1,21 @@ +package inu.codin.codin.domain.lecture.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Keyword { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String keywordDescription; + + @OneToMany(mappedBy = "keyword", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + private List lectureKeywords; +} diff --git a/src/main/java/inu/codin/codin/domain/lecture/entity/LectureKeyword.java b/src/main/java/inu/codin/codin/domain/lecture/entity/LectureKeyword.java new file mode 100644 index 0000000..7aade15 --- /dev/null +++ b/src/main/java/inu/codin/codin/domain/lecture/entity/LectureKeyword.java @@ -0,0 +1,23 @@ +package inu.codin.codin.domain.lecture.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class LectureKeyword { + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "lecture_id") + private Lecture lecture; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "keyword_id") + private Keyword keyword; +} diff --git a/src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java b/src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java index 790d77f..44e0242 100644 --- a/src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java +++ b/src/main/java/inu/codin/codin/domain/lecture/service/LectureUploadService.java @@ -1,6 +1,7 @@ package inu.codin.codin.domain.lecture.service; import inu.codin.codin.domain.elasticsearch.indexer.LectureStartupIndexer; +import inu.codin.codin.domain.lecture.dto.MetaMode; import inu.codin.codin.domain.lecture.exception.LectureErrorCode; import inu.codin.codin.domain.lecture.exception.LectureUploadException; import lombok.RequiredArgsConstructor; @@ -12,6 +13,9 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; @Slf4j @Service @@ -28,6 +32,8 @@ public class LectureUploadService { private String ROOM_PROGRAM = "dayTimeOfRoom.py"; private String LECTURE_PROGRAM = "infoOfLecture.py"; + private static final String META_PROGRAM = "load_metadata.py"; + public void uploadNewSemesterLectures(MultipartFile file){ try { @@ -68,6 +74,63 @@ public void uploadNewSemesterRooms(MultipartFile file) { } } + public void uploadLectureMeta(MultipartFile file, MetaMode mode) { + try { + saveFile(file); // 기존 그대로 사용 + executeMetaLoader(file, mode); + log.info("[uploadLectureMeta] {} 메타 업데이트 완료", file.getOriginalFilename()); + + try { indexer.lectureIndex(); } catch (Exception e) { + log.warn("[uploadLectureMeta] 색인 갱신 경고: {}", e.getMessage()); + } + + } catch (LectureUploadException e) { + throw e; + } catch (Exception e) { + log.error(e.getMessage(), e); + throw new LectureUploadException(LectureErrorCode.LECTURE_UPLOAD_FAIL, e.getMessage()); + } + } + + + /** 메타 로더 실행 헬퍼: load_metadata.py [--keywords] [--tags] [--pre-courses] | --all */ + private void executeMetaLoader(MultipartFile file,MetaMode mode) { + String scriptPath = Paths.get(UPLOAD_DIR, META_PROGRAM).toString(); + String excelPath = Paths.get(UPLOAD_DIR, file.getOriginalFilename()).toString(); + + List cmd = new ArrayList<>(); + cmd.add(PYTHON_DIR); // ex) /usr/bin/python3 or venv/bin/python + cmd.add(scriptPath); // load_metadata.py 절대/상대 경로 + cmd.add(excelPath); // 업로드된 엑셀 파일 경로 + + switch (mode) { + case ALL -> cmd.add("--all"); + case KEYWORDS -> cmd.add("--keywords"); + case TAGS -> cmd.add("--tags"); + case PRE_COURSES -> cmd.add("--pre-courses"); + } + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); + + try { + Process p = pb.start(); + StringBuilder out = new StringBuilder(); + try (var br = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()))) { + String line; + while ((line = br.readLine()) != null) { + out.append(line).append('\n'); + log.debug("[Python Output] {}", line); + } + } + int exit = p.waitFor(); + if (exit != 0) { + throw new LectureUploadException(LectureErrorCode.LECTURE_UPLOAD_FAIL, out.toString()); + } + } catch (IOException | InterruptedException e) { + throw new LectureUploadException(LectureErrorCode.LECTURE_UPLOAD_FAIL, e.getMessage()); + } + } + private void saveFile(MultipartFile file) { String originalName = file.getOriginalFilename(); if (originalName == null || originalName.isBlank()) {