diff --git a/src/main/java/com/haru/api/domain/lastOpened/service/UserDocumentLastOpenedServiceImpl.java b/src/main/java/com/haru/api/domain/lastOpened/service/UserDocumentLastOpenedServiceImpl.java index 55ee2866..68997067 100644 --- a/src/main/java/com/haru/api/domain/lastOpened/service/UserDocumentLastOpenedServiceImpl.java +++ b/src/main/java/com/haru/api/domain/lastOpened/service/UserDocumentLastOpenedServiceImpl.java @@ -49,6 +49,7 @@ public void updateLastOpened(UserDocumentId userDocumentId, Long workspaceId, St } @Override + @Transactional public void createInitialRecordsForWorkspaceUsers(List usersInWorkspace, Documentable document) { // 저장할 엔티티 리스트 생성 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 6cf6f77f..03ff0436 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 @@ -2,17 +2,20 @@ import com.haru.api.domain.snsEvent.dto.SnsEventRequestDTO; import com.haru.api.domain.snsEvent.dto.SnsEventResponseDTO; +import com.haru.api.domain.snsEvent.entity.SnsEvent; import com.haru.api.domain.snsEvent.entity.enums.Format; import com.haru.api.domain.snsEvent.entity.enums.ListType; import com.haru.api.domain.snsEvent.service.SnsEventCommandService; import com.haru.api.domain.snsEvent.service.SnsEventQueryService; -import com.haru.api.domain.user.security.jwt.SecurityUtil; +import com.haru.api.domain.user.entity.User; +import com.haru.api.domain.workspace.entity.Workspace; +import com.haru.api.global.annotation.AuthSnsEvent; +import com.haru.api.global.annotation.AuthUser; +import com.haru.api.global.annotation.AuthWorkspace; import com.haru.api.global.apiPayload.ApiResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @@ -31,10 +34,12 @@ public class SnsEventController { @PostMapping("/{workspaceId}") public ApiResponse instagramOauthRedirectUri( @PathVariable String workspaceId, - @RequestBody SnsEventRequestDTO.CreateSnsRequest request + @RequestBody SnsEventRequestDTO.CreateSnsRequest request, + @Parameter(hidden = true) @AuthUser User user, + @Parameter(hidden = true) @AuthWorkspace Workspace workspace ) { return ApiResponse.onSuccess( - snsEventCommandService.createSnsEvent(Long.parseLong(workspaceId), request) + snsEventCommandService.createSnsEvent(user, workspace, request) ); } @@ -58,11 +63,12 @@ public ApiResponse instagramRedirectUri( @PostMapping("/{workspaceId}/link-instagram") public ApiResponse linkInstagramAccount( @RequestHeader("code") String code, - @PathVariable String workspaceId + @PathVariable String workspaceId, + @Parameter(hidden = true) @AuthWorkspace Workspace workspace ) { System.out.println("Received accessToken: " + code); return ApiResponse.onSuccess( - snsEventCommandService.getInstagramAccessTokenAndAccount(code, Long.parseLong(workspaceId)) + snsEventCommandService.getInstagramAccessTokenAndAccount(code, workspace) ); } @@ -73,11 +79,13 @@ public ApiResponse linkInstagr ) @GetMapping("/{workspaceId}/list") public ApiResponse getSnsEventList( - @PathVariable String workspaceId + @PathVariable String workspaceId, + @Parameter(hidden = true) @AuthUser User user, + @Parameter(hidden = true) @AuthWorkspace Workspace workspace ) { - Long userId = SecurityUtil.getCurrentUserId(); + return ApiResponse.onSuccess( - snsEventQueryService.getSnsEventList(userId, Long.parseLong(workspaceId)) + snsEventQueryService.getSnsEventList(user, workspace) ); } @@ -86,14 +94,18 @@ public ApiResponse getSnsEventList( description = "# [v1.0 (2025-08-05)](https://www.notion.so/2265da7802c580e8b883e3e4481fd61d?v=2265da7802c5816ab095000cc1ddadca&p=22a5da7802c580d3bed7c57de0b88492&pm=s)" + " SNS 이벤트명 수정 API입니다. Header에 access token을 넣고 Path Variable에는 snsEvnetId를 Request Body에 SNS 이벤트 수정 정보(title)를 담아 요청해주세요." ) - @PatchMapping("/{snsEvnetId}") + @PatchMapping("/{snsEventId}") public ApiResponse updateSnsEventTitle( - @PathVariable String snsEvnetId, - @RequestBody SnsEventRequestDTO.UpdateSnsEventRequest request + @PathVariable String snsEventId, + @RequestBody SnsEventRequestDTO.UpdateSnsEventRequest request, + @Parameter(hidden = true) @AuthUser User user, + @Parameter(hidden = true) @AuthSnsEvent SnsEvent snsEvent ) { - Long userId = SecurityUtil.getCurrentUserId(); - snsEventCommandService.updateSnsEventTitle(userId, Long.parseLong(snsEvnetId), request); + + snsEventCommandService.updateSnsEventTitle(user, snsEvent, request); + return ApiResponse.onSuccess(""); + } @Operation( @@ -101,13 +113,18 @@ public ApiResponse updateSnsEventTitle( description = "# [v1.0 (2025-08-05)](https://www.notion.so/2265da7802c580e8b883e3e4481fd61d?v=2265da7802c5816ab095000cc1ddadca&p=2265da7802c5809b84d3d8c09f95c36b&pm=s)" + " SNS 이벤트 삭제 API입니다. Header에 access token을 넣고 Path Variable에는 삭제할 SNS Event의 snsEvnetId를 담아 요청해주세요." ) - @DeleteMapping("/{snsEvnetId}") + @DeleteMapping("/{snsEventId}") public ApiResponse deleteSnsEvent( - @PathVariable String snsEvnetId + @PathVariable String snsEventId, + @Parameter(hidden = true) @AuthUser User user, + @Parameter(hidden = true) @AuthSnsEvent SnsEvent snsEvent + ) { - Long userId = SecurityUtil.getCurrentUserId(); - snsEventCommandService.deleteSnsEvent(userId, Long.parseLong(snsEvnetId)); + + snsEventCommandService.deleteSnsEvent(user, snsEvent); + return ApiResponse.onSuccess(""); + } @Operation( @@ -117,11 +134,12 @@ public ApiResponse deleteSnsEvent( ) @GetMapping("/{snsEventId}") public ApiResponse getSnsEvent( - @PathVariable String snsEventId + @PathVariable String snsEventId, + @Parameter(hidden = true) @AuthUser User user, + @Parameter(hidden = true) @AuthSnsEvent SnsEvent snsEvent ) { - Long userId = SecurityUtil.getCurrentUserId(); return ApiResponse.onSuccess( - snsEventQueryService.getSnsEvent(userId, Long.parseLong(snsEventId)) + snsEventQueryService.getSnsEvent(user, snsEvent) ); } @@ -134,27 +152,18 @@ public ApiResponse getSnsEvent( public ApiResponse downloadList( @PathVariable String snsEventId, @RequestParam ListType listType, - @RequestParam Format format + @RequestParam Format format, + @Parameter(hidden = true) @AuthUser User user, + @Parameter(hidden = true) @AuthSnsEvent SnsEvent snsEvent ) { - Long userId = SecurityUtil.getCurrentUserId(); return ApiResponse.onSuccess( snsEventCommandService.downloadList( - userId, - Long.parseLong(snsEventId), + user, + snsEvent, 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); } + } 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 127bced6..11ffc926 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 @@ -1,5 +1,6 @@ package com.haru.api.domain.snsEvent.dto; +import com.haru.api.global.common.entity.TitleHolder; import lombok.*; import java.time.LocalDateTime; @@ -33,7 +34,7 @@ public static class SnsCondition { @Builder @NoArgsConstructor @AllArgsConstructor - public static class UpdateSnsEventRequest { + public static class UpdateSnsEventRequest implements TitleHolder { private String title; } } 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 69406f33..9fa17e19 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 @@ -30,13 +30,12 @@ public class SnsEvent extends BaseEntity implements Documentable { @Column(nullable = false, length = 100) private String title; - @Column(length = 255) private String snsLink; @Column(length = 200) private String snsLinkTitle; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "user_id") private User creator; diff --git a/src/main/java/com/haru/api/domain/snsEvent/repository/SnsEventRepository.java b/src/main/java/com/haru/api/domain/snsEvent/repository/SnsEventRepository.java index 54984d8c..1b6b827e 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/repository/SnsEventRepository.java +++ b/src/main/java/com/haru/api/domain/snsEvent/repository/SnsEventRepository.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; @Repository public interface SnsEventRepository extends JpaRepository { @@ -22,4 +23,11 @@ public interface SnsEventRepository extends JpaRepository { "WHERE mt.workspace.id = :workspaceId " + "AND mt.createdAt BETWEEN :startDate AND :endDate") List findAllDocumentForCalendars(Long workspaceId, LocalDateTime startDate, LocalDateTime endDate); + + @Query("SELECT se FROM SnsEvent se " + + "WHERE se.id = :snsEventId AND EXISTS (" + + " SELECT 1 FROM UserWorkspace uw " + + " WHERE uw.user.id = :userId AND uw.workspace.id = se.workspace.id" + + ")") + Optional findSnsEventByIdIfUserHasAccess(Long userId, Long snsEventId); } 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 c7ab30ad..cda950e2 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 @@ -2,17 +2,20 @@ import com.haru.api.domain.snsEvent.dto.SnsEventRequestDTO; import com.haru.api.domain.snsEvent.dto.SnsEventResponseDTO; +import com.haru.api.domain.snsEvent.entity.SnsEvent; import com.haru.api.domain.snsEvent.entity.enums.Format; import com.haru.api.domain.snsEvent.entity.enums.ListType; +import com.haru.api.domain.user.entity.User; +import com.haru.api.domain.workspace.entity.Workspace; public interface SnsEventCommandService { - SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent(Long workspaceId, SnsEventRequestDTO.CreateSnsRequest request); + SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent(User user, Workspace workspace, SnsEventRequestDTO.CreateSnsRequest request); - SnsEventResponseDTO.LinkInstagramAccountResponse getInstagramAccessTokenAndAccount(String code, Long workspaceId); + SnsEventResponseDTO.LinkInstagramAccountResponse getInstagramAccessTokenAndAccount(String code, Workspace workspace); - void updateSnsEventTitle(Long userId, Long snsEventId, SnsEventRequestDTO.UpdateSnsEventRequest request); + void updateSnsEventTitle(User user, SnsEvent snsEvent, SnsEventRequestDTO.UpdateSnsEventRequest request); - void deleteSnsEvent(Long userId, Long snsEventId); + void deleteSnsEvent(User user, SnsEvent snsEvent); - SnsEventResponseDTO.ListDownLoadLinkResponse downloadList(Long userId, Long snsEventId, ListType listType, Format format); + SnsEventResponseDTO.ListDownLoadLinkResponse downloadList(User user, SnsEvent snsEvent, 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 5b08770e..0496e02c 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 @@ -1,6 +1,5 @@ package com.haru.api.domain.snsEvent.service; -import com.haru.api.domain.lastOpened.repository.UserDocumentLastOpenedRepository; import com.haru.api.domain.lastOpened.service.UserDocumentLastOpenedService; import com.haru.api.domain.snsEvent.converter.SnsEventConverter; import com.haru.api.domain.snsEvent.dto.SnsEventRequestDTO; @@ -14,13 +13,13 @@ import com.haru.api.domain.snsEvent.repository.SnsEventRepository; import com.haru.api.domain.snsEvent.repository.WinnerRepository; import com.haru.api.domain.user.entity.User; -import com.haru.api.domain.user.repository.UserRepository; -import com.haru.api.domain.user.security.jwt.SecurityUtil; import com.haru.api.domain.userWorkspace.entity.UserWorkspace; import com.haru.api.domain.userWorkspace.entity.enums.Auth; import com.haru.api.domain.userWorkspace.repository.UserWorkspaceRepository; import com.haru.api.domain.workspace.entity.Workspace; import com.haru.api.domain.workspace.repository.WorkspaceRepository; +import com.haru.api.global.annotation.DeleteDocument; +import com.haru.api.global.annotation.UpdateDocumentTitle; import com.haru.api.global.apiPayload.exception.handler.MemberHandler; import com.haru.api.global.apiPayload.exception.handler.SnsEventHandler; import com.haru.api.global.apiPayload.exception.handler.WorkspaceHandler; @@ -34,7 +33,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.xwpf.usermodel.*; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClientException; @@ -56,20 +54,13 @@ public class SnsEventCommandServiceImpl implements SnsEventCommandService{ private final SpringTemplateEngine templateEngine; - @Value("${instagram.client.id}") - private String instagramClientId; - @Value("${instagram.client.secret}") - private String instagramClientSecret; - @Value("${instagram.redirect.uri}") - private String instagramRedirectUri; + private final SnsEventRepository snsEventRepository; - private final UserRepository userRepository; private final WorkspaceRepository workspaceRepository; private final UserWorkspaceRepository userWorkspaceRepository; private final ParticipantRepository participantRepository; private final WinnerRepository winnerRepository; private final RestTemplate restTemplate; - private final UserDocumentLastOpenedRepository userDocumentLastOpenedRepository; private final UserDocumentLastOpenedService userDocumentLastOpenedService; private final InstagramOauth2RestTemplate instagramOauth2RestTemplate; private final int WORD_TABLE_SIZE = 40; // 페이지당 총 아이디 수 @@ -81,22 +72,16 @@ public class SnsEventCommandServiceImpl implements SnsEventCommandService{ @Override @Transactional public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent( - Long workspaceId, + User user, + Workspace workspace, SnsEventRequestDTO.CreateSnsRequest request ) { // SNS 이벤트 생성 및 저장 - Long userId = SecurityUtil.getCurrentUserId(); - User foundUser = userRepository.findById(userId) - .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - Workspace foundWorkspace = workspaceRepository.findById(workspaceId) - .orElseThrow(() -> new WorkspaceHandler(WORKSPACE_NOT_FOUND)); - UserWorkspace foundUserWorkSapce = userWorkspaceRepository.findByUserAndWorkspace(foundUser, foundWorkspace) - .orElseThrow(() -> new MemberHandler(NOT_BELONG_TO_WORKSPACE)); - SnsEvent createdSnsEvent = SnsEventConverter.toSnsEvent(request, foundUser); - createdSnsEvent.setWorkspace(foundWorkspace); + SnsEvent createdSnsEvent = SnsEventConverter.toSnsEvent(request, user); + createdSnsEvent.setWorkspace(workspace); - // Instagarm API 호출 후 참여라 리스트, 당첨자 리스트 생성 및 저장 - String accessToken = foundWorkspace.getInstagramAccessToken(); + // Instagram API 호출 후 참여자 리스트, 당첨자 리스트 생성 및 저장 + String accessToken = workspace.getInstagramAccessToken(); if (accessToken == null || accessToken.isEmpty()) { throw new SnsEventHandler(SNS_EVENT_NO_ACCESS_TOKEN); } @@ -171,7 +156,7 @@ public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent( // sns event 생성 시 워크스페이스에 속해있는 모든 유저에 대해 // last opened 테이블에 마지막으로 연 시간은 null로하여 추가 - List usersInWorkspace = userWorkspaceRepository.findUsersByWorkspaceId(savedSnsEvent.getId()); + List usersInWorkspace = userWorkspaceRepository.findUsersByWorkspaceId(workspace.getId()); userDocumentLastOpenedService.createInitialRecordsForWorkspaceUsers(usersInWorkspace, savedSnsEvent); return SnsEventResponseDTO.CreateSnsEventResponse.builder() @@ -179,6 +164,131 @@ public SnsEventResponseDTO.CreateSnsEventResponse createSnsEvent( .build(); } + + @Override + @Transactional + public SnsEventResponseDTO.LinkInstagramAccountResponse getInstagramAccessTokenAndAccount( + String code, + Workspace workspace + ) { + String shortLivedAccessToken; + String longLivedAccessToken; + Map userInfo; + try { + // 1. Access Token 요청 + shortLivedAccessToken = instagramOauth2RestTemplate.getShortLivedAccessTokenUrl(code); + // 2. 단기 토큰을 장기(Long-Lived) 토큰으로 교환 + longLivedAccessToken = instagramOauth2RestTemplate.getLongLivedAccessToken(shortLivedAccessToken); + // 3. 장기 토큰으로 사용자 계정 정보 요청 + userInfo = instagramOauth2RestTemplate.getInstagramAccountInfo(longLivedAccessToken); + } catch (Exception e) { + log.error("Instagram OAuth2 처리 중 오류 발생: {}", e.getMessage()); + throw new SnsEventHandler(SNS_EVENT_INSTAGRAM_API_ERROR); + } + // 4. 워크스페이스에 인스타그램 계정 정보 저장 + String instagramId = (String) userInfo.get("user_id"); + Workspace foundWorkspace = workspaceRepository.findById(workspace.getId()) + .orElseThrow(() -> new WorkspaceHandler(WORKSPACE_NOT_FOUND)); + if (foundWorkspace.getInstagramId() != null && foundWorkspace.getInstagramId().equals(instagramId)) { + throw new SnsEventHandler(SNS_EVENT_INSTAGRAM_ALREADY_LINKED); + } + foundWorkspace.saveInstagramId(instagramId); + foundWorkspace.saveInstagramAccessToken(longLivedAccessToken); + foundWorkspace.saveInstagramAccountName((String) userInfo.get("username")); + return SnsEventConverter.toLinkInstagramAccountResponse((String) userInfo.get("username")); + } + + @Override + @Transactional + @UpdateDocumentTitle + public void updateSnsEventTitle( + User user, + SnsEvent snsEvent, + SnsEventRequestDTO.UpdateSnsEventRequest request + ) { + + UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceAndAuth(snsEvent.getWorkspace(), Auth.ADMIN) + .orElseThrow(() -> new MemberHandler(WORKSPACE_CREATOR_NOT_FOUND)); + + // 수정 권한 확인 (워크스페이스 생성자 혹은 SNS 이벤트의 생성자만 수정 가능) + if (!foundUserWorkspace.getUser().getId().equals(user.getId()) || !snsEvent.getCreator().getId().equals(user.getId())) { + throw new SnsEventHandler(SNS_EVENT_NO_AUTHORITY); + } + + snsEvent.updateTitle(request.getTitle()); + snsEventRepository.save(snsEvent); + + } + + @Override + @Transactional + @DeleteDocument + public void deleteSnsEvent( + User user, + SnsEvent snsEvent + ) { + + UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceAndAuth(snsEvent.getWorkspace(), Auth.ADMIN) + .orElseThrow(() -> new MemberHandler(WORKSPACE_CREATOR_NOT_FOUND)); + + // 수정 권한 확인 (워크스페이스 생성자 혹은 SNS 이벤트의 생성자만 삭제 가능) + if (!foundUserWorkspace.getUser().getId().equals(user.getId()) || !snsEvent.getCreator().getId().equals(user.getId())) { + throw new SnsEventHandler(SNS_EVENT_NO_AUTHORITY); + } + snsEventRepository.delete(snsEvent); + + } + + @Override + public SnsEventResponseDTO.ListDownLoadLinkResponse downloadList( + User user, + SnsEvent snsEvent, + ListType listType, + Format format + ) { + String downloadLink = ""; + + String snsEventTitle = snsEvent.getTitle(); + if (listType == ListType.PARTICIPANT) { + if (format == Format.PDF) { + String keyName = snsEvent.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 = snsEvent.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 (listType == ListType.WINNER) { + if (format == Format.PDF) { + String keyName = snsEvent.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 = snsEvent.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_LIST_TYPE); + } + return SnsEventResponseDTO.ListDownLoadLinkResponse.builder() + .downloadLink(downloadLink) + .build(); + } + private String createListHtml( SnsEvent snsEvent, ListType listType @@ -305,7 +415,7 @@ private SnsEventResponseDTO.InstagramMediaResponse fetchInstagramMedia( } - public List getComments( + private List getComments( String mediaId, String accessToken ) { @@ -347,7 +457,7 @@ private int countOccurrences( return count; } - public List pickWinners(Set participants, int n) { + private List pickWinners(Set participants, int n) { List list = new ArrayList<>(participants); // Set → List로 변환 Collections.shuffle(list); // 무작위 섞기 @@ -358,157 +468,6 @@ public List pickWinners(Set participants, int n) { return list.subList(0, n); // 앞에서 n개만 추출 } - @Override - @Transactional - public SnsEventResponseDTO.LinkInstagramAccountResponse getInstagramAccessTokenAndAccount( - String code, - Long workspaceId - ) { - String shortLivedAccessToken; - String longLivedAccessToken; - Map userInfo; - try { - // 1. Access Token 요청 - shortLivedAccessToken = instagramOauth2RestTemplate.getShortLivedAccessTokenUrl(code); - // 2. 단기 토큰을 장기(Long-Lived) 토큰으로 교환 - longLivedAccessToken = instagramOauth2RestTemplate.getLongLivedAccessToken(shortLivedAccessToken); - // 3. 장기 토큰으로 사용자 계정 정보 요청 - userInfo = instagramOauth2RestTemplate.getInstagramAccountInfo(longLivedAccessToken); - } catch (Exception e) { - log.error("Instagram OAuth2 처리 중 오류 발생: {}", e.getMessage()); - throw new SnsEventHandler(SNS_EVENT_INSTAGRAM_API_ERROR); - } - // 4. 워크스페이스에 인스타그램 계정 정보 저장 - String instagramId = (String) userInfo.get("user_id"); - Workspace foundWorkspace = workspaceRepository.findById(workspaceId) - .orElseThrow(() -> new WorkspaceHandler(WORKSPACE_NOT_FOUND)); - if (foundWorkspace.getInstagramId() != null && foundWorkspace.getInstagramId().equals(instagramId)) { - throw new SnsEventHandler(SNS_EVENT_INSTAGRAM_ALREADY_LINKED); - } - foundWorkspace.saveInstagramId(instagramId); - foundWorkspace.saveInstagramAccessToken(longLivedAccessToken); - foundWorkspace.saveInstagramAccountName((String) userInfo.get("username")); - return SnsEventConverter.toLinkInstagramAccountResponse((String) userInfo.get("username")); - } - - @Override - @Transactional - public void updateSnsEventTitle( - Long userId, - Long snsEventId, - SnsEventRequestDTO.UpdateSnsEventRequest request - ) { - User foundUser = userRepository.findById(userId) - .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - SnsEvent foundSnsEvent = snsEventRepository.findById(snsEventId) - .orElseThrow(() -> new SnsEventHandler(SNS_EVENT_NOT_FOUND)); - UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceAndAuth(foundSnsEvent.getWorkspace(), Auth.ADMIN) - .orElseThrow(() -> new MemberHandler(WORKSPACE_CREATOR_NOT_FOUND)); - // 수정 권한 확인 (워크스페이스 생성자 혹은 SNS 이벤트의 생성자만 수정 가능) - if (!foundUserWorkspace.getUser().getId().equals(foundUser.getId()) || !foundSnsEvent.getCreator().getId().equals(foundUser.getId())) { - throw new SnsEventHandler(SNS_EVENT_NO_AUTHORITY); - } - foundSnsEvent.updateTitle(request.getTitle()); - snsEventRepository.save(foundSnsEvent); - - // sns event 수정 시 워크스페이스에 속해있는 모든 유저에 대해 - // last opened 테이블에서 해당 문서 정보 업데이트 - userDocumentLastOpenedService.updateRecordsForWorkspaceUsers(foundSnsEvent); - } - - @Override - @Transactional - public void deleteSnsEvent( - Long userId, - Long snsEventId - ) { - User foundUser = userRepository.findById(userId) - .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - SnsEvent foundSnsEvent = snsEventRepository.findById(snsEventId) - .orElseThrow(() -> new SnsEventHandler(SNS_EVENT_NOT_FOUND)); - UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByWorkspaceAndAuth(foundSnsEvent.getWorkspace(), Auth.ADMIN) - .orElseThrow(() -> new MemberHandler(WORKSPACE_CREATOR_NOT_FOUND)); - // 수정 권한 확인 (워크스페이스 생성자 혹은 SNS 이벤트의 생성자만 삭제 가능) - if (!foundUserWorkspace.getUser().getId().equals(foundUser.getId()) || !foundSnsEvent.getCreator().getId().equals(foundUser.getId())) { - throw new SnsEventHandler(SNS_EVENT_NO_AUTHORITY); - } - snsEventRepository.delete(foundSnsEvent); - - // sns event 삭제 시 워크스페이스에 속해있는 모든 유저에 대해 - // last opened 테이블에서 해당 문서 id를 가지고 있는 튜플 모두 삭제 - userDocumentLastOpenedService.deleteRecordsForWorkspaceUsers(foundSnsEvent); - } - - @Override - public SnsEventResponseDTO.ListDownLoadLinkResponse downloadList( - Long userId, - Long snsEventId, - ListType listType, - Format format - ) { - String downloadLink = ""; - User foundUser = userRepository.findById(userId) - .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - SnsEvent foundSnsEvent = snsEventRepository.findById(snsEventId) - .orElseThrow(() -> new SnsEventHandler(SNS_EVENT_NOT_FOUND)); - String snsEventTitle = foundSnsEvent.getTitle(); - if (listType == ListType.PARTICIPANT) { - if (format == Format.PDF) { - 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 (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_LIST_TYPE); - } - return SnsEventResponseDTO.ListDownLoadLinkResponse.builder() - .downloadLink(downloadLink) - .build(); - } - - private String injectHead(String html) { - String fontCss = """ - - """; - if (html.toLowerCase().contains("")) { - // 바로 뒤에 스타일 삽입 - return html.replaceFirst("(?i)", "" + fontCss); - } else { - // head가 없으면 생성 - return html.replaceFirst("(?i)", "" + fontCss + ""); - } - } - private byte[] addPdfTitle(byte[] pdfBytes, String text, byte[] fontBytes) throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); PdfReader reader = new PdfReader(new ByteArrayInputStream(pdfBytes)); diff --git a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryService.java b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryService.java index e73ef735..3c1f0427 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryService.java +++ b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryService.java @@ -1,10 +1,13 @@ package com.haru.api.domain.snsEvent.service; import com.haru.api.domain.snsEvent.dto.SnsEventResponseDTO; +import com.haru.api.domain.snsEvent.entity.SnsEvent; +import com.haru.api.domain.user.entity.User; +import com.haru.api.domain.workspace.entity.Workspace; public interface SnsEventQueryService { - SnsEventResponseDTO.GetSnsEventListRequest getSnsEventList(Long userId, Long workspaceId); + SnsEventResponseDTO.GetSnsEventListRequest getSnsEventList(User user, Workspace workspace); - SnsEventResponseDTO.GetSnsEventRequest getSnsEvent(Long userId, Long snsEventId); + SnsEventResponseDTO.GetSnsEventRequest getSnsEvent(User user, SnsEvent snsEvent); } diff --git a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryServiceImpl.java b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryServiceImpl.java index a4d817b1..cbe8128a 100644 --- a/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryServiceImpl.java +++ b/src/main/java/com/haru/api/domain/snsEvent/service/SnsEventQueryServiceImpl.java @@ -10,58 +10,43 @@ import com.haru.api.domain.snsEvent.repository.SnsEventRepository; import com.haru.api.domain.snsEvent.repository.WinnerRepository; import com.haru.api.domain.user.entity.User; -import com.haru.api.domain.user.repository.UserRepository; -import com.haru.api.domain.userWorkspace.entity.UserWorkspace; -import com.haru.api.domain.userWorkspace.repository.UserWorkspaceRepository; import com.haru.api.domain.workspace.entity.Workspace; -import com.haru.api.domain.workspace.repository.WorkspaceRepository; import com.haru.api.global.annotation.TrackLastOpened; -import com.haru.api.global.apiPayload.exception.handler.MemberHandler; -import com.haru.api.global.apiPayload.exception.handler.SnsEventHandler; -import com.haru.api.global.apiPayload.exception.handler.WorkspaceHandler; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; -import static com.haru.api.global.apiPayload.code.status.ErrorStatus.*; - @Service @RequiredArgsConstructor public class SnsEventQueryServiceImpl implements SnsEventQueryService { - private final UserRepository userRepository; - private final WorkspaceRepository workspaceRepository; - private final UserWorkspaceRepository userWorkspaceRepository; private final SnsEventRepository snsEventRepository; private final ParticipantRepository participantRepository; private final WinnerRepository winnerRepository; @Override - public SnsEventResponseDTO.GetSnsEventListRequest getSnsEventList(Long userId, Long workspaceId) { - User foundUser = userRepository.findById(userId) - .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - Workspace foundWorkspace = workspaceRepository.findById(workspaceId) - .orElseThrow(() -> new WorkspaceHandler(WORKSPACE_NOT_FOUND)); - UserWorkspace foundUserWorkSpace = userWorkspaceRepository.findByUserAndWorkspace(foundUser, foundWorkspace) - .orElseThrow(() -> new MemberHandler(NOT_BELONG_TO_WORKSPACE)); - List snsEventList = snsEventRepository.findAllByWorkspace(foundWorkspace); + public SnsEventResponseDTO.GetSnsEventListRequest getSnsEventList(User user, Workspace workspace) { + + List snsEventList = snsEventRepository.findAllByWorkspace(workspace); + return SnsEventConverter.toGetSnsEventListRequest(snsEventList); + } @Override @TrackLastOpened(type = DocumentType.SNS_EVENT_ASSISTANT) - public SnsEventResponseDTO.GetSnsEventRequest getSnsEvent(Long userId, Long snsEventId) { - User foundUser = userRepository.findById(userId) - .orElseThrow(() -> new MemberHandler(MEMBER_NOT_FOUND)); - SnsEvent foundSnsEvent = snsEventRepository.findById(snsEventId) - .orElseThrow(() -> new SnsEventHandler(SNS_EVENT_NOT_FOUND)); - List participantList = participantRepository.findAllBySnsEvent(foundSnsEvent); - List winnerList = winnerRepository.findAllBySnsEvent(foundSnsEvent); + public SnsEventResponseDTO.GetSnsEventRequest getSnsEvent(User user, SnsEvent snsEvent) { + + List participantList = participantRepository.findAllBySnsEvent(snsEvent); + + List winnerList = winnerRepository.findAllBySnsEvent(snsEvent); + return SnsEventConverter.toGetSnsEventRequest( - foundSnsEvent, + snsEvent, participantList, winnerList ); + } } diff --git a/src/main/java/com/haru/api/domain/workspace/entity/Workspace.java b/src/main/java/com/haru/api/domain/workspace/entity/Workspace.java index eb0f9416..04ff62cf 100644 --- a/src/main/java/com/haru/api/domain/workspace/entity/Workspace.java +++ b/src/main/java/com/haru/api/domain/workspace/entity/Workspace.java @@ -34,7 +34,7 @@ public class Workspace extends BaseEntity { private String instagramId; - @Column(length = 200) + @Column(columnDefinition="TEXT") private String instagramAccessToken; @Column(length = 50) diff --git a/src/main/java/com/haru/api/global/annotation/AuthSnsEvent.java b/src/main/java/com/haru/api/global/annotation/AuthSnsEvent.java new file mode 100644 index 00000000..ca8fca63 --- /dev/null +++ b/src/main/java/com/haru/api/global/annotation/AuthSnsEvent.java @@ -0,0 +1,14 @@ +package com.haru.api.global.annotation; + +import com.haru.api.domain.lastOpened.entity.enums.DocumentType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@AuthDocument(documentType = DocumentType.SNS_EVENT_ASSISTANT, pathVariableName = "snsEventId") +public @interface AuthSnsEvent { +} diff --git a/src/main/java/com/haru/api/global/argumentResolver/AuthDocumentArgumentResolver.java b/src/main/java/com/haru/api/global/argumentResolver/AuthDocumentArgumentResolver.java index a5b4bf67..b6a40caf 100644 --- a/src/main/java/com/haru/api/global/argumentResolver/AuthDocumentArgumentResolver.java +++ b/src/main/java/com/haru/api/global/argumentResolver/AuthDocumentArgumentResolver.java @@ -86,6 +86,6 @@ private AuthDocument findAuthDocumentAnnotation(MethodParameter parameter) { return annotation.annotationType().getAnnotation(AuthDocument.class); } } - throw new IllegalStateException("AuthDocument 어노테이션을 찾을 수 없습니다."); + return null; } } diff --git a/src/main/java/com/haru/api/global/documentFinder/SnsEventFinder.java b/src/main/java/com/haru/api/global/documentFinder/SnsEventFinder.java new file mode 100644 index 00000000..c02f8df3 --- /dev/null +++ b/src/main/java/com/haru/api/global/documentFinder/SnsEventFinder.java @@ -0,0 +1,26 @@ +package com.haru.api.global.documentFinder; + +import com.haru.api.domain.lastOpened.entity.enums.DocumentType; +import com.haru.api.domain.snsEvent.repository.SnsEventRepository; +import com.haru.api.global.apiPayload.code.status.ErrorStatus; +import com.haru.api.global.apiPayload.exception.handler.SnsEventHandler; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SnsEventFinder implements DocumentFinder { + + private final SnsEventRepository snsEventRepository; + + @Override + public DocumentType getSupportType() { + return DocumentType.SNS_EVENT_ASSISTANT; + } + + @Override + public Object findById(Object id) { + return snsEventRepository.findById((Long) id) + .orElseThrow(() -> new SnsEventHandler(ErrorStatus.SNS_EVENT_NOT_FOUND)); + } +} diff --git a/src/main/java/com/haru/api/global/interceptor/DocumentMemberAuthInterceptor.java b/src/main/java/com/haru/api/global/interceptor/DocumentMemberAuthInterceptor.java index 0d386243..1bbea75e 100644 --- a/src/main/java/com/haru/api/global/interceptor/DocumentMemberAuthInterceptor.java +++ b/src/main/java/com/haru/api/global/interceptor/DocumentMemberAuthInterceptor.java @@ -2,6 +2,7 @@ import com.haru.api.domain.lastOpened.entity.enums.DocumentType; import com.haru.api.domain.meeting.repository.MeetingRepository; +import com.haru.api.domain.snsEvent.repository.SnsEventRepository; import com.haru.api.domain.user.entity.User; import com.haru.api.domain.user.repository.UserRepository; import com.haru.api.domain.user.security.jwt.SecurityUtil; @@ -9,6 +10,7 @@ import com.haru.api.global.annotation.AuthUser; import com.haru.api.global.apiPayload.code.status.ErrorStatus; import com.haru.api.global.apiPayload.exception.handler.MemberHandler; +import com.haru.api.global.apiPayload.exception.handler.SnsEventHandler; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -28,6 +30,7 @@ public class DocumentMemberAuthInterceptor implements HandlerInterceptor { private final UserRepository userRepository; private final MeetingRepository meetingRepository; + private final SnsEventRepository snsEventRepository; @Override public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { @@ -59,6 +62,7 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp } } + // AuthUser, AuthDocument가 모두 존재하는 경우 if (hasAuthUserParam && authDocumentInfo != null) { // AuthDocument에서 DocumentType, pathVariableName 추출 @@ -80,11 +84,17 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp Object foundDocument = null; + // 유저가 해당 문서가 속한 워크스페이스에 속해있는지 확인하고, 해당 객체를 반환함 switch (documentType) { case AI_MEETING_MANAGER: foundDocument = meetingRepository.findMeetingByIdIfUserHasAccess(userId, documentId) .orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_HAS_NO_ACCESS_TO_MEETING)); break; + + case SNS_EVENT_ASSISTANT: + foundDocument = snsEventRepository.findSnsEventByIdIfUserHasAccess(userId, documentId) + .orElseThrow(() -> new SnsEventHandler(ErrorStatus.SNS_EVENT_NOT_FOUND)); + break; } // 유저 조회