diff --git a/src/main/java/space/space_spring/config/WebConfig.java b/src/main/java/space/space_spring/config/WebConfig.java index c54accb6..3a0fca90 100644 --- a/src/main/java/space/space_spring/config/WebConfig.java +++ b/src/main/java/space/space_spring/config/WebConfig.java @@ -8,9 +8,10 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import space.space_spring.argumentResolver.jwtLogin.JwtLoginAuthHandlerArgumentResolver; -import space.space_spring.argumentResolver.userSpace.UserSpaceAuth; import space.space_spring.argumentResolver.userSpace.UserSpaceAuthHandlerArgumentResolver; import space.space_spring.argumentResolver.userSpace.UserSpaceIdHandlerArgumentResolver; +import space.space_spring.config.interceptorURL.JwtLoginInterceptorURL; +import space.space_spring.config.interceptorURL.UserSpaceValidationInterceptorURL; import space.space_spring.interceptor.UserSpaceValidationInterceptor; import space.space_spring.interceptor.jwtLogin.JwtLoginAuthInterceptor; @@ -34,7 +35,7 @@ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtLoginAuthInterceptor) .order(1); - for (InterceptorURL interceptorURL : InterceptorURL.values()) { + for (JwtLoginInterceptorURL interceptorURL : JwtLoginInterceptorURL.values()) { registration.addPathPatterns(interceptorURL.getUrlPattern()); } @@ -60,8 +61,8 @@ public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("http://localhost:3000/", "http://localhost:5173/", "https://localhost:5173/", "http://localhost:5173/KUIT-Space-Front/", "https://localhost:5173/KUIT-Space-Front/", - "https://kuit-space.github.io/KUIT-Space-front/") - .allowedMethods("GET", "POST", "PUT", "DELETE") + "https://kuit-space.github.io/", "https://kuit-space-front.vercel.app/") + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH") .exposedHeaders("location", "Authorization") .allowedHeaders("*") .allowCredentials(true); diff --git a/src/main/java/space/space_spring/config/WebSocketConfig.java b/src/main/java/space/space_spring/config/WebSocketConfig.java index 62534fb5..1559e4ca 100644 --- a/src/main/java/space/space_spring/config/WebSocketConfig.java +++ b/src/main/java/space/space_spring/config/WebSocketConfig.java @@ -1,12 +1,15 @@ package space.space_spring.config; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; +import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; import space.space_spring.interceptor.jwtSocket.JwtChannelInterceptor; @Configuration @@ -24,6 +27,7 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { .withSockJS(); } + @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 메세지 구독(수신) 요청의 엔드포인트 @@ -37,4 +41,21 @@ public void configureMessageBroker(MessageBrokerRegistry registry) { public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(jwtChannelInterceptor); } + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registration) { + registration.setMessageSizeLimit(50 * 1024 * 1024); // 50MB + registration.setSendBufferSizeLimit(50 * 1024 * 1024); // 50MB + registration.setSendTimeLimit(20 * 10000); // 더 긴 시간 제한 설정 + } + + @Bean + public ServletServerContainerFactoryBean createServletServerContainerFactoryBean() { + ServletServerContainerFactoryBean factoryBean = new ServletServerContainerFactoryBean(); + factoryBean.setMaxTextMessageBufferSize(50 * 1028 * 1024); + factoryBean.setMaxBinaryMessageBufferSize(50 * 1028 * 1024); + factoryBean.setMaxSessionIdleTimeout(20 * 10000L); + factoryBean.setAsyncSendTimeout(20 * 10000L); + return factoryBean; + } } diff --git a/src/main/java/space/space_spring/config/InterceptorURL.java b/src/main/java/space/space_spring/config/interceptorURL/JwtLoginInterceptorURL.java similarity index 67% rename from src/main/java/space/space_spring/config/InterceptorURL.java rename to src/main/java/space/space_spring/config/interceptorURL/JwtLoginInterceptorURL.java index 81ff2fef..21886d4e 100644 --- a/src/main/java/space/space_spring/config/InterceptorURL.java +++ b/src/main/java/space/space_spring/config/interceptorURL/JwtLoginInterceptorURL.java @@ -1,9 +1,9 @@ -package space.space_spring.config; +package space.space_spring.config.interceptorURL; import lombok.Getter; @Getter -public enum InterceptorURL { +public enum JwtLoginInterceptorURL { SPACE("/space/**"), TEST("/test/**"), SPACE_LIST_FOR_USER("/user/space-choice"), @@ -12,7 +12,7 @@ public enum InterceptorURL { private final String urlPattern; - InterceptorURL(String urlPattern) { + JwtLoginInterceptorURL(String urlPattern) { this.urlPattern = urlPattern; } } diff --git a/src/main/java/space/space_spring/config/UserSpaceValidationInterceptorURL.java b/src/main/java/space/space_spring/config/interceptorURL/UserSpaceValidationInterceptorURL.java similarity index 62% rename from src/main/java/space/space_spring/config/UserSpaceValidationInterceptorURL.java rename to src/main/java/space/space_spring/config/interceptorURL/UserSpaceValidationInterceptorURL.java index 3ef064a7..2268f017 100644 --- a/src/main/java/space/space_spring/config/UserSpaceValidationInterceptorURL.java +++ b/src/main/java/space/space_spring/config/interceptorURL/UserSpaceValidationInterceptorURL.java @@ -1,4 +1,4 @@ -package space.space_spring.config; +package space.space_spring.config.interceptorURL; import lombok.Getter; @@ -6,7 +6,9 @@ public enum UserSpaceValidationInterceptorURL { //SPACE("/space/**"), TEST("/space/{spaceId}/test/**"), - VOICEROOM("/space/{spaceId}/voiceRoom/**") +// VOICEROOM("/space/{spaceId}/voiceRoom/**"), +// CHATROOM("/space/{spaceId}/chat/**") + SPACE("/space/{spaceId}/**") ; private final String urlPattern; diff --git a/src/main/java/space/space_spring/controller/ChatRoomController.java b/src/main/java/space/space_spring/controller/ChatRoomController.java index 976f6fb5..55386451 100644 --- a/src/main/java/space/space_spring/controller/ChatRoomController.java +++ b/src/main/java/space/space_spring/controller/ChatRoomController.java @@ -5,6 +5,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import space.space_spring.argumentResolver.jwtLogin.JwtLoginAuth; +import space.space_spring.argumentResolver.userSpace.UserSpaceAuth; import space.space_spring.dto.chat.request.CreateChatRoomRequest; import space.space_spring.dto.chat.request.JoinChatRoomRequest; import space.space_spring.dto.chat.response.ChatSuccessResponse; @@ -19,6 +20,7 @@ import java.io.IOException; +import static space.space_spring.entity.enumStatus.UserSpaceAuth.MANAGER; import static space.space_spring.response.status.BaseExceptionResponseStatus.*; import static space.space_spring.util.bindingResult.BindingResultUtils.getErrorMessage; @@ -45,17 +47,16 @@ public BaseResponse readChatRooms(@JwtLoginAuth Long userI public BaseResponse createChatRoom( @JwtLoginAuth Long userId, @PathVariable Long spaceId, + @UserSpaceAuth String userSpaceAuth, @Validated @ModelAttribute CreateChatRoomRequest createChatRoomRequest, BindingResult bindingResult) throws IOException { - if (!userSpaceUtils.isUserManager(userId, spaceId)) { - throw new CustomException(UNAUTHORIZED_USER); - } - if (bindingResult.hasErrors()) { throw new CustomException(INVALID_CHATROOM_CREATE, getErrorMessage(bindingResult)); } + validateManagerPermission(userSpaceAuth); + String chatRoomDirName = "chatRoomImg"; String chatRoomImgUrl = s3Uploader.upload(createChatRoomRequest.getImg(), chatRoomDirName); @@ -66,11 +67,13 @@ public BaseResponse createChatRoom( * 특정 채팅방의 모든 유저 정보 조회 */ @GetMapping("/{chatRoomId}/member") - public BaseResponse readChatRoomMembers(@JwtLoginAuth Long userId, @PathVariable Long spaceId, @PathVariable Long chatRoomId) { + public BaseResponse readChatRoomMembers( + @JwtLoginAuth Long userId, + @PathVariable Long spaceId, + @PathVariable Long chatRoomId, + @UserSpaceAuth String userSpaceAuth) { - if (!userSpaceUtils.isUserManager(userId, spaceId)) { - throw new CustomException(UNAUTHORIZED_USER); - } + validateManagerPermission(userSpaceAuth); return new BaseResponse<>(chatRoomService.readChatRoomMembers(spaceId, chatRoomId)); } @@ -83,17 +86,16 @@ public BaseResponse joinChatRoom( @JwtLoginAuth Long userId, @PathVariable Long spaceId, @PathVariable Long chatRoomId, + @UserSpaceAuth String userSpaceAuth, @RequestBody JoinChatRoomRequest joinChatRoomRequest, BindingResult bindingResult) { - if (!userSpaceUtils.isUserManager(userId, spaceId)) { - throw new CustomException(UNAUTHORIZED_USER); - } - if(bindingResult.hasErrors()){ throw new CustomException(INVALID_CHATROOM_JOIN,getErrorMessage(bindingResult)); } + validateManagerPermission(userSpaceAuth); + return new BaseResponse<>(chatRoomService.joinChatRoom(chatRoomId, joinChatRoomRequest)); } @@ -105,11 +107,10 @@ public BaseResponse modifyChatRoomName( @JwtLoginAuth Long userId, @PathVariable Long spaceId, @PathVariable Long chatRoomId, - @RequestParam String name) { + @RequestParam String name, + @UserSpaceAuth String userSpaceAuth) { - if (!userSpaceUtils.isUserManager(userId, spaceId)) { - throw new CustomException(UNAUTHORIZED_USER); - } + validateManagerPermission(userSpaceAuth); return new BaseResponse<>(chatRoomService.modifyChatRoomName(chatRoomId, name)); } @@ -132,12 +133,17 @@ public BaseResponse exitChatRoom( public BaseResponse deleteChatRoom( @JwtLoginAuth Long userId, @PathVariable Long spaceId, - @PathVariable Long chatRoomId) { + @PathVariable Long chatRoomId, + @UserSpaceAuth String userSpaceAuth) { - if (!userSpaceUtils.isUserManager(userId, spaceId)) { - throw new CustomException(UNAUTHORIZED_USER); - } + validateManagerPermission(userSpaceAuth); return new BaseResponse<>(chatRoomService.deleteChatRoom(chatRoomId)); } + + private void validateManagerPermission(String userSpaceAuth){ + if(!userSpaceAuth.equals(MANAGER.getAuth())){ + throw new CustomException(UNAUTHORIZED_USER); + } + } } diff --git a/src/main/java/space/space_spring/controller/ChattingController.java b/src/main/java/space/space_spring/controller/ChattingController.java index ef8f54fa..d39e6faa 100644 --- a/src/main/java/space/space_spring/controller/ChattingController.java +++ b/src/main/java/space/space_spring/controller/ChattingController.java @@ -8,12 +8,14 @@ import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.socket.messaging.SessionDisconnectEvent; +import space.space_spring.argumentResolver.userSpace.CheckUserSpace; import space.space_spring.dto.chat.request.ChatMessageRequest; import space.space_spring.dto.chat.response.ChatMessageLogResponse; import space.space_spring.dto.chat.response.ChatMessageResponse; import space.space_spring.service.ChattingService; import space.space_spring.service.UserChatRoomService; +import java.io.IOException; import java.util.Map; @Slf4j @@ -27,8 +29,9 @@ public class ChattingController { @MessageMapping("/chat/{chatRoomId}") // {chatRoomId} 채팅방으로 보낸 메세지 매핑 @SendTo("/topic/chat/{chatRoomId}") // {chatRoomId} 채팅방을 구독한 곳들로 메세지 전송 + @CheckUserSpace(required = false) public ChatMessageResponse sendChatMessage (@Payload ChatMessageRequest chatMessageRequest, @DestinationVariable Long chatRoomId, - @Header("simpSessionAttributes") Map sessionAttributes) { + @Header("simpSessionAttributes") Map sessionAttributes) throws IOException { Long senderId = (Long) sessionAttributes.get("userId"); // log.info(senderId + " 님이 " + chatRoomId + " 채팅방으로 " + chatMessageRequest.getContent() + " 전송"); @@ -36,6 +39,7 @@ public ChatMessageResponse sendChatMessage (@Payload ChatMessageRequest chatMess } @SubscribeMapping("/chat/{chatRoomId}") // {chatRoomId} 채팅방을 구독 + @CheckUserSpace(required = false) public ChatMessageLogResponse subscribeChatRoom (@DestinationVariable Long chatRoomId, @Header("simpSessionAttributes") Map sessionAttributes) { // log.info(chatRoomId + " 채팅방 구독"); sessionAttributes.put("chatRoomId", chatRoomId); @@ -44,6 +48,7 @@ public ChatMessageLogResponse subscribeChatRoom (@DestinationVariable Long chatR // socket disconnect 시 호출 @EventListener + @CheckUserSpace(required = false) public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage()); Map sessionAttributes = headerAccessor.getSessionAttributes(); diff --git a/src/main/java/space/space_spring/controller/SpaceController.java b/src/main/java/space/space_spring/controller/SpaceController.java index 102fadf4..9f750012 100644 --- a/src/main/java/space/space_spring/controller/SpaceController.java +++ b/src/main/java/space/space_spring/controller/SpaceController.java @@ -7,6 +7,11 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import space.space_spring.argumentResolver.jwtLogin.JwtLoginAuth; +import space.space_spring.argumentResolver.userSpace.UserSpaceAuth; +import space.space_spring.argumentResolver.userSpace.UserSpaceId; +import space.space_spring.dto.pay.dto.PayReceiveInfoDto; +import space.space_spring.dto.pay.dto.PayRequestInfoDto; +import space.space_spring.dto.space.GetSpaceHomeDto; import space.space_spring.dto.space.GetSpaceJoinDto; import space.space_spring.dto.space.PostSpaceJoinDto; import space.space_spring.dto.space.request.PostSpaceCreateDto; @@ -17,11 +22,14 @@ import space.space_spring.entity.Space; import space.space_spring.exception.CustomException; import space.space_spring.response.BaseResponse; +import space.space_spring.service.PayService; +import space.space_spring.service.PostService; import space.space_spring.service.S3Uploader; import space.space_spring.service.SpaceService; import space.space_spring.util.userSpace.UserSpaceUtils; import java.io.IOException; +import java.util.List; import java.util.Optional; import static space.space_spring.response.status.BaseExceptionResponseStatus.*; @@ -38,6 +46,8 @@ public class SpaceController { private final String spaceImgDirName = "spaceImg"; private final String userProfileImgDirName = "userProfileImg"; private final UserSpaceUtils userSpaceUtils; + private final PayService payService; + private final PostService postService; @PostMapping("") public BaseResponse createSpace(@JwtLoginAuth Long userId, @Validated @ModelAttribute PostSpaceCreateDto.Request postSpaceCreateRequest, BindingResult bindingResult) throws IOException { @@ -162,7 +172,7 @@ private String processUserProfileImage(MultipartFile userProfileImg) throws IOEx * 유저의 스페이스 가입 처리 */ @PostMapping("/{spaceId}/join") - BaseResponse joinUserToSpace(@JwtLoginAuth Long userId, @PathVariable Long spaceId, @Validated @ModelAttribute PostSpaceJoinDto.Request request, BindingResult bindingResult) throws IOException { + public BaseResponse joinUserToSpace(@JwtLoginAuth Long userId, @PathVariable Long spaceId, @Validated @ModelAttribute PostSpaceJoinDto.Request request, BindingResult bindingResult) throws IOException { if (bindingResult.hasErrors()) { throw new CustomException(INVALID_SPACE_JOIN_REQUEST, getErrorMessage(bindingResult)); } @@ -184,4 +194,36 @@ BaseResponse joinUserToSpace(@JwtLoginAuth Long userId, @PathVariable Lo return new BaseResponse<>("유저의 스페이스 가입 처리 성공"); } + + /** + * 스페이스 홈 화면 + */ + @GetMapping("/{spaceId}") + public BaseResponse showSpaceHome(@JwtLoginAuth Long userId, @PathVariable Long spaceId, @UserSpaceId Long userSpaceId, @UserSpaceAuth String userAuth) { + log.info("userId = {}, spaceId = {}, userSpaceID = {}, userAuth = {}", userId, spaceId, userSpaceId, userAuth); + + // TODO 1. 스페이스 정보 get + GetSpaceHomeDto.SpaceInfoForHome spaceInfoForHome = spaceService.getSpaceInfoForHome(spaceId); + + // TODO 2. 해당 스페이스에서의 유저 정산 정보 get + // 유저가 요청한 정산 중 현재 진행중인 정산 리스트 + // AND + // 유저가 요청받은 정산 중 현재 진행중인 정산 리스트 + List payRequestInfoForUser = payService.getPayRequestInfoForUser(userId, spaceId, false); + List payReceiveInfoForUser = payService.getPayReceiveInfoForUser(userId, spaceId, false); + + // TODO 3. 해당 스페이스의 공지사항 get + List noticeInfoForHome = postService.getNoticeInfoForHome(spaceId); + + // TODO 4. return + return new BaseResponse<>(new GetSpaceHomeDto.Response( + spaceInfoForHome.getSpaceName(), + spaceInfoForHome.getSpaceProfileImg(), + payRequestInfoForUser, + payReceiveInfoForUser, + noticeInfoForHome, + spaceInfoForHome.getMemberNum(), + userAuth + )); + } } diff --git a/src/main/java/space/space_spring/controller/VoiceRoomController.java b/src/main/java/space/space_spring/controller/VoiceRoomController.java index fc555a74..61c605d9 100644 --- a/src/main/java/space/space_spring/controller/VoiceRoomController.java +++ b/src/main/java/space/space_spring/controller/VoiceRoomController.java @@ -185,7 +185,7 @@ private boolean validateVoiceRoomInSpace(long spaceId,long voiceRoomId){ private boolean validateManagerPermission(String userSpaceAuth){ //해당 유저가 현재 space에 대해 관리자 권한을 갖고 있는지 확인 if(!userSpaceAuth.equals(MANAGER.getAuth())){ - throw new CustomException(VOICEROOM_DO_NOT_HAVE_PERMISSION); + throw new CustomException(UNAUTHORIZED_USER); } return true; } diff --git a/src/main/java/space/space_spring/dao/PostDao.java b/src/main/java/space/space_spring/dao/PostDao.java index 82219020..04a3d8f7 100644 --- a/src/main/java/space/space_spring/dao/PostDao.java +++ b/src/main/java/space/space_spring/dao/PostDao.java @@ -1,5 +1,6 @@ package space.space_spring.dao; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -20,4 +21,7 @@ public interface PostDao extends JpaRepository { "FROM PostLike l WHERE l.post.postId = :postId AND l.user.userId = :userId") boolean isUserLikedPost(@Param("postId") Long postId, @Param("userId") Long userId); + @Query("SELECT p FROM Post p WHERE p.space = :space AND p.type = :type AND p.status = 'ACTIVE' ORDER BY p.createdAt DESC") + List findBySpaceAndTypeSortedByNewest(@Param("space") Space space, @Param("type") String type, Pageable pageable); + } diff --git a/src/main/java/space/space_spring/dao/VoiceRoomDao.java b/src/main/java/space/space_spring/dao/VoiceRoomDao.java index 95b9195c..36639cdb 100644 --- a/src/main/java/space/space_spring/dao/VoiceRoomDao.java +++ b/src/main/java/space/space_spring/dao/VoiceRoomDao.java @@ -29,24 +29,6 @@ public Long createVoiceRoom(String name,int order,Space space){ return null; } } - @Transactional - public Integer findMaxOrderBySpace(Space space) { - String jpql = "SELECT MAX(r.order) FROM voice_room r WHERE r.space = :space"; - - return entityManager.createQuery(jpql, Integer.class) - .setParameter("space", space) - .getSingleResult(); - } - - @Transactional - public VoiceRoom findRoomWithMaxOrderBySpace(Space space) { - String jpql = "SELECT r FROM VoiceRoom r WHERE r.space = :space AND r.order = (SELECT MAX(r2.order) FROM VoiceRoom r2 WHERE r2.space = :space)"; - - return entityManager.createQuery(jpql, VoiceRoom.class) - .setParameter("space", space) - .getSingleResult(); - } - } diff --git a/src/main/java/space/space_spring/dto/chat/request/ChatMessageRequest.java b/src/main/java/space/space_spring/dto/chat/request/ChatMessageRequest.java index bc88346b..423dce12 100644 --- a/src/main/java/space/space_spring/dto/chat/request/ChatMessageRequest.java +++ b/src/main/java/space/space_spring/dto/chat/request/ChatMessageRequest.java @@ -1,6 +1,8 @@ package space.space_spring.dto.chat.request; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import space.space_spring.entity.enumStatus.ChatMessageType; @@ -11,12 +13,12 @@ @NoArgsConstructor public class ChatMessageRequest { - @NotBlank(message = "메시지 내용은 공백일 수 없습니다.") + @Size(min=1, message = "메시지 내용은 공백일 수 없습니다.") private HashMap content; @NotBlank(message = "스페이스 아이디는 공백일 수 없습니다.") private Long spaceId; - @NotBlank(message = "메시지 타입은 공백일 수 없습니다.") + @NotNull(message = "메시지 타입은 공백일 수 없습니다.") private ChatMessageType messageType; } diff --git a/src/main/java/space/space_spring/dto/chat/request/CreateChatRoomRequest.java b/src/main/java/space/space_spring/dto/chat/request/CreateChatRoomRequest.java index bc941218..c492d0b9 100644 --- a/src/main/java/space/space_spring/dto/chat/request/CreateChatRoomRequest.java +++ b/src/main/java/space/space_spring/dto/chat/request/CreateChatRoomRequest.java @@ -1,10 +1,12 @@ package space.space_spring.dto.chat.request; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.hibernate.validator.constraints.Length; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -14,11 +16,13 @@ @NoArgsConstructor public class CreateChatRoomRequest { - @Length(min = 2, max = 15, message = "채팅방 이름은 2자 이상, 15자 이내의 문자열이어야 합니다.") + @Size(min = 2, max = 15, message = "채팅방 이름은 2자 이상, 15자 이내의 문자열이어야 합니다.") @NotBlank(message = "채팅방 이름은 공백일 수 없습니다.") private String name; + @NotNull(message = "채팅방 이미지는 공백일 수 없습니다.") private MultipartFile img; + @NotEmpty(message = "1명 이상의 멤버를 초대해야 합니다.") private List memberList; } diff --git a/src/main/java/space/space_spring/dto/chat/request/JoinChatRoomRequest.java b/src/main/java/space/space_spring/dto/chat/request/JoinChatRoomRequest.java index 5a36190e..63d41259 100644 --- a/src/main/java/space/space_spring/dto/chat/request/JoinChatRoomRequest.java +++ b/src/main/java/space/space_spring/dto/chat/request/JoinChatRoomRequest.java @@ -1,6 +1,6 @@ package space.space_spring.dto.chat.request; -import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotEmpty; import lombok.Getter; import java.util.List; @@ -8,6 +8,6 @@ @Getter public class JoinChatRoomRequest { - @Nullable + @NotEmpty(message = "1명 이상의 멤버를 초대해야 합니다.") private List memberList; } diff --git a/src/main/java/space/space_spring/dto/space/GetSpaceHomeDto.java b/src/main/java/space/space_spring/dto/space/GetSpaceHomeDto.java new file mode 100644 index 00000000..aac016b0 --- /dev/null +++ b/src/main/java/space/space_spring/dto/space/GetSpaceHomeDto.java @@ -0,0 +1,50 @@ +package space.space_spring.dto.space; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import space.space_spring.dto.pay.dto.PayReceiveInfoDto; +import space.space_spring.dto.pay.dto.PayRequestInfoDto; +import java.util.List; + +public class GetSpaceHomeDto { + + @Getter + @AllArgsConstructor + public static class Response { + + private String spaceName; + + private String spaceProfileImg; + + private List payRequestInfoDtoList; + + private List payReceiveInfoDtoList; + + private List noticeList; + + private int memberNum; + + private String userAuth; + } + + @Getter + @AllArgsConstructor + public static class SpaceHomeNotice { + + private Long postId; + + private String title; + } + + @Getter + @AllArgsConstructor + public static class SpaceInfoForHome { + + private String spaceName; + + private String spaceProfileImg; + + private int memberNum; + } +} diff --git a/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java b/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java index 47dc44bb..f10102f8 100644 --- a/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java +++ b/src/main/java/space/space_spring/response/status/BaseExceptionResponseStatus.java @@ -31,8 +31,8 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { * 4000: Authorization 오류 */ JWT_ERROR(4000, HttpStatus.UNAUTHORIZED, "JWT에서 오류가 발생하였습니다."), - TOKEN_NOT_FOUND(4001, HttpStatus.BAD_REQUEST, "토큰이 HTTP Header에 없습니다."), - UNSUPPORTED_TOKEN_TYPE(4002, HttpStatus.BAD_REQUEST, "지원되지 않는 토큰 형식입니다."), + TOKEN_NOT_FOUND(4001, HttpStatus.UNAUTHORIZED, "토큰이 HTTP Header에 없습니다."), + UNSUPPORTED_TOKEN_TYPE(4002, HttpStatus.UNAUTHORIZED, "지원되지 않는 토큰 형식입니다."), INVALID_TOKEN(4003, HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), MALFORMED_TOKEN(4004, HttpStatus.UNAUTHORIZED, "토큰이 올바르게 구성되지 않았습니다."), EXPIRED_TOKEN(4005, HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), @@ -46,66 +46,65 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { INVALID_USER_SIGNUP(5000, HttpStatus.BAD_REQUEST, "회원가입 요청에서 잘못된 값이 존재합니다."), DUPLICATE_EMAIL(5001, HttpStatus.BAD_REQUEST, "이미 존재하는 이메일입니다."), DUPLICATE_NICKNAME(5002, HttpStatus.BAD_REQUEST, "이미 존재하는 닉네임입니다."), - USER_NOT_FOUND(4003, HttpStatus.BAD_REQUEST, "존재하지 않는 회원입니다."), + USER_NOT_FOUND(4003, HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), PASSWORD_NO_MATCH(4004, HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), INVALID_USER_STATUS(4005, HttpStatus.BAD_REQUEST, "잘못된 회원 status 값입니다."), - EMAIL_NOT_FOUND(4006, HttpStatus.BAD_REQUEST, "존재하지 않는 이메일입니다."), + EMAIL_NOT_FOUND(4006, HttpStatus.NOT_FOUND, "존재하지 않는 이메일입니다."), INVALID_USER_LOGIN(4007, HttpStatus.BAD_REQUEST, "로그인 요청에서 잘못된 값이 존재합니다."), /** * 6000: Space 오류 */ INVALID_SPACE_CREATE(6000, HttpStatus.BAD_REQUEST, "스페이스 생성 요청에서 잘못된 값이 존재합니다."), - - SPACE_NOT_FOUND(6001, HttpStatus.BAD_REQUEST, "존재하지 않는 스페이스입니다."), + SPACE_NOT_FOUND(6001, HttpStatus.NOT_FOUND, "존재하지 않는 스페이스입니다."), INVALID_USER_SPACE_PROFILE(6002, HttpStatus.BAD_REQUEST, "스페이스 별 유저 프로필 정보 수정 요청에서 잘못된 값이 존재합니다."), INVALID_SPACE_JOIN_REQUEST(6003, HttpStatus.BAD_REQUEST, "스페이스 가입 요청에서 잘못된 값이 존재합니다."), nff(6004, HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), gnf(6005, HttpStatus.BAD_REQUEST, "잘못된 회원 status 값입니다."), - fb(6006, HttpStatus.BAD_REQUEST, "존재하지 않는 이메일입니다."), + fb(6006, HttpStatus.NOT_FOUND, "존재하지 않는 이메일입니다."), /** * 7000: UserSpace 오류 */ - USER_IS_NOT_IN_SPACE(7000, HttpStatus.BAD_REQUEST, "해당 스페이스에 속하지 않는 유저입니다."), - UNAUTHORIZED_USER(7001, HttpStatus.UNAUTHORIZED, "해당 스페이스에 관리자 권한이 없는 유저입니다."), + USER_IS_NOT_IN_SPACE(7000, HttpStatus.NOT_FOUND, "해당 스페이스에 속하지 않는 유저입니다."), + UNAUTHORIZED_USER(7001, HttpStatus.FORBIDDEN, "해당 스페이스에 관리자 권한이 없는 유저입니다."), USER_IS_ALREADY_IN_SPACE(7002, HttpStatus.BAD_REQUEST, "해당 스페이스에 이미 가입되어 있는 유저입니다"), - D(7003, HttpStatus.BAD_REQUEST, "존재하지 않는 회원입니다."), + D(7003, HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."), E(7004, HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."), F(7005, HttpStatus.BAD_REQUEST, "잘못된 회원 status 값입니다."), - G(7006, HttpStatus.BAD_REQUEST, "존재하지 않는 이메일입니다."), + G(7006, HttpStatus.NOT_FOUND, "존재하지 않는 이메일입니다."), /** * 8000: Chat 오류 */ INVALID_CHATROOM_CREATE(8000, HttpStatus.BAD_REQUEST, "채팅방 생성 요청에서 잘못된 값이 존재합니다."), - CHATROOM_NOT_EXIST(8001, HttpStatus.BAD_REQUEST, "존재하지 않는 채팅방입니다."), + CHATROOM_NOT_EXIST(8001, HttpStatus.NOT_FOUND, "존재하지 않는 채팅방입니다."), INVALID_CHATROOM_JOIN(8001, HttpStatus.BAD_REQUEST, "채팅방 멤버 초대 요청에서 잘못된 값이 존재합니다."), + BASE64_CONVERT_FAIL_IN_MEMORY(8002, HttpStatus.INTERNAL_SERVER_ERROR, "base64 파일 변환 과정에서 문제가 생겼습니다."), /** * 9000 : MultipartFile 오류 */ - IS_NOT_IMAGE_FILE(9000, HttpStatus.BAD_REQUEST, "지원되는 이미지 파일의 형식이 아닙니다."), - MULTIPARTFILE_CONVERT_FAILE_IN_MEMORY(9001,HttpStatus.INTERNAL_SERVER_ERROR,"multipartFile memory 변환 과정에서 문제가 생겼습니다."), + IS_NOT_IMAGE_FILE(9000, HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원되는 이미지 파일의 형식이 아닙니다."), + MULTIPARTFILE_CONVERT_FAIL_IN_MEMORY(9001,HttpStatus.INTERNAL_SERVER_ERROR,"multipartFile memory 변환 과정에서 문제가 생겼습니다."), /** * 10000: voice room 오류 */ - VOICEROOM_NOT_EXIST(10001, HttpStatus.BAD_REQUEST,"존재하지 않는 보이스룸 id입니다."), + VOICEROOM_NOT_EXIST(10001, HttpStatus.NOT_FOUND,"존재하지 않는 보이스룸 id입니다."), INVALID_VOICEROOM_REQUEST(10002, HttpStatus.BAD_REQUEST,"잘못된 요청 인자 입니다."), VOICEROOM_NAME_ALREADY_EXIST(10003, HttpStatus.BAD_REQUEST,"이미 존재하는 VoiceRoom 이름 입니다."), VOICEROOM_NOT_IN_SPACE(10004, HttpStatus.BAD_REQUEST,"이미 존재하는 VoiceRoom 이름 입니다."), - VOICEROOM_DO_NOT_HAVE_PERMISSION(10005,HttpStatus.FORBIDDEN,"해당 작업은 관리자 권한이 필요합니다." ), /** * 11000: Post 오류 */ INVALID_POST_CREATE(11000, HttpStatus.BAD_REQUEST, "게시글 생성 요청에서 잘못된 값이 존재합니다."), - POST_NOT_EXIST(11001, HttpStatus.BAD_REQUEST, "존재하지 않는 게시글 id입니다."), - POST_IS_NOT_IN_SPACE(11002, HttpStatus.BAD_REQUEST, "해당 게시글은 이 스페이스에 속하지 않습니다."), + POST_NOT_EXIST(11001, HttpStatus.NOT_FOUND, "존재하지 않는 게시글 id입니다."), + POST_IS_NOT_IN_SPACE(11002, HttpStatus.NOT_FOUND, "해당 게시글은 이 스페이스에 속하지 않습니다."), ALREADY_LIKED_THE_POST(11003, HttpStatus.BAD_REQUEST, "해당 게시글에 이미 좋아요를 눌렀습니다."), NOT_LIKED_THE_POST_YET(11003, HttpStatus.BAD_REQUEST, "유저가 해당 게시글에 좋아요를 누르지 않았습니다."); diff --git a/src/main/java/space/space_spring/service/ChattingService.java b/src/main/java/space/space_spring/service/ChattingService.java index 87bc8c0c..72b0e836 100644 --- a/src/main/java/space/space_spring/service/ChattingService.java +++ b/src/main/java/space/space_spring/service/ChattingService.java @@ -9,9 +9,12 @@ import space.space_spring.dto.chat.response.ChatMessageResponse; import space.space_spring.entity.UserSpace; import space.space_spring.entity.document.ChatMessage; +import space.space_spring.entity.enumStatus.ChatMessageType; import space.space_spring.exception.CustomException; import space.space_spring.util.userSpace.UserSpaceUtils; +import java.io.IOException; +import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; @@ -22,11 +25,12 @@ @RequiredArgsConstructor public class ChattingService { + private final S3Uploader s3Uploader; private final UserSpaceUtils userSpaceUtils; private final ChattingDao chattingDao; @Transactional - public ChatMessageResponse sendChatMessage(Long senderId, ChatMessageRequest chatMessageRequest, Long chatRoomId) { + public ChatMessageResponse sendChatMessage(Long senderId, ChatMessageRequest chatMessageRequest, Long chatRoomId) throws IOException { // TODO 1: 메시지 메타데이터 저장 위해 전송자 찾기 UserSpace senderInSpace = userSpaceUtils.isUserInSpace(senderId, chatMessageRequest.getSpaceId()) @@ -36,7 +40,23 @@ public ChatMessageResponse sendChatMessage(Long senderId, ChatMessageRequest cha String senderName = senderInSpace.getUserName(); String senderProfileImg = senderInSpace.getUserProfileImg(); - // TODO 3: DB에 메시지 저장 + // TODO 3: 이미지 및 파일 포함하는 경우 S3 업로드 + String s3Url = switch (chatMessageRequest.getMessageType()) { + case IMG -> s3Uploader.uploadBase64File(chatMessageRequest.getContent().get("image") , "chattingImg", "img"); + case FILE -> s3Uploader.uploadBase64File(chatMessageRequest.getContent().get("file") , "chattingFile", chatMessageRequest.getContent().get("fileName")); + default -> ""; + }; + + // TODO 4: DB에 S3 url 저장 위한 content 처리 + if (!s3Url.isEmpty()) { + if (chatMessageRequest.getMessageType().equals(ChatMessageType.IMG)) { + chatMessageRequest.getContent().put("image", s3Url); + } else { + chatMessageRequest.getContent().put("file", s3Url); + } + } + + // TODO 4: DB에 메시지 저장 ChatMessage message = chattingDao.insert(ChatMessage.of( chatMessageRequest, chatRoomId, diff --git a/src/main/java/space/space_spring/service/PostService.java b/src/main/java/space/space_spring/service/PostService.java index 1ea116a4..27d2ba20 100644 --- a/src/main/java/space/space_spring/service/PostService.java +++ b/src/main/java/space/space_spring/service/PostService.java @@ -3,11 +3,13 @@ import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import space.space_spring.dao.PostDao; import space.space_spring.dto.post.request.CreatePostRequest; import space.space_spring.dto.post.response.ReadPostDetailResponse; import space.space_spring.dto.post.response.ReadPostsResponse; +import space.space_spring.dto.space.GetSpaceHomeDto; import space.space_spring.entity.*; import space.space_spring.exception.CustomException; import space.space_spring.util.space.SpaceUtils; @@ -15,6 +17,7 @@ import space.space_spring.util.userSpace.UserSpaceUtils; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -117,4 +120,23 @@ public ReadPostDetailResponse getPostDetail(Long userId, Long spaceId, Long post // TODO 6: ReadPostDetailResponse 객체로 변환 return ReadPostDetailResponse.of(post, userSpace.orElse(null), isLike); } + + public List getNoticeInfoForHome(Long spaceId) { + + // TODO 1. spaceId로 Space find + Space spaceBySpaceId = spaceUtils.findSpaceBySpaceId(spaceId); + + // TODO 2. Space에 해당하는 notice 게시글 get + // 공지사항 중 3개만 return + List noticeList = postDao.findBySpaceAndTypeSortedByNewest(spaceBySpaceId, "notice", Pageable.ofSize(3)); + + // TODO 3. return + List spaceHomeNoticeList = new ArrayList<>(); + for (Post post : noticeList) { + GetSpaceHomeDto.SpaceHomeNotice notice = new GetSpaceHomeDto.SpaceHomeNotice(post.getPostId(), post.getTitle()); + spaceHomeNoticeList.add(notice); + } + + return spaceHomeNoticeList; + } } diff --git a/src/main/java/space/space_spring/service/S3Uploader.java b/src/main/java/space/space_spring/service/S3Uploader.java index fe4d6f06..7e720f6a 100644 --- a/src/main/java/space/space_spring/service/S3Uploader.java +++ b/src/main/java/space/space_spring/service/S3Uploader.java @@ -1,7 +1,6 @@ package space.space_spring.service; import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; @@ -17,9 +16,12 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Base64; import java.util.Optional; +import java.util.UUID; -import static space.space_spring.response.status.BaseExceptionResponseStatus.MULTIPARTFILE_CONVERT_FAILE_IN_MEMORY; +import static space.space_spring.response.status.BaseExceptionResponseStatus.BASE64_CONVERT_FAIL_IN_MEMORY; +import static space.space_spring.response.status.BaseExceptionResponseStatus.MULTIPARTFILE_CONVERT_FAIL_IN_MEMORY; @Slf4j @RequiredArgsConstructor @@ -53,7 +55,7 @@ public String upload(MultipartFile file, String dirName) throws IOException{ return amazonS3Client.getUrl(bucket, fileName).toString(); } catch (IOException e) { log.error("Error uploading file: {}", fileName, e); - throw new CustomException(MULTIPARTFILE_CONVERT_FAILE_IN_MEMORY,"Failed to upload file"); + throw new CustomException(MULTIPARTFILE_CONVERT_FAIL_IN_MEMORY,"Failed to upload file"); } } @@ -99,4 +101,64 @@ public boolean isFileImage(MultipartFile file) { return AllowedImageFileExtensions.contains(extension); } + + public String uploadBase64File(String base64File, String dirName, String fileName) throws IOException { + String[] base64Components = base64File.split(","); // ","을 기준으로 바이트 코드를 나눠준다 + byte[] decodedBytes; + String fileExtension = ""; + + if (base64Components.length > 1) { + // TODO 1: base64 디코딩 + decodedBytes = Base64.getDecoder().decode(base64Components[1]); + + // TODO 2: 확장자 설정 + fileExtension = getFileExtension(base64Components); + } else { + log.error("No prefix found"); + decodedBytes = Base64.getDecoder().decode(base64File); + } + + // TODO 3: S3에 업로드될 고유한 파일명 설정 + String decodedFileName = "upload_" + UUID.randomUUID() + "_" + fileName + "_" +fileExtension; + String originalFileName = dirName + "/" + decodedFileName; + + // TODO 4: 임시 리소스 생성 + File uploadFile = new File(System.getProperty("java.io.tmpdir"), decodedFileName); + try (FileOutputStream fos = new FileOutputStream(uploadFile)) { + fos.write(decodedBytes); + } catch (IOException e) { + log.error("Error writing bytes to file: {}", e.getMessage()); + throw new CustomException(BASE64_CONVERT_FAIL_IN_MEMORY,"Failed to upload file"); + } + + // TODO 5: S3에 파일 업로드 및 임시 리소스 삭제 + String uploadImageUrl = putS3(uploadFile, originalFileName); + uploadFile.delete(); + + return uploadImageUrl; + } + + private String getFileExtension(String[] base64Components) { + String filePrefix = base64Components[0].split(";")[0].split(":")[1]; + return switch (filePrefix) { + case "image/jpeg" -> ".jpeg"; + case "image/png" -> ".png"; + case "image/jpg" -> ".jpg"; + case "image/gif" -> ".gif"; + case "image/webp" -> ".webp"; + case "image/svg+xml" -> ".svg"; + case "image/bmp" -> ".bmp"; + case "image/tif" -> ".tif"; + case "image/tiff" -> ".tiff"; + case "image/heic" -> ".heic"; + case "application/pdf" -> ".pdf"; + case "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" -> ".docx"; + case "application/vnd.hwp" -> ".hwp"; + case "text/plain" -> ".txt"; + default -> { + log.info("Unsupported file prefix: " + filePrefix); + yield ".bin"; // 기본 확장자 + } + }; + } } diff --git a/src/main/java/space/space_spring/service/SpaceService.java b/src/main/java/space/space_spring/service/SpaceService.java index f1dd8bcc..5cda5445 100644 --- a/src/main/java/space/space_spring/service/SpaceService.java +++ b/src/main/java/space/space_spring/service/SpaceService.java @@ -6,6 +6,7 @@ import space.space_spring.dao.SpaceDao; import space.space_spring.dao.UserDao; import space.space_spring.dao.UserSpaceDao; +import space.space_spring.dto.space.GetSpaceHomeDto; import space.space_spring.dto.space.GetSpaceJoinDto; import space.space_spring.dto.space.PostSpaceJoinDto; import space.space_spring.dto.space.response.GetUserInfoBySpaceResponse; @@ -154,4 +155,23 @@ public void createUserSpace(Long userId, Long spaceId, PostSpaceJoinDto postSpac userSpace.changeUserName(postSpaceJoinDto.getUserName()); userSpace.changeUserProfileMsg(postSpaceJoinDto.getUserProfileMsg()); } + + public GetSpaceHomeDto.SpaceInfoForHome getSpaceInfoForHome(Long spaceId) { + + // TODO 1. spaceId로 Space find + Space spaceBySpaceId = spaceUtils.findSpaceBySpaceId(spaceId); + + // TODO 2. Space의 이름, 프로필 이미지, 멤버 수 get + String spaceName = spaceBySpaceId.getSpaceName(); + String spaceProfileImg = spaceBySpaceId.getSpaceProfileImg(); + int memberNum = userSpaceDao.calculateSpaceMemberNum(spaceBySpaceId); + + // TODO 3. return + return new GetSpaceHomeDto.SpaceInfoForHome( + spaceName, + spaceProfileImg, + memberNum + ); + } + }