diff --git a/build.gradle b/build.gradle index 710d3bf1..63739e70 100644 --- a/build.gradle +++ b/build.gradle @@ -128,8 +128,12 @@ dependencies { // Apache POI의 XWPF(XML Word Processing Format) implementation 'org.apache.poi:poi-ooxml:5.2.5' + //thymeleaf + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + // audio encoder implementation "ws.schild:jave-all-deps:3.5.0" + } dependencyManagement { diff --git a/src/main/java/com/haru/api/domain/lastOpened/entity/UserDocumentLastOpened.java b/src/main/java/com/haru/api/domain/lastOpened/entity/UserDocumentLastOpened.java index ac93d08c..280549f1 100644 --- a/src/main/java/com/haru/api/domain/lastOpened/entity/UserDocumentLastOpened.java +++ b/src/main/java/com/haru/api/domain/lastOpened/entity/UserDocumentLastOpened.java @@ -40,6 +40,9 @@ public class UserDocumentLastOpened { @Column(name = "last_opened") private LocalDateTime lastOpened; + @Column(columnDefinition = "TEXT") + private String thumbnailKeyName; + public void updateLastOpened(LocalDateTime lastOpened) { this.lastOpened = lastOpened; } @@ -48,4 +51,8 @@ public void updateTitle(String title) { this.title = title; } + public void updateThumbnailKeyName(String thumbnailKeyName) { + this.thumbnailKeyName = thumbnailKeyName; + } + } diff --git a/src/main/java/com/haru/api/domain/snsEvent/controller/SnsEventController.java b/src/main/java/com/haru/api/domain/snsEvent/controller/SnsEventController.java index d0dfd92c..6cf6f77f 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/controller/SnsEventController.java +++ b/src/main/java/com/haru/api/domain/snsEvent/controller/SnsEventController.java @@ -131,30 +131,30 @@ public ApiResponse getSnsEvent( " 참여자 및 당첨자 리스트 다운로드 API입니다. Header에 access token을 넣고 Path Variable에는 snsEventId를 넣어 요청해주세요. Query String에는 다운로드 형식을 넣어주시고 다운로드 형식이 docx라면 리스트의 HTML을 Request Body에 넣어주세요." ) @PostMapping("/{snsEventId}/list/download") - public ResponseEntity downloadList( + public ApiResponse downloadList( @PathVariable String snsEventId, @RequestParam ListType listType, - @RequestParam Format format, - @RequestBody SnsEventRequestDTO.DownloadListRequest request + @RequestParam Format format ) { Long userId = SecurityUtil.getCurrentUserId(); - byte[] fileBytes = snsEventCommandService.downloadList( - userId, - Long.parseLong(snsEventId), - listType, - format, - request + return ApiResponse.onSuccess( + snsEventCommandService.downloadList( + userId, + Long.parseLong(snsEventId), + listType, + format + ) ); - String listTypefileName = listType == ListType.PARTICIPANT ? "참여자" : "당첨자"; - String filename = format == Format.PDF ? listTypefileName + ".pdf" : listTypefileName + ".docx"; - String contentType = format == Format.PDF - ? MediaType.APPLICATION_PDF_VALUE - : "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") - .contentType(MediaType.parseMediaType(contentType)) - .body(fileBytes); +// String listTypefileName = listType == ListType.PARTICIPANT ? "참여자" : "당첨자"; +// String filename = format == Format.PDF ? listTypefileName + ".pdf" : listTypefileName + ".docx"; +// String contentType = format == Format.PDF +// ? MediaType.APPLICATION_PDF_VALUE +// : "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; +// +// return ResponseEntity.ok() +// .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"") +// .contentType(MediaType.parseMediaType(contentType)) +// .body(fileBytes); } } diff --git a/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventRequestDTO.java b/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventRequestDTO.java index b6388fe3..127bced6 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventRequestDTO.java +++ b/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventRequestDTO.java @@ -36,12 +36,4 @@ public static class SnsCondition { public static class UpdateSnsEventRequest { private String title; } - - @Getter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class DownloadListRequest { - private String listHtml; - } } diff --git a/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventResponseDTO.java b/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventResponseDTO.java index 72e68d58..3640bc43 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventResponseDTO.java +++ b/src/main/java/com/haru/api/domain/snsEvent/dto/SnsEventResponseDTO.java @@ -109,4 +109,12 @@ public static class ParticipantResponse { public static class WinnerResponse { private String account; } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ListDownLoadLinkResponse { + private String downloadLink; + } } diff --git a/src/main/java/com/haru/api/domain/snsEvent/entity/SnsEvent.java b/src/main/java/com/haru/api/domain/snsEvent/entity/SnsEvent.java index da9775c1..ab816c2e 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/entity/SnsEvent.java +++ b/src/main/java/com/haru/api/domain/snsEvent/entity/SnsEvent.java @@ -42,6 +42,21 @@ public class SnsEvent extends BaseEntity { @JoinColumn(name = "workspace_id") private Workspace workspace; + @Column(columnDefinition = "TEXT") + private String keyNameParticipantPdf; + + @Column(columnDefinition = "TEXT") + private String keyNameParticipantWord; + + @Column(columnDefinition = "TEXT") + private String keyNameWinnerPdf; + + @Column(columnDefinition = "TEXT") + private String keyNameWinnerWord; + + @Column(columnDefinition = "TEXT") + private String thumbnailKey; + @OneToMany(mappedBy = "snsEvent", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List participantList = new ArrayList<>(); @@ -59,4 +74,19 @@ public void setWorkspace(Workspace workspace) { public void updateTitle(String title) { this.title = title; } + + public void updateKeyNameParticipantPdf( + String keyNameParticipantPdf, + String keyNameParicipantWord, + String keyNameWinnerPdf, + String keyNameWinnerWord) { + this.keyNameParticipantPdf = keyNameParticipantPdf; + this.keyNameParticipantWord = keyNameParicipantWord; + this.keyNameWinnerPdf = keyNameWinnerPdf; + this.keyNameWinnerWord = keyNameWinnerWord; + } + + public void updateThumbnailKey(String thumbnailKey) { + this.thumbnailKey = thumbnailKey; + } } diff --git a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandService.java b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandService.java index c14a9976..c7ab30ad 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandService.java +++ b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandService.java @@ -14,5 +14,5 @@ public interface SnsEventCommandService { void deleteSnsEvent(Long userId, Long snsEventId); - byte[] downloadList(Long userId, Long snsEventId, ListType listType, Format format, SnsEventRequestDTO.DownloadListRequest request); + SnsEventResponseDTO.ListDownLoadLinkResponse downloadList(Long userId, Long snsEventId, ListType listType, Format format); } diff --git a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandServiceImpl.java b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandServiceImpl.java index 76c8399a..13cb107d 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandServiceImpl.java +++ b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventCommandServiceImpl.java @@ -29,6 +29,8 @@ import com.haru.api.global.apiPayload.exception.handler.UserDocumentLastOpenedHandler; import com.haru.api.global.apiPayload.exception.handler.WorkspaceHandler; import com.haru.api.infra.api.restTemplate.InstagramOauth2RestTemplate; +import com.haru.api.infra.s3.AmazonS3Manager; +import com.haru.api.infra.s3.MarkdownFileUploader; import com.lowagie.text.Element; import com.lowagie.text.pdf.*; import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; @@ -42,6 +44,8 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; import java.io.*; import java.net.URL; @@ -56,6 +60,7 @@ @RequiredArgsConstructor public class SnsEventCommandServiceImpl implements SnsEventCommandService{ + private final SpringTemplateEngine templateEngine; @Value("${instagram.client.id}") private String instagramClientId; @Value("${instagram.client.secret}") @@ -73,8 +78,11 @@ public class SnsEventCommandServiceImpl implements SnsEventCommandService{ private final InstagramOauth2RestTemplate instagramOauth2RestTemplate; private final int WORD_TABLE_SIZE = 40; // 페이지당 총 아이디 수 private final int PER_COL = WORD_TABLE_SIZE/ 2; // 한쪽 컬럼에 들어갈 개수 + private final AmazonS3Manager amazonS3Manager; + private final MarkdownFileUploader markdownFileUploader; @Override + @Transactional public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent( Long workspaceId, SnsEventRequestDTO.CreateSnsRequest request @@ -93,10 +101,8 @@ public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent( // mood tracker 생성 시 last opened에 추가 // 마지막으로 연 시간은 null - UserDocumentId documentId = new UserDocumentId(foundUser.getId(), savedSnsEvent.getId(), DocumentType.SNS_EVENT_ASSISTANT); - - userDocumentLastOpenedRepository.save( + UserDocumentLastOpened savedUserDocumentLastOpened = userDocumentLastOpenedRepository.save( UserDocumentLastOpened.builder() .id(documentId) .user(foundUser) @@ -168,11 +174,112 @@ public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent( winnerList.add(winner); } winnerRepository.saveAll(winnerList); + + // PDF, DOCX파일 바이트 배열로 생성 및 썸네일 생성 & 업로드 / DB에 keyName저장 + createAndUploadListFileAndThumbnail( + request, + savedSnsEvent, + savedUserDocumentLastOpened + ); + return SnsEventResponseDTO.CreateSnsEventResponse.builder() .snsEventId(createdSnsEvent.getId()) .build(); } + private String createListHtml( + SnsEvent snsEvent, + ListType listType + ) { + if (listType == ListType.PARTICIPANT) { + List participantList = participantRepository.findAllBySnsEvent(snsEvent); + List leftList = new ArrayList<>(); + List rightList = new ArrayList<>(); + int total = participantList.size(); + int mid = (total + 1) / 2; + leftList = participantList.subList(0, mid); + rightList = participantList.subList(mid, total); + // Thymeleaf context에 데이터 세팅 + Context context = new Context(); + context.setVariable("leftList", leftList); + context.setVariable("rightList", rightList); + // 템플릿 렌더링 → HTML 문자열 생성 + return templateEngine.process("sns-event-list-pdf-template", context); + } else if (listType == ListType.WINNER) { + List winnerList = winnerRepository.findAllBySnsEvent(snsEvent); + List leftList = new ArrayList<>(); + List rightList = new ArrayList<>(); + int total = winnerList.size(); + int mid = (total + 1) / 2; + leftList = winnerList.subList(0, mid); + rightList = winnerList.subList(mid, total); + // Thymeleaf context에 데이터 세팅 + Context context = new Context(); + context.setVariable("leftList", leftList); + context.setVariable("rightList", rightList); + // 템플릿 렌더링 → HTML 문자열 생성 + return templateEngine.process("sns-event-list-pdf-template", context); + } else { + throw new SnsEventHandler(SNS_EVENT_WRONG_FORMAT); + } + + } + + private void createAndUploadListFileAndThumbnail( + SnsEventRequestDTO.CreateSnsRequest request, + SnsEvent snsEvent, + UserDocumentLastOpened userDocumentLastOpened + ){ + String listHtmlParticipant = createListHtml(snsEvent, ListType.PARTICIPANT); + String listHtmlWinner = createListHtml(snsEvent, ListType.WINNER); + byte[] pdfBytesParticipant; + byte[] pdfBytesWinner; + byte[] docxBytesParticipant; + byte[] docxBytesWinner; + try { + // 폰트 경로 + URL resource = getClass().getClassLoader().getResource("templates/NotoSansKR-Regular.ttf"); + File reg = new File(resource.toURI()); // catch에서 Exception 따로 처리해주기 + listHtmlParticipant = injectPageMarginStyle(listHtmlParticipant); + listHtmlWinner = injectPageMarginStyle(listHtmlWinner); + byte[] shiftedPdfBytesParticipant = convertHtmlToPdf(listHtmlParticipant, reg); + byte[] shiftedPdfBytesWinner = convertHtmlToPdf(listHtmlWinner, reg); + pdfBytesParticipant = addPdfTitle(shiftedPdfBytesParticipant, request.getTitle(), reg.getAbsolutePath()); + pdfBytesWinner = addPdfTitle(shiftedPdfBytesWinner, request.getTitle(), reg.getAbsolutePath()); + docxBytesParticipant = createWord(ListType.PARTICIPANT, request.getTitle(), snsEvent); + docxBytesWinner = createWord(ListType.WINNER, request.getTitle(), snsEvent ); + } catch (Exception e) { + log.error("Error creating document: {}", e.getMessage()); + throw new SnsEventHandler(SNS_EVENT_DOWNLOAD_LIST_ERROR); + } + // PDF, DOCS파일, 썸네일 S3에 업로드 및 DB에 keyName저장 + String fullPath = "sns-event/" + snsEvent.getId(); + String keyNameParicipantPdf = amazonS3Manager.generateKeyName(fullPath) + "." + "pdf"; + String keyNameParicipantWord = amazonS3Manager.generateKeyName(fullPath) + "." + "docx"; + String keyNameWinnerPdf = amazonS3Manager.generateKeyName(fullPath) + "." + "pdf"; + String keyNameWinnerWord = amazonS3Manager.generateKeyName(fullPath) + "." + "docx"; + amazonS3Manager.uploadFile(keyNameParicipantPdf, pdfBytesParticipant, "application/pdf"); + amazonS3Manager.uploadFile(keyNameWinnerPdf, pdfBytesWinner, "application/pdf"); + amazonS3Manager.uploadFile(keyNameParicipantWord, docxBytesParticipant, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + amazonS3Manager.uploadFile(keyNameWinnerWord, docxBytesWinner, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + // SNS 이벤트에 keyName 저장 + snsEvent.updateKeyNameParticipantPdf( + keyNameParicipantPdf, + keyNameParicipantWord, + keyNameWinnerPdf, + keyNameWinnerWord + ); + // SNS 이벤트 당첨자 PDF의 첫페이지 썸네일로 S3에 업로드 + String thumbnailKey = markdownFileUploader.createOrUpdateThumbnailForSnsEvent( + pdfBytesWinner, + "sns-event", + null + ); + snsEvent.updateThumbnailKey(thumbnailKey); + // UserDocumentLastOpened Entity에도 thmbnailKeyName추가 + userDocumentLastOpened.updateThumbnailKeyName(thumbnailKey); + } + private SnsEventResponseDTO.InstagramMediaResponse fetchInstagramMedia( String accessToken ) { @@ -345,36 +452,56 @@ public void deleteSnsEvent( } @Override - public byte[] downloadList( + public SnsEventResponseDTO.ListDownLoadLinkResponse downloadList( Long userId, Long snsEventId, ListType listType, - Format format, - SnsEventRequestDTO.DownloadListRequest request + Format format ) { + String downloadLink = ""; User foundUser = userRepository.findById(userId) .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - String pdfTitle = snsEventRepository.findById(snsEventId) - .orElseThrow(() -> new SnsEventHandler(SNS_EVENT_NOT_FOUND)) - .getTitle(); - try { + SnsEvent foundSnsEvent = snsEventRepository.findById(snsEventId) + .orElseThrow(() -> new SnsEventHandler(SNS_EVENT_NOT_FOUND)); + String snsEventTitle = foundSnsEvent.getTitle(); + if (listType == ListType.PARTICIPANT) { if (format == Format.PDF) { - // 폰트 경로 - URL resource = getClass().getClassLoader().getResource("templates/NotoSansKR-Regular.ttf"); - File reg = new File(resource.toURI()); // catch에서 Exception 따로 처리해주기 - String listHtml = injectHead(request.getListHtml()); - listHtml = injectPageMarginStyle(listHtml); - byte[] shiftedPdfByte = convertHtmlToPdf(listHtml, reg); - return addPdfTitle(shiftedPdfByte, pdfTitle, reg.getAbsolutePath()); + String keyName = foundSnsEvent.getKeyNameParticipantPdf(); + if (keyName == null || keyName.isEmpty()) { + throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); + } + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_participnat_list.pdf"); + } else if (format == Format.DOCX) { + String keyName = foundSnsEvent.getKeyNameParticipantWord(); + if (keyName == null || keyName.isEmpty()) { + throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); + } + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_participnat_list.docx"); + } else { + throw new SnsEventHandler(SNS_EVENT_WRONG_FORMAT); } - else if (format == Format.DOCX) { - return createWord(listType, pdfTitle); + } else if (listType == ListType.WINNER) { + if (format == Format.PDF) { + String keyName = foundSnsEvent.getKeyNameWinnerPdf(); + if (keyName == null || keyName.isEmpty()) { + throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); + } + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_winner_list.pdf"); + } else if (format == Format.DOCX) { + String keyName = foundSnsEvent.getKeyNameWinnerWord(); + if (keyName == null || keyName.isEmpty()) { + throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); + } + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_winner_list.docx"); + } else { + throw new SnsEventHandler(SNS_EVENT_WRONG_FORMAT); } - else throw new SnsEventHandler(SNS_EVENT_WRONG_FORMAT); - } catch (Exception e) { - log.error("Error creating document: {}", e.getMessage()); - throw new SnsEventHandler(SNS_EVENT_DOWNLOAD_LIST_ERROR); + } else { + throw new SnsEventHandler(SNS_EVENT_WRONG_LIST_TYPE); } + return SnsEventResponseDTO.ListDownLoadLinkResponse.builder() + .downloadLink(downloadLink) + .build(); } private String injectHead(String html) { @@ -403,7 +530,7 @@ private String injectPageMarginStyle(String html) { margin-bottom: 80pt; } @page :first { - margin-top: 100pt; /* 첫 페이지만 위 여백 크게 */ + margin-top: 90pt; /* 첫 페이지만 위 여백 크게 */ } """; @@ -447,8 +574,8 @@ private byte[] addPdfTitle(byte[] pdfBytes, String text, String fontPath) throws over.setFontAndSize(bf, 28f); // 글씨 크게 (28pt) // 페이지 폭 중앙 계산 float x = reader.getPageSize(i).getWidth() / 2; - // 페이지 상단에서 약간 내려오게 (40pt 여백) - float y = reader.getPageSize(i).getTop() - 60f; + // 페이지 상단에서 약간 내려오게 (70pt 여백) + float y = reader.getPageSize(i).getTop() - 70f; over.showTextAligned(Element.ALIGN_CENTER, text, x, y, 0); over.endText(); } @@ -457,16 +584,16 @@ private byte[] addPdfTitle(byte[] pdfBytes, String text, String fontPath) throws return out.toByteArray(); } - private byte[] createWord(ListType listType, String listTitle) throws Exception { // 참여자 또는 당첨자 리스트 DB에서 가져와 표로 만들어 word로 변환해서 응답주기 + private byte[] createWord(ListType listType, String listTitle, SnsEvent snsEvent) throws Exception { // 참여자 또는 당첨자 리스트 DB에서 가져와 표로 만들어 word로 변환해서 응답주기 List list = new ArrayList<>(); if (listType == ListType.PARTICIPANT) { - List participantList = participantRepository.findAll(); + List participantList = participantRepository.findAllBySnsEvent(snsEvent); for (Participant participant : participantList) { list.add(participant.getNickname()); } return createTable(list, listTitle); } else { - List winnerList = winnerRepository.findAll(); + List winnerList = winnerRepository.findAllBySnsEvent(snsEvent); for (Winner winner : winnerList) { list.add(winner.getNickname()); } diff --git a/src/main/java/com/haru/api/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/com/haru/api/global/apiPayload/code/status/ErrorStatus.java index 8d32b1d6..c532554b 100644 --- a/src/main/java/com/haru/api/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/com/haru/api/global/apiPayload/code/status/ErrorStatus.java @@ -73,6 +73,8 @@ public enum ErrorStatus implements BaseErrorCode { SNS_EVENT_INSTAGRAM_ALREADY_LINKED(HttpStatus.BAD_REQUEST, "SNS_EVENT4008", "이미 연동된 인스타그램 계정입니다."), SNS_EVENT_WRONG_FORMAT(HttpStatus.BAD_REQUEST, "SNS_EVENT4009", "잘못된 다운로드 파일 형식입니다."), SNS_EVENT_DOWNLOAD_LIST_ERROR(HttpStatus.BAD_REQUEST, "SNS_EVENT4010", "리스트 다운로드중 오류가 발생했습니다."), + SNS_EVENT_LIST_KEYNAME_NOT_FOUND(HttpStatus.BAD_REQUEST, "SNS_EVENT4011", "리스트 다운로드중 키 이름이 존재하지 않습니다."), + SNS_EVENT_WRONG_LIST_TYPE(HttpStatus.BAD_REQUEST, "SNS_EVENT4012", "리스트 다운로드중 잘못된 리스트 타입(참여자, 당첨자)입니다."), // last opened 관련 에러 USER_DOCUMENT_LAST_OPENED_NOT_FOUND(HttpStatus.NOT_FOUND, "LASTOPENED4001", "해당 문서에 대한 마지막 조회 데이터가 존재하지 않습니다."), diff --git a/src/main/java/com/haru/api/infra/s3/AmazonS3Manager.java b/src/main/java/com/haru/api/infra/s3/AmazonS3Manager.java index 29719015..777268aa 100644 --- a/src/main/java/com/haru/api/infra/s3/AmazonS3Manager.java +++ b/src/main/java/com/haru/api/infra/s3/AmazonS3Manager.java @@ -85,6 +85,25 @@ public String generatePresignedUrl(String keyName) { return presignedRequest.url().toString(); } + // 프론트로 수정한 파일명으로 다운로드 가능한 url을 보내기 위해 사용하는 메서드 + public String generatePresignedUrlForDownloadPdfAndWord(String keyName, String fileName) { + if (keyName == null || keyName.isBlank()) return null; + + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(amazonConfig.getBucket()) + .key(keyName) + .responseContentDisposition("attachment; filename=\"" + fileName + "\"") // S3파일(PDF) 링크 클릭 시 즉시 다운가능,파일 다운로드 시 이름 설정 + .build(); + + GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(10)) + .getObjectRequest(getObjectRequest) + .build(); + + PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest); + return presignedRequest.url().toString(); + } + //S3에서 key에 해당하는 파일을 다운로드하여 byte 배열로 반환 -> 썸네일 생성시 활용 public byte[] downloadFile(String keyName) { if (keyName == null || keyName.isBlank()) { diff --git a/src/main/java/com/haru/api/infra/s3/MarkdownFileUploader.java b/src/main/java/com/haru/api/infra/s3/MarkdownFileUploader.java index 263dc376..c5a081b2 100644 --- a/src/main/java/com/haru/api/infra/s3/MarkdownFileUploader.java +++ b/src/main/java/com/haru/api/infra/s3/MarkdownFileUploader.java @@ -83,4 +83,28 @@ public String createOrUpdateThumbnail(String pdfKey, String featurePath, String // 5. 사용된 썸네일의 key를 반환 return thumbnailKeyToUse; } + + public String createOrUpdateThumbnailForSnsEvent(byte[] pdfBytes, String featurePath, String existingThumbnailKey) { + // 1. 다운로드한 PDF로 새로운 썸네일 데이터 생성 + byte[] newThumbnailBytes = thumbnailGeneratorService.generate(pdfBytes); + + // 2. 사용할 썸네일 키 결정 + String thumbnailKeyToUse; + if (existingThumbnailKey != null && !existingThumbnailKey.isBlank()) { + // 기존 키가 제공되면, 그 키를 그대로 사용하여 갱신(덮어쓰기) + thumbnailKeyToUse = existingThumbnailKey; + log.info("기존 썸네일 갱신을 시작합니다. Key: {}", thumbnailKeyToUse); + } else { + // 기존 키가 없으면, 새로운 키를 생성 + thumbnailKeyToUse = amazonS3Manager.generateKeyName("thumbnails/" + featurePath) + "." + IMAGE_FORMAT; + log.info("새로운 썸네일 생성을 시작합니다. New Key: {}", thumbnailKeyToUse); + } + + // 3. 결정된 키로 썸네일을 S3에 업로드 + amazonS3Manager.uploadFile(thumbnailKeyToUse, newThumbnailBytes, "image/" + IMAGE_FORMAT); + log.info("썸네일 업로드/갱신 성공. Key: {}", thumbnailKeyToUse); + + // 4. 사용된 썸네일의 key를 반환 + return thumbnailKeyToUse; + } } diff --git a/src/main/resources/templates/sns-event-list-pdf-template.html b/src/main/resources/templates/sns-event-list-pdf-template.html new file mode 100644 index 00000000..44f3b4ef --- /dev/null +++ b/src/main/resources/templates/sns-event-list-pdf-template.html @@ -0,0 +1,79 @@ + + + + + SNS 이벤트 PDF 리스트 HTML 템플릿 + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
번호ID번호ID
+ +