diff --git a/src/main/java/com/haru/api/domain/moodTracker/controller/MoodTrackerController.java b/src/main/java/com/haru/api/domain/moodTracker/controller/MoodTrackerController.java index 552073d5..fb739eb7 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/controller/MoodTrackerController.java +++ b/src/main/java/com/haru/api/domain/moodTracker/controller/MoodTrackerController.java @@ -48,7 +48,7 @@ public ApiResponse getMoodTrackerPreviewList @Parameter(hidden = true) @AuthWorkspace Workspace workspace ) { - MoodTrackerResponseDTO.PreviewList result = moodTrackerQueryService.getMoodTrackerPreviewList(user, workspace); + MoodTrackerResponseDTO.PreviewList result = moodTrackerQueryService.getPreviewList(user, workspace); return ApiResponse.onSuccess(result); @@ -145,7 +145,7 @@ public ApiResponse sendMoodTrackerSurveyLink( }) public ApiResponse submitMoodTrackerSurveyAnswers( @PathVariable("mood-tracker-hashed-Id") String moodTrackerHashedId, - @RequestBody MoodTrackerRequestDTO.SurveyAnswerList request, + @Valid @RequestBody MoodTrackerRequestDTO.SurveyAnswerList request, @Parameter(hidden = true) @AuthMoodTracker MoodTracker moodTracker ) { @@ -155,6 +155,26 @@ public ApiResponse submitMoodTrackerSurveyAnswers( } + @GetMapping("/{mood-tracker-hashed-Id}") + @Operation( + summary = "분위기 트래커 설문 팀분위기 베이스 정보 조회 API", + description = "# [v1.0 (2025-08-19)](https://www.notion.so/2545da7802c580dd9742d971d3a4bc08?source=copy_link) 분위기 트래커(moodTrackerId)에 대한 베이스 정보를 조회합니다." + ) + @Parameters({ + @Parameter(name = "mood-tracker-hashed-Id", description = "분위기 트래커 ID (Hashed, Path Variable)", required = true) + }) + public ApiResponse getMoodTrackerBaseResult( + @PathVariable(name = "mood-tracker-hashed-Id") String moodTrackerHashedId, + @Parameter(hidden = true) @AuthUser User user, + @Parameter(hidden = true) @AuthMoodTracker MoodTracker moodTracker + ) { + + MoodTrackerResponseDTO.BaseResult result = moodTrackerQueryService.getBaseResult(user, moodTracker); + + return ApiResponse.onSuccess(result); + + } + @GetMapping("/{mood-tracker-hashed-Id}/questions") @Operation( summary = "분위기 트래커 설문 문항 조회 API", diff --git a/src/main/java/com/haru/api/domain/moodTracker/converter/MoodTrackerConverter.java b/src/main/java/com/haru/api/domain/moodTracker/converter/MoodTrackerConverter.java index 13e7ac3b..d679e356 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/converter/MoodTrackerConverter.java +++ b/src/main/java/com/haru/api/domain/moodTracker/converter/MoodTrackerConverter.java @@ -158,6 +158,23 @@ public static List toCheckboxChoiceAnswerList( return answers; } + /** + * 분위기 트래커 설문 Base 정보 변환 + */ + public static MoodTrackerResponseDTO.BaseResult toBaseResultDTO(MoodTracker moodTracker, HashIdUtil hashIdUtil) { + return MoodTrackerResponseDTO.BaseResult.builder() + .workspaceId(moodTracker.getWorkspace().getId()) + .moodTrackerHashedId(hashIdUtil.encode(moodTracker.getId())) + .title(moodTracker.getTitle()) + .creatorId(moodTracker.getCreator().getId()) + .creatorName(moodTracker.getCreator().getName()) + .updatedAt(moodTracker.getUpdatedAt()) + .dueDate(moodTracker.getDueDate()) + .respondentsNum(moodTracker.getRespondentsNum()) + .build(); + } + + /** * 분위기 트래커 리포트 DTO 변환 */ diff --git a/src/main/java/com/haru/api/domain/moodTracker/dto/MoodTrackerRequestDTO.java b/src/main/java/com/haru/api/domain/moodTracker/dto/MoodTrackerRequestDTO.java index e2cd96ce..8d153f46 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/dto/MoodTrackerRequestDTO.java +++ b/src/main/java/com/haru/api/domain/moodTracker/dto/MoodTrackerRequestDTO.java @@ -8,9 +8,7 @@ import com.haru.api.global.util.json.ToLongListDeserializer; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.*; import lombok.*; import org.springframework.validation.annotation.Validated; @@ -31,6 +29,7 @@ public static class CreateRequest { private String description; @NotNull + @Future private LocalDateTime dueDate; @NotNull diff --git a/src/main/java/com/haru/api/domain/moodTracker/repository/CheckboxChoiceRepository.java b/src/main/java/com/haru/api/domain/moodTracker/repository/CheckboxChoiceRepository.java index f17cb890..0082bf23 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/repository/CheckboxChoiceRepository.java +++ b/src/main/java/com/haru/api/domain/moodTracker/repository/CheckboxChoiceRepository.java @@ -4,6 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface CheckboxChoiceRepository extends JpaRepository { + // 특정 질문(questionId)에 속한 여러 선택지(ids) 조회 + List findAllByIdInAndSurveyQuestionId(List ids, Long questionId); } diff --git a/src/main/java/com/haru/api/domain/moodTracker/repository/MultipleChoiceRepository.java b/src/main/java/com/haru/api/domain/moodTracker/repository/MultipleChoiceRepository.java index ee945cd6..0b8b4ff8 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/repository/MultipleChoiceRepository.java +++ b/src/main/java/com/haru/api/domain/moodTracker/repository/MultipleChoiceRepository.java @@ -4,6 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface MultipleChoiceRepository extends JpaRepository { + // 특정 질문(questionId)에 속한 특정 선택지(id)만 조회 + Optional findByIdAndSurveyQuestionId(Long id, Long questionId); } diff --git a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerCommandServiceImpl.java b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerCommandServiceImpl.java index 785ad404..0f245e22 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerCommandServiceImpl.java +++ b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerCommandServiceImpl.java @@ -1,6 +1,5 @@ package com.haru.api.domain.moodTracker.service; -import com.haru.api.domain.lastOpened.repository.UserDocumentLastOpenedRepository; import com.haru.api.domain.lastOpened.service.UserDocumentLastOpenedService; import com.haru.api.domain.moodTracker.converter.MoodTrackerConverter; import com.haru.api.domain.moodTracker.dto.MoodTrackerRequestDTO; @@ -108,7 +107,6 @@ public void updateTitle( MoodTracker moodTracker, MoodTrackerRequestDTO.UpdateTitleRequest request ) { - UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceIdAndUserId(moodTracker.getWorkspace().getId(), user.getId()) .orElseThrow(() -> new UserWorkspaceHandler(ErrorStatus.USER_WORKSPACE_NOT_FOUND)); @@ -117,8 +115,17 @@ public void updateTitle( || moodTracker.getCreator().getId().equals(user.getId()))) throw new MoodTrackerHandler(ErrorStatus.MOOD_TRACKER_MODIFY_NOT_ALLOWED); + // 엔티티 업데이트 moodTracker.updateTitle(request.getTitle()); - + moodTrackerRepository.save(moodTracker); + + // 마감일 이후 && 썸네일이 생성된 시점이라면, + if(moodTracker.getDueDate().isBefore(LocalDateTime.now()) && moodTracker.getThumbnailKey()!=null) { + // 기존 썸네일 및 다운로드 파일 삭제 + moodTrackerReportService.deleteReportFileAndThumbnail(moodTracker.getId()); + // S3에서 썸네일 및 다운로드 파일 업데이트 + moodTrackerReportService.updateAndUploadReportFileAndThumbnail(moodTracker.getId()); + } } /** @@ -131,7 +138,6 @@ public void delete( User user, MoodTracker moodTracker ) { - UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceIdAndUserId(moodTracker.getWorkspace().getId(), user.getId()) .orElseThrow(() -> new UserWorkspaceHandler(ErrorStatus.USER_WORKSPACE_NOT_FOUND)); @@ -140,8 +146,14 @@ public void delete( || moodTracker.getCreator().getId().equals(user.getId()))) throw new MoodTrackerHandler(ErrorStatus.MOOD_TRACKER_MODIFY_NOT_ALLOWED); - moodTrackerRepository.delete(moodTracker); + // 마감일 이후 && 썸네일이 생성된 시점이라면, + if(moodTracker.getDueDate().isBefore(LocalDateTime.now()) && moodTracker.getThumbnailKey()!=null) { + // S3에서 썸네일 및 다운로드 파일 삭제 + moodTrackerReportService.deleteReportFileAndThumbnail(moodTracker.getId()); + } + // 엔티티 삭제 + moodTrackerRepository.delete(moodTracker); } /** @@ -171,6 +183,11 @@ public void submitSurveyAnswers( MoodTracker moodTracker, MoodTrackerRequestDTO.SurveyAnswerList request ) { + // 마감일 이후이면 답변 불가능 + if(moodTracker.getDueDate().isBefore(LocalDateTime.now())){ + throw new MoodTrackerHandler(ErrorStatus.MOOD_TRACKER_FINISHED); + } + List subjectiveAnswers = new ArrayList<>(); List multipleChoiceAnswers = new ArrayList<>(); List checkboxChoiceAnswers = new ArrayList<>(); @@ -193,16 +210,25 @@ public void submitSurveyAnswers( switch (dto.getType()) { case MULTIPLE_CHOICE -> { - // 선택지 엔티티 조회 후 추가 - MultipleChoice foundMultipleChoice = multipleChoiceRepository.findById(dto.getMultipleChoiceId()) - .orElseThrow(); + // 질문 id와 선택지 id 함께 객관식 선택지 엔티티 조회 후 추가 + MultipleChoice foundMultipleChoice = multipleChoiceRepository + .findByIdAndSurveyQuestionId(dto.getMultipleChoiceId(), dto.getQuestionId()) + .orElseThrow(() -> new MoodTrackerHandler(ErrorStatus.INVALID_CHOICE_FOR_QUESTION)); + multipleChoiceAnswers.add( MoodTrackerConverter.toMultipleChoiceAnswer(foundMultipleChoice) ); } case CHECKBOX_CHOICE -> { - // 체크박스 선택지 엔티티 목록 조회 후 추가 - List foundCheckboxChoices = checkboxChoiceRepository.findAllById(dto.getCheckboxChoiceIdList()); + // 질문 id와 선택지 id 함께 체크박스 선택지 엔티티 목록 조회 후 추가 + List foundCheckboxChoices = checkboxChoiceRepository + .findAllByIdInAndSurveyQuestionId(dto.getCheckboxChoiceIdList(), dto.getQuestionId()); + + // 요청 개수와 조회 개수가 다르면 → 유효하지 않은 선택지 포함 + if (foundCheckboxChoices.size() != dto.getCheckboxChoiceIdList().size()) { + throw new MoodTrackerHandler(ErrorStatus.INVALID_CHOICE_FOR_QUESTION); + } + checkboxChoiceAnswers.addAll( MoodTrackerConverter.toCheckboxChoiceAnswerList(foundCheckboxChoices) ); diff --git a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryService.java b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryService.java index fe7a7641..ec232ffb 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryService.java +++ b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryService.java @@ -6,7 +6,9 @@ import com.haru.api.domain.workspace.entity.Workspace; public interface MoodTrackerQueryService { - MoodTrackerResponseDTO.PreviewList getMoodTrackerPreviewList(User user, Workspace workspace); + MoodTrackerResponseDTO.PreviewList getPreviewList(User user, Workspace workspace); + + MoodTrackerResponseDTO.BaseResult getBaseResult(User user, MoodTracker moodTracker); MoodTrackerResponseDTO.QuestionResult getQuestionResult(User user, MoodTracker moodTracker); diff --git a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryServiceImpl.java b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryServiceImpl.java index 6638a7d1..4ac6b189 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryServiceImpl.java +++ b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerQueryServiceImpl.java @@ -38,7 +38,7 @@ public class MoodTrackerQueryServiceImpl implements MoodTrackerQueryService { private final SurveyQuestionRepository surveyQuestionRepository; @Override - public MoodTrackerResponseDTO.PreviewList getMoodTrackerPreviewList(User user, Workspace workspace) { + public MoodTrackerResponseDTO.PreviewList getPreviewList(User user, Workspace workspace) { UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceIdAndUserId(workspace.getId(), user.getId()) .orElseThrow(() -> new UserWorkspaceHandler(ErrorStatus.USER_WORKSPACE_NOT_FOUND)); @@ -64,6 +64,32 @@ public MoodTrackerResponseDTO.PreviewList getMoodTrackerPreviewList(User user, W return previewList; } + @Override + @Transactional(readOnly = true) + @TrackLastOpened + public MoodTrackerResponseDTO.BaseResult getBaseResult(User user, MoodTracker moodTracker) { + + // 워크스페이스 권한 조회 + UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceIdAndUserId( + moodTracker.getWorkspace().getId(), user.getId() + ).orElseThrow(() -> new UserWorkspaceHandler(ErrorStatus.USER_WORKSPACE_NOT_FOUND)); + + // 권한 검증 + boolean hasAccess = + // 워크스페이스 생성자 + foundUserWorkspace.getAuth().equals(Auth.ADMIN) + // 해당 MoodTracker 생성자 + || moodTracker.getCreator().getId().equals(user.getId()) + // 공개된 설문 + || moodTracker.getVisibility().equals(MoodTrackerVisibility.PUBLIC); + + if (!hasAccess) { + throw new MoodTrackerHandler(ErrorStatus.MOOD_TRACKER_ACCESS_DENIED); + } + + return MoodTrackerConverter.toBaseResultDTO(moodTracker, hashIdUtil); + } + @Override @Transactional(readOnly = true) @TrackLastOpened diff --git a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportService.java b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportService.java index 9e5451b7..270c3dc9 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportService.java +++ b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportService.java @@ -9,6 +9,12 @@ public interface MoodTrackerReportService { void generateAndUploadReportFileAndThumbnail( Long moodTrackerId ); + void updateAndUploadReportFileAndThumbnail( + Long moodTrackerId + ); + void deleteReportFileAndThumbnail( + Long moodTrackerId + ); String generateDownloadLink( MoodTracker moodTracker, Format format diff --git a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportServiceImpl.java b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportServiceImpl.java index 103b7f10..a9c4dcfa 100644 --- a/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportServiceImpl.java +++ b/src/main/java/com/haru/api/domain/moodTracker/service/MoodTrackerReportServiceImpl.java @@ -10,7 +10,6 @@ import com.haru.api.infra.api.client.ChatGPTClient; import com.haru.api.infra.s3.AmazonS3Manager; import com.haru.api.infra.s3.MarkdownFileUploader; -import com.lowagie.text.pdf.BaseFont; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.xwpf.usermodel.ParagraphAlignment; @@ -201,15 +200,11 @@ public String generateDownloadLink( return downloadLink; } - @Override - public void generateAndUploadReportFileAndThumbnail(Long moodTrackerId){ - - // 리포트 생성 - generateReport(moodTrackerId); - + private void uploadReportFileAndThumbnail(Long moodTrackerId){ MoodTracker foundMoodTracker = moodTrackerRepository.findById(moodTrackerId) .orElseThrow(() -> new MoodTrackerHandler(ErrorStatus.MOOD_TRACKER_NOT_FOUND)); + // 리포트 파일 생성 byte[] pdfReportBytes; byte[] docxReportBytes; @@ -251,9 +246,9 @@ public void generateAndUploadReportFileAndThumbnail(Long moodTrackerId){ amazonS3Manager.uploadFile(pdfReportKey, pdfReportBytes, "application/pdf"); amazonS3Manager.uploadFile(wordReportKey, docxReportBytes, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); - foundMoodTracker.updateReportKeyName(pdfReportKey, wordReportKey); + // 리포트 썸네일 생성 String thumbnailKey = markdownFileUploader.createOrUpdateThumbnailWithPdfBytes( pdfReportBytes, "mood-tracker", @@ -262,6 +257,31 @@ public void generateAndUploadReportFileAndThumbnail(Long moodTrackerId){ foundMoodTracker.updateThumbnailKey(thumbnailKey); } + @Override + public void generateAndUploadReportFileAndThumbnail(Long moodTrackerId){ + // 리포트 생성 + generateReport(moodTrackerId); + + // 리포트 파일 및 썸네일 업데이트 + uploadReportFileAndThumbnail(moodTrackerId); + } + + @Override + public void updateAndUploadReportFileAndThumbnail(Long moodTrackerId) { + // 리포트 파일 및 썸네일 업데이트 + uploadReportFileAndThumbnail(moodTrackerId); + } + + @Override + public void deleteReportFileAndThumbnail(Long moodTrackerId) { + MoodTracker foundMoodTracker = moodTrackerRepository.findById(moodTrackerId) + .orElseThrow(() -> new MoodTrackerHandler(ErrorStatus.MOOD_TRACKER_NOT_FOUND)); + + amazonS3Manager.deleteFile(foundMoodTracker.getPdfReportKey()); + amazonS3Manager.deleteFile(foundMoodTracker.getWordReportKey()); + amazonS3Manager.deleteFile(foundMoodTracker.getThumbnailKey()); + } + /* ========================= 헬퍼들 ========================= */ // PDF: 제목 + 메타(작성자/마감일) + 본문(마크다운) → 템플릿 렌더링 후 HTML→PDF 변환 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 0496e02c..1ea75f12 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 @@ -146,10 +146,7 @@ public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent( SnsEvent savedSnsEvent = snsEventRepository.save(createdSnsEvent); // PDF, DOCX파일 바이트 배열로 생성 및 썸네일 생성 & 업로드 / DB에 keyName저장 - String thumbnailKeyName = createAndUploadListFileAndThumbnail( - request, - savedSnsEvent - ); + String thumbnailKeyName = createAndUploadListFileAndThumbnail(savedSnsEvent); // sns event 썸네일 key name 초기화 savedSnsEvent.initThumbnailKeyName(thumbnailKeyName); @@ -216,8 +213,13 @@ public void updateSnsEventTitle( } snsEvent.updateTitle(request.getTitle()); - snsEventRepository.save(snsEvent); + SnsEvent savedSnsEvent = snsEventRepository.save(snsEvent); + // S3문서 제목, S3 문서내 제목, 썸네일 이미지의 제목 변경 + deleteS3FileAndThumnailImage(savedSnsEvent); + String thumbnailKeyName = createAndUploadListFileAndThumbnail(savedSnsEvent); + // sns event 썸네일 key name 초기화 + savedSnsEvent.initThumbnailKeyName(thumbnailKeyName); } @Override @@ -236,7 +238,8 @@ public void deleteSnsEvent( throw new SnsEventHandler(SNS_EVENT_NO_AUTHORITY); } snsEventRepository.delete(snsEvent); - + // S3의 문서 및 썸네일 이미지 삭제 + deleteS3FileAndThumnailImage(snsEvent); } @Override @@ -255,13 +258,13 @@ public SnsEventResponseDTO.ListDownLoadLinkResponse downloadList( if (keyName == null || keyName.isEmpty()) { throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); } - downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_participnat_list.pdf"); + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_참여자_리스트.pdf"); } else if (format == Format.DOCX) { String keyName = snsEvent.getKeyNameParticipantWord(); if (keyName == null || keyName.isEmpty()) { throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); } - downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_participnat_list.docx"); + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_참여자_리스트.docx"); } else { throw new SnsEventHandler(SNS_EVENT_WRONG_FORMAT); } @@ -271,13 +274,13 @@ public SnsEventResponseDTO.ListDownLoadLinkResponse downloadList( if (keyName == null || keyName.isEmpty()) { throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); } - downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_winner_list.pdf"); + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_당첨자_리스트.pdf"); } else if (format == Format.DOCX) { String keyName = snsEvent.getKeyNameWinnerWord(); if (keyName == null || keyName.isEmpty()) { throw new SnsEventHandler(SNS_EVENT_LIST_KEYNAME_NOT_FOUND); } - downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_winner_list.docx"); + downloadLink = amazonS3Manager.generatePresignedUrlForDownloadPdfAndWord(keyName, snsEventTitle + "_당첨자_리스트.docx"); } else { throw new SnsEventHandler(SNS_EVENT_WRONG_FORMAT); } @@ -324,13 +327,9 @@ private String createListHtml( } else { throw new SnsEventHandler(SNS_EVENT_WRONG_FORMAT); } - } - private String createAndUploadListFileAndThumbnail( - SnsEventRequestDTO.CreateSnsRequest request, - SnsEvent snsEvent - ){ + private String createAndUploadListFileAndThumbnail(SnsEvent snsEvent){ String listHtmlParticipant = createListHtml(snsEvent, ListType.PARTICIPANT); String listHtmlWinner = createListHtml(snsEvent, ListType.WINNER); byte[] pdfBytesParticipant; @@ -352,10 +351,10 @@ private String createAndUploadListFileAndThumbnail( listHtmlWinner = fileConvertHelper.injectPageMarginStyle(listHtmlWinner); byte[] shiftedPdfBytesParticipant = fileConvertHelper.convertHtmlToPdf(listHtmlParticipant, fontBytes); byte[] shiftedPdfBytesWinner = fileConvertHelper.convertHtmlToPdf(listHtmlWinner, fontBytes); - pdfBytesParticipant = addPdfTitle(shiftedPdfBytesParticipant, request.getTitle(), fontBytes); - pdfBytesWinner = addPdfTitle(shiftedPdfBytesWinner, request.getTitle(), fontBytes); - docxBytesParticipant = createWord(ListType.PARTICIPANT, request.getTitle(), snsEvent); - docxBytesWinner = createWord(ListType.WINNER, request.getTitle(), snsEvent ); + pdfBytesParticipant = addPdfTitle(shiftedPdfBytesParticipant, snsEvent.getTitle() + " 참여자 리스트", fontBytes); + pdfBytesWinner = addPdfTitle(shiftedPdfBytesWinner, snsEvent.getTitle() + " 당첨자 리스트", fontBytes); + docxBytesParticipant = createWord(ListType.PARTICIPANT, snsEvent.getTitle() + " 참여자 리스트", snsEvent); + docxBytesWinner = createWord(ListType.WINNER, snsEvent.getTitle() + " 당첨자 리스트", snsEvent ); } catch (Exception e) { log.error("Error creating document: {}", e.getMessage()); throw new SnsEventHandler(SNS_EVENT_DOWNLOAD_LIST_ERROR); @@ -487,7 +486,7 @@ private byte[] addPdfTitle(byte[] pdfBytes, String text, byte[] fontBytes) throw for (int i = 1; i <= totalPages; i++) { PdfContentByte over = stamper.getOverContent(i); over.beginText(); - over.setFontAndSize(bf, 28f); // 글씨 크게 (28pt) + over.setFontAndSize(bf, 26f); // 글씨 크게 (28pt) // 페이지 폭 중앙 계산 float x = reader.getPageSize(i).getWidth() / 2; // 페이지 상단에서 약간 내려오게 (70pt 여백) @@ -631,4 +630,12 @@ private void addTitle(XWPFDocument doc, String titleText, int fontSize) { run.setBold(true); // 굵게 run.addBreak(); // 제목과 표 사이 한 줄 띄움 } + + private void deleteS3FileAndThumnailImage(SnsEvent snsEvent) { + amazonS3Manager.deleteFile(snsEvent.getKeyNameParticipantPdf()); + amazonS3Manager.deleteFile(snsEvent.getKeyNameParticipantWord()); + amazonS3Manager.deleteFile(snsEvent.getKeyNameWinnerPdf()); + amazonS3Manager.deleteFile(snsEvent.getKeyNameWinnerWord()); + amazonS3Manager.deleteFile(snsEvent.getThumbnailKeyName()); + } } diff --git a/src/main/java/com/haru/api/domain/user/service/UserCommandServiceImpl.java b/src/main/java/com/haru/api/domain/user/service/UserCommandServiceImpl.java index 74e6e9a5..3def7432 100644 --- a/src/main/java/com/haru/api/domain/user/service/UserCommandServiceImpl.java +++ b/src/main/java/com/haru/api/domain/user/service/UserCommandServiceImpl.java @@ -181,7 +181,7 @@ public UserResponseDTO.LoginResponse signupAndLoginAndInviteAccept(UserRequestDT userRepository.save(user); if(token != null) { - workspaceCommandService.acceptInvite(token); + workspaceCommandService.acceptInvite(token, user); } return login(UserRequestDTO.LoginRequest.builder() diff --git a/src/main/java/com/haru/api/domain/workspace/service/WorkspaceCommandServiceImpl.java b/src/main/java/com/haru/api/domain/workspace/service/WorkspaceCommandServiceImpl.java index 5e735803..e0b47af2 100644 --- a/src/main/java/com/haru/api/domain/workspace/service/WorkspaceCommandServiceImpl.java +++ b/src/main/java/com/haru/api/domain/workspace/service/WorkspaceCommandServiceImpl.java @@ -106,6 +106,7 @@ public WorkspaceResponseDTO.Workspace updateWorkspace(User user, Workspace works } @Override + @Transactional public WorkspaceResponseDTO.InvitationAcceptResult acceptInvite(String token) { WorkspaceInvitation foundWorkspaceInvitation = workspaceInvitationRepository.findByToken(token) 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 607921c6..41e8f903 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 @@ -64,6 +64,8 @@ public enum ErrorStatus implements BaseErrorCode { MOOD_TRACKER_WRONG_FORMAT(HttpStatus.BAD_REQUEST, "MOODTRACKER4007", "분위기 트래커의 잘못된 다운로드 파일 형식입니다."), MOOD_TRACKER_DOWNLOAD_ERROR(HttpStatus.BAD_REQUEST, "MOODTRACKER4008", "분위기 트래커 다운로드중 오류가 발생했습니다."), MOOD_TRACKER_KEYNAME_NOT_FOUND(HttpStatus.BAD_REQUEST, "MOODTRACKER4009", "분위기 트래커 다운로드중 키 이름이 존재하지 않습니다."), + MOOD_TRACKER_FINISHED(HttpStatus.BAD_REQUEST, "MOODTRACKER4010", "분위기 트래커 마감일 이후입니다."), + INVALID_CHOICE_FOR_QUESTION(HttpStatus.BAD_REQUEST, "MOODTRACKER4011", "분위기 트래커 질문에 유효하지 않은 선택지입니다."), // 메일 관련 에러 MAIL_SEND_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "MAIL500", "이메일 전송에 실패했습니다."), diff --git a/src/main/java/com/haru/api/infra/api/client/ChatGPTClient.java b/src/main/java/com/haru/api/infra/api/client/ChatGPTClient.java index 89451a01..99f23930 100644 --- a/src/main/java/com/haru/api/infra/api/client/ChatGPTClient.java +++ b/src/main/java/com/haru/api/infra/api/client/ChatGPTClient.java @@ -97,15 +97,16 @@ public Mono getAIQuestions(String userMessageContent) { public String getMoodTrackerReportRaw(String userMessageContent) { StringBuilder sb = new StringBuilder(); - sb.append("너는 팀 심리 및 조직 문화 분석가야. 아래의 설문 응답을 통해 전체 설문을 종합한 마크다운 형식의 분석 리포트를 작성하고, 설문 질문 별로 개선 제안을 각 1개씩 제시해줘.\n\n"); + sb.append("너는 팀 심리 및 조직 문화 분석가야. 아래의 설문 응답을 통해 전체 설문을 종합한 마크다운 형식의 분석 리포트를 작성하고, 설문 질문 별로 개선 제안을 각 1개씩 제시해줘. 아래의 형식에서 ###이 붙은 제목은 고정이고, 내용은 너가 채워주면 돼.\n\n"); - sb.append("💡 최종 리포트 형식은 다음과 같아야 합니다:\n"); - sb.append("1. {title} + 리포트\n"); + sb.append("💡 최종 리포트 형식은 무조건 다음과 같은 마크다운 형식이어야만 합니다:\n"); + sb.append("### 1. 팀 분위기 트래커 리포트\n"); sb.append(" - 대상과 목적, 분석 방식 등을 간단히 정리\n"); - sb.append("2. 주요 인사이트 요약 (AI가 뽑은 핵심 요약)\n"); + sb.append("### 2. 주요 인사이트 요약 (HaRu AI가 뽑은 핵심 요약)\n"); sb.append(" - 사용자의 응답 중 반복되거나 주목할 만한 인사이트를 요약\n"); sb.append(" - 전체 응답자의 몇 %가 어떤 패턴을 보였는지도 서술\n"); - sb.append("3. 자유 응답 기반 주요 키워드 정리 (많이 등장한 순서대로)\n"); + sb.append(" - 서술시에 질문 숫자 id가 아닌 질문 내용 텍스트 기반으로 설명\n"); + sb.append("### 3. 자유 응답 기반 주요 키워드 정리\n"); sb.append(" - 예: 잦힌 (37건), 말은 덜 불분명 (29건) 등\n\n"); sb.append("응답은 반드시 다음 JSON 형식으로 해줘. 형식만 따르고, 값은 생성한 값을 넣어줘야해. 질문에 대한 제안은 입력받은 질문 Id와 해당 질문에 매칭되는 제안 내용을 넣어줘. : \n"); @@ -142,18 +143,19 @@ public String getMoodTrackerReportRaw(String userMessageContent) { public Mono getMoodTrackerReport(String userMessageContent) { StringBuilder sb = new StringBuilder(); - sb.append("너는 팀 심리 및 조직 문화 분석가야. 아래의 설문 응답을 통해 전체 설문을 종합한 마크다운 형식의 분석 리포트를 작성하고, 설문 질문 별로 개선 제안을 각 1개씩 제시해줘.\n\n"); + sb.append("너는 팀 심리 및 조직 문화 분석가야. 아래의 설문 응답을 통해 전체 설문을 종합한 마크다운 형식의 분석 리포트를 작성하고, 설문 질문 별로 개선 제안을 각 1개씩 제시해줘. 아래의 형식에서 ###이 붙은 제목은 고정이고, 내용은 너가 채워주면 돼.\n\n"); sb.append("💡 최종 리포트 형식은 무조건 다음과 같은 마크다운 형식이어야만 합니다:\n"); - sb.append("### 1. {title} + 리포트\n"); + sb.append("### 1. 팀 분위기 트래커 리포트\n"); sb.append(" - 대상과 목적, 분석 방식 등을 간단히 정리\n"); sb.append("### 2. 주요 인사이트 요약 (HaRu AI가 뽑은 핵심 요약)\n"); sb.append(" - 사용자의 응답 중 반복되거나 주목할 만한 인사이트를 요약\n"); sb.append(" - 전체 응답자의 몇 %가 어떤 패턴을 보였는지도 서술\n"); + sb.append(" - 서술시에 질문 숫자 id가 아닌 질문 내용 텍스트 기반으로 설명\n"); sb.append("### 3. 자유 응답 기반 주요 키워드 정리\n"); sb.append(" - 예: 잦힌 (37건), 말은 덜 불분명 (29건) 등\n\n"); - sb.append("주요 인사이트 요약은 핵심을 요약해주고, 자유 응답 기반 주요 키워드 정리는 많이 등장한 순서대로 정렬해줘야해. 응답은 반드시 응답은 JSON 문자열 형식으로 주고, 백틱이나 마크다운 없이 순수 JSON만 반환해줘. 다음 JSON 형식으로 해줘. 형식만 따르고, 값은 생성한 값을 넣어줘야해. 질문에 대한 제안은 입력받은 질문 Id와 해당 질문에 매칭되는 제안 내용을 넣어줘. : \n"); + sb.append("응답은 반드시 다음 JSON 형식으로 해줘. 형식만 따르고, 값은 생성한 값을 넣어줘야해. 질문에 대한 제안은 입력받은 질문 Id와 해당 질문에 매칭되는 제안 내용을 넣어줘. : \n"); sb.append("{\n"); sb.append(" \"report\": \"전체 리포트 마크다운 텍스트\",\n"); sb.append(" \"suggestionsByQuestionId\": {\n");