From 8acd83e54dc1ae69f004cd8ac0c5afd96c09755a Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Sun, 28 Sep 2025 11:40:27 +0900 Subject: [PATCH 1/4] =?UTF-8?q?:sparkles:=20(#322)=20pptx=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20pdf=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FileToMultipartFileConverter.java | 58 ++++++++++++++ .../converter/LibreOfficeConverter.java | 40 ++++++++++ .../service/LectureNoteServiceImpl.java | 77 ++++++++++++++----- 3 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 backend/src/main/java/org/example/backend/domain/lectureNote/converter/FileToMultipartFileConverter.java create mode 100644 backend/src/main/java/org/example/backend/domain/lectureNote/converter/LibreOfficeConverter.java diff --git a/backend/src/main/java/org/example/backend/domain/lectureNote/converter/FileToMultipartFileConverter.java b/backend/src/main/java/org/example/backend/domain/lectureNote/converter/FileToMultipartFileConverter.java new file mode 100644 index 00000000..c3d0e707 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/lectureNote/converter/FileToMultipartFileConverter.java @@ -0,0 +1,58 @@ +package org.example.backend.domain.lectureNote.converter; + +import org.springframework.web.multipart.MultipartFile; +import java.io.*; + +public class FileToMultipartFileConverter implements MultipartFile { + + private final File file; + private final String contentType; + + // file -> multipartFile 변환 + public FileToMultipartFileConverter(File file, String contentType) { + this.file = file; + this.contentType = contentType; + } + + @Override + public String getName() { + return file.getName(); + } + + @Override + public String getOriginalFilename() { + return file.getName(); + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return file.length() == 0; + } + + @Override + public long getSize() { + return file.length(); + } + + @Override + public byte[] getBytes() throws IOException { + return java.nio.file.Files.readAllBytes(file.toPath()); + } + + @Override + public InputStream getInputStream() throws IOException { + return new FileInputStream(file); + } + + @Override + public void transferTo(File dest) throws IOException { + try (InputStream in = getInputStream(); OutputStream out = new FileOutputStream(dest)) { + in.transferTo(out); + } + } +} diff --git a/backend/src/main/java/org/example/backend/domain/lectureNote/converter/LibreOfficeConverter.java b/backend/src/main/java/org/example/backend/domain/lectureNote/converter/LibreOfficeConverter.java new file mode 100644 index 00000000..5dcdc3f6 --- /dev/null +++ b/backend/src/main/java/org/example/backend/domain/lectureNote/converter/LibreOfficeConverter.java @@ -0,0 +1,40 @@ +package org.example.backend.domain.lectureNote.converter; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; + +@Component +public class LibreOfficeConverter { + + // pptx -> pdf 변환 + public File convertPptxToPdf(File pptxFile) throws IOException { + try { + String outputDir = pptxFile.getParent(); + + String baseName = pptxFile.getName().replaceAll("(?i)\\.pptx$", ""); + File outputPdf = new File(outputDir, baseName + ".pdf"); + + ProcessBuilder pb = new ProcessBuilder( + "libreoffice", "--headless", + "--convert-to", "pdf:writer_pdf_Export", + pptxFile.getAbsolutePath(), + "--outdir", outputDir + ); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new IOException("LibreOffice 변환 실패: exitCode=" + exitCode); + } + + if (!outputPdf.exists()) { + throw new IOException("LibreOffice 변환 실패: 출력 PDF 없음"); + } + + return outputPdf; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("LibreOffice 변환 중 인터럽트 발생", e); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/example/backend/domain/lectureNote/service/LectureNoteServiceImpl.java b/backend/src/main/java/org/example/backend/domain/lectureNote/service/LectureNoteServiceImpl.java index 19566053..5e98f862 100644 --- a/backend/src/main/java/org/example/backend/domain/lectureNote/service/LectureNoteServiceImpl.java +++ b/backend/src/main/java/org/example/backend/domain/lectureNote/service/LectureNoteServiceImpl.java @@ -5,6 +5,8 @@ import org.example.backend.domain.classroom.exception.ClassroomErrorCode; import org.example.backend.domain.classroom.exception.ClassroomException; import org.example.backend.domain.classroom.repository.ClassroomRepository; +import org.example.backend.domain.lectureNote.converter.FileToMultipartFileConverter; +import org.example.backend.domain.lectureNote.converter.LibreOfficeConverter; import org.example.backend.domain.lectureNote.dto.response.LectureNoteKeyResponseDTO; import org.example.backend.domain.lectureNote.dto.response.LectureNoteResponseDTO; import org.example.backend.domain.lectureNote.entity.LectureNote; @@ -17,6 +19,7 @@ import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -29,31 +32,67 @@ public class LectureNoteServiceImpl implements LectureNoteService { private final S3Service s3Service; private final LectureNoteRepository lectureNoteRepository; - private final ClassroomRepository classroomRepository; // ✅ 추가 + private final ClassroomRepository classroomRepository; private final LectureNoteMappingRepository lectureNoteMappingRepository; + private final LibreOfficeConverter libreOfficeConverter; public List uploadLectureNotes(UUID classId, List files) throws IOException { - // 1. 여러 파일에 대해 처리 List lectureNotes = new ArrayList<>(); - // 2. 각 파일 업로드 처리 for (MultipartFile file : files) { - // 1) S3에 업로드 - String key = "lecture_note/" + classId + "/" + UUID.randomUUID() + "/" + file.getOriginalFilename(); - String fileUrl = s3Service.uploadFile(file, key); - - // 2) classId로 Classroom 엔티티 조회 - Classroom classroom = classroomRepository.findById(classId) - .orElseThrow(() -> new ClassroomException(ClassroomErrorCode.CLASS_NOT_FOUND)); - - // 3) LectureNote 객체 생성 - LectureNote lectureNote = LectureNote.builder() - .noteUrl(key) - .classroom(classroom) // Classroom 객체 넣기 - .build(); - - // 4) LectureNote 저장 - lectureNotes.add(lectureNoteRepository.save(lectureNote)); + String originalFilename = Objects.requireNonNull(file.getOriginalFilename(), "파일명이 필요합니다"); + boolean isPptx = originalFilename.toLowerCase().endsWith(".pptx"); + + File tempSrc = null; + File convertedPdf = null; + String uploadFileName; + String key; + MultipartFile pdfMultipart; + + try { + if (isPptx) { + // MultipartFile -> File + tempSrc = new File(System.getProperty("java.io.tmpdir"), originalFilename); + file.transferTo(tempSrc); + + try { + // PPTX → PDF 변환 + convertedPdf = libreOfficeConverter.convertPptxToPdf(tempSrc); + + // File -> MultipartFile + pdfMultipart = new FileToMultipartFileConverter(convertedPdf, "application/pdf"); + } catch (Exception e) { + throw new IOException("PPTX→PDF 변환 실패: " + originalFilename, e); + } + + uploadFileName = convertedPdf.getName(); + + // S3 업로드 + key = "lecture_note/" + classId + "/" + UUID.randomUUID() + "/" + uploadFileName; + s3Service.uploadFile(pdfMultipart, key); + + } else { + // pptx 외 다른 파일들 그대로 업로드 + uploadFileName = originalFilename; + key = "lecture_note/" + classId + "/" + UUID.randomUUID() + "/" + uploadFileName; + s3Service.uploadFile(file, key); + } + + Classroom classroom = classroomRepository.findById(classId) + .orElseThrow(() -> new ClassroomException(ClassroomErrorCode.CLASS_NOT_FOUND)); + + LectureNote lectureNote = LectureNote.builder() + .noteUrl(key) + .classroom(classroom) + .build(); + + lectureNotes.add(lectureNoteRepository.save(lectureNote)); + + } finally { + // pdf로 변환하며 생긴 임시 파일 삭제 + if (tempSrc != null && tempSrc.exists()) tempSrc.delete(); + if (convertedPdf != null && convertedPdf.exists()) convertedPdf.delete(); + } } return lectureNotes; From 61ac4391a1ecbd833a3fc506bfbd0916e9c2a70e Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Sun, 28 Sep 2025 12:01:55 +0900 Subject: [PATCH 2/4] =?UTF-8?q?:sparkles:=20(#322)=20lecture=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0=20saveAudio,=20isLectureStart=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/example/backend/domain/lecture/entity/Lecture.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/org/example/backend/domain/lecture/entity/Lecture.java b/backend/src/main/java/org/example/backend/domain/lecture/entity/Lecture.java index 2260e5fa..6e7c2369 100644 --- a/backend/src/main/java/org/example/backend/domain/lecture/entity/Lecture.java +++ b/backend/src/main/java/org/example/backend/domain/lecture/entity/Lecture.java @@ -39,8 +39,10 @@ public class Lecture extends BaseEntity { @Column(name = "audio_url") private String audioUrl; + @Column(name = "is_lecture_start") private Boolean isLectureStart; + @Column(name = "save_audio") private Boolean saveAudio; @Column(name = "lecture_date", nullable = false) From 58398c39e46e3d4ae0ad67f2adbaf7dae0468ecc Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Sun, 28 Sep 2025 12:02:18 +0900 Subject: [PATCH 3/4] =?UTF-8?q?:sparkles:=20(#322)=20=EB=85=B9=EC=9D=8C?= =?UTF-8?q?=EB=B3=B8=20=EC=A0=80=EC=9E=A5=EC=8B=9C=20=EA=B0=95=EC=9D=98=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=EC=9C=BC=EB=A1=9C=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/lecture/service/LectureServiceImpl.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/org/example/backend/domain/lecture/service/LectureServiceImpl.java b/backend/src/main/java/org/example/backend/domain/lecture/service/LectureServiceImpl.java index 341b33ed..e4a03301 100644 --- a/backend/src/main/java/org/example/backend/domain/lecture/service/LectureServiceImpl.java +++ b/backend/src/main/java/org/example/backend/domain/lecture/service/LectureServiceImpl.java @@ -152,25 +152,28 @@ public void deleteLecture(UUID lectureId) { //녹음본 저장 public LectureRecordingResponseDTO uploadLectureRecording(UUID lectureId, MultipartFile file) { + Lecture lecture = lectureRepository.findById(lectureId) .orElseThrow(() -> new LectureException(LectureErrorCode.LECTURE_NOT_FOUND)); - String key = "recordings/" + lectureId +"/" + UUID.randomUUID() + "/" + file.getOriginalFilename(); + String safeLectureName = lecture.getLectureName().replaceAll("\\s+", "_"); + String uploadFileName = safeLectureName + ".mp3"; + + String key = "recordings/" + lectureId + "/" + UUID.randomUUID() + "/" + uploadFileName; try { - s3Service.uploadFile(file, key); // private 업로드 + s3Service.uploadFile(file, key); } catch (IOException e) { throw new S3Exception(S3ErrorCode.UPLOAD_FAIL); } lecture.setAudioUrl(key); lectureRepository.save(lecture); - - String audioName = key.substring(key.lastIndexOf('/') + 1); + lecture.setSaveAudio(true); return LectureRecordingResponseDTO.builder() .lectureId(lecture.getId()) - .audioName(audioName) + .audioName(uploadFileName) .audioUrl(s3Service.getPresignedUrl(key)) .build(); } From dc5d458808003ba13fabce7d9d4045ce12513e02 Mon Sep 17 00:00:00 2001 From: Haemin Kim Date: Sat, 11 Oct 2025 23:13:35 +0900 Subject: [PATCH 4/4] =?UTF-8?q?:sparkles:=20(#322)=20Dockerfile=EC=97=90?= =?UTF-8?q?=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 08fdb22a..2f25faee 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,9 @@ -FROM openjdk:17-jdk +FROM eclipse-temurin:17-jdk + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libreoffice \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* COPY build/libs/backend-0.0.1-SNAPSHOT.jar /app.jar